Update dependencies and tests

This commit updates the dependencies, vendor files and updates tests
to take into account changes to the DB driver.

Signed-off-by: Gabriel Adrian Samfira <gsamfira@cloudbasesolutions.com>
This commit is contained in:
Gabriel Adrian Samfira 2024-04-22 13:38:51 +00:00
parent 069bdd8b6b
commit 97d03dd38d
693 changed files with 86307 additions and 28214 deletions

View file

@ -267,6 +267,12 @@ func nameInlinedSchemas(opts *FlattenOpts) error {
}
func removeUnused(opts *FlattenOpts) {
for removeUnusedSinglePass(opts) {
// continue until no unused definition remains
}
}
func removeUnusedSinglePass(opts *FlattenOpts) (hasRemoved bool) {
expected := make(map[string]struct{})
for k := range opts.Swagger().Definitions {
expected[path.Join(definitionsPath, jsonpointer.Escape(k))] = struct{}{}
@ -277,6 +283,7 @@ func removeUnused(opts *FlattenOpts) {
}
for k := range expected {
hasRemoved = true
debugLog("removing unused definition %s", path.Base(k))
if opts.Verbose {
log.Printf("info: removing unused definition: %s", path.Base(k))
@ -285,6 +292,8 @@ func removeUnused(opts *FlattenOpts) {
}
opts.Spec.reload() // re-analyze
return hasRemoved
}
func importKnownRef(entry sortref.RefRevIdx, refStr, newName string, opts *FlattenOpts) error {
@ -331,7 +340,7 @@ func importNewRef(entry sortref.RefRevIdx, refStr string, opts *FlattenOpts) err
}
// generate a unique name - isOAIGen means that a naming conflict was resolved by changing the name
newName, isOAIGen = uniqifyName(opts.Swagger().Definitions, nameFromRef(entry.Ref))
newName, isOAIGen = uniqifyName(opts.Swagger().Definitions, nameFromRef(entry.Ref, opts))
debugLog("new name for [%s]: %s - with name conflict:%t", strings.Join(entry.Keys, ", "), newName, isOAIGen)
opts.flattenContext.resolved[refStr] = newName
@ -649,6 +658,7 @@ func namePointers(opts *FlattenOpts) error {
refsToReplace := make(map[string]SchemaRef, len(opts.Spec.references.schemas))
for k, ref := range opts.Spec.references.allRefs {
debugLog("name pointers: %q => %#v", k, ref)
if path.Dir(ref.String()) == definitionsPath {
// this a ref to a top-level definition: ok
continue
@ -766,6 +776,10 @@ func flattenAnonPointer(key string, v SchemaRef, refsToReplace map[string]Schema
// identifying edge case when the namer did nothing because we point to a non-schema object
// no definition is created and we expand the $ref for all callers
debugLog("decide what to do with the schema pointed to: asch.IsSimpleSchema=%t, len(callers)=%d, parts.IsSharedParam=%t, parts.IsSharedResponse=%t",
asch.IsSimpleSchema, len(callers), parts.IsSharedParam(), parts.IsSharedResponse(),
)
if (!asch.IsSimpleSchema || len(callers) > 1) && !parts.IsSharedParam() && !parts.IsSharedResponse() {
debugLog("replace JSON pointer at [%s] by definition: %s", key, v.Ref.String())
if err := namer.Name(v.Ref.String(), v.Schema, asch); err != nil {
@ -788,6 +802,7 @@ func flattenAnonPointer(key string, v SchemaRef, refsToReplace map[string]Schema
return nil
}
// everything that is a simple schema and not factorizable is expanded
debugLog("expand JSON pointer for key=%s", key)
if err := replace.UpdateRefWithSchema(opts.Swagger(), key, v.Schema); err != nil {

View file

@ -33,12 +33,14 @@ func (isn *InlineSchemaNamer) Name(key string, schema *spec.Schema, aschema *Ana
}
// create unique name
newName, isOAIGen := uniqifyName(isn.Spec.Definitions, swag.ToJSONName(name))
mangle := mangler(isn.opts)
newName, isOAIGen := uniqifyName(isn.Spec.Definitions, mangle(name))
// clone schema
sch := schutils.Clone(schema)
// replace values on schema
debugLog("rewriting schema to ref: key=%s with new name: %s", key, newName)
if err := replace.RewriteSchemaToRef(isn.Spec, key,
spec.MustCreateRef(path.Join(definitionsPath, newName))); err != nil {
return fmt.Errorf("error while creating definition %q from inline schema: %w", newName, err)
@ -149,13 +151,15 @@ func namesFromKey(parts sortref.SplitKey, aschema *AnalyzedSchema, operations ma
startIndex int
)
if parts.IsOperation() {
switch {
case parts.IsOperation():
baseNames, startIndex = namesForOperation(parts, operations)
}
// definitions
if parts.IsDefinition() {
case parts.IsDefinition():
baseNames, startIndex = namesForDefinition(parts)
default:
// this a non-standard pointer: build a name by concatenating its parts
baseNames = [][]string{parts}
startIndex = len(baseNames) + 1
}
result := make([]string, 0, len(baseNames))
@ -169,6 +173,7 @@ func namesFromKey(parts sortref.SplitKey, aschema *AnalyzedSchema, operations ma
}
sort.Strings(result)
debugLog("names from parts: %v => %v", parts, result)
return result
}
@ -256,10 +261,20 @@ func partAdder(aschema *AnalyzedSchema) sortref.PartAdder {
}
}
func nameFromRef(ref spec.Ref) string {
func mangler(o *FlattenOpts) func(string) string {
if o.KeepNames {
return func(in string) string { return in }
}
return swag.ToJSONName
}
func nameFromRef(ref spec.Ref, o *FlattenOpts) string {
mangle := mangler(o)
u := ref.GetURL()
if u.Fragment != "" {
return swag.ToJSONName(path.Base(u.Fragment))
return mangle(path.Base(u.Fragment))
}
if u.Path != "" {
@ -267,14 +282,14 @@ func nameFromRef(ref spec.Ref) string {
if bn != "" && bn != "/" {
ext := path.Ext(bn)
if ext != "" {
return swag.ToJSONName(bn[:len(bn)-len(ext)])
return mangle(bn[:len(bn)-len(ext)])
}
return swag.ToJSONName(bn)
return mangle(bn)
}
}
return swag.ToJSONName(strings.ReplaceAll(u.Host, ".", " "))
return mangle(strings.ReplaceAll(u.Host, ".", " "))
}
// GenLocation indicates from which section of the specification (models or operations) a definition has been created.

View file

@ -26,6 +26,7 @@ type FlattenOpts struct {
Verbose bool // enable some reporting on possible name conflicts detected
RemoveUnused bool // When true, remove unused parameters, responses and definitions after expansion/flattening
ContinueOnError bool // Continue when spec expansion issues are found
KeepNames bool // Do not attempt to jsonify names from references when flattening
/* Extra keys */
_ struct{} // require keys

View file

@ -29,7 +29,7 @@ var (
// GetLogger provides a prefix debug logger
func GetLogger(prefix string, debug bool) func(string, ...interface{}) {
if debug {
logger := log.New(output, fmt.Sprintf("%s:", prefix), log.LstdFlags)
logger := log.New(output, prefix+":", log.LstdFlags)
return func(msg string, args ...interface{}) {
_, file1, pos1, _ := runtime.Caller(1)
@ -37,5 +37,5 @@ func GetLogger(prefix string, debug bool) func(string, ...interface{}) {
}
}
return func(msg string, args ...interface{}) {}
return func(_ string, _ ...interface{}) {}
}

View file

@ -1,6 +1,7 @@
package replace
import (
"encoding/json"
"fmt"
"net/url"
"os"
@ -40,6 +41,8 @@ func RewriteSchemaToRef(sp *spec.Swagger, key string, ref spec.Ref) error {
if refable.Schema != nil {
refable.Schema = &spec.Schema{SchemaProps: spec.SchemaProps{Ref: ref}}
}
case map[string]interface{}: // this happens e.g. if a schema points to an extension unmarshaled as map[string]interface{}
return rewriteParentRef(sp, key, ref)
default:
return fmt.Errorf("no schema with ref found at %s for %T", key, value)
}
@ -120,6 +123,9 @@ func rewriteParentRef(sp *spec.Swagger, key string, ref spec.Ref) error {
case spec.SchemaProperties:
container[entry] = spec.Schema{SchemaProps: spec.SchemaProps{Ref: ref}}
case *interface{}:
*container = spec.Schema{SchemaProps: spec.SchemaProps{Ref: ref}}
// NOTE: can't have case *spec.SchemaOrBool = parent in this case is *Schema
default:
@ -385,8 +391,9 @@ DOWNREF:
err := asSchema.UnmarshalJSON(asJSON)
if err != nil {
return nil,
fmt.Errorf("invalid type for resolved JSON pointer %s. Expected a schema a, got: %T",
currentRef.String(), value)
fmt.Errorf("invalid type for resolved JSON pointer %s. Expected a schema a, got: %T (%v)",
currentRef.String(), value, err,
)
}
warnings = append(warnings, fmt.Sprintf("found $ref %q (response) interpreted as schema", currentRef.String()))
@ -402,8 +409,9 @@ DOWNREF:
var asSchema spec.Schema
if err := asSchema.UnmarshalJSON(asJSON); err != nil {
return nil,
fmt.Errorf("invalid type for resolved JSON pointer %s. Expected a schema a, got: %T",
currentRef.String(), value)
fmt.Errorf("invalid type for resolved JSON pointer %s. Expected a schema a, got: %T (%v)",
currentRef.String(), value, err,
)
}
warnings = append(warnings, fmt.Sprintf("found $ref %q (parameter) interpreted as schema", currentRef.String()))
@ -414,9 +422,25 @@ DOWNREF:
currentRef = asSchema.Ref
default:
return nil,
fmt.Errorf("unhandled type to resolve JSON pointer %s. Expected a Schema, got: %T",
currentRef.String(), value)
// fallback: attempts to resolve the pointer as a schema
if refable == nil {
break DOWNREF
}
asJSON, _ := json.Marshal(refable)
var asSchema spec.Schema
if err := asSchema.UnmarshalJSON(asJSON); err != nil {
return nil,
fmt.Errorf("unhandled type to resolve JSON pointer %s. Expected a Schema, got: %T (%v)",
currentRef.String(), value, err,
)
}
warnings = append(warnings, fmt.Sprintf("found $ref %q (%T) interpreted as schema", currentRef.String(), refable))
if asSchema.Ref.String() == "" {
break DOWNREF
}
currentRef = asSchema.Ref
}
}

View file

@ -69,7 +69,7 @@ func KeyParts(key string) SplitKey {
return res
}
// SplitKey holds of the parts of a /-separated key, soi that their location may be determined.
// SplitKey holds of the parts of a /-separated key, so that their location may be determined.
type SplitKey []string
// IsDefinition is true when the split key is in the #/definitions section of a spec

View file

@ -53,7 +53,7 @@ import (
// collisions.
func Mixin(primary *spec.Swagger, mixins ...*spec.Swagger) []string {
skipped := make([]string, 0, len(mixins))
opIds := getOpIds(primary)
opIDs := getOpIDs(primary)
initPrimary(primary)
for i, m := range mixins {
@ -74,7 +74,7 @@ func Mixin(primary *spec.Swagger, mixins ...*spec.Swagger) []string {
skipped = append(skipped, mergeDefinitions(primary, m)...)
// merging paths requires a map of operationIDs to work with
skipped = append(skipped, mergePaths(primary, m, opIds, i)...)
skipped = append(skipped, mergePaths(primary, m, opIDs, i)...)
skipped = append(skipped, mergeParameters(primary, m)...)
@ -84,9 +84,9 @@ func Mixin(primary *spec.Swagger, mixins ...*spec.Swagger) []string {
return skipped
}
// getOpIds extracts all the paths.<path>.operationIds from the given
// getOpIDs extracts all the paths.<path>.operationIds from the given
// spec and returns them as the keys in a map with 'true' values.
func getOpIds(s *spec.Swagger) map[string]bool {
func getOpIDs(s *spec.Swagger) map[string]bool {
rv := make(map[string]bool)
if s.Paths == nil {
return rv
@ -179,7 +179,7 @@ func mergeDefinitions(primary *spec.Swagger, m *spec.Swagger) (skipped []string)
return
}
func mergePaths(primary *spec.Swagger, m *spec.Swagger, opIds map[string]bool, mixIndex int) (skipped []string) {
func mergePaths(primary *spec.Swagger, m *spec.Swagger, opIDs map[string]bool, mixIndex int) (skipped []string) {
if m.Paths != nil {
for k, v := range m.Paths.Paths {
if _, exists := primary.Paths.Paths[k]; exists {
@ -198,10 +198,10 @@ func mergePaths(primary *spec.Swagger, m *spec.Swagger, opIds map[string]bool, m
// all the proivded specs are already unique.
piops := pathItemOps(v)
for _, piop := range piops {
if opIds[piop.ID] {
if opIDs[piop.ID] {
piop.ID = fmt.Sprintf("%v%v%v", piop.ID, "Mixin", mixIndex)
}
opIds[piop.ID] = true
opIDs[piop.ID] = true
}
primary.Paths.Paths[k] = v
}

View file

@ -1,7 +1,7 @@
package analysis
import (
"fmt"
"errors"
"github.com/go-openapi/spec"
"github.com/go-openapi/strfmt"
@ -19,7 +19,7 @@ type SchemaOpts struct {
// patterns.
func Schema(opts SchemaOpts) (*AnalyzedSchema, error) {
if opts.Schema == nil {
return nil, fmt.Errorf("no schema to analyze")
return nil, errors.New("no schema to analyze")
}
a := &AnalyzedSchema{

View file

@ -110,16 +110,36 @@ func SetForToken(document any, decodedToken string, value any) (any, error) {
return document, setSingleImpl(document, value, decodedToken, swag.DefaultJSONNameProvider)
}
func isNil(input any) bool {
if input == nil {
return true
}
kind := reflect.TypeOf(input).Kind()
switch kind { //nolint:exhaustive
case reflect.Ptr, reflect.Map, reflect.Slice, reflect.Chan:
return reflect.ValueOf(input).IsNil()
default:
return false
}
}
func getSingleImpl(node any, decodedToken string, nameProvider *swag.NameProvider) (any, reflect.Kind, error) {
rValue := reflect.Indirect(reflect.ValueOf(node))
kind := rValue.Kind()
if isNil(node) {
return nil, kind, fmt.Errorf("nil value has not field %q", decodedToken)
}
if rValue.Type().Implements(jsonPointableType) {
r, err := node.(JSONPointable).JSONLookup(decodedToken)
switch typed := node.(type) {
case JSONPointable:
r, err := typed.JSONLookup(decodedToken)
if err != nil {
return nil, kind, err
}
return r, kind, nil
case *any: // case of a pointer to interface, that is not resolved by reflect.Indirect
return getSingleImpl(*typed, decodedToken, nameProvider)
}
switch kind { //nolint:exhaustive
@ -244,7 +264,7 @@ func (p *Pointer) set(node, data any, nameProvider *swag.NameProvider) error {
knd := reflect.ValueOf(node).Kind()
if knd != reflect.Ptr && knd != reflect.Struct && knd != reflect.Map && knd != reflect.Slice && knd != reflect.Array {
return fmt.Errorf("only structs, pointers, maps and slices are supported for setting values")
return errors.New("only structs, pointers, maps and slices are supported for setting values")
}
if nameProvider == nil {

View file

@ -1,4 +1,4 @@
# Loads OAI specs [![Build Status](https://github.com/go-openapi/loads/actions/workflows/go-test.yml/badge.svg)](https://github.com/go-openapi/loads/actions?query=workflow%3A"go+test") [![codecov](https://codecov.io/gh/go-openapi/loads/branch/master/graph/badge.svg)](https://codecov.io/gh/go-openapi/lods)
# Loads OAI specs [![Build Status](https://github.com/go-openapi/loads/actions/workflows/go-test.yml/badge.svg)](https://github.com/go-openapi/loads/actions?query=workflow%3A"go+test") [![codecov](https://codecov.io/gh/go-openapi/loads/branch/master/graph/badge.svg)](https://codecov.io/gh/go-openapi/loads)
[![license](http://img.shields.io/badge/license-Apache%20v2-orange.svg)](https://raw.githubusercontent.com/go-openapi/loads/master/LICENSE) [![GoDoc](https://godoc.org/github.com/go-openapi/loads?status.svg)](http://godoc.org/github.com/go-openapi/loads)
[![Go Report Card](https://goreportcard.com/badge/github.com/go-openapi/loads)](https://goreportcard.com/report/github.com/go-openapi/loads)

View file

@ -1,3 +0,0 @@
[x] why not filepath in JSONDoc()
[] integration tests package
[] relint

View file

@ -21,7 +21,7 @@ var (
func init() {
jsonLoader := &loader{
DocLoaderWithMatch: DocLoaderWithMatch{
Match: func(pth string) bool {
Match: func(_ string) bool {
return true
},
Fn: JSONDoc,

View file

@ -248,7 +248,8 @@ func (d *Document) ResetDefinitions() *Document {
// Pristine creates a new pristine document instance based on the input data
func (d *Document) Pristine() *Document {
dd, _ := Analyzed(d.Raw(), d.Version())
raw, _ := json.Marshal(d.Spec())
dd, _ := Analyzed(raw, d.Version())
dd.pathLoader = d.pathLoader
dd.specFilePath = d.specFilePath

View file

@ -38,9 +38,16 @@ type byteStreamOpts struct {
Close bool
}
// ByteStreamConsumer creates a consumer for byte streams,
// takes a Writer/BinaryUnmarshaler interface or binary slice by reference,
// and reads from the provided reader
// ByteStreamConsumer creates a consumer for byte streams.
//
// The consumer consumes from a provided reader into the data passed by reference.
//
// Supported output underlying types and interfaces, prioritized in this order:
// - io.ReaderFrom (for maximum control)
// - io.Writer (performs io.Copy)
// - encoding.BinaryUnmarshaler
// - *string
// - *[]byte
func ByteStreamConsumer(opts ...byteStreamOpt) Consumer {
var vals byteStreamOpts
for _, opt := range opts {
@ -51,10 +58,13 @@ func ByteStreamConsumer(opts ...byteStreamOpt) Consumer {
if reader == nil {
return errors.New("ByteStreamConsumer requires a reader") // early exit
}
if data == nil {
return errors.New("nil destination for ByteStreamConsumer")
}
closer := defaultCloser
if vals.Close {
if cl, ok := reader.(io.Closer); ok {
if cl, isReaderCloser := reader.(io.Closer); isReaderCloser {
closer = cl.Close
}
}
@ -62,34 +72,56 @@ func ByteStreamConsumer(opts ...byteStreamOpt) Consumer {
_ = closer()
}()
if wrtr, ok := data.(io.Writer); ok {
_, err := io.Copy(wrtr, reader)
if readerFrom, isReaderFrom := data.(io.ReaderFrom); isReaderFrom {
_, err := readerFrom.ReadFrom(reader)
return err
}
buf := new(bytes.Buffer)
if writer, isDataWriter := data.(io.Writer); isDataWriter {
_, err := io.Copy(writer, reader)
return err
}
// buffers input before writing to data
var buf bytes.Buffer
_, err := buf.ReadFrom(reader)
if err != nil {
return err
}
b := buf.Bytes()
if bu, ok := data.(encoding.BinaryUnmarshaler); ok {
return bu.UnmarshalBinary(b)
}
switch destinationPointer := data.(type) {
case encoding.BinaryUnmarshaler:
return destinationPointer.UnmarshalBinary(b)
case *any:
switch (*destinationPointer).(type) {
case string:
*destinationPointer = string(b)
return nil
case []byte:
*destinationPointer = b
if data != nil {
if str, ok := data.(*string); ok {
*str = string(b)
return nil
}
}
default:
// check for the underlying type to be pointer to []byte or string,
if ptr := reflect.TypeOf(data); ptr.Kind() != reflect.Ptr {
return errors.New("destination must be a pointer")
}
if t := reflect.TypeOf(data); data != nil && t.Kind() == reflect.Ptr {
v := reflect.Indirect(reflect.ValueOf(data))
if t = v.Type(); t.Kind() == reflect.Slice && t.Elem().Kind() == reflect.Uint8 {
t := v.Type()
switch {
case t.Kind() == reflect.Slice && t.Elem().Kind() == reflect.Uint8:
v.SetBytes(b)
return nil
case t.Kind() == reflect.String:
v.SetString(string(b))
return nil
}
}
@ -98,21 +130,35 @@ func ByteStreamConsumer(opts ...byteStreamOpt) Consumer {
})
}
// ByteStreamProducer creates a producer for byte streams,
// takes a Reader/BinaryMarshaler interface or binary slice,
// and writes to a writer (essentially a pipe)
// ByteStreamProducer creates a producer for byte streams.
//
// The producer takes input data then writes to an output writer (essentially as a pipe).
//
// Supported input underlying types and interfaces, prioritized in this order:
// - io.WriterTo (for maximum control)
// - io.Reader (performs io.Copy). A ReadCloser is closed before exiting.
// - encoding.BinaryMarshaler
// - error (writes as a string)
// - []byte
// - string
// - struct, other slices: writes as JSON
func ByteStreamProducer(opts ...byteStreamOpt) Producer {
var vals byteStreamOpts
for _, opt := range opts {
opt(&vals)
}
return ProducerFunc(func(writer io.Writer, data interface{}) error {
if writer == nil {
return errors.New("ByteStreamProducer requires a writer") // early exit
}
if data == nil {
return errors.New("nil data for ByteStreamProducer")
}
closer := defaultCloser
if vals.Close {
if cl, ok := writer.(io.Closer); ok {
if cl, isWriterCloser := writer.(io.Closer); isWriterCloser {
closer = cl.Close
}
}
@ -120,46 +166,51 @@ func ByteStreamProducer(opts ...byteStreamOpt) Producer {
_ = closer()
}()
if rc, ok := data.(io.ReadCloser); ok {
if rc, isDataCloser := data.(io.ReadCloser); isDataCloser {
defer rc.Close()
}
if rdr, ok := data.(io.Reader); ok {
_, err := io.Copy(writer, rdr)
switch origin := data.(type) {
case io.WriterTo:
_, err := origin.WriteTo(writer)
return err
}
if bm, ok := data.(encoding.BinaryMarshaler); ok {
bytes, err := bm.MarshalBinary()
case io.Reader:
_, err := io.Copy(writer, origin)
return err
case encoding.BinaryMarshaler:
bytes, err := origin.MarshalBinary()
if err != nil {
return err
}
_, err = writer.Write(bytes)
return err
}
if data != nil {
if str, ok := data.(string); ok {
_, err := writer.Write([]byte(str))
return err
}
if e, ok := data.(error); ok {
_, err := writer.Write([]byte(e.Error()))
return err
}
case error:
_, err := writer.Write([]byte(origin.Error()))
return err
default:
v := reflect.Indirect(reflect.ValueOf(data))
if t := v.Type(); t.Kind() == reflect.Slice && t.Elem().Kind() == reflect.Uint8 {
t := v.Type()
switch {
case t.Kind() == reflect.Slice && t.Elem().Kind() == reflect.Uint8:
_, err := writer.Write(v.Bytes())
return err
}
if t := v.Type(); t.Kind() == reflect.Struct || t.Kind() == reflect.Slice {
case t.Kind() == reflect.String:
_, err := writer.Write([]byte(v.String()))
return err
case t.Kind() == reflect.Struct || t.Kind() == reflect.Slice:
b, err := swag.WriteJSON(data)
if err != nil {
return err
}
_, err = writer.Write(b)
return err
}

View file

@ -36,7 +36,7 @@ import (
)
// NewRequest creates a new swagger http client request
func newRequest(method, pathPattern string, writer runtime.ClientRequestWriter) (*request, error) {
func newRequest(method, pathPattern string, writer runtime.ClientRequestWriter) *request {
return &request{
pathPattern: pathPattern,
method: method,
@ -45,7 +45,7 @@ func newRequest(method, pathPattern string, writer runtime.ClientRequestWriter)
query: make(url.Values),
timeout: DefaultTimeout,
getBody: getRequestBuffer,
}, nil
}
}
// Request represents a swagger client request.

View file

@ -22,6 +22,7 @@ import (
"crypto/tls"
"crypto/x509"
"encoding/pem"
"errors"
"fmt"
"mime"
"net/http"
@ -31,12 +32,13 @@ import (
"sync"
"time"
"github.com/go-openapi/strfmt"
"github.com/opentracing/opentracing-go"
"github.com/go-openapi/runtime"
"github.com/go-openapi/runtime/logger"
"github.com/go-openapi/runtime/middleware"
"github.com/go-openapi/runtime/yamlpc"
"github.com/go-openapi/strfmt"
"github.com/opentracing/opentracing-go"
)
const (
@ -142,7 +144,7 @@ func TLSClientAuth(opts TLSClientOptions) (*tls.Config, error) {
return nil, fmt.Errorf("tls client priv key: %v", err)
}
default:
return nil, fmt.Errorf("tls client priv key: unsupported key type")
return nil, errors.New("tls client priv key: unsupported key type")
}
block = pem.Block{Type: "PRIVATE KEY", Bytes: keyBytes}
@ -378,14 +380,11 @@ func (r *Runtime) EnableConnectionReuse() {
func (r *Runtime) createHttpRequest(operation *runtime.ClientOperation) (*request, *http.Request, error) { //nolint:revive,stylecheck
params, _, auth := operation.Params, operation.Reader, operation.AuthInfo
request, err := newRequest(operation.Method, operation.PathPattern, params)
if err != nil {
return nil, nil, err
}
request := newRequest(operation.Method, operation.PathPattern, params)
var accept []string
accept = append(accept, operation.ProducesMediaTypes...)
if err = request.SetHeaderParam(runtime.HeaderAccept, accept...); err != nil {
if err := request.SetHeaderParam(runtime.HeaderAccept, accept...); err != nil {
return nil, nil, err
}
@ -457,27 +456,36 @@ func (r *Runtime) Submit(operation *runtime.ClientOperation) (interface{}, error
r.logger.Debugf("%s\n", string(b))
}
var hasTimeout bool
pctx := operation.Context
if pctx == nil {
pctx = r.Context
} else {
hasTimeout = true
var parentCtx context.Context
switch {
case operation.Context != nil:
parentCtx = operation.Context
case r.Context != nil:
parentCtx = r.Context
default:
parentCtx = context.Background()
}
if pctx == nil {
pctx = context.Background()
}
var ctx context.Context
var cancel context.CancelFunc
if hasTimeout {
ctx, cancel = context.WithCancel(pctx)
var (
ctx context.Context
cancel context.CancelFunc
)
if request.timeout == 0 {
// There may be a deadline in the context passed to the operation.
// Otherwise, there is no timeout set.
ctx, cancel = context.WithCancel(parentCtx)
} else {
ctx, cancel = context.WithTimeout(pctx, request.timeout)
// Sets the timeout passed from request params (by default runtime.DefaultTimeout).
// If there is already a deadline in the parent context, the shortest will
// apply.
ctx, cancel = context.WithTimeout(parentCtx, request.timeout)
}
defer cancel()
client := operation.Client
if client == nil {
var client *http.Client
if operation.Client != nil {
client = operation.Client
} else {
client = r.client
}
req = req.WithContext(ctx)

View file

@ -16,62 +16,335 @@ package runtime
import (
"bytes"
"context"
"encoding"
"encoding/csv"
"errors"
"fmt"
"io"
"reflect"
"golang.org/x/sync/errgroup"
)
// CSVConsumer creates a new CSV consumer
func CSVConsumer() Consumer {
// CSVConsumer creates a new CSV consumer.
//
// The consumer consumes CSV records from a provided reader into the data passed by reference.
//
// CSVOpts options may be specified to alter the default CSV behavior on the reader and the writer side (e.g. separator, skip header, ...).
// The defaults are those of the standard library's csv.Reader and csv.Writer.
//
// Supported output underlying types and interfaces, prioritized in this order:
// - *csv.Writer
// - CSVWriter (writer options are ignored)
// - io.Writer (as raw bytes)
// - io.ReaderFrom (as raw bytes)
// - encoding.BinaryUnmarshaler (as raw bytes)
// - *[][]string (as a collection of records)
// - *[]byte (as raw bytes)
// - *string (a raw bytes)
//
// The consumer prioritizes situations where buffering the input is not required.
func CSVConsumer(opts ...CSVOpt) Consumer {
o := csvOptsWithDefaults(opts)
return ConsumerFunc(func(reader io.Reader, data interface{}) error {
if reader == nil {
return errors.New("CSVConsumer requires a reader")
}
if data == nil {
return errors.New("nil destination for CSVConsumer")
}
csvReader := csv.NewReader(reader)
writer, ok := data.(io.Writer)
if !ok {
return errors.New("data type must be io.Writer")
}
csvWriter := csv.NewWriter(writer)
records, err := csvReader.ReadAll()
if err != nil {
return err
}
for _, r := range records {
if err := csvWriter.Write(r); err != nil {
return err
o.applyToReader(csvReader)
closer := defaultCloser
if o.closeStream {
if cl, isReaderCloser := reader.(io.Closer); isReaderCloser {
closer = cl.Close
}
}
defer func() {
_ = closer()
}()
switch destination := data.(type) {
case *csv.Writer:
csvWriter := destination
o.applyToWriter(csvWriter)
return pipeCSV(csvWriter, csvReader, o)
case CSVWriter:
csvWriter := destination
// no writer options available
return pipeCSV(csvWriter, csvReader, o)
case io.Writer:
csvWriter := csv.NewWriter(destination)
o.applyToWriter(csvWriter)
return pipeCSV(csvWriter, csvReader, o)
case io.ReaderFrom:
var buf bytes.Buffer
csvWriter := csv.NewWriter(&buf)
o.applyToWriter(csvWriter)
if err := bufferedCSV(csvWriter, csvReader, o); err != nil {
return err
}
_, err := destination.ReadFrom(&buf)
return err
case encoding.BinaryUnmarshaler:
var buf bytes.Buffer
csvWriter := csv.NewWriter(&buf)
o.applyToWriter(csvWriter)
if err := bufferedCSV(csvWriter, csvReader, o); err != nil {
return err
}
return destination.UnmarshalBinary(buf.Bytes())
default:
// support *[][]string, *[]byte, *string
if ptr := reflect.TypeOf(data); ptr.Kind() != reflect.Ptr {
return errors.New("destination must be a pointer")
}
v := reflect.Indirect(reflect.ValueOf(data))
t := v.Type()
switch {
case t.Kind() == reflect.Slice && t.Elem().Kind() == reflect.Slice && t.Elem().Elem().Kind() == reflect.String:
csvWriter := &csvRecordsWriter{}
// writer options are ignored
if err := pipeCSV(csvWriter, csvReader, o); err != nil {
return err
}
v.Grow(len(csvWriter.records))
v.SetCap(len(csvWriter.records)) // in case Grow was unnessary, trim down the capacity
v.SetLen(len(csvWriter.records))
reflect.Copy(v, reflect.ValueOf(csvWriter.records))
return nil
case t.Kind() == reflect.Slice && t.Elem().Kind() == reflect.Uint8:
var buf bytes.Buffer
csvWriter := csv.NewWriter(&buf)
o.applyToWriter(csvWriter)
if err := bufferedCSV(csvWriter, csvReader, o); err != nil {
return err
}
v.SetBytes(buf.Bytes())
return nil
case t.Kind() == reflect.String:
var buf bytes.Buffer
csvWriter := csv.NewWriter(&buf)
o.applyToWriter(csvWriter)
if err := bufferedCSV(csvWriter, csvReader, o); err != nil {
return err
}
v.SetString(buf.String())
return nil
default:
return fmt.Errorf("%v (%T) is not supported by the CSVConsumer, %s",
data, data, "can be resolved by supporting CSVWriter/Writer/BinaryUnmarshaler interface",
)
}
}
csvWriter.Flush()
return nil
})
}
// CSVProducer creates a new CSV producer
func CSVProducer() Producer {
// CSVProducer creates a new CSV producer.
//
// The producer takes input data then writes as CSV to an output writer (essentially as a pipe).
//
// Supported input underlying types and interfaces, prioritized in this order:
// - *csv.Reader
// - CSVReader (reader options are ignored)
// - io.Reader
// - io.WriterTo
// - encoding.BinaryMarshaler
// - [][]string
// - []byte
// - string
//
// The producer prioritizes situations where buffering the input is not required.
func CSVProducer(opts ...CSVOpt) Producer {
o := csvOptsWithDefaults(opts)
return ProducerFunc(func(writer io.Writer, data interface{}) error {
if writer == nil {
return errors.New("CSVProducer requires a writer")
}
dataBytes, ok := data.([]byte)
if !ok {
return errors.New("data type must be byte array")
if data == nil {
return errors.New("nil data for CSVProducer")
}
csvReader := csv.NewReader(bytes.NewBuffer(dataBytes))
records, err := csvReader.ReadAll()
if err != nil {
return err
}
csvWriter := csv.NewWriter(writer)
for _, r := range records {
if err := csvWriter.Write(r); err != nil {
return err
o.applyToWriter(csvWriter)
closer := defaultCloser
if o.closeStream {
if cl, isWriterCloser := writer.(io.Closer); isWriterCloser {
closer = cl.Close
}
}
defer func() {
_ = closer()
}()
if rc, isDataCloser := data.(io.ReadCloser); isDataCloser {
defer rc.Close()
}
switch origin := data.(type) {
case *csv.Reader:
csvReader := origin
o.applyToReader(csvReader)
return pipeCSV(csvWriter, csvReader, o)
case CSVReader:
csvReader := origin
// no reader options available
return pipeCSV(csvWriter, csvReader, o)
case io.Reader:
csvReader := csv.NewReader(origin)
o.applyToReader(csvReader)
return pipeCSV(csvWriter, csvReader, o)
case io.WriterTo:
// async piping of the writes performed by WriteTo
r, w := io.Pipe()
csvReader := csv.NewReader(r)
o.applyToReader(csvReader)
pipe, _ := errgroup.WithContext(context.Background())
pipe.Go(func() error {
_, err := origin.WriteTo(w)
_ = w.Close()
return err
})
pipe.Go(func() error {
defer func() {
_ = r.Close()
}()
return pipeCSV(csvWriter, csvReader, o)
})
return pipe.Wait()
case encoding.BinaryMarshaler:
buf, err := origin.MarshalBinary()
if err != nil {
return err
}
rdr := bytes.NewBuffer(buf)
csvReader := csv.NewReader(rdr)
return bufferedCSV(csvWriter, csvReader, o)
default:
// support [][]string, []byte, string (or pointers to those)
v := reflect.Indirect(reflect.ValueOf(data))
t := v.Type()
switch {
case t.Kind() == reflect.Slice && t.Elem().Kind() == reflect.Slice && t.Elem().Elem().Kind() == reflect.String:
csvReader := &csvRecordsWriter{
records: make([][]string, v.Len()),
}
reflect.Copy(reflect.ValueOf(csvReader.records), v)
return pipeCSV(csvWriter, csvReader, o)
case t.Kind() == reflect.Slice && t.Elem().Kind() == reflect.Uint8:
buf := bytes.NewBuffer(v.Bytes())
csvReader := csv.NewReader(buf)
o.applyToReader(csvReader)
return bufferedCSV(csvWriter, csvReader, o)
case t.Kind() == reflect.String:
buf := bytes.NewBufferString(v.String())
csvReader := csv.NewReader(buf)
o.applyToReader(csvReader)
return bufferedCSV(csvWriter, csvReader, o)
default:
return fmt.Errorf("%v (%T) is not supported by the CSVProducer, %s",
data, data, "can be resolved by supporting CSVReader/Reader/BinaryMarshaler interface",
)
}
}
csvWriter.Flush()
return nil
})
}
// pipeCSV copies CSV records from a CSV reader to a CSV writer
func pipeCSV(csvWriter CSVWriter, csvReader CSVReader, opts csvOpts) error {
for ; opts.skippedLines > 0; opts.skippedLines-- {
_, err := csvReader.Read()
if err != nil {
if errors.Is(err, io.EOF) {
return nil
}
return err
}
}
for {
record, err := csvReader.Read()
if err != nil {
if errors.Is(err, io.EOF) {
break
}
return err
}
if err := csvWriter.Write(record); err != nil {
return err
}
}
csvWriter.Flush()
return csvWriter.Error()
}
// bufferedCSV copies CSV records from a CSV reader to a CSV writer,
// by first reading all records then writing them at once.
func bufferedCSV(csvWriter *csv.Writer, csvReader *csv.Reader, opts csvOpts) error {
for ; opts.skippedLines > 0; opts.skippedLines-- {
_, err := csvReader.Read()
if err != nil {
if errors.Is(err, io.EOF) {
return nil
}
return err
}
}
records, err := csvReader.ReadAll()
if err != nil {
return err
}
return csvWriter.WriteAll(records)
}

121
vendor/github.com/go-openapi/runtime/csv_options.go generated vendored Normal file
View file

@ -0,0 +1,121 @@
package runtime
import (
"encoding/csv"
"io"
)
// CSVOpts alter the behavior of the CSV consumer or producer.
type CSVOpt func(*csvOpts)
type csvOpts struct {
csvReader csv.Reader
csvWriter csv.Writer
skippedLines int
closeStream bool
}
// WithCSVReaderOpts specifies the options to csv.Reader
// when reading CSV.
func WithCSVReaderOpts(reader csv.Reader) CSVOpt {
return func(o *csvOpts) {
o.csvReader = reader
}
}
// WithCSVWriterOpts specifies the options to csv.Writer
// when writing CSV.
func WithCSVWriterOpts(writer csv.Writer) CSVOpt {
return func(o *csvOpts) {
o.csvWriter = writer
}
}
// WithCSVSkipLines will skip header lines.
func WithCSVSkipLines(skipped int) CSVOpt {
return func(o *csvOpts) {
o.skippedLines = skipped
}
}
func WithCSVClosesStream() CSVOpt {
return func(o *csvOpts) {
o.closeStream = true
}
}
func (o csvOpts) applyToReader(in *csv.Reader) {
if o.csvReader.Comma != 0 {
in.Comma = o.csvReader.Comma
}
if o.csvReader.Comment != 0 {
in.Comment = o.csvReader.Comment
}
if o.csvReader.FieldsPerRecord != 0 {
in.FieldsPerRecord = o.csvReader.FieldsPerRecord
}
in.LazyQuotes = o.csvReader.LazyQuotes
in.TrimLeadingSpace = o.csvReader.TrimLeadingSpace
in.ReuseRecord = o.csvReader.ReuseRecord
}
func (o csvOpts) applyToWriter(in *csv.Writer) {
if o.csvWriter.Comma != 0 {
in.Comma = o.csvWriter.Comma
}
in.UseCRLF = o.csvWriter.UseCRLF
}
func csvOptsWithDefaults(opts []CSVOpt) csvOpts {
var o csvOpts
for _, apply := range opts {
apply(&o)
}
return o
}
type CSVWriter interface {
Write([]string) error
Flush()
Error() error
}
type CSVReader interface {
Read() ([]string, error)
}
var (
_ CSVWriter = &csvRecordsWriter{}
_ CSVReader = &csvRecordsWriter{}
)
// csvRecordsWriter is an internal container to move CSV records back and forth
type csvRecordsWriter struct {
i int
records [][]string
}
func (w *csvRecordsWriter) Write(record []string) error {
w.records = append(w.records, record)
return nil
}
func (w *csvRecordsWriter) Read() ([]string, error) {
if w.i >= len(w.records) {
return nil, io.EOF
}
defer func() {
w.i++
}()
return w.records[w.i], nil
}
func (w *csvRecordsWriter) Flush() {}
func (w *csvRecordsWriter) Error() error {
return nil
}

View file

@ -5,6 +5,8 @@ import (
"os"
)
var _ Logger = StandardLogger{}
type StandardLogger struct{}
func (StandardLogger) Printf(format string, args ...interface{}) {

View file

@ -18,6 +18,8 @@ import (
stdContext "context"
"fmt"
"net/http"
"net/url"
"path"
"strings"
"sync"
@ -35,12 +37,21 @@ import (
// Debug when true turns on verbose logging
var Debug = logger.DebugEnabled()
// Logger is the standard libray logger used for printing debug messages
var Logger logger.Logger = logger.StandardLogger{}
func debugLog(format string, args ...interface{}) { //nolint:goprintffuncname
if Debug {
Logger.Printf(format, args...)
func debugLogfFunc(lg logger.Logger) func(string, ...any) {
if logger.DebugEnabled() {
if lg == nil {
return Logger.Debugf
}
return lg.Debugf
}
// muted logger
return func(_ string, _ ...any) {}
}
// A Builder can create middlewares
@ -73,10 +84,11 @@ func (fn ResponderFunc) WriteResponse(rw http.ResponseWriter, pr runtime.Produce
// used throughout to store request context with the standard context attached
// to the http.Request
type Context struct {
spec *loads.Document
analyzer *analysis.Spec
api RoutableAPI
router Router
spec *loads.Document
analyzer *analysis.Spec
api RoutableAPI
router Router
debugLogf func(string, ...any) // a logging function to debug context and all components using it
}
type routableUntypedAPI struct {
@ -189,7 +201,9 @@ func (r *routableUntypedAPI) DefaultConsumes() string {
return r.defaultConsumes
}
// NewRoutableContext creates a new context for a routable API
// NewRoutableContext creates a new context for a routable API.
//
// If a nil Router is provided, the DefaultRouter (denco-based) will be used.
func NewRoutableContext(spec *loads.Document, routableAPI RoutableAPI, routes Router) *Context {
var an *analysis.Spec
if spec != nil {
@ -199,26 +213,40 @@ func NewRoutableContext(spec *loads.Document, routableAPI RoutableAPI, routes Ro
return NewRoutableContextWithAnalyzedSpec(spec, an, routableAPI, routes)
}
// NewRoutableContextWithAnalyzedSpec is like NewRoutableContext but takes in input the analysed spec too
// NewRoutableContextWithAnalyzedSpec is like NewRoutableContext but takes as input an already analysed spec.
//
// If a nil Router is provided, the DefaultRouter (denco-based) will be used.
func NewRoutableContextWithAnalyzedSpec(spec *loads.Document, an *analysis.Spec, routableAPI RoutableAPI, routes Router) *Context {
// Either there are no spec doc and analysis, or both of them.
if !((spec == nil && an == nil) || (spec != nil && an != nil)) {
panic(errors.New(http.StatusInternalServerError, "routable context requires either both spec doc and analysis, or none of them"))
}
ctx := &Context{spec: spec, api: routableAPI, analyzer: an, router: routes}
return ctx
return &Context{
spec: spec,
api: routableAPI,
analyzer: an,
router: routes,
debugLogf: debugLogfFunc(nil),
}
}
// NewContext creates a new context wrapper
// NewContext creates a new context wrapper.
//
// If a nil Router is provided, the DefaultRouter (denco-based) will be used.
func NewContext(spec *loads.Document, api *untyped.API, routes Router) *Context {
var an *analysis.Spec
if spec != nil {
an = analysis.New(spec.Spec())
}
ctx := &Context{spec: spec, analyzer: an}
ctx := &Context{
spec: spec,
analyzer: an,
router: routes,
debugLogf: debugLogfFunc(nil),
}
ctx.api = newRoutableUntypedAPI(spec, api, ctx)
ctx.router = routes
return ctx
}
@ -282,6 +310,13 @@ func (c *Context) BasePath() string {
return c.spec.BasePath()
}
// SetLogger allows for injecting a logger to catch debug entries.
//
// The logger is enabled in DEBUG mode only.
func (c *Context) SetLogger(lg logger.Logger) {
c.debugLogf = debugLogfFunc(lg)
}
// RequiredProduces returns the accepted content types for responses
func (c *Context) RequiredProduces() []string {
return c.analyzer.RequiredProduces()
@ -299,6 +334,7 @@ func (c *Context) BindValidRequest(request *http.Request, route *MatchedRoute, b
if err != nil {
res = append(res, err)
} else {
c.debugLogf("validating content type for %q against [%s]", ct, strings.Join(route.Consumes, ", "))
if err := validateContentType(route.Consumes, ct); err != nil {
res = append(res, err)
}
@ -397,16 +433,16 @@ func (c *Context) ResponseFormat(r *http.Request, offers []string) (string, *htt
var rCtx = r.Context()
if v, ok := rCtx.Value(ctxResponseFormat).(string); ok {
debugLog("[%s %s] found response format %q in context", r.Method, r.URL.Path, v)
c.debugLogf("[%s %s] found response format %q in context", r.Method, r.URL.Path, v)
return v, r
}
format := NegotiateContentType(r, offers, "")
if format != "" {
debugLog("[%s %s] set response format %q in context", r.Method, r.URL.Path, format)
c.debugLogf("[%s %s] set response format %q in context", r.Method, r.URL.Path, format)
r = r.WithContext(stdContext.WithValue(rCtx, ctxResponseFormat, format))
}
debugLog("[%s %s] negotiated response format %q", r.Method, r.URL.Path, format)
c.debugLogf("[%s %s] negotiated response format %q", r.Method, r.URL.Path, format)
return format, r
}
@ -469,7 +505,7 @@ func (c *Context) BindAndValidate(request *http.Request, matched *MatchedRoute)
var rCtx = request.Context()
if v, ok := rCtx.Value(ctxBoundParams).(*validation); ok {
debugLog("got cached validation (valid: %t)", len(v.result) == 0)
c.debugLogf("got cached validation (valid: %t)", len(v.result) == 0)
if len(v.result) > 0 {
return v.bound, request, errors.CompositeValidationError(v.result...)
}
@ -481,7 +517,7 @@ func (c *Context) BindAndValidate(request *http.Request, matched *MatchedRoute)
if len(result.result) > 0 {
return result.bound, request, errors.CompositeValidationError(result.result...)
}
debugLog("no validation errors found")
c.debugLogf("no validation errors found")
return result.bound, request, nil
}
@ -492,7 +528,7 @@ func (c *Context) NotFound(rw http.ResponseWriter, r *http.Request) {
// Respond renders the response after doing some content negotiation
func (c *Context) Respond(rw http.ResponseWriter, r *http.Request, produces []string, route *MatchedRoute, data interface{}) {
debugLog("responding to %s %s with produces: %v", r.Method, r.URL.Path, produces)
c.debugLogf("responding to %s %s with produces: %v", r.Method, r.URL.Path, produces)
offers := []string{}
for _, mt := range produces {
if mt != c.api.DefaultProduces() {
@ -501,7 +537,7 @@ func (c *Context) Respond(rw http.ResponseWriter, r *http.Request, produces []st
}
// the default producer is last so more specific producers take precedence
offers = append(offers, c.api.DefaultProduces())
debugLog("offers: %v", offers)
c.debugLogf("offers: %v", offers)
var format string
format, r = c.ResponseFormat(r, offers)
@ -584,45 +620,92 @@ func (c *Context) Respond(rw http.ResponseWriter, r *http.Request, produces []st
c.api.ServeErrorFor(route.Operation.ID)(rw, r, errors.New(http.StatusInternalServerError, "can't produce response"))
}
func (c *Context) APIHandlerSwaggerUI(builder Builder) http.Handler {
// APIHandlerSwaggerUI returns a handler to serve the API.
//
// This handler includes a swagger spec, router and the contract defined in the swagger spec.
//
// A spec UI (SwaggerUI) is served at {API base path}/docs and the spec document at /swagger.json
// (these can be modified with uiOptions).
func (c *Context) APIHandlerSwaggerUI(builder Builder, opts ...UIOption) http.Handler {
b := builder
if b == nil {
b = PassthroughBuilder
}
var title string
sp := c.spec.Spec()
if sp != nil && sp.Info != nil && sp.Info.Title != "" {
title = sp.Info.Title
}
specPath, uiOpts, specOpts := c.uiOptionsForHandler(opts)
var swaggerUIOpts SwaggerUIOpts
fromCommonToAnyOptions(uiOpts, &swaggerUIOpts)
swaggerUIOpts := SwaggerUIOpts{
BasePath: c.BasePath(),
Title: title,
}
return Spec("", c.spec.Raw(), SwaggerUI(swaggerUIOpts, c.RoutesHandler(b)))
return Spec(specPath, c.spec.Raw(), SwaggerUI(swaggerUIOpts, c.RoutesHandler(b)), specOpts...)
}
// APIHandler returns a handler to serve the API, this includes a swagger spec, router and the contract defined in the swagger spec
func (c *Context) APIHandler(builder Builder) http.Handler {
// APIHandlerRapiDoc returns a handler to serve the API.
//
// This handler includes a swagger spec, router and the contract defined in the swagger spec.
//
// A spec UI (RapiDoc) is served at {API base path}/docs and the spec document at /swagger.json
// (these can be modified with uiOptions).
func (c *Context) APIHandlerRapiDoc(builder Builder, opts ...UIOption) http.Handler {
b := builder
if b == nil {
b = PassthroughBuilder
}
specPath, uiOpts, specOpts := c.uiOptionsForHandler(opts)
var rapidocUIOpts RapiDocOpts
fromCommonToAnyOptions(uiOpts, &rapidocUIOpts)
return Spec(specPath, c.spec.Raw(), RapiDoc(rapidocUIOpts, c.RoutesHandler(b)), specOpts...)
}
// APIHandler returns a handler to serve the API.
//
// This handler includes a swagger spec, router and the contract defined in the swagger spec.
//
// A spec UI (Redoc) is served at {API base path}/docs and the spec document at /swagger.json
// (these can be modified with uiOptions).
func (c *Context) APIHandler(builder Builder, opts ...UIOption) http.Handler {
b := builder
if b == nil {
b = PassthroughBuilder
}
specPath, uiOpts, specOpts := c.uiOptionsForHandler(opts)
var redocOpts RedocOpts
fromCommonToAnyOptions(uiOpts, &redocOpts)
return Spec(specPath, c.spec.Raw(), Redoc(redocOpts, c.RoutesHandler(b)), specOpts...)
}
func (c Context) uiOptionsForHandler(opts []UIOption) (string, uiOptions, []SpecOption) {
var title string
sp := c.spec.Spec()
if sp != nil && sp.Info != nil && sp.Info.Title != "" {
title = sp.Info.Title
}
redocOpts := RedocOpts{
BasePath: c.BasePath(),
Title: title,
// default options (may be overridden)
optsForContext := []UIOption{
WithUIBasePath(c.BasePath()),
WithUITitle(title),
}
optsForContext = append(optsForContext, opts...)
uiOpts := uiOptionsWithDefaults(optsForContext)
// If spec URL is provided, there is a non-default path to serve the spec.
// This makes sure that the UI middleware is aligned with the Spec middleware.
u, _ := url.Parse(uiOpts.SpecURL)
var specPath string
if u != nil {
specPath = u.Path
}
return Spec("", c.spec.Raw(), Redoc(redocOpts, c.RoutesHandler(b)))
pth, doc := path.Split(specPath)
if pth == "." {
pth = ""
}
return pth, uiOpts, []SpecOption{WithSpecDocument(doc)}
}
// RoutesHandler returns a handler to serve the API, just the routes and the contract defined in the swagger spec

View file

@ -2,6 +2,7 @@
package denco
import (
"errors"
"fmt"
"sort"
"strings"
@ -29,13 +30,13 @@ const (
// Router represents a URL router.
type Router struct {
param *doubleArray
// SizeHint expects the maximum number of path parameters in records to Build.
// SizeHint will be used to determine the capacity of the memory to allocate.
// By default, SizeHint will be determined from given records to Build.
SizeHint int
static map[string]interface{}
param *doubleArray
}
// New returns a new Router.
@ -71,7 +72,7 @@ func (rt *Router) Lookup(path string) (data interface{}, params Params, found bo
func (rt *Router) Build(records []Record) error {
statics, params := makeRecords(records)
if len(params) > MaxSize {
return fmt.Errorf("denco: too many records")
return errors.New("denco: too many records")
}
if rt.SizeHint < 0 {
rt.SizeHint = 0
@ -197,24 +198,29 @@ func (da *doubleArray) lookup(path string, params []Param, idx int) (*node, []Pa
if next := nextIndex(da.bc[idx].Base(), TerminationCharacter); next < len(da.bc) && da.bc[next].Check() == TerminationCharacter {
return da.node[da.bc[next].Base()], params, true
}
BACKTRACKING:
for j := len(indices) - 1; j >= 0; j-- {
i, idx := int(indices[j]>>32), int(indices[j]&0xffffffff)
if da.bc[idx].IsSingleParam() {
idx := nextIndex(da.bc[idx].Base(), ParamCharacter) //nolint:govet
if idx >= len(da.bc) {
nextIdx := nextIndex(da.bc[idx].Base(), ParamCharacter)
if nextIdx >= len(da.bc) {
break
}
next := NextSeparator(path, i)
params := append(params, Param{Value: path[i:next]}) //nolint:govet
if nd, params, found := da.lookup(path[next:], params, idx); found { //nolint:govet
return nd, params, true
nextParams := params
nextParams = append(nextParams, Param{Value: path[i:next]})
if nd, nextNextParams, found := da.lookup(path[next:], nextParams, nextIdx); found {
return nd, nextNextParams, true
}
}
if da.bc[idx].IsWildcardParam() {
idx := nextIndex(da.bc[idx].Base(), WildcardCharacter) //nolint:govet
params := append(params, Param{Value: path[i:]}) //nolint:govet
return da.node[da.bc[idx].Base()], params, true
nextIdx := nextIndex(da.bc[idx].Base(), WildcardCharacter)
nextParams := params
nextParams = append(nextParams, Param{Value: path[i:]})
return da.node[da.bc[nextIdx].Base()], nextParams, true
}
}
return nil, nil, false
@ -326,7 +332,7 @@ func (da *doubleArray) arrange(records []*record, idx, depth int, usedBase map[i
}
base = da.findBase(siblings, idx, usedBase)
if base > MaxSize {
return -1, nil, nil, fmt.Errorf("denco: too many elements of internal slice")
return -1, nil, nil, errors.New("denco: too many elements of internal slice")
}
da.setBase(idx, base)
return base, siblings, leaf, err
@ -387,7 +393,7 @@ func makeSiblings(records []*record, depth int) (sib []sibling, leaf *record, er
case pc == c:
continue
default:
return nil, nil, fmt.Errorf("denco: BUG: routing table hasn't been sorted")
return nil, nil, errors.New("denco: BUG: routing table hasn't been sorted")
}
if n > 0 {
sib[n-1].end = i

View file

@ -1,10 +0,0 @@
//go:build go1.8
// +build go1.8
package middleware
import "net/url"
func pathUnescape(path string) (string, error) {
return url.PathUnescape(path)
}

View file

@ -1,9 +0,0 @@
// +build !go1.8
package middleware
import "net/url"
func pathUnescape(path string) (string, error) {
return url.QueryUnescape(path)
}

View file

@ -1,4 +1,3 @@
//nolint:dupl
package middleware
import (
@ -11,66 +10,57 @@ import (
// RapiDocOpts configures the RapiDoc middlewares
type RapiDocOpts struct {
// BasePath for the UI path, defaults to: /
// BasePath for the UI, defaults to: /
BasePath string
// Path combines with BasePath for the full UI path, defaults to: docs
// Path combines with BasePath to construct the path to the UI, defaults to: "docs".
Path string
// SpecURL the url to find the spec for
// SpecURL is the URL of the spec document.
//
// Defaults to: /swagger.json
SpecURL string
// RapiDocURL for the js that generates the rapidoc site, defaults to: https://cdn.jsdelivr.net/npm/rapidoc/bundles/rapidoc.standalone.js
RapiDocURL string
// Title for the documentation site, default to: API documentation
Title string
// Template specifies a custom template to serve the UI
Template string
// RapiDocURL points to the js asset that generates the rapidoc site.
//
// Defaults to https://unpkg.com/rapidoc/dist/rapidoc-min.js
RapiDocURL string
}
// EnsureDefaults in case some options are missing
func (r *RapiDocOpts) EnsureDefaults() {
if r.BasePath == "" {
r.BasePath = "/"
}
if r.Path == "" {
r.Path = defaultDocsPath
}
if r.SpecURL == "" {
r.SpecURL = defaultDocsURL
}
common := toCommonUIOptions(r)
common.EnsureDefaults()
fromCommonToAnyOptions(common, r)
// rapidoc-specifics
if r.RapiDocURL == "" {
r.RapiDocURL = rapidocLatest
}
if r.Title == "" {
r.Title = defaultDocsTitle
if r.Template == "" {
r.Template = rapidocTemplate
}
}
// RapiDoc creates a middleware to serve a documentation site for a swagger spec.
//
// This allows for altering the spec before starting the http listener.
func RapiDoc(opts RapiDocOpts, next http.Handler) http.Handler {
opts.EnsureDefaults()
pth := path.Join(opts.BasePath, opts.Path)
tmpl := template.Must(template.New("rapidoc").Parse(rapidocTemplate))
tmpl := template.Must(template.New("rapidoc").Parse(opts.Template))
assets := bytes.NewBuffer(nil)
if err := tmpl.Execute(assets, opts); err != nil {
panic(fmt.Errorf("cannot execute template: %w", err))
}
buf := bytes.NewBuffer(nil)
_ = tmpl.Execute(buf, opts)
b := buf.Bytes()
return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
if r.URL.Path == pth {
rw.Header().Set("Content-Type", "text/html; charset=utf-8")
rw.WriteHeader(http.StatusOK)
_, _ = rw.Write(b)
return
}
if next == nil {
rw.Header().Set("Content-Type", "text/plain")
rw.WriteHeader(http.StatusNotFound)
_, _ = rw.Write([]byte(fmt.Sprintf("%q not found", pth)))
return
}
next.ServeHTTP(rw, r)
})
return serveUI(pth, assets.Bytes(), next)
}
const (

View file

@ -1,4 +1,3 @@
//nolint:dupl
package middleware
import (
@ -11,66 +10,58 @@ import (
// RedocOpts configures the Redoc middlewares
type RedocOpts struct {
// BasePath for the UI path, defaults to: /
// BasePath for the UI, defaults to: /
BasePath string
// Path combines with BasePath for the full UI path, defaults to: docs
// Path combines with BasePath to construct the path to the UI, defaults to: "docs".
Path string
// SpecURL the url to find the spec for
// SpecURL is the URL of the spec document.
//
// Defaults to: /swagger.json
SpecURL string
// RedocURL for the js that generates the redoc site, defaults to: https://cdn.jsdelivr.net/npm/redoc/bundles/redoc.standalone.js
RedocURL string
// Title for the documentation site, default to: API documentation
Title string
// Template specifies a custom template to serve the UI
Template string
// RedocURL points to the js that generates the redoc site.
//
// Defaults to: https://cdn.jsdelivr.net/npm/redoc/bundles/redoc.standalone.js
RedocURL string
}
// EnsureDefaults in case some options are missing
func (r *RedocOpts) EnsureDefaults() {
if r.BasePath == "" {
r.BasePath = "/"
}
if r.Path == "" {
r.Path = defaultDocsPath
}
if r.SpecURL == "" {
r.SpecURL = defaultDocsURL
}
common := toCommonUIOptions(r)
common.EnsureDefaults()
fromCommonToAnyOptions(common, r)
// redoc-specifics
if r.RedocURL == "" {
r.RedocURL = redocLatest
}
if r.Title == "" {
r.Title = defaultDocsTitle
if r.Template == "" {
r.Template = redocTemplate
}
}
// Redoc creates a middleware to serve a documentation site for a swagger spec.
//
// This allows for altering the spec before starting the http listener.
func Redoc(opts RedocOpts, next http.Handler) http.Handler {
opts.EnsureDefaults()
pth := path.Join(opts.BasePath, opts.Path)
tmpl := template.Must(template.New("redoc").Parse(redocTemplate))
tmpl := template.Must(template.New("redoc").Parse(opts.Template))
assets := bytes.NewBuffer(nil)
if err := tmpl.Execute(assets, opts); err != nil {
panic(fmt.Errorf("cannot execute template: %w", err))
}
buf := bytes.NewBuffer(nil)
_ = tmpl.Execute(buf, opts)
b := buf.Bytes()
return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
if r.URL.Path == pth {
rw.Header().Set("Content-Type", "text/html; charset=utf-8")
rw.WriteHeader(http.StatusOK)
_, _ = rw.Write(b)
return
}
if next == nil {
rw.Header().Set("Content-Type", "text/plain")
rw.WriteHeader(http.StatusNotFound)
_, _ = rw.Write([]byte(fmt.Sprintf("%q not found", pth)))
return
}
next.ServeHTTP(rw, r)
})
return serveUI(pth, assets.Bytes(), next)
}
const (

View file

@ -19,10 +19,10 @@ import (
"reflect"
"github.com/go-openapi/errors"
"github.com/go-openapi/runtime"
"github.com/go-openapi/runtime/logger"
"github.com/go-openapi/spec"
"github.com/go-openapi/strfmt"
"github.com/go-openapi/runtime"
)
// UntypedRequestBinder binds and validates the data from a http request
@ -31,6 +31,7 @@ type UntypedRequestBinder struct {
Parameters map[string]spec.Parameter
Formats strfmt.Registry
paramBinders map[string]*untypedParamBinder
debugLogf func(string, ...any) // a logging function to debug context and all components using it
}
// NewUntypedRequestBinder creates a new binder for reading a request.
@ -44,6 +45,7 @@ func NewUntypedRequestBinder(parameters map[string]spec.Parameter, spec *spec.Sw
paramBinders: binders,
Spec: spec,
Formats: formats,
debugLogf: debugLogfFunc(nil),
}
}
@ -52,10 +54,10 @@ func (o *UntypedRequestBinder) Bind(request *http.Request, routeParams RoutePara
val := reflect.Indirect(reflect.ValueOf(data))
isMap := val.Kind() == reflect.Map
var result []error
debugLog("binding %d parameters for %s %s", len(o.Parameters), request.Method, request.URL.EscapedPath())
o.debugLogf("binding %d parameters for %s %s", len(o.Parameters), request.Method, request.URL.EscapedPath())
for fieldName, param := range o.Parameters {
binder := o.paramBinders[fieldName]
debugLog("binding parameter %s for %s %s", fieldName, request.Method, request.URL.EscapedPath())
o.debugLogf("binding parameter %s for %s %s", fieldName, request.Method, request.URL.EscapedPath())
var target reflect.Value
if !isMap {
binder.Name = fieldName
@ -102,3 +104,14 @@ func (o *UntypedRequestBinder) Bind(request *http.Request, routeParams RoutePara
return nil
}
// SetLogger allows for injecting a logger to catch debug entries.
//
// The logger is enabled in DEBUG mode only.
func (o *UntypedRequestBinder) SetLogger(lg logger.Logger) {
o.debugLogf = debugLogfFunc(lg)
}
func (o *UntypedRequestBinder) setDebugLogf(fn func(string, ...any)) {
o.debugLogf = fn
}

View file

@ -17,10 +17,12 @@ package middleware
import (
"fmt"
"net/http"
"net/url"
fpath "path"
"regexp"
"strings"
"github.com/go-openapi/runtime/logger"
"github.com/go-openapi/runtime/security"
"github.com/go-openapi/swag"
@ -67,10 +69,10 @@ func (r RouteParams) GetOK(name string) ([]string, bool, bool) {
return nil, false, false
}
// NewRouter creates a new context aware router middleware
// NewRouter creates a new context-aware router middleware
func NewRouter(ctx *Context, next http.Handler) http.Handler {
if ctx.router == nil {
ctx.router = DefaultRouter(ctx.spec, ctx.api)
ctx.router = DefaultRouter(ctx.spec, ctx.api, WithDefaultRouterLoggerFunc(ctx.debugLogf))
}
return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
@ -103,41 +105,75 @@ type RoutableAPI interface {
DefaultConsumes() string
}
// Router represents a swagger aware router
// Router represents a swagger-aware router
type Router interface {
Lookup(method, path string) (*MatchedRoute, bool)
OtherMethods(method, path string) []string
}
type defaultRouteBuilder struct {
spec *loads.Document
analyzer *analysis.Spec
api RoutableAPI
records map[string][]denco.Record
spec *loads.Document
analyzer *analysis.Spec
api RoutableAPI
records map[string][]denco.Record
debugLogf func(string, ...any) // a logging function to debug context and all components using it
}
type defaultRouter struct {
spec *loads.Document
routers map[string]*denco.Router
spec *loads.Document
routers map[string]*denco.Router
debugLogf func(string, ...any) // a logging function to debug context and all components using it
}
func newDefaultRouteBuilder(spec *loads.Document, api RoutableAPI) *defaultRouteBuilder {
func newDefaultRouteBuilder(spec *loads.Document, api RoutableAPI, opts ...DefaultRouterOpt) *defaultRouteBuilder {
var o defaultRouterOpts
for _, apply := range opts {
apply(&o)
}
if o.debugLogf == nil {
o.debugLogf = debugLogfFunc(nil) // defaults to standard logger
}
return &defaultRouteBuilder{
spec: spec,
analyzer: analysis.New(spec.Spec()),
api: api,
records: make(map[string][]denco.Record),
spec: spec,
analyzer: analysis.New(spec.Spec()),
api: api,
records: make(map[string][]denco.Record),
debugLogf: o.debugLogf,
}
}
// DefaultRouter creates a default implemenation of the router
func DefaultRouter(spec *loads.Document, api RoutableAPI) Router {
builder := newDefaultRouteBuilder(spec, api)
// DefaultRouterOpt allows to inject optional behavior to the default router.
type DefaultRouterOpt func(*defaultRouterOpts)
type defaultRouterOpts struct {
debugLogf func(string, ...any)
}
// WithDefaultRouterLogger sets the debug logger for the default router.
//
// This is enabled only in DEBUG mode.
func WithDefaultRouterLogger(lg logger.Logger) DefaultRouterOpt {
return func(o *defaultRouterOpts) {
o.debugLogf = debugLogfFunc(lg)
}
}
// WithDefaultRouterLoggerFunc sets a logging debug method for the default router.
func WithDefaultRouterLoggerFunc(fn func(string, ...any)) DefaultRouterOpt {
return func(o *defaultRouterOpts) {
o.debugLogf = fn
}
}
// DefaultRouter creates a default implementation of the router
func DefaultRouter(spec *loads.Document, api RoutableAPI, opts ...DefaultRouterOpt) Router {
builder := newDefaultRouteBuilder(spec, api, opts...)
if spec != nil {
for method, paths := range builder.analyzer.Operations() {
for path, operation := range paths {
fp := fpath.Join(spec.BasePath(), path)
debugLog("adding route %s %s %q", method, fp, operation.ID)
builder.debugLogf("adding route %s %s %q", method, fp, operation.ID)
builder.AddRoute(method, fp, operation)
}
}
@ -319,24 +355,24 @@ func (m *MatchedRoute) NeedsAuth() bool {
func (d *defaultRouter) Lookup(method, path string) (*MatchedRoute, bool) {
mth := strings.ToUpper(method)
debugLog("looking up route for %s %s", method, path)
d.debugLogf("looking up route for %s %s", method, path)
if Debug {
if len(d.routers) == 0 {
debugLog("there are no known routers")
d.debugLogf("there are no known routers")
}
for meth := range d.routers {
debugLog("got a router for %s", meth)
d.debugLogf("got a router for %s", meth)
}
}
if router, ok := d.routers[mth]; ok {
if m, rp, ok := router.Lookup(fpath.Clean(path)); ok && m != nil {
if entry, ok := m.(*routeEntry); ok {
debugLog("found a route for %s %s with %d parameters", method, path, len(entry.Parameters))
d.debugLogf("found a route for %s %s with %d parameters", method, path, len(entry.Parameters))
var params RouteParams
for _, p := range rp {
v, err := pathUnescape(p.Value)
v, err := url.PathUnescape(p.Value)
if err != nil {
debugLog("failed to escape %q: %v", p.Value, err)
d.debugLogf("failed to escape %q: %v", p.Value, err)
v = p.Value
}
// a workaround to handle fragment/composing parameters until they are supported in denco router
@ -356,10 +392,10 @@ func (d *defaultRouter) Lookup(method, path string) (*MatchedRoute, bool) {
return &MatchedRoute{routeEntry: *entry, Params: params}, true
}
} else {
debugLog("couldn't find a route by path for %s %s", method, path)
d.debugLogf("couldn't find a route by path for %s %s", method, path)
}
} else {
debugLog("couldn't find a route by method for %s %s", method, path)
d.debugLogf("couldn't find a route by method for %s %s", method, path)
}
return nil, false
}
@ -378,6 +414,10 @@ func (d *defaultRouter) OtherMethods(method, path string) []string {
return methods
}
func (d *defaultRouter) SetLogger(lg logger.Logger) {
d.debugLogf = debugLogfFunc(lg)
}
// convert swagger parameters per path segment into a denco parameter as multiple parameters per segment are not supported in denco
var pathConverter = regexp.MustCompile(`{(.+?)}([^/]*)`)
@ -413,7 +453,7 @@ func (d *defaultRouteBuilder) AddRoute(method, path string, operation *spec.Oper
bp = bp[:len(bp)-1]
}
debugLog("operation: %#v", *operation)
d.debugLogf("operation: %#v", *operation)
if handler, ok := d.api.HandlerFor(method, strings.TrimPrefix(path, bp)); ok {
consumes := d.analyzer.ConsumesFor(operation)
produces := d.analyzer.ProducesFor(operation)
@ -428,6 +468,8 @@ func (d *defaultRouteBuilder) AddRoute(method, path string, operation *spec.Oper
produces = append(produces, defProduces)
}
requestBinder := NewUntypedRequestBinder(parameters, d.spec.Spec(), d.api.Formats())
requestBinder.setDebugLogf(d.debugLogf)
record := denco.NewRecord(pathConverter.ReplaceAllString(path, ":$1"), &routeEntry{
BasePath: bp,
PathPattern: path,
@ -439,7 +481,7 @@ func (d *defaultRouteBuilder) AddRoute(method, path string, operation *spec.Oper
Producers: d.api.ProducersFor(normalizeOffers(produces)),
Parameters: parameters,
Formats: d.api.Formats(),
Binder: NewUntypedRequestBinder(parameters, d.spec.Spec(), d.api.Formats()),
Binder: requestBinder,
Authenticators: d.buildAuthenticators(operation),
Authorizer: d.api.Authorizer(),
})
@ -482,7 +524,8 @@ func (d *defaultRouteBuilder) Build() *defaultRouter {
routers[method] = router
}
return &defaultRouter{
spec: d.spec,
routers: routers,
spec: d.spec,
routers: routers,
debugLogf: d.debugLogf,
}
}

View file

@ -19,29 +19,84 @@ import (
"path"
)
// Spec creates a middleware to serve a swagger spec.
const (
contentTypeHeader = "Content-Type"
applicationJSON = "application/json"
)
// SpecOption can be applied to the Spec serving middleware
type SpecOption func(*specOptions)
var defaultSpecOptions = specOptions{
Path: "",
Document: "swagger.json",
}
type specOptions struct {
Path string
Document string
}
func specOptionsWithDefaults(opts []SpecOption) specOptions {
o := defaultSpecOptions
for _, apply := range opts {
apply(&o)
}
return o
}
// Spec creates a middleware to serve a swagger spec as a JSON document.
//
// This allows for altering the spec before starting the http listener.
// This can be useful if you want to serve the swagger spec from another path than /swagger.json
func Spec(basePath string, b []byte, next http.Handler) http.Handler {
//
// The basePath argument indicates the path of the spec document (defaults to "/").
// Additional SpecOption can be used to change the name of the document (defaults to "swagger.json").
func Spec(basePath string, b []byte, next http.Handler, opts ...SpecOption) http.Handler {
if basePath == "" {
basePath = "/"
}
pth := path.Join(basePath, "swagger.json")
o := specOptionsWithDefaults(opts)
pth := path.Join(basePath, o.Path, o.Document)
return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
if r.URL.Path == pth {
rw.Header().Set("Content-Type", "application/json")
if path.Clean(r.URL.Path) == pth {
rw.Header().Set(contentTypeHeader, applicationJSON)
rw.WriteHeader(http.StatusOK)
//#nosec
_, _ = rw.Write(b)
return
}
if next == nil {
rw.Header().Set("Content-Type", "application/json")
rw.WriteHeader(http.StatusNotFound)
if next != nil {
next.ServeHTTP(rw, r)
return
}
next.ServeHTTP(rw, r)
rw.Header().Set(contentTypeHeader, applicationJSON)
rw.WriteHeader(http.StatusNotFound)
})
}
// WithSpecPath sets the path to be joined to the base path of the Spec middleware.
//
// This is empty by default.
func WithSpecPath(pth string) SpecOption {
return func(o *specOptions) {
o.Path = pth
}
}
// WithSpecDocument sets the name of the JSON document served as a spec.
//
// By default, this is "swagger.json"
func WithSpecDocument(doc string) SpecOption {
return func(o *specOptions) {
if doc == "" {
return
}
o.Document = doc
}
}

View file

@ -8,40 +8,65 @@ import (
"path"
)
// SwaggerUIOpts configures the Swaggerui middlewares
// SwaggerUIOpts configures the SwaggerUI middleware
type SwaggerUIOpts struct {
// BasePath for the UI path, defaults to: /
// BasePath for the API, defaults to: /
BasePath string
// Path combines with BasePath for the full UI path, defaults to: docs
// Path combines with BasePath to construct the path to the UI, defaults to: "docs".
Path string
// SpecURL the url to find the spec for
// SpecURL is the URL of the spec document.
//
// Defaults to: /swagger.json
SpecURL string
// Title for the documentation site, default to: API documentation
Title string
// Template specifies a custom template to serve the UI
Template string
// OAuthCallbackURL the url called after OAuth2 login
OAuthCallbackURL string
// The three components needed to embed swagger-ui
SwaggerURL string
// SwaggerURL points to the js that generates the SwaggerUI site.
//
// Defaults to: https://unpkg.com/swagger-ui-dist/swagger-ui-bundle.js
SwaggerURL string
SwaggerPresetURL string
SwaggerStylesURL string
Favicon32 string
Favicon16 string
// Title for the documentation site, default to: API documentation
Title string
}
// EnsureDefaults in case some options are missing
func (r *SwaggerUIOpts) EnsureDefaults() {
if r.BasePath == "" {
r.BasePath = "/"
r.ensureDefaults()
if r.Template == "" {
r.Template = swaggeruiTemplate
}
if r.Path == "" {
r.Path = defaultDocsPath
}
if r.SpecURL == "" {
r.SpecURL = defaultDocsURL
}
func (r *SwaggerUIOpts) EnsureDefaultsOauth2() {
r.ensureDefaults()
if r.Template == "" {
r.Template = swaggerOAuthTemplate
}
}
func (r *SwaggerUIOpts) ensureDefaults() {
common := toCommonUIOptions(r)
common.EnsureDefaults()
fromCommonToAnyOptions(common, r)
// swaggerui-specifics
if r.OAuthCallbackURL == "" {
r.OAuthCallbackURL = path.Join(r.BasePath, r.Path, "oauth2-callback")
}
@ -60,40 +85,22 @@ func (r *SwaggerUIOpts) EnsureDefaults() {
if r.Favicon32 == "" {
r.Favicon32 = swaggerFavicon32Latest
}
if r.Title == "" {
r.Title = defaultDocsTitle
}
}
// SwaggerUI creates a middleware to serve a documentation site for a swagger spec.
//
// This allows for altering the spec before starting the http listener.
func SwaggerUI(opts SwaggerUIOpts, next http.Handler) http.Handler {
opts.EnsureDefaults()
pth := path.Join(opts.BasePath, opts.Path)
tmpl := template.Must(template.New("swaggerui").Parse(swaggeruiTemplate))
tmpl := template.Must(template.New("swaggerui").Parse(opts.Template))
assets := bytes.NewBuffer(nil)
if err := tmpl.Execute(assets, opts); err != nil {
panic(fmt.Errorf("cannot execute template: %w", err))
}
buf := bytes.NewBuffer(nil)
_ = tmpl.Execute(buf, &opts)
b := buf.Bytes()
return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
if path.Join(r.URL.Path) == pth {
rw.Header().Set("Content-Type", "text/html; charset=utf-8")
rw.WriteHeader(http.StatusOK)
_, _ = rw.Write(b)
return
}
if next == nil {
rw.Header().Set("Content-Type", "text/plain")
rw.WriteHeader(http.StatusNotFound)
_, _ = rw.Write([]byte(fmt.Sprintf("%q not found", pth)))
return
}
next.ServeHTTP(rw, r)
})
return serveUI(pth, assets.Bytes(), next)
}
const (

View file

@ -4,37 +4,20 @@ import (
"bytes"
"fmt"
"net/http"
"path"
"text/template"
)
func SwaggerUIOAuth2Callback(opts SwaggerUIOpts, next http.Handler) http.Handler {
opts.EnsureDefaults()
opts.EnsureDefaultsOauth2()
pth := opts.OAuthCallbackURL
tmpl := template.Must(template.New("swaggeroauth").Parse(swaggerOAuthTemplate))
tmpl := template.Must(template.New("swaggeroauth").Parse(opts.Template))
assets := bytes.NewBuffer(nil)
if err := tmpl.Execute(assets, opts); err != nil {
panic(fmt.Errorf("cannot execute template: %w", err))
}
buf := bytes.NewBuffer(nil)
_ = tmpl.Execute(buf, &opts)
b := buf.Bytes()
return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
if path.Join(r.URL.Path) == pth {
rw.Header().Set("Content-Type", "text/html; charset=utf-8")
rw.WriteHeader(http.StatusOK)
_, _ = rw.Write(b)
return
}
if next == nil {
rw.Header().Set("Content-Type", "text/plain")
rw.WriteHeader(http.StatusNotFound)
_, _ = rw.Write([]byte(fmt.Sprintf("%q not found", pth)))
return
}
next.ServeHTTP(rw, r)
})
return serveUI(pth, assets.Bytes(), next)
}
const (

View file

@ -1,8 +0,0 @@
package middleware
const (
// constants that are common to all UI-serving middlewares
defaultDocsPath = "docs"
defaultDocsURL = "/swagger.json"
defaultDocsTitle = "API Documentation"
)

View file

@ -0,0 +1,173 @@
package middleware
import (
"bytes"
"encoding/gob"
"fmt"
"net/http"
"path"
"strings"
)
const (
// constants that are common to all UI-serving middlewares
defaultDocsPath = "docs"
defaultDocsURL = "/swagger.json"
defaultDocsTitle = "API Documentation"
)
// uiOptions defines common options for UI serving middlewares.
type uiOptions struct {
// BasePath for the UI, defaults to: /
BasePath string
// Path combines with BasePath to construct the path to the UI, defaults to: "docs".
Path string
// SpecURL is the URL of the spec document.
//
// Defaults to: /swagger.json
SpecURL string
// Title for the documentation site, default to: API documentation
Title string
// Template specifies a custom template to serve the UI
Template string
}
// toCommonUIOptions converts any UI option type to retain the common options.
//
// This uses gob encoding/decoding to convert common fields from one struct to another.
func toCommonUIOptions(opts interface{}) uiOptions {
var buf bytes.Buffer
enc := gob.NewEncoder(&buf)
dec := gob.NewDecoder(&buf)
var o uiOptions
err := enc.Encode(opts)
if err != nil {
panic(err)
}
err = dec.Decode(&o)
if err != nil {
panic(err)
}
return o
}
func fromCommonToAnyOptions[T any](source uiOptions, target *T) {
var buf bytes.Buffer
enc := gob.NewEncoder(&buf)
dec := gob.NewDecoder(&buf)
err := enc.Encode(source)
if err != nil {
panic(err)
}
err = dec.Decode(target)
if err != nil {
panic(err)
}
}
// UIOption can be applied to UI serving middleware, such as Context.APIHandler or
// Context.APIHandlerSwaggerUI to alter the defaut behavior.
type UIOption func(*uiOptions)
func uiOptionsWithDefaults(opts []UIOption) uiOptions {
var o uiOptions
for _, apply := range opts {
apply(&o)
}
return o
}
// WithUIBasePath sets the base path from where to serve the UI assets.
//
// By default, Context middleware sets this value to the API base path.
func WithUIBasePath(base string) UIOption {
return func(o *uiOptions) {
if !strings.HasPrefix(base, "/") {
base = "/" + base
}
o.BasePath = base
}
}
// WithUIPath sets the path from where to serve the UI assets (i.e. /{basepath}/{path}.
func WithUIPath(pth string) UIOption {
return func(o *uiOptions) {
o.Path = pth
}
}
// WithUISpecURL sets the path from where to serve swagger spec document.
//
// This may be specified as a full URL or a path.
//
// By default, this is "/swagger.json"
func WithUISpecURL(specURL string) UIOption {
return func(o *uiOptions) {
o.SpecURL = specURL
}
}
// WithUITitle sets the title of the UI.
//
// By default, Context middleware sets this value to the title found in the API spec.
func WithUITitle(title string) UIOption {
return func(o *uiOptions) {
o.Title = title
}
}
// WithTemplate allows to set a custom template for the UI.
//
// UI middleware will panic if the template does not parse or execute properly.
func WithTemplate(tpl string) UIOption {
return func(o *uiOptions) {
o.Template = tpl
}
}
// EnsureDefaults in case some options are missing
func (r *uiOptions) EnsureDefaults() {
if r.BasePath == "" {
r.BasePath = "/"
}
if r.Path == "" {
r.Path = defaultDocsPath
}
if r.SpecURL == "" {
r.SpecURL = defaultDocsURL
}
if r.Title == "" {
r.Title = defaultDocsTitle
}
}
// serveUI creates a middleware that serves a templated asset as text/html.
func serveUI(pth string, assets []byte, next http.Handler) http.Handler {
return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
if path.Clean(r.URL.Path) == pth {
rw.Header().Set(contentTypeHeader, "text/html; charset=utf-8")
rw.WriteHeader(http.StatusOK)
_, _ = rw.Write(assets)
return
}
if next != nil {
next.ServeHTTP(rw, r)
return
}
rw.Header().Set(contentTypeHeader, "text/plain")
rw.WriteHeader(http.StatusNotFound)
_, _ = rw.Write([]byte(fmt.Sprintf("%q not found", pth)))
})
}

View file

@ -35,7 +35,6 @@ type validation struct {
// ContentType validates the content type of a request
func validateContentType(allowed []string, actual string) error {
debugLog("validating content type for %q against [%s]", actual, strings.Join(allowed, ", "))
if len(allowed) == 0 {
return nil
}
@ -57,13 +56,13 @@ func validateContentType(allowed []string, actual string) error {
}
func validateRequest(ctx *Context, request *http.Request, route *MatchedRoute) *validation {
debugLog("validating request %s %s", request.Method, request.URL.EscapedPath())
validate := &validation{
context: ctx,
request: request,
route: route,
bound: make(map[string]interface{}),
}
validate.debugLogf("validating request %s %s", request.Method, request.URL.EscapedPath())
validate.contentType()
if len(validate.result) == 0 {
@ -76,8 +75,12 @@ func validateRequest(ctx *Context, request *http.Request, route *MatchedRoute) *
return validate
}
func (v *validation) debugLogf(format string, args ...any) {
v.context.debugLogf(format, args...)
}
func (v *validation) parameters() {
debugLog("validating request parameters for %s %s", v.request.Method, v.request.URL.EscapedPath())
v.debugLogf("validating request parameters for %s %s", v.request.Method, v.request.URL.EscapedPath())
if result := v.route.Binder.Bind(v.request, v.route.Params, v.route.Consumer, v.bound); result != nil {
if result.Error() == "validation failure list" {
for _, e := range result.(*errors.Validation).Value.([]interface{}) {
@ -91,7 +94,7 @@ func (v *validation) parameters() {
func (v *validation) contentType() {
if len(v.result) == 0 && runtime.HasBody(v.request) {
debugLog("validating body content type for %s %s", v.request.Method, v.request.URL.EscapedPath())
v.debugLogf("validating body content type for %s %s", v.request.Method, v.request.URL.EscapedPath())
ct, _, req, err := v.context.ContentType(v.request)
if err != nil {
v.result = append(v.result, err)
@ -100,6 +103,7 @@ func (v *validation) contentType() {
}
if len(v.result) == 0 {
v.debugLogf("validating content type for %q against [%s]", ct, strings.Join(v.route.Consumes, ", "))
if err := validateContentType(v.route.Consumes, ct); err != nil {
v.result = append(v.result, err)
}

View file

@ -29,3 +29,26 @@ The object model for OpenAPI specification documents.
> This [discussion thread](https://github.com/go-openapi/spec/issues/21) relates the full story.
>
> An early attempt to support Swagger 3 may be found at: https://github.com/go-openapi/spec3
* Does the unmarshaling support YAML?
> Not directly. The exposed types know only how to unmarshal from JSON.
>
> In order to load a YAML document as a Swagger spec, you need to use the loaders provided by
> github.com/go-openapi/loads
>
> Take a look at the example there: https://pkg.go.dev/github.com/go-openapi/loads#example-Spec
>
> See also https://github.com/go-openapi/spec/issues/164
* How can I validate a spec?
> Validation is provided by [the validate package](http://github.com/go-openapi/validate)
* Why do we have an `ID` field for `Schema` which is not part of the swagger spec?
> We found jsonschema compatibility more important: since `id` in jsonschema influences
> how `$ref` are resolved.
> This `id` does not conflict with any property named `id`.
>
> See also https://github.com/go-openapi/spec/issues/23

View file

@ -57,7 +57,7 @@ func ExpandSpec(spec *Swagger, options *ExpandOptions) error {
if !options.SkipSchemas {
for key, definition := range spec.Definitions {
parentRefs := make([]string, 0, 10)
parentRefs = append(parentRefs, fmt.Sprintf("#/definitions/%s", key))
parentRefs = append(parentRefs, "#/definitions/"+key)
def, err := expandSchema(definition, parentRefs, resolver, specBasePath)
if resolver.shouldStopOnError(err) {
@ -213,7 +213,19 @@ func expandSchema(target Schema, parentRefs []string, resolver *schemaLoader, ba
}
if target.Ref.String() != "" {
return expandSchemaRef(target, parentRefs, resolver, basePath)
if !resolver.options.SkipSchemas {
return expandSchemaRef(target, parentRefs, resolver, basePath)
}
// when "expand" with SkipSchema, we just rebase the existing $ref without replacing
// the full schema.
rebasedRef, err := NewRef(normalizeURI(target.Ref.String(), basePath))
if err != nil {
return nil, err
}
target.Ref = denormalizeRef(&rebasedRef, resolver.context.basePath, resolver.context.rootID)
return &target, nil
}
for k := range target.Definitions {
@ -525,21 +537,25 @@ func getRefAndSchema(input interface{}) (*Ref, *Schema, error) {
}
func expandParameterOrResponse(input interface{}, resolver *schemaLoader, basePath string) error {
ref, _, err := getRefAndSchema(input)
ref, sch, err := getRefAndSchema(input)
if err != nil {
return err
}
if ref == nil {
if ref == nil && sch == nil { // nothing to do
return nil
}
parentRefs := make([]string, 0, 10)
if err = resolver.deref(input, parentRefs, basePath); resolver.shouldStopOnError(err) {
return err
if ref != nil {
// dereference this $ref
if err = resolver.deref(input, parentRefs, basePath); resolver.shouldStopOnError(err) {
return err
}
ref, sch, _ = getRefAndSchema(input)
}
ref, sch, _ := getRefAndSchema(input)
if ref.String() != "" {
transitiveResolver := resolver.transitiveResolver(basePath, *ref)
basePath = resolver.updateBasePath(transitiveResolver, basePath)
@ -551,6 +567,7 @@ func expandParameterOrResponse(input interface{}, resolver *schemaLoader, basePa
if ref != nil {
*ref = Ref{}
}
return nil
}
@ -560,38 +577,29 @@ func expandParameterOrResponse(input interface{}, resolver *schemaLoader, basePa
return ern
}
switch {
case resolver.isCircular(&rebasedRef, basePath, parentRefs...):
if resolver.isCircular(&rebasedRef, basePath, parentRefs...) {
// this is a circular $ref: stop expansion
if !resolver.options.AbsoluteCircularRef {
sch.Ref = denormalizeRef(&rebasedRef, resolver.context.basePath, resolver.context.rootID)
} else {
sch.Ref = rebasedRef
}
case !resolver.options.SkipSchemas:
// schema expanded to a $ref in another root
sch.Ref = rebasedRef
debugLog("rebased to: %s", sch.Ref.String())
default:
// skip schema expansion but rebase $ref to schema
sch.Ref = denormalizeRef(&rebasedRef, resolver.context.basePath, resolver.context.rootID)
}
}
// $ref expansion or rebasing is performed by expandSchema below
if ref != nil {
*ref = Ref{}
}
// expand schema
if !resolver.options.SkipSchemas {
s, err := expandSchema(*sch, parentRefs, resolver, basePath)
if resolver.shouldStopOnError(err) {
return err
}
if s == nil {
// guard for when continuing on error
return nil
}
// yes, we do it even if options.SkipSchema is true: we have to go down that rabbit hole and rebase nested $ref)
s, err := expandSchema(*sch, parentRefs, resolver, basePath)
if resolver.shouldStopOnError(err) {
return err
}
if s != nil { // guard for when continuing on error
*sch = *s
}

View file

@ -25,6 +25,7 @@ import (
"strings"
"github.com/asaskevich/govalidator"
"github.com/google/uuid"
"go.mongodb.org/mongo-driver/bson"
)
@ -57,24 +58,35 @@ const (
// - long top-level domain names (e.g. example.london) are permitted
// - symbol unicode points are permitted (e.g. emoji) (not for top-level domain)
HostnamePattern = `^([a-zA-Z0-9\p{S}\p{L}]((-?[a-zA-Z0-9\p{S}\p{L}]{0,62})?)|([a-zA-Z0-9\p{S}\p{L}](([a-zA-Z0-9-\p{S}\p{L}]{0,61}[a-zA-Z0-9\p{S}\p{L}])?)(\.)){1,}([a-zA-Z\p{L}]){2,63})$`
// UUIDPattern Regex for UUID that allows uppercase
UUIDPattern = `(?i)^[0-9a-f]{8}-?[0-9a-f]{4}-?[0-9a-f]{4}-?[0-9a-f]{4}-?[0-9a-f]{12}$`
// UUID3Pattern Regex for UUID3 that allows uppercase
UUID3Pattern = `(?i)^[0-9a-f]{8}-?[0-9a-f]{4}-?3[0-9a-f]{3}-?[0-9a-f]{4}-?[0-9a-f]{12}$`
// UUID4Pattern Regex for UUID4 that allows uppercase
UUID4Pattern = `(?i)^[0-9a-f]{8}-?[0-9a-f]{4}-?4[0-9a-f]{3}-?[89ab][0-9a-f]{3}-?[0-9a-f]{12}$`
// UUID5Pattern Regex for UUID5 that allows uppercase
UUID5Pattern = `(?i)^[0-9a-f]{8}-?[0-9a-f]{4}-?5[0-9a-f]{3}-?[89ab][0-9a-f]{3}-?[0-9a-f]{12}$`
// json null type
jsonNull = "null"
)
const (
// UUIDPattern Regex for UUID that allows uppercase
//
// Deprecated: strfmt no longer uses regular expressions to validate UUIDs.
UUIDPattern = `(?i)(^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$)|(^[0-9a-f]{32}$)`
// UUID3Pattern Regex for UUID3 that allows uppercase
//
// Deprecated: strfmt no longer uses regular expressions to validate UUIDs.
UUID3Pattern = `(?i)(^[0-9a-f]{8}-[0-9a-f]{4}-3[0-9a-f]{3}-[0-9a-f]{4}-[0-9a-f]{12}$)|(^[0-9a-f]{12}3[0-9a-f]{3}?[0-9a-f]{16}$)`
// UUID4Pattern Regex for UUID4 that allows uppercase
//
// Deprecated: strfmt no longer uses regular expressions to validate UUIDs.
UUID4Pattern = `(?i)(^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$)|(^[0-9a-f]{12}4[0-9a-f]{3}[89ab][0-9a-f]{15}$)`
// UUID5Pattern Regex for UUID5 that allows uppercase
//
// Deprecated: strfmt no longer uses regular expressions to validate UUIDs.
UUID5Pattern = `(?i)(^[0-9a-f]{8}-[0-9a-f]{4}-5[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$)|(^[0-9a-f]{12}5[0-9a-f]{3}[89ab][0-9a-f]{15}$)`
)
var (
rxHostname = regexp.MustCompile(HostnamePattern)
rxUUID = regexp.MustCompile(UUIDPattern)
rxUUID3 = regexp.MustCompile(UUID3Pattern)
rxUUID4 = regexp.MustCompile(UUID4Pattern)
rxUUID5 = regexp.MustCompile(UUID5Pattern)
)
// IsHostname returns true when the string is a valid hostname
@ -99,24 +111,28 @@ func IsHostname(str string) bool {
return valid
}
// IsUUID returns true is the string matches a UUID, upper case is allowed
// IsUUID returns true is the string matches a UUID (in any version, including v6 and v7), upper case is allowed
func IsUUID(str string) bool {
return rxUUID.MatchString(str)
_, err := uuid.Parse(str)
return err == nil
}
// IsUUID3 returns true is the string matches a UUID, upper case is allowed
// IsUUID3 returns true is the string matches a UUID v3, upper case is allowed
func IsUUID3(str string) bool {
return rxUUID3.MatchString(str)
id, err := uuid.Parse(str)
return err == nil && id.Version() == uuid.Version(3)
}
// IsUUID4 returns true is the string matches a UUID, upper case is allowed
// IsUUID4 returns true is the string matches a UUID v4, upper case is allowed
func IsUUID4(str string) bool {
return rxUUID4.MatchString(str)
id, err := uuid.Parse(str)
return err == nil && id.Version() == uuid.Version(4)
}
// IsUUID5 returns true is the string matches a UUID, upper case is allowed
// IsUUID5 returns true is the string matches a UUID v5, upper case is allowed
func IsUUID5(str string) bool {
return rxUUID5.MatchString(str)
id, err := uuid.Parse(str)
return err == nil && id.Version() == uuid.Version(5)
}
// IsEmail validates an email address.

View file

@ -16,6 +16,7 @@ package strfmt
import (
"encoding"
stderrors "errors"
"fmt"
"reflect"
"strings"
@ -117,7 +118,7 @@ func (f *defaultFormats) MapStructureHookFunc() mapstructure.DecodeHookFunc {
case "datetime":
input := data
if len(input) == 0 {
return nil, fmt.Errorf("empty string is an invalid datetime format")
return nil, stderrors.New("empty string is an invalid datetime format")
}
return ParseDateTime(input)
case "duration":

52
vendor/github.com/go-openapi/swag/BENCHMARK.md generated vendored Normal file
View file

@ -0,0 +1,52 @@
# Benchmarks
## Name mangling utilities
```bash
go test -bench XXX -run XXX -benchtime 30s
```
### Benchmarks at b3e7a5386f996177e4808f11acb2aa93a0f660df
```
goos: linux
goarch: amd64
pkg: github.com/go-openapi/swag
cpu: Intel(R) Core(TM) i5-6200U CPU @ 2.30GHz
BenchmarkToXXXName/ToGoName-4 862623 44101 ns/op 10450 B/op 732 allocs/op
BenchmarkToXXXName/ToVarName-4 853656 40728 ns/op 10468 B/op 734 allocs/op
BenchmarkToXXXName/ToFileName-4 1268312 27813 ns/op 9785 B/op 617 allocs/op
BenchmarkToXXXName/ToCommandName-4 1276322 27903 ns/op 9785 B/op 617 allocs/op
BenchmarkToXXXName/ToHumanNameLower-4 895334 40354 ns/op 10472 B/op 731 allocs/op
BenchmarkToXXXName/ToHumanNameTitle-4 882441 40678 ns/op 10566 B/op 749 allocs/op
```
### Benchmarks after PR #79
~ x10 performance improvement and ~ /100 memory allocations.
```
goos: linux
goarch: amd64
pkg: github.com/go-openapi/swag
cpu: Intel(R) Core(TM) i5-6200U CPU @ 2.30GHz
BenchmarkToXXXName/ToGoName-4 9595830 3991 ns/op 42 B/op 5 allocs/op
BenchmarkToXXXName/ToVarName-4 9194276 3984 ns/op 62 B/op 7 allocs/op
BenchmarkToXXXName/ToFileName-4 17002711 2123 ns/op 147 B/op 7 allocs/op
BenchmarkToXXXName/ToCommandName-4 16772926 2111 ns/op 147 B/op 7 allocs/op
BenchmarkToXXXName/ToHumanNameLower-4 9788331 3749 ns/op 92 B/op 6 allocs/op
BenchmarkToXXXName/ToHumanNameTitle-4 9188260 3941 ns/op 104 B/op 6 allocs/op
```
```
goos: linux
goarch: amd64
pkg: github.com/go-openapi/swag
cpu: AMD Ryzen 7 5800X 8-Core Processor
BenchmarkToXXXName/ToGoName-16 18527378 1972 ns/op 42 B/op 5 allocs/op
BenchmarkToXXXName/ToVarName-16 15552692 2093 ns/op 62 B/op 7 allocs/op
BenchmarkToXXXName/ToFileName-16 32161176 1117 ns/op 147 B/op 7 allocs/op
BenchmarkToXXXName/ToCommandName-16 32256634 1137 ns/op 147 B/op 7 allocs/op
BenchmarkToXXXName/ToHumanNameLower-16 18599661 1946 ns/op 92 B/op 6 allocs/op
BenchmarkToXXXName/ToHumanNameTitle-16 17581353 2054 ns/op 105 B/op 6 allocs/op
```

View file

@ -16,9 +16,130 @@ package swag
import (
"sort"
"strings"
"sync"
)
var (
// commonInitialisms are common acronyms that are kept as whole uppercased words.
commonInitialisms *indexOfInitialisms
// initialisms is a slice of sorted initialisms
initialisms []string
// a copy of initialisms pre-baked as []rune
initialismsRunes [][]rune
initialismsUpperCased [][]rune
isInitialism func(string) bool
maxAllocMatches int
)
func init() {
// Taken from https://github.com/golang/lint/blob/3390df4df2787994aea98de825b964ac7944b817/lint.go#L732-L769
configuredInitialisms := map[string]bool{
"ACL": true,
"API": true,
"ASCII": true,
"CPU": true,
"CSS": true,
"DNS": true,
"EOF": true,
"GUID": true,
"HTML": true,
"HTTPS": true,
"HTTP": true,
"ID": true,
"IP": true,
"IPv4": true,
"IPv6": true,
"JSON": true,
"LHS": true,
"OAI": true,
"QPS": true,
"RAM": true,
"RHS": true,
"RPC": true,
"SLA": true,
"SMTP": true,
"SQL": true,
"SSH": true,
"TCP": true,
"TLS": true,
"TTL": true,
"UDP": true,
"UI": true,
"UID": true,
"UUID": true,
"URI": true,
"URL": true,
"UTF8": true,
"VM": true,
"XML": true,
"XMPP": true,
"XSRF": true,
"XSS": true,
}
// a thread-safe index of initialisms
commonInitialisms = newIndexOfInitialisms().load(configuredInitialisms)
initialisms = commonInitialisms.sorted()
initialismsRunes = asRunes(initialisms)
initialismsUpperCased = asUpperCased(initialisms)
maxAllocMatches = maxAllocHeuristic(initialismsRunes)
// a test function
isInitialism = commonInitialisms.isInitialism
}
func asRunes(in []string) [][]rune {
out := make([][]rune, len(in))
for i, initialism := range in {
out[i] = []rune(initialism)
}
return out
}
func asUpperCased(in []string) [][]rune {
out := make([][]rune, len(in))
for i, initialism := range in {
out[i] = []rune(upper(trim(initialism)))
}
return out
}
func maxAllocHeuristic(in [][]rune) int {
heuristic := make(map[rune]int)
for _, initialism := range in {
heuristic[initialism[0]]++
}
var maxAlloc int
for _, val := range heuristic {
if val > maxAlloc {
maxAlloc = val
}
}
return maxAlloc
}
// AddInitialisms add additional initialisms
func AddInitialisms(words ...string) {
for _, word := range words {
// commonInitialisms[upper(word)] = true
commonInitialisms.add(upper(word))
}
// sort again
initialisms = commonInitialisms.sorted()
initialismsRunes = asRunes(initialisms)
initialismsUpperCased = asUpperCased(initialisms)
}
// indexOfInitialisms is a thread-safe implementation of the sorted index of initialisms.
// Since go1.9, this may be implemented with sync.Map.
type indexOfInitialisms struct {
@ -55,7 +176,7 @@ func (m *indexOfInitialisms) add(key string) *indexOfInitialisms {
func (m *indexOfInitialisms) sorted() (result []string) {
m.sortMutex.Lock()
defer m.sortMutex.Unlock()
m.index.Range(func(key, value interface{}) bool {
m.index.Range(func(key, _ interface{}) bool {
k := key.(string)
result = append(result, k)
return true
@ -63,3 +184,19 @@ func (m *indexOfInitialisms) sorted() (result []string) {
sort.Sort(sort.Reverse(byInitialism(result)))
return
}
type byInitialism []string
func (s byInitialism) Len() int {
return len(s)
}
func (s byInitialism) Swap(i, j int) {
s[i], s[j] = s[j], s[i]
}
func (s byInitialism) Less(i, j int) bool {
if len(s[i]) != len(s[j]) {
return len(s[i]) < len(s[j])
}
return strings.Compare(s[i], s[j]) > 0
}

View file

@ -14,74 +14,80 @@
package swag
import "unicode"
import (
"unicode"
"unicode/utf8"
)
type (
nameLexem interface {
GetUnsafeGoName() string
GetOriginal() string
IsInitialism() bool
}
lexemKind uint8
initialismNameLexem struct {
nameLexem struct {
original string
matchedInitialism string
}
casualNameLexem struct {
original string
kind lexemKind
}
)
func newInitialismNameLexem(original, matchedInitialism string) *initialismNameLexem {
return &initialismNameLexem{
const (
lexemKindCasualName lexemKind = iota
lexemKindInitialismName
)
func newInitialismNameLexem(original, matchedInitialism string) nameLexem {
return nameLexem{
kind: lexemKindInitialismName,
original: original,
matchedInitialism: matchedInitialism,
}
}
func newCasualNameLexem(original string) *casualNameLexem {
return &casualNameLexem{
func newCasualNameLexem(original string) nameLexem {
return nameLexem{
kind: lexemKindCasualName,
original: original,
}
}
func (l *initialismNameLexem) GetUnsafeGoName() string {
return l.matchedInitialism
}
func (l nameLexem) GetUnsafeGoName() string {
if l.kind == lexemKindInitialismName {
return l.matchedInitialism
}
var (
first rune
rest string
)
func (l *casualNameLexem) GetUnsafeGoName() string {
var first rune
var rest string
for i, orig := range l.original {
if i == 0 {
first = orig
continue
}
if i > 0 {
rest = l.original[i:]
break
}
}
if len(l.original) > 1 {
return string(unicode.ToUpper(first)) + lower(rest)
b := poolOfBuffers.BorrowBuffer(utf8.UTFMax + len(rest))
defer func() {
poolOfBuffers.RedeemBuffer(b)
}()
b.WriteRune(unicode.ToUpper(first))
b.WriteString(lower(rest))
return b.String()
}
return l.original
}
func (l *initialismNameLexem) GetOriginal() string {
func (l nameLexem) GetOriginal() string {
return l.original
}
func (l *casualNameLexem) GetOriginal() string {
return l.original
}
func (l *initialismNameLexem) IsInitialism() bool {
return true
}
func (l *casualNameLexem) IsInitialism() bool {
return false
func (l nameLexem) IsInitialism() bool {
return l.kind == lexemKindInitialismName
}

View file

@ -15,124 +15,269 @@
package swag
import (
"bytes"
"sync"
"unicode"
"unicode/utf8"
)
var nameReplaceTable = map[rune]string{
'@': "At ",
'&': "And ",
'|': "Pipe ",
'$': "Dollar ",
'!': "Bang ",
'-': "",
'_': "",
}
type (
splitter struct {
postSplitInitialismCheck bool
initialisms []string
initialismsRunes [][]rune
initialismsUpperCased [][]rune // initialisms cached in their trimmed, upper-cased version
postSplitInitialismCheck bool
}
splitterOption func(*splitter) *splitter
splitterOption func(*splitter)
initialismMatch struct {
body []rune
start, end int
complete bool
}
initialismMatches []initialismMatch
)
// split calls the splitter; splitter provides more control and post options
func split(str string) []string {
lexems := newSplitter().split(str)
result := make([]string, 0, len(lexems))
type (
// memory pools of temporary objects.
//
// These are used to recycle temporarily allocated objects
// and relieve the GC from undue pressure.
for _, lexem := range lexems {
matchesPool struct {
*sync.Pool
}
buffersPool struct {
*sync.Pool
}
lexemsPool struct {
*sync.Pool
}
splittersPool struct {
*sync.Pool
}
)
var (
// poolOfMatches holds temporary slices for recycling during the initialism match process
poolOfMatches = matchesPool{
Pool: &sync.Pool{
New: func() any {
s := make(initialismMatches, 0, maxAllocMatches)
return &s
},
},
}
poolOfBuffers = buffersPool{
Pool: &sync.Pool{
New: func() any {
return new(bytes.Buffer)
},
},
}
poolOfLexems = lexemsPool{
Pool: &sync.Pool{
New: func() any {
s := make([]nameLexem, 0, maxAllocMatches)
return &s
},
},
}
poolOfSplitters = splittersPool{
Pool: &sync.Pool{
New: func() any {
s := newSplitter()
return &s
},
},
}
)
// nameReplaceTable finds a word representation for special characters.
func nameReplaceTable(r rune) (string, bool) {
switch r {
case '@':
return "At ", true
case '&':
return "And ", true
case '|':
return "Pipe ", true
case '$':
return "Dollar ", true
case '!':
return "Bang ", true
case '-':
return "", true
case '_':
return "", true
default:
return "", false
}
}
// split calls the splitter.
//
// Use newSplitter for more control and options
func split(str string) []string {
s := poolOfSplitters.BorrowSplitter()
lexems := s.split(str)
result := make([]string, 0, len(*lexems))
for _, lexem := range *lexems {
result = append(result, lexem.GetOriginal())
}
poolOfLexems.RedeemLexems(lexems)
poolOfSplitters.RedeemSplitter(s)
return result
}
func (s *splitter) split(str string) []nameLexem {
return s.toNameLexems(str)
}
func newSplitter(options ...splitterOption) *splitter {
splitter := &splitter{
func newSplitter(options ...splitterOption) splitter {
s := splitter{
postSplitInitialismCheck: false,
initialisms: initialisms,
initialismsRunes: initialismsRunes,
initialismsUpperCased: initialismsUpperCased,
}
for _, option := range options {
splitter = option(splitter)
option(&s)
}
return splitter
}
// withPostSplitInitialismCheck allows to catch initialisms after main split process
func withPostSplitInitialismCheck(s *splitter) *splitter {
s.postSplitInitialismCheck = true
return s
}
type (
initialismMatch struct {
start, end int
body []rune
complete bool
}
initialismMatches []*initialismMatch
)
// withPostSplitInitialismCheck allows to catch initialisms after main split process
func withPostSplitInitialismCheck(s *splitter) {
s.postSplitInitialismCheck = true
}
func (s *splitter) toNameLexems(name string) []nameLexem {
func (p matchesPool) BorrowMatches() *initialismMatches {
s := p.Get().(*initialismMatches)
*s = (*s)[:0] // reset slice, keep allocated capacity
return s
}
func (p buffersPool) BorrowBuffer(size int) *bytes.Buffer {
s := p.Get().(*bytes.Buffer)
s.Reset()
if s.Cap() < size {
s.Grow(size)
}
return s
}
func (p lexemsPool) BorrowLexems() *[]nameLexem {
s := p.Get().(*[]nameLexem)
*s = (*s)[:0] // reset slice, keep allocated capacity
return s
}
func (p splittersPool) BorrowSplitter(options ...splitterOption) *splitter {
s := p.Get().(*splitter)
s.postSplitInitialismCheck = false // reset options
for _, apply := range options {
apply(s)
}
return s
}
func (p matchesPool) RedeemMatches(s *initialismMatches) {
p.Put(s)
}
func (p buffersPool) RedeemBuffer(s *bytes.Buffer) {
p.Put(s)
}
func (p lexemsPool) RedeemLexems(s *[]nameLexem) {
p.Put(s)
}
func (p splittersPool) RedeemSplitter(s *splitter) {
p.Put(s)
}
func (m initialismMatch) isZero() bool {
return m.start == 0 && m.end == 0
}
func (s splitter) split(name string) *[]nameLexem {
nameRunes := []rune(name)
matches := s.gatherInitialismMatches(nameRunes)
if matches == nil {
return poolOfLexems.BorrowLexems()
}
return s.mapMatchesToNameLexems(nameRunes, matches)
}
func (s *splitter) gatherInitialismMatches(nameRunes []rune) initialismMatches {
matches := make(initialismMatches, 0)
func (s splitter) gatherInitialismMatches(nameRunes []rune) *initialismMatches {
var matches *initialismMatches
for currentRunePosition, currentRune := range nameRunes {
newMatches := make(initialismMatches, 0, len(matches))
// recycle these allocations as we loop over runes
// with such recycling, only 2 slices should be allocated per call
// instead of o(n).
newMatches := poolOfMatches.BorrowMatches()
// check current initialism matches
for _, match := range matches {
if keepCompleteMatch := match.complete; keepCompleteMatch {
newMatches = append(newMatches, match)
continue
}
// drop failed match
currentMatchRune := match.body[currentRunePosition-match.start]
if !s.initialismRuneEqual(currentMatchRune, currentRune) {
continue
}
// try to complete ongoing match
if currentRunePosition-match.start == len(match.body)-1 {
// we are close; the next step is to check the symbol ahead
// if it is a small letter, then it is not the end of match
// but beginning of the next word
if currentRunePosition < len(nameRunes)-1 {
nextRune := nameRunes[currentRunePosition+1]
if newWord := unicode.IsLower(nextRune); newWord {
// oh ok, it was the start of a new word
continue
}
if matches != nil { // skip first iteration
for _, match := range *matches {
if keepCompleteMatch := match.complete; keepCompleteMatch {
*newMatches = append(*newMatches, match)
continue
}
match.complete = true
match.end = currentRunePosition
}
// drop failed match
currentMatchRune := match.body[currentRunePosition-match.start]
if currentMatchRune != currentRune {
continue
}
newMatches = append(newMatches, match)
// try to complete ongoing match
if currentRunePosition-match.start == len(match.body)-1 {
// we are close; the next step is to check the symbol ahead
// if it is a small letter, then it is not the end of match
// but beginning of the next word
if currentRunePosition < len(nameRunes)-1 {
nextRune := nameRunes[currentRunePosition+1]
if newWord := unicode.IsLower(nextRune); newWord {
// oh ok, it was the start of a new word
continue
}
}
match.complete = true
match.end = currentRunePosition
}
*newMatches = append(*newMatches, match)
}
}
// check for new initialism matches
for _, initialism := range s.initialisms {
initialismRunes := []rune(initialism)
if s.initialismRuneEqual(initialismRunes[0], currentRune) {
newMatches = append(newMatches, &initialismMatch{
for i := range s.initialisms {
initialismRunes := s.initialismsRunes[i]
if initialismRunes[0] == currentRune {
*newMatches = append(*newMatches, initialismMatch{
start: currentRunePosition,
body: initialismRunes,
complete: false,
@ -140,24 +285,28 @@ func (s *splitter) gatherInitialismMatches(nameRunes []rune) initialismMatches {
}
}
if matches != nil {
poolOfMatches.RedeemMatches(matches)
}
matches = newMatches
}
// up to the caller to redeem this last slice
return matches
}
func (s *splitter) mapMatchesToNameLexems(nameRunes []rune, matches initialismMatches) []nameLexem {
nameLexems := make([]nameLexem, 0)
func (s splitter) mapMatchesToNameLexems(nameRunes []rune, matches *initialismMatches) *[]nameLexem {
nameLexems := poolOfLexems.BorrowLexems()
var lastAcceptedMatch *initialismMatch
for _, match := range matches {
var lastAcceptedMatch initialismMatch
for _, match := range *matches {
if !match.complete {
continue
}
if firstMatch := lastAcceptedMatch == nil; firstMatch {
nameLexems = append(nameLexems, s.breakCasualString(nameRunes[:match.start])...)
nameLexems = append(nameLexems, s.breakInitialism(string(match.body)))
if firstMatch := lastAcceptedMatch.isZero(); firstMatch {
s.appendBrokenDownCasualString(nameLexems, nameRunes[:match.start])
*nameLexems = append(*nameLexems, s.breakInitialism(string(match.body)))
lastAcceptedMatch = match
@ -169,63 +318,66 @@ func (s *splitter) mapMatchesToNameLexems(nameRunes []rune, matches initialismMa
}
middle := nameRunes[lastAcceptedMatch.end+1 : match.start]
nameLexems = append(nameLexems, s.breakCasualString(middle)...)
nameLexems = append(nameLexems, s.breakInitialism(string(match.body)))
s.appendBrokenDownCasualString(nameLexems, middle)
*nameLexems = append(*nameLexems, s.breakInitialism(string(match.body)))
lastAcceptedMatch = match
}
// we have not found any accepted matches
if lastAcceptedMatch == nil {
return s.breakCasualString(nameRunes)
if lastAcceptedMatch.isZero() {
*nameLexems = (*nameLexems)[:0]
s.appendBrokenDownCasualString(nameLexems, nameRunes)
} else if lastAcceptedMatch.end+1 != len(nameRunes) {
rest := nameRunes[lastAcceptedMatch.end+1:]
s.appendBrokenDownCasualString(nameLexems, rest)
}
if lastAcceptedMatch.end+1 != len(nameRunes) {
rest := nameRunes[lastAcceptedMatch.end+1:]
nameLexems = append(nameLexems, s.breakCasualString(rest)...)
}
poolOfMatches.RedeemMatches(matches)
return nameLexems
}
func (s *splitter) initialismRuneEqual(a, b rune) bool {
return a == b
}
func (s *splitter) breakInitialism(original string) nameLexem {
func (s splitter) breakInitialism(original string) nameLexem {
return newInitialismNameLexem(original, original)
}
func (s *splitter) breakCasualString(str []rune) []nameLexem {
segments := make([]nameLexem, 0)
currentSegment := ""
func (s splitter) appendBrokenDownCasualString(segments *[]nameLexem, str []rune) {
currentSegment := poolOfBuffers.BorrowBuffer(len(str)) // unlike strings.Builder, bytes.Buffer initial storage can reused
defer func() {
poolOfBuffers.RedeemBuffer(currentSegment)
}()
addCasualNameLexem := func(original string) {
segments = append(segments, newCasualNameLexem(original))
*segments = append(*segments, newCasualNameLexem(original))
}
addInitialismNameLexem := func(original, match string) {
segments = append(segments, newInitialismNameLexem(original, match))
*segments = append(*segments, newInitialismNameLexem(original, match))
}
addNameLexem := func(original string) {
if s.postSplitInitialismCheck {
for _, initialism := range s.initialisms {
if upper(initialism) == upper(original) {
addInitialismNameLexem(original, initialism)
var addNameLexem func(string)
if s.postSplitInitialismCheck {
addNameLexem = func(original string) {
for i := range s.initialisms {
if isEqualFoldIgnoreSpace(s.initialismsUpperCased[i], original) {
addInitialismNameLexem(original, s.initialisms[i])
return
}
}
}
addCasualNameLexem(original)
addCasualNameLexem(original)
}
} else {
addNameLexem = addCasualNameLexem
}
for _, rn := range string(str) {
if replace, found := nameReplaceTable[rn]; found {
if currentSegment != "" {
addNameLexem(currentSegment)
currentSegment = ""
for _, rn := range str {
if replace, found := nameReplaceTable(rn); found {
if currentSegment.Len() > 0 {
addNameLexem(currentSegment.String())
currentSegment.Reset()
}
if replace != "" {
@ -236,27 +388,121 @@ func (s *splitter) breakCasualString(str []rune) []nameLexem {
}
if !unicode.In(rn, unicode.L, unicode.M, unicode.N, unicode.Pc) {
if currentSegment != "" {
addNameLexem(currentSegment)
currentSegment = ""
if currentSegment.Len() > 0 {
addNameLexem(currentSegment.String())
currentSegment.Reset()
}
continue
}
if unicode.IsUpper(rn) {
if currentSegment != "" {
addNameLexem(currentSegment)
if currentSegment.Len() > 0 {
addNameLexem(currentSegment.String())
}
currentSegment = ""
currentSegment.Reset()
}
currentSegment += string(rn)
currentSegment.WriteRune(rn)
}
if currentSegment != "" {
addNameLexem(currentSegment)
if currentSegment.Len() > 0 {
addNameLexem(currentSegment.String())
}
return segments
}
// isEqualFoldIgnoreSpace is the same as strings.EqualFold, but
// it ignores leading and trailing blank spaces in the compared
// string.
//
// base is assumed to be composed of upper-cased runes, and be already
// trimmed.
//
// This code is heavily inspired from strings.EqualFold.
func isEqualFoldIgnoreSpace(base []rune, str string) bool {
var i, baseIndex int
// equivalent to b := []byte(str), but without data copy
b := hackStringBytes(str)
for i < len(b) {
if c := b[i]; c < utf8.RuneSelf {
// fast path for ASCII
if c != ' ' && c != '\t' {
break
}
i++
continue
}
// unicode case
r, size := utf8.DecodeRune(b[i:])
if !unicode.IsSpace(r) {
break
}
i += size
}
if i >= len(b) {
return len(base) == 0
}
for _, baseRune := range base {
if i >= len(b) {
break
}
if c := b[i]; c < utf8.RuneSelf {
// single byte rune case (ASCII)
if baseRune >= utf8.RuneSelf {
return false
}
baseChar := byte(baseRune)
if c != baseChar &&
!('a' <= c && c <= 'z' && c-'a'+'A' == baseChar) {
return false
}
baseIndex++
i++
continue
}
// unicode case
r, size := utf8.DecodeRune(b[i:])
if unicode.ToUpper(r) != baseRune {
return false
}
baseIndex++
i += size
}
if baseIndex != len(base) {
return false
}
// all passed: now we should only have blanks
for i < len(b) {
if c := b[i]; c < utf8.RuneSelf {
// fast path for ASCII
if c != ' ' && c != '\t' {
return false
}
i++
continue
}
// unicode case
r, size := utf8.DecodeRune(b[i:])
if !unicode.IsSpace(r) {
return false
}
i += size
}
return true
}

8
vendor/github.com/go-openapi/swag/string_bytes.go generated vendored Normal file
View file

@ -0,0 +1,8 @@
package swag
import "unsafe"
// hackStringBytes returns the (unsafe) underlying bytes slice of a string.
func hackStringBytes(str string) []byte {
return unsafe.Slice(unsafe.StringData(str), len(str))
}

View file

@ -18,76 +18,25 @@ import (
"reflect"
"strings"
"unicode"
"unicode/utf8"
)
// commonInitialisms are common acronyms that are kept as whole uppercased words.
var commonInitialisms *indexOfInitialisms
// initialisms is a slice of sorted initialisms
var initialisms []string
var isInitialism func(string) bool
// GoNamePrefixFunc sets an optional rule to prefix go names
// which do not start with a letter.
//
// The prefix function is assumed to return a string that starts with an upper case letter.
//
// e.g. to help convert "123" into "{prefix}123"
//
// The default is to prefix with "X"
var GoNamePrefixFunc func(string) string
func init() {
// Taken from https://github.com/golang/lint/blob/3390df4df2787994aea98de825b964ac7944b817/lint.go#L732-L769
var configuredInitialisms = map[string]bool{
"ACL": true,
"API": true,
"ASCII": true,
"CPU": true,
"CSS": true,
"DNS": true,
"EOF": true,
"GUID": true,
"HTML": true,
"HTTPS": true,
"HTTP": true,
"ID": true,
"IP": true,
"IPv4": true,
"IPv6": true,
"JSON": true,
"LHS": true,
"OAI": true,
"QPS": true,
"RAM": true,
"RHS": true,
"RPC": true,
"SLA": true,
"SMTP": true,
"SQL": true,
"SSH": true,
"TCP": true,
"TLS": true,
"TTL": true,
"UDP": true,
"UI": true,
"UID": true,
"UUID": true,
"URI": true,
"URL": true,
"UTF8": true,
"VM": true,
"XML": true,
"XMPP": true,
"XSRF": true,
"XSS": true,
func prefixFunc(name, in string) string {
if GoNamePrefixFunc == nil {
return "X" + in
}
// a thread-safe index of initialisms
commonInitialisms = newIndexOfInitialisms().load(configuredInitialisms)
initialisms = commonInitialisms.sorted()
// a test function
isInitialism = commonInitialisms.isInitialism
return GoNamePrefixFunc(name) + in
}
const (
@ -156,25 +105,9 @@ func SplitByFormat(data, format string) []string {
return result
}
type byInitialism []string
func (s byInitialism) Len() int {
return len(s)
}
func (s byInitialism) Swap(i, j int) {
s[i], s[j] = s[j], s[i]
}
func (s byInitialism) Less(i, j int) bool {
if len(s[i]) != len(s[j]) {
return len(s[i]) < len(s[j])
}
return strings.Compare(s[i], s[j]) > 0
}
// Removes leading whitespaces
func trim(str string) string {
return strings.Trim(str, " ")
return strings.TrimSpace(str)
}
// Shortcut to strings.ToUpper()
@ -188,15 +121,20 @@ func lower(str string) string {
}
// Camelize an uppercased word
func Camelize(word string) (camelized string) {
func Camelize(word string) string {
camelized := poolOfBuffers.BorrowBuffer(len(word))
defer func() {
poolOfBuffers.RedeemBuffer(camelized)
}()
for pos, ru := range []rune(word) {
if pos > 0 {
camelized += string(unicode.ToLower(ru))
camelized.WriteRune(unicode.ToLower(ru))
} else {
camelized += string(unicode.ToUpper(ru))
camelized.WriteRune(unicode.ToUpper(ru))
}
}
return
return camelized.String()
}
// ToFileName lowercases and underscores a go type name
@ -224,33 +162,40 @@ func ToCommandName(name string) string {
// ToHumanNameLower represents a code name as a human series of words
func ToHumanNameLower(name string) string {
in := newSplitter(withPostSplitInitialismCheck).split(name)
out := make([]string, 0, len(in))
s := poolOfSplitters.BorrowSplitter(withPostSplitInitialismCheck)
in := s.split(name)
poolOfSplitters.RedeemSplitter(s)
out := make([]string, 0, len(*in))
for _, w := range in {
for _, w := range *in {
if !w.IsInitialism() {
out = append(out, lower(w.GetOriginal()))
} else {
out = append(out, w.GetOriginal())
out = append(out, trim(w.GetOriginal()))
}
}
poolOfLexems.RedeemLexems(in)
return strings.Join(out, " ")
}
// ToHumanNameTitle represents a code name as a human series of words with the first letters titleized
func ToHumanNameTitle(name string) string {
in := newSplitter(withPostSplitInitialismCheck).split(name)
s := poolOfSplitters.BorrowSplitter(withPostSplitInitialismCheck)
in := s.split(name)
poolOfSplitters.RedeemSplitter(s)
out := make([]string, 0, len(in))
for _, w := range in {
original := w.GetOriginal()
out := make([]string, 0, len(*in))
for _, w := range *in {
original := trim(w.GetOriginal())
if !w.IsInitialism() {
out = append(out, Camelize(original))
} else {
out = append(out, original)
}
}
poolOfLexems.RedeemLexems(in)
return strings.Join(out, " ")
}
@ -264,7 +209,7 @@ func ToJSONName(name string) string {
out = append(out, lower(w))
continue
}
out = append(out, Camelize(w))
out = append(out, Camelize(trim(w)))
}
return strings.Join(out, "")
}
@ -283,35 +228,70 @@ func ToVarName(name string) string {
// ToGoName translates a swagger name which can be underscored or camel cased to a name that golint likes
func ToGoName(name string) string {
lexems := newSplitter(withPostSplitInitialismCheck).split(name)
s := poolOfSplitters.BorrowSplitter(withPostSplitInitialismCheck)
lexems := s.split(name)
poolOfSplitters.RedeemSplitter(s)
defer func() {
poolOfLexems.RedeemLexems(lexems)
}()
lexemes := *lexems
result := ""
for _, lexem := range lexems {
if len(lexemes) == 0 {
return ""
}
result := poolOfBuffers.BorrowBuffer(len(name))
defer func() {
poolOfBuffers.RedeemBuffer(result)
}()
// check if not starting with a letter, upper case
firstPart := lexemes[0].GetUnsafeGoName()
if lexemes[0].IsInitialism() {
firstPart = upper(firstPart)
}
if c := firstPart[0]; c < utf8.RuneSelf {
// ASCII
switch {
case 'A' <= c && c <= 'Z':
result.WriteString(firstPart)
case 'a' <= c && c <= 'z':
result.WriteByte(c - 'a' + 'A')
result.WriteString(firstPart[1:])
default:
result.WriteString(prefixFunc(name, firstPart))
// NOTE: no longer check if prefixFunc returns a string that starts with uppercase:
// assume this is always the case
}
} else {
// unicode
firstRune, _ := utf8.DecodeRuneInString(firstPart)
switch {
case !unicode.IsLetter(firstRune):
result.WriteString(prefixFunc(name, firstPart))
case !unicode.IsUpper(firstRune):
result.WriteString(prefixFunc(name, firstPart))
/*
result.WriteRune(unicode.ToUpper(firstRune))
result.WriteString(firstPart[offset:])
*/
default:
result.WriteString(firstPart)
}
}
for _, lexem := range lexemes[1:] {
goName := lexem.GetUnsafeGoName()
// to support old behavior
if lexem.IsInitialism() {
goName = upper(goName)
}
result += goName
result.WriteString(goName)
}
if len(result) > 0 {
// Only prefix with X when the first character isn't an ascii letter
first := []rune(result)[0]
if !unicode.IsLetter(first) || (first > unicode.MaxASCII && !unicode.IsUpper(first)) {
if GoNamePrefixFunc == nil {
return "X" + result
}
result = GoNamePrefixFunc(name) + result
}
first = []rune(result)[0]
if unicode.IsLetter(first) && !unicode.IsUpper(first) {
result = string(append([]rune{unicode.ToUpper(first)}, []rune(result)[1:]...))
}
}
return result
return result.String()
}
// ContainsStrings searches a slice of strings for a case-sensitive match
@ -376,16 +356,6 @@ func IsZero(data interface{}) bool {
}
}
// AddInitialisms add additional initialisms
func AddInitialisms(words ...string) {
for _, word := range words {
// commonInitialisms[upper(word)] = true
commonInitialisms.add(upper(word))
}
// sort again
initialisms = commonInitialisms.sorted()
}
// CommandLineOptionsGroup represents a group of user-defined command line options
type CommandLineOptionsGroup struct {
ShortDescription string

View file

@ -16,8 +16,11 @@ package swag
import (
"encoding/json"
"errors"
"fmt"
"path/filepath"
"reflect"
"sort"
"strconv"
"github.com/mailru/easyjson/jlexer"
@ -48,7 +51,7 @@ func BytesToYAMLDoc(data []byte) (interface{}, error) {
return nil, err
}
if document.Kind != yaml.DocumentNode || len(document.Content) != 1 || document.Content[0].Kind != yaml.MappingNode {
return nil, fmt.Errorf("only YAML documents that are objects are supported")
return nil, errors.New("only YAML documents that are objects are supported")
}
return &document, nil
}
@ -245,7 +248,27 @@ func (s JSONMapSlice) MarshalYAML() (interface{}, error) {
return yaml.Marshal(&n)
}
func isNil(input interface{}) bool {
if input == nil {
return true
}
kind := reflect.TypeOf(input).Kind()
switch kind { //nolint:exhaustive
case reflect.Ptr, reflect.Map, reflect.Slice, reflect.Chan:
return reflect.ValueOf(input).IsNil()
default:
return false
}
}
func json2yaml(item interface{}) (*yaml.Node, error) {
if isNil(item) {
return &yaml.Node{
Kind: yaml.ScalarNode,
Value: "null",
}, nil
}
switch val := item.(type) {
case JSONMapSlice:
var n yaml.Node
@ -265,7 +288,14 @@ func json2yaml(item interface{}) (*yaml.Node, error) {
case map[string]interface{}:
var n yaml.Node
n.Kind = yaml.MappingNode
for k, v := range val {
keys := make([]string, 0, len(val))
for k := range val {
keys = append(keys, k)
}
sort.Strings(keys)
for _, k := range keys {
v := val[k]
childNode, err := json2yaml(v)
if err != nil {
return nil, err
@ -318,9 +348,9 @@ func json2yaml(item interface{}) (*yaml.Node, error) {
Tag: yamlBoolScalar,
Value: strconv.FormatBool(val),
}, nil
default:
return nil, fmt.Errorf("unhandled type: %T", val)
}
return nil, nil //nolint:nilnil
}
// JSONMapItem represents the value of a key in a JSON object held by JSONMapSlice

31
vendor/github.com/go-openapi/validate/BENCHMARK.md generated vendored Normal file
View file

@ -0,0 +1,31 @@
# Benchmark
Validating the Kubernetes Swagger API
## v0.22.6: 60,000,000 allocs
```
goos: linux
goarch: amd64
pkg: github.com/go-openapi/validate
cpu: AMD Ryzen 7 5800X 8-Core Processor
Benchmark_KubernetesSpec/validating_kubernetes_API-16 1 8549863982 ns/op 7067424936 B/op 59583275 allocs/op
```
## After refact PR: minor but noticable improvements: 25,000,000 allocs
```
go test -bench Spec
goos: linux
goarch: amd64
pkg: github.com/go-openapi/validate
cpu: AMD Ryzen 7 5800X 8-Core Processor
Benchmark_KubernetesSpec/validating_kubernetes_API-16 1 4064535557 ns/op 3379715592 B/op 25320330 allocs/op
```
## After reduce GC pressure PR: 17,000,000 allocs
```
goos: linux
goarch: amd64
pkg: github.com/go-openapi/validate
cpu: AMD Ryzen 7 5800X 8-Core Processor
Benchmark_KubernetesSpec/validating_kubernetes_API-16 1 3758414145 ns/op 2593881496 B/op 17111373 allocs/op
```

View file

@ -25,48 +25,55 @@ import (
// According to Swagger spec, default values MUST validate their schema.
type defaultValidator struct {
SpecValidator *SpecValidator
visitedSchemas map[string]bool
visitedSchemas map[string]struct{}
schemaOptions *SchemaValidatorOptions
}
// resetVisited resets the internal state of visited schemas
func (d *defaultValidator) resetVisited() {
d.visitedSchemas = map[string]bool{}
if d.visitedSchemas == nil {
d.visitedSchemas = make(map[string]struct{})
return
}
// TODO(go1.21): clear(ex.visitedSchemas)
for k := range d.visitedSchemas {
delete(d.visitedSchemas, k)
}
}
func isVisited(path string, visitedSchemas map[string]bool) bool {
found := visitedSchemas[path]
if !found {
// search for overlapping paths
frags := strings.Split(path, ".")
if len(frags) < 2 {
// shortcut exit on smaller paths
return found
func isVisited(path string, visitedSchemas map[string]struct{}) bool {
_, found := visitedSchemas[path]
if found {
return true
}
// search for overlapping paths
var (
parent string
suffix string
)
for i := len(path) - 2; i >= 0; i-- {
r := path[i]
if r != '.' {
continue
}
last := len(frags) - 1
var currentFragStr, parent string
for i := range frags {
if i == 0 {
currentFragStr = frags[last]
} else {
currentFragStr = strings.Join([]string{frags[last-i], currentFragStr}, ".")
}
if i < last {
parent = strings.Join(frags[0:last-i], ".")
} else {
parent = ""
}
if strings.HasSuffix(parent, currentFragStr) {
found = true
break
}
parent = path[0:i]
suffix = path[i+1:]
if strings.HasSuffix(parent, suffix) {
return true
}
}
return found
return false
}
// beingVisited asserts a schema is being visited
func (d *defaultValidator) beingVisited(path string) {
d.visitedSchemas[path] = true
d.visitedSchemas[path] = struct{}{}
}
// isVisited tells if a path has already been visited
@ -75,8 +82,9 @@ func (d *defaultValidator) isVisited(path string) bool {
}
// Validate validates the default values declared in the swagger spec
func (d *defaultValidator) Validate() (errs *Result) {
errs = new(Result)
func (d *defaultValidator) Validate() *Result {
errs := pools.poolOfResults.BorrowResult() // will redeem when merged
if d == nil || d.SpecValidator == nil {
return errs
}
@ -89,7 +97,7 @@ func (d *defaultValidator) validateDefaultValueValidAgainstSchema() *Result {
// every default value that is specified must validate against the schema for that property
// headers, items, parameters, schema
res := new(Result)
res := pools.poolOfResults.BorrowResult() // will redeem when merged
s := d.SpecValidator
for method, pathItem := range s.expandedAnalyzer().Operations() {
@ -107,10 +115,12 @@ func (d *defaultValidator) validateDefaultValueValidAgainstSchema() *Result {
// default values provided must validate against their inline definition (no explicit schema)
if param.Default != nil && param.Schema == nil {
// check param default value is valid
red := NewParamValidator(&param, s.KnownFormats).Validate(param.Default) //#nosec
red := newParamValidator(&param, s.KnownFormats, d.schemaOptions).Validate(param.Default) //#nosec
if red.HasErrorsOrWarnings() {
res.AddErrors(defaultValueDoesNotValidateMsg(param.Name, param.In))
res.Merge(red)
} else if red.wantsRedeemOnMerge {
pools.poolOfResults.RedeemResult(red)
}
}
@ -120,6 +130,8 @@ func (d *defaultValidator) validateDefaultValueValidAgainstSchema() *Result {
if red.HasErrorsOrWarnings() {
res.AddErrors(defaultValueItemsDoesNotValidateMsg(param.Name, param.In))
res.Merge(red)
} else if red.wantsRedeemOnMerge {
pools.poolOfResults.RedeemResult(red)
}
}
@ -129,6 +141,8 @@ func (d *defaultValidator) validateDefaultValueValidAgainstSchema() *Result {
if red.HasErrorsOrWarnings() {
res.AddErrors(defaultValueDoesNotValidateMsg(param.Name, param.In))
res.Merge(red)
} else if red.wantsRedeemOnMerge {
pools.poolOfResults.RedeemResult(red)
}
}
}
@ -154,7 +168,7 @@ func (d *defaultValidator) validateDefaultValueValidAgainstSchema() *Result {
// reset explored schemas to get depth-first recursive-proof exploration
d.resetVisited()
for nm, sch := range s.spec.Spec().Definitions {
res.Merge(d.validateDefaultValueSchemaAgainstSchema(fmt.Sprintf("definitions.%s", nm), "body", &sch)) //#nosec
res.Merge(d.validateDefaultValueSchemaAgainstSchema("definitions."+nm, "body", &sch)) //#nosec
}
}
return res
@ -176,10 +190,12 @@ func (d *defaultValidator) validateDefaultInResponse(resp *spec.Response, respon
d.resetVisited()
if h.Default != nil {
red := NewHeaderValidator(nm, &h, s.KnownFormats).Validate(h.Default) //#nosec
red := newHeaderValidator(nm, &h, s.KnownFormats, d.schemaOptions).Validate(h.Default) //#nosec
if red.HasErrorsOrWarnings() {
res.AddErrors(defaultValueHeaderDoesNotValidateMsg(operationID, nm, responseName))
res.Merge(red)
} else if red.wantsRedeemOnMerge {
pools.poolOfResults.RedeemResult(red)
}
}
@ -189,6 +205,8 @@ func (d *defaultValidator) validateDefaultInResponse(resp *spec.Response, respon
if red.HasErrorsOrWarnings() {
res.AddErrors(defaultValueHeaderItemsDoesNotValidateMsg(operationID, nm, responseName))
res.Merge(red)
} else if red.wantsRedeemOnMerge {
pools.poolOfResults.RedeemResult(red)
}
}
@ -208,6 +226,8 @@ func (d *defaultValidator) validateDefaultInResponse(resp *spec.Response, respon
// Additional message to make sure the context of the error is not lost
res.AddErrors(defaultValueInDoesNotValidateMsg(operationID, responseName))
res.Merge(red)
} else if red.wantsRedeemOnMerge {
pools.poolOfResults.RedeemResult(red)
}
}
return res
@ -219,11 +239,13 @@ func (d *defaultValidator) validateDefaultValueSchemaAgainstSchema(path, in stri
return nil
}
d.beingVisited(path)
res := new(Result)
res := pools.poolOfResults.BorrowResult()
s := d.SpecValidator
if schema.Default != nil {
res.Merge(NewSchemaValidator(schema, s.spec.Spec(), path+".default", s.KnownFormats, SwaggerSchema(true)).Validate(schema.Default))
res.Merge(
newSchemaValidator(schema, s.spec.Spec(), path+".default", s.KnownFormats, d.schemaOptions).Validate(schema.Default),
)
}
if schema.Items != nil {
if schema.Items.Schema != nil {
@ -241,7 +263,7 @@ func (d *defaultValidator) validateDefaultValueSchemaAgainstSchema(path, in stri
}
if schema.AdditionalItems != nil && schema.AdditionalItems.Schema != nil {
// NOTE: we keep validating values, even though additionalItems is not supported by Swagger 2.0 (and 3.0 as well)
res.Merge(d.validateDefaultValueSchemaAgainstSchema(fmt.Sprintf("%s.additionalItems", path), in, schema.AdditionalItems.Schema))
res.Merge(d.validateDefaultValueSchemaAgainstSchema(path+".additionalItems", in, schema.AdditionalItems.Schema))
}
for propName, prop := range schema.Properties {
res.Merge(d.validateDefaultValueSchemaAgainstSchema(path+"."+propName, in, &prop)) //#nosec
@ -250,7 +272,7 @@ func (d *defaultValidator) validateDefaultValueSchemaAgainstSchema(path, in stri
res.Merge(d.validateDefaultValueSchemaAgainstSchema(path+"."+propName, in, &prop)) //#nosec
}
if schema.AdditionalProperties != nil && schema.AdditionalProperties.Schema != nil {
res.Merge(d.validateDefaultValueSchemaAgainstSchema(fmt.Sprintf("%s.additionalProperties", path), in, schema.AdditionalProperties.Schema))
res.Merge(d.validateDefaultValueSchemaAgainstSchema(path+".additionalProperties", in, schema.AdditionalProperties.Schema))
}
if schema.AllOf != nil {
for i, aoSch := range schema.AllOf {
@ -263,11 +285,13 @@ func (d *defaultValidator) validateDefaultValueSchemaAgainstSchema(path, in stri
// TODO: Temporary duplicated code. Need to refactor with examples
func (d *defaultValidator) validateDefaultValueItemsAgainstSchema(path, in string, root interface{}, items *spec.Items) *Result {
res := new(Result)
res := pools.poolOfResults.BorrowResult()
s := d.SpecValidator
if items != nil {
if items.Default != nil {
res.Merge(newItemsValidator(path, in, items, root, s.KnownFormats).Validate(0, items.Default))
res.Merge(
newItemsValidator(path, in, items, root, s.KnownFormats, d.schemaOptions).Validate(0, items.Default),
)
}
if items.Items != nil {
res.Merge(d.validateDefaultValueItemsAgainstSchema(path+"[0].default", in, root, items.Items))

View file

@ -23,17 +23,27 @@ import (
// ExampleValidator validates example values defined in a spec
type exampleValidator struct {
SpecValidator *SpecValidator
visitedSchemas map[string]bool
visitedSchemas map[string]struct{}
schemaOptions *SchemaValidatorOptions
}
// resetVisited resets the internal state of visited schemas
func (ex *exampleValidator) resetVisited() {
ex.visitedSchemas = map[string]bool{}
if ex.visitedSchemas == nil {
ex.visitedSchemas = make(map[string]struct{})
return
}
// TODO(go1.21): clear(ex.visitedSchemas)
for k := range ex.visitedSchemas {
delete(ex.visitedSchemas, k)
}
}
// beingVisited asserts a schema is being visited
func (ex *exampleValidator) beingVisited(path string) {
ex.visitedSchemas[path] = true
ex.visitedSchemas[path] = struct{}{}
}
// isVisited tells if a path has already been visited
@ -48,8 +58,9 @@ func (ex *exampleValidator) isVisited(path string) bool {
// - schemas
// - individual property
// - responses
func (ex *exampleValidator) Validate() (errs *Result) {
errs = new(Result)
func (ex *exampleValidator) Validate() *Result {
errs := pools.poolOfResults.BorrowResult()
if ex == nil || ex.SpecValidator == nil {
return errs
}
@ -64,7 +75,7 @@ func (ex *exampleValidator) validateExampleValueValidAgainstSchema() *Result {
// in: schemas, properties, object, items
// not in: headers, parameters without schema
res := new(Result)
res := pools.poolOfResults.BorrowResult()
s := ex.SpecValidator
for method, pathItem := range s.expandedAnalyzer().Operations() {
@ -82,10 +93,12 @@ func (ex *exampleValidator) validateExampleValueValidAgainstSchema() *Result {
// default values provided must validate against their inline definition (no explicit schema)
if param.Example != nil && param.Schema == nil {
// check param default value is valid
red := NewParamValidator(&param, s.KnownFormats).Validate(param.Example) //#nosec
red := newParamValidator(&param, s.KnownFormats, ex.schemaOptions).Validate(param.Example) //#nosec
if red.HasErrorsOrWarnings() {
res.AddWarnings(exampleValueDoesNotValidateMsg(param.Name, param.In))
res.MergeAsWarnings(red)
} else if red.wantsRedeemOnMerge {
pools.poolOfResults.RedeemResult(red)
}
}
@ -95,6 +108,8 @@ func (ex *exampleValidator) validateExampleValueValidAgainstSchema() *Result {
if red.HasErrorsOrWarnings() {
res.AddWarnings(exampleValueItemsDoesNotValidateMsg(param.Name, param.In))
res.Merge(red)
} else if red.wantsRedeemOnMerge {
pools.poolOfResults.RedeemResult(red)
}
}
@ -104,6 +119,8 @@ func (ex *exampleValidator) validateExampleValueValidAgainstSchema() *Result {
if red.HasErrorsOrWarnings() {
res.AddWarnings(exampleValueDoesNotValidateMsg(param.Name, param.In))
res.Merge(red)
} else if red.wantsRedeemOnMerge {
pools.poolOfResults.RedeemResult(red)
}
}
}
@ -129,7 +146,7 @@ func (ex *exampleValidator) validateExampleValueValidAgainstSchema() *Result {
// reset explored schemas to get depth-first recursive-proof exploration
ex.resetVisited()
for nm, sch := range s.spec.Spec().Definitions {
res.Merge(ex.validateExampleValueSchemaAgainstSchema(fmt.Sprintf("definitions.%s", nm), "body", &sch)) //#nosec
res.Merge(ex.validateExampleValueSchemaAgainstSchema("definitions."+nm, "body", &sch)) //#nosec
}
}
return res
@ -151,10 +168,12 @@ func (ex *exampleValidator) validateExampleInResponse(resp *spec.Response, respo
ex.resetVisited()
if h.Example != nil {
red := NewHeaderValidator(nm, &h, s.KnownFormats).Validate(h.Example) //#nosec
red := newHeaderValidator(nm, &h, s.KnownFormats, ex.schemaOptions).Validate(h.Example) //#nosec
if red.HasErrorsOrWarnings() {
res.AddWarnings(exampleValueHeaderDoesNotValidateMsg(operationID, nm, responseName))
res.MergeAsWarnings(red)
} else if red.wantsRedeemOnMerge {
pools.poolOfResults.RedeemResult(red)
}
}
@ -164,6 +183,8 @@ func (ex *exampleValidator) validateExampleInResponse(resp *spec.Response, respo
if red.HasErrorsOrWarnings() {
res.AddWarnings(exampleValueHeaderItemsDoesNotValidateMsg(operationID, nm, responseName))
res.MergeAsWarnings(red)
} else if red.wantsRedeemOnMerge {
pools.poolOfResults.RedeemResult(red)
}
}
@ -183,13 +204,17 @@ func (ex *exampleValidator) validateExampleInResponse(resp *spec.Response, respo
// Additional message to make sure the context of the error is not lost
res.AddWarnings(exampleValueInDoesNotValidateMsg(operationID, responseName))
res.Merge(red)
} else if red.wantsRedeemOnMerge {
pools.poolOfResults.RedeemResult(red)
}
}
if response.Examples != nil {
if response.Schema != nil {
if example, ok := response.Examples["application/json"]; ok {
res.MergeAsWarnings(NewSchemaValidator(response.Schema, s.spec.Spec(), path+".examples", s.KnownFormats, SwaggerSchema(true)).Validate(example))
res.MergeAsWarnings(
newSchemaValidator(response.Schema, s.spec.Spec(), path+".examples", s.KnownFormats, s.schemaOptions).Validate(example),
)
} else {
// TODO: validate other media types too
res.AddWarnings(examplesMimeNotSupportedMsg(operationID, responseName))
@ -208,10 +233,12 @@ func (ex *exampleValidator) validateExampleValueSchemaAgainstSchema(path, in str
}
ex.beingVisited(path)
s := ex.SpecValidator
res := new(Result)
res := pools.poolOfResults.BorrowResult()
if schema.Example != nil {
res.MergeAsWarnings(NewSchemaValidator(schema, s.spec.Spec(), path+".example", s.KnownFormats, SwaggerSchema(true)).Validate(schema.Example))
res.MergeAsWarnings(
newSchemaValidator(schema, s.spec.Spec(), path+".example", s.KnownFormats, ex.schemaOptions).Validate(schema.Example),
)
}
if schema.Items != nil {
if schema.Items.Schema != nil {
@ -229,7 +256,7 @@ func (ex *exampleValidator) validateExampleValueSchemaAgainstSchema(path, in str
}
if schema.AdditionalItems != nil && schema.AdditionalItems.Schema != nil {
// NOTE: we keep validating values, even though additionalItems is unsupported in Swagger 2.0 (and 3.0 as well)
res.Merge(ex.validateExampleValueSchemaAgainstSchema(fmt.Sprintf("%s.additionalItems", path), in, schema.AdditionalItems.Schema))
res.Merge(ex.validateExampleValueSchemaAgainstSchema(path+".additionalItems", in, schema.AdditionalItems.Schema))
}
for propName, prop := range schema.Properties {
res.Merge(ex.validateExampleValueSchemaAgainstSchema(path+"."+propName, in, &prop)) //#nosec
@ -238,7 +265,7 @@ func (ex *exampleValidator) validateExampleValueSchemaAgainstSchema(path, in str
res.Merge(ex.validateExampleValueSchemaAgainstSchema(path+"."+propName, in, &prop)) //#nosec
}
if schema.AdditionalProperties != nil && schema.AdditionalProperties.Schema != nil {
res.Merge(ex.validateExampleValueSchemaAgainstSchema(fmt.Sprintf("%s.additionalProperties", path), in, schema.AdditionalProperties.Schema))
res.Merge(ex.validateExampleValueSchemaAgainstSchema(path+".additionalProperties", in, schema.AdditionalProperties.Schema))
}
if schema.AllOf != nil {
for i, aoSch := range schema.AllOf {
@ -252,11 +279,13 @@ func (ex *exampleValidator) validateExampleValueSchemaAgainstSchema(path, in str
//
func (ex *exampleValidator) validateExampleValueItemsAgainstSchema(path, in string, root interface{}, items *spec.Items) *Result {
res := new(Result)
res := pools.poolOfResults.BorrowResult()
s := ex.SpecValidator
if items != nil {
if items.Example != nil {
res.MergeAsWarnings(newItemsValidator(path, in, items, root, s.KnownFormats).Validate(0, items.Example))
res.MergeAsWarnings(
newItemsValidator(path, in, items, root, s.KnownFormats, ex.schemaOptions).Validate(0, items.Example),
)
}
if items.Items != nil {
res.Merge(ex.validateExampleValueItemsAgainstSchema(path+"[0].example", in, root, items.Items))
@ -265,5 +294,6 @@ func (ex *exampleValidator) validateExampleValueItemsAgainstSchema(path, in stri
res.AddErrors(invalidPatternInMsg(path, in, items.Pattern))
}
}
return res
}

View file

@ -22,10 +22,32 @@ import (
)
type formatValidator struct {
Format string
Path string
In string
Format string
KnownFormats strfmt.Registry
Options *SchemaValidatorOptions
}
func newFormatValidator(path, in, format string, formats strfmt.Registry, opts *SchemaValidatorOptions) *formatValidator {
if opts == nil {
opts = new(SchemaValidatorOptions)
}
var f *formatValidator
if opts.recycleValidators {
f = pools.poolOfFormatValidators.BorrowValidator()
} else {
f = new(formatValidator)
}
f.Path = path
f.In = in
f.Format = format
f.KnownFormats = formats
f.Options = opts
return f
}
func (f *formatValidator) SetPath(path string) {
@ -33,37 +55,45 @@ func (f *formatValidator) SetPath(path string) {
}
func (f *formatValidator) Applies(source interface{}, kind reflect.Kind) bool {
doit := func() bool {
if source == nil {
return false
}
switch source := source.(type) {
case *spec.Items:
return kind == reflect.String && f.KnownFormats.ContainsName(source.Format)
case *spec.Parameter:
return kind == reflect.String && f.KnownFormats.ContainsName(source.Format)
case *spec.Schema:
return kind == reflect.String && f.KnownFormats.ContainsName(source.Format)
case *spec.Header:
return kind == reflect.String && f.KnownFormats.ContainsName(source.Format)
}
if source == nil || f.KnownFormats == nil {
return false
}
switch source := source.(type) {
case *spec.Items:
return kind == reflect.String && f.KnownFormats.ContainsName(source.Format)
case *spec.Parameter:
return kind == reflect.String && f.KnownFormats.ContainsName(source.Format)
case *spec.Schema:
return kind == reflect.String && f.KnownFormats.ContainsName(source.Format)
case *spec.Header:
return kind == reflect.String && f.KnownFormats.ContainsName(source.Format)
default:
return false
}
r := doit()
debugLog("format validator for %q applies %t for %T (kind: %v)\n", f.Path, r, source, kind)
return r
}
func (f *formatValidator) Validate(val interface{}) *Result {
result := new(Result)
debugLog("validating \"%v\" against format: %s", val, f.Format)
if f.Options.recycleValidators {
defer func() {
f.redeem()
}()
}
var result *Result
if f.Options.recycleResult {
result = pools.poolOfResults.BorrowResult()
} else {
result = new(Result)
}
if err := FormatOf(f.Path, f.In, f.Format, val.(string), f.KnownFormats); err != nil {
result.AddErrors(err)
}
if result.HasErrors() {
return result
}
return nil
return result
}
func (f *formatValidator) redeem() {
pools.poolOfFormatValidators.RedeemValidator(f)
}

View file

@ -101,9 +101,17 @@ type errorHelper struct {
// A collection of unexported helpers for error construction
}
func (h *errorHelper) sErr(err errors.Error) *Result {
func (h *errorHelper) sErr(err errors.Error, recycle bool) *Result {
// Builds a Result from standard errors.Error
return &Result{Errors: []error{err}}
var result *Result
if recycle {
result = pools.poolOfResults.BorrowResult()
} else {
result = new(Result)
}
result.Errors = []error{err}
return result
}
func (h *errorHelper) addPointerError(res *Result, err error, ref string, fromPath string) *Result {
@ -225,7 +233,7 @@ func (h *paramHelper) safeExpandedParamsFor(path, method, operationID string, re
operation.Parameters = resolvedParams
for _, ppr := range s.expandedAnalyzer().SafeParamsFor(method, path,
func(p spec.Parameter, err error) bool {
func(_ spec.Parameter, err error) bool {
// since params have already been expanded, there are few causes for error
res.AddErrors(someParametersBrokenMsg(path, method, operationID))
// original error from analyzer
@ -306,6 +314,7 @@ func (r *responseHelper) expandResponseRef(
errorHelp.addPointerError(res, err, response.Ref.String(), path)
return nil, res
}
return response, res
}

View file

@ -15,8 +15,8 @@
package validate
import (
"fmt"
"reflect"
"regexp"
"strings"
"github.com/go-openapi/errors"
@ -35,62 +35,116 @@ type objectValidator struct {
PatternProperties map[string]spec.Schema
Root interface{}
KnownFormats strfmt.Registry
Options SchemaValidatorOptions
Options *SchemaValidatorOptions
splitPath []string
}
func newObjectValidator(path, in string,
maxProperties, minProperties *int64, required []string, properties spec.SchemaProperties,
additionalProperties *spec.SchemaOrBool, patternProperties spec.SchemaProperties,
root interface{}, formats strfmt.Registry, opts *SchemaValidatorOptions) *objectValidator {
if opts == nil {
opts = new(SchemaValidatorOptions)
}
var v *objectValidator
if opts.recycleValidators {
v = pools.poolOfObjectValidators.BorrowValidator()
} else {
v = new(objectValidator)
}
v.Path = path
v.In = in
v.MaxProperties = maxProperties
v.MinProperties = minProperties
v.Required = required
v.Properties = properties
v.AdditionalProperties = additionalProperties
v.PatternProperties = patternProperties
v.Root = root
v.KnownFormats = formats
v.Options = opts
v.splitPath = strings.Split(v.Path, ".")
return v
}
func (o *objectValidator) SetPath(path string) {
o.Path = path
o.splitPath = strings.Split(path, ".")
}
func (o *objectValidator) Applies(source interface{}, kind reflect.Kind) bool {
// TODO: this should also work for structs
// there is a problem in the type validator where it will be unhappy about null values
// so that requires more testing
r := reflect.TypeOf(source) == specSchemaType && (kind == reflect.Map || kind == reflect.Struct)
debugLog("object validator for %q applies %t for %T (kind: %v)\n", o.Path, r, source, kind)
return r
_, isSchema := source.(*spec.Schema)
return isSchema && (kind == reflect.Map || kind == reflect.Struct)
}
func (o *objectValidator) isProperties() bool {
p := strings.Split(o.Path, ".")
p := o.splitPath
return len(p) > 1 && p[len(p)-1] == jsonProperties && p[len(p)-2] != jsonProperties
}
func (o *objectValidator) isDefault() bool {
p := strings.Split(o.Path, ".")
p := o.splitPath
return len(p) > 1 && p[len(p)-1] == jsonDefault && p[len(p)-2] != jsonDefault
}
func (o *objectValidator) isExample() bool {
p := strings.Split(o.Path, ".")
p := o.splitPath
return len(p) > 1 && (p[len(p)-1] == swaggerExample || p[len(p)-1] == swaggerExamples) && p[len(p)-2] != swaggerExample
}
func (o *objectValidator) checkArrayMustHaveItems(res *Result, val map[string]interface{}) {
// for swagger 2.0 schemas, there is an additional constraint to have array items defined explicitly.
// with pure jsonschema draft 4, one may have arrays with undefined items (i.e. any type).
if t, typeFound := val[jsonType]; typeFound {
if tpe, ok := t.(string); ok && tpe == arrayType {
if item, itemsKeyFound := val[jsonItems]; !itemsKeyFound {
res.AddErrors(errors.Required(jsonItems, o.Path, item))
}
}
if val == nil {
return
}
t, typeFound := val[jsonType]
if !typeFound {
return
}
tpe, isString := t.(string)
if !isString || tpe != arrayType {
return
}
item, itemsKeyFound := val[jsonItems]
if itemsKeyFound {
return
}
res.AddErrors(errors.Required(jsonItems, o.Path, item))
}
func (o *objectValidator) checkItemsMustBeTypeArray(res *Result, val map[string]interface{}) {
if !o.isProperties() && !o.isDefault() && !o.isExample() {
if _, itemsKeyFound := val[jsonItems]; itemsKeyFound {
t, typeFound := val[jsonType]
if typeFound {
if tpe, ok := t.(string); !ok || tpe != arrayType {
res.AddErrors(errors.InvalidType(o.Path, o.In, arrayType, nil))
}
} else {
// there is no type
res.AddErrors(errors.Required(jsonType, o.Path, t))
}
}
if val == nil {
return
}
if o.isProperties() || o.isDefault() || o.isExample() {
return
}
_, itemsKeyFound := val[jsonItems]
if !itemsKeyFound {
return
}
t, typeFound := val[jsonType]
if !typeFound {
// there is no type
res.AddErrors(errors.Required(jsonType, o.Path, t))
}
if tpe, isString := t.(string); !isString || tpe != arrayType {
res.AddErrors(errors.InvalidType(o.Path, o.In, arrayType, nil))
}
}
@ -104,176 +158,274 @@ func (o *objectValidator) precheck(res *Result, val map[string]interface{}) {
}
func (o *objectValidator) Validate(data interface{}) *Result {
val := data.(map[string]interface{})
// TODO: guard against nil data
if o.Options.recycleValidators {
defer func() {
o.redeem()
}()
}
var val map[string]interface{}
if data != nil {
var ok bool
val, ok = data.(map[string]interface{})
if !ok {
return errorHelp.sErr(invalidObjectMsg(o.Path, o.In), o.Options.recycleResult)
}
}
numKeys := int64(len(val))
if o.MinProperties != nil && numKeys < *o.MinProperties {
return errorHelp.sErr(errors.TooFewProperties(o.Path, o.In, *o.MinProperties))
return errorHelp.sErr(errors.TooFewProperties(o.Path, o.In, *o.MinProperties), o.Options.recycleResult)
}
if o.MaxProperties != nil && numKeys > *o.MaxProperties {
return errorHelp.sErr(errors.TooManyProperties(o.Path, o.In, *o.MaxProperties))
return errorHelp.sErr(errors.TooManyProperties(o.Path, o.In, *o.MaxProperties), o.Options.recycleResult)
}
res := new(Result)
var res *Result
if o.Options.recycleResult {
res = pools.poolOfResults.BorrowResult()
} else {
res = new(Result)
}
o.precheck(res, val)
// check validity of field names
if o.AdditionalProperties != nil && !o.AdditionalProperties.Allows {
// Case: additionalProperties: false
for k := range val {
_, regularProperty := o.Properties[k]
matched := false
for pk := range o.PatternProperties {
if matches, _ := regexp.MatchString(pk, k); matches {
matched = true
break
}
}
if !regularProperty && k != "$schema" && k != "id" && !matched {
// Special properties "$schema" and "id" are ignored
res.AddErrors(errors.PropertyNotAllowed(o.Path, o.In, k))
// BUG(fredbi): This section should move to a part dedicated to spec validation as
// it will conflict with regular schemas where a property "headers" is defined.
//
// Croaks a more explicit message on top of the standard one
// on some recognized cases.
//
// NOTE: edge cases with invalid type assertion are simply ignored here.
// NOTE: prefix your messages here by "IMPORTANT!" so there are not filtered
// by higher level callers (the IMPORTANT! tag will be eventually
// removed).
if k == "headers" && val[k] != nil {
// $ref is forbidden in header
if headers, mapOk := val[k].(map[string]interface{}); mapOk {
for headerKey, headerBody := range headers {
if headerBody != nil {
if headerSchema, mapOfMapOk := headerBody.(map[string]interface{}); mapOfMapOk {
if _, found := headerSchema["$ref"]; found {
var msg string
if refString, stringOk := headerSchema["$ref"].(string); stringOk {
msg = strings.Join([]string{", one may not use $ref=\":", refString, "\""}, "")
}
res.AddErrors(refNotAllowedInHeaderMsg(o.Path, headerKey, msg))
}
}
}
}
}
/*
case "$ref":
if val[k] != nil {
// TODO: check context of that ref: warn about siblings, check against invalid context
}
*/
}
}
}
o.validateNoAdditionalProperties(val, res)
} else {
// Cases: no additionalProperties (implying: true), or additionalProperties: true, or additionalProperties: { <<schema>> }
for key, value := range val {
_, regularProperty := o.Properties[key]
// Validates property against "patternProperties" if applicable
// BUG(fredbi): succeededOnce is always false
// NOTE: how about regular properties which do not match patternProperties?
matched, succeededOnce, _ := o.validatePatternProperty(key, value, res)
if !(regularProperty || matched || succeededOnce) {
// Cases: properties which are not regular properties and have not been matched by the PatternProperties validator
if o.AdditionalProperties != nil && o.AdditionalProperties.Schema != nil {
// AdditionalProperties as Schema
r := NewSchemaValidator(o.AdditionalProperties.Schema, o.Root, o.Path+"."+key, o.KnownFormats, o.Options.Options()...).Validate(value)
res.mergeForField(data.(map[string]interface{}), key, r)
} else if regularProperty && !(matched || succeededOnce) {
// TODO: this is dead code since regularProperty=false here
res.AddErrors(errors.FailedAllPatternProperties(o.Path, o.In, key))
}
}
}
// Valid cases: additionalProperties: true or undefined
// Cases: empty additionalProperties (implying: true), or additionalProperties: true, or additionalProperties: { <<schema>> }
o.validateAdditionalProperties(val, res)
}
createdFromDefaults := map[string]bool{}
// Property types:
// - regular Property
for pName := range o.Properties {
pSchema := o.Properties[pName] // one instance per iteration
rName := pName
if o.Path != "" {
rName = o.Path + "." + pName
}
// Recursively validates each property against its schema
if v, ok := val[pName]; ok {
r := NewSchemaValidator(&pSchema, o.Root, rName, o.KnownFormats, o.Options.Options()...).Validate(v)
res.mergeForField(data.(map[string]interface{}), pName, r)
} else if pSchema.Default != nil {
// If a default value is defined, creates the property from defaults
// NOTE: JSON schema does not enforce default values to be valid against schema. Swagger does.
createdFromDefaults[pName] = true
res.addPropertySchemata(data.(map[string]interface{}), pName, &pSchema)
}
}
// Check required properties
if len(o.Required) > 0 {
for _, k := range o.Required {
if v, ok := val[k]; !ok && !createdFromDefaults[k] {
res.AddErrors(errors.Required(o.Path+"."+k, o.In, v))
continue
}
}
}
o.validatePropertiesSchema(val, res)
// Check patternProperties
// TODO: it looks like we have done that twice in many cases
for key, value := range val {
_, regularProperty := o.Properties[key]
matched, _ /*succeededOnce*/, patterns := o.validatePatternProperty(key, value, res)
if !regularProperty && (matched /*|| succeededOnce*/) {
for _, pName := range patterns {
if v, ok := o.PatternProperties[pName]; ok {
r := NewSchemaValidator(&v, o.Root, o.Path+"."+key, o.KnownFormats, o.Options.Options()...).Validate(value)
res.mergeForField(data.(map[string]interface{}), key, r)
}
matched, _, patterns := o.validatePatternProperty(key, value, res) // applies to regular properties as well
if regularProperty || !matched {
continue
}
for _, pName := range patterns {
if v, ok := o.PatternProperties[pName]; ok {
r := newSchemaValidator(&v, o.Root, o.Path+"."+key, o.KnownFormats, o.Options).Validate(value)
res.mergeForField(data.(map[string]interface{}), key, r)
}
}
}
return res
}
func (o *objectValidator) validateNoAdditionalProperties(val map[string]interface{}, res *Result) {
for k := range val {
if k == "$schema" || k == "id" {
// special properties "$schema" and "id" are ignored
continue
}
_, regularProperty := o.Properties[k]
if regularProperty {
continue
}
matched := false
for pk := range o.PatternProperties {
re, err := compileRegexp(pk)
if err != nil {
continue
}
if matches := re.MatchString(k); matches {
matched = true
break
}
}
if matched {
continue
}
res.AddErrors(errors.PropertyNotAllowed(o.Path, o.In, k))
// BUG(fredbi): This section should move to a part dedicated to spec validation as
// it will conflict with regular schemas where a property "headers" is defined.
//
// Croaks a more explicit message on top of the standard one
// on some recognized cases.
//
// NOTE: edge cases with invalid type assertion are simply ignored here.
// NOTE: prefix your messages here by "IMPORTANT!" so there are not filtered
// by higher level callers (the IMPORTANT! tag will be eventually
// removed).
if k != "headers" || val[k] == nil {
continue
}
// $ref is forbidden in header
headers, mapOk := val[k].(map[string]interface{})
if !mapOk {
continue
}
for headerKey, headerBody := range headers {
if headerBody == nil {
continue
}
headerSchema, mapOfMapOk := headerBody.(map[string]interface{})
if !mapOfMapOk {
continue
}
_, found := headerSchema["$ref"]
if !found {
continue
}
refString, stringOk := headerSchema["$ref"].(string)
if !stringOk {
continue
}
msg := strings.Join([]string{", one may not use $ref=\":", refString, "\""}, "")
res.AddErrors(refNotAllowedInHeaderMsg(o.Path, headerKey, msg))
/*
case "$ref":
if val[k] != nil {
// TODO: check context of that ref: warn about siblings, check against invalid context
}
*/
}
}
}
func (o *objectValidator) validateAdditionalProperties(val map[string]interface{}, res *Result) {
for key, value := range val {
_, regularProperty := o.Properties[key]
if regularProperty {
continue
}
// Validates property against "patternProperties" if applicable
// BUG(fredbi): succeededOnce is always false
// NOTE: how about regular properties which do not match patternProperties?
matched, succeededOnce, _ := o.validatePatternProperty(key, value, res)
if matched || succeededOnce {
continue
}
if o.AdditionalProperties == nil || o.AdditionalProperties.Schema == nil {
continue
}
// Cases: properties which are not regular properties and have not been matched by the PatternProperties validator
// AdditionalProperties as Schema
r := newSchemaValidator(o.AdditionalProperties.Schema, o.Root, o.Path+"."+key, o.KnownFormats, o.Options).Validate(value)
res.mergeForField(val, key, r)
}
// Valid cases: additionalProperties: true or undefined
}
func (o *objectValidator) validatePropertiesSchema(val map[string]interface{}, res *Result) {
createdFromDefaults := map[string]struct{}{}
// Property types:
// - regular Property
pSchema := pools.poolOfSchemas.BorrowSchema() // recycle a spec.Schema object which lifespan extends only to the validation of properties
defer func() {
pools.poolOfSchemas.RedeemSchema(pSchema)
}()
for pName := range o.Properties {
*pSchema = o.Properties[pName]
var rName string
if o.Path == "" {
rName = pName
} else {
rName = o.Path + "." + pName
}
// Recursively validates each property against its schema
v, ok := val[pName]
if ok {
r := newSchemaValidator(pSchema, o.Root, rName, o.KnownFormats, o.Options).Validate(v)
res.mergeForField(val, pName, r)
continue
}
if pSchema.Default != nil {
// if a default value is defined, creates the property from defaults
// NOTE: JSON schema does not enforce default values to be valid against schema. Swagger does.
createdFromDefaults[pName] = struct{}{}
if !o.Options.skipSchemataResult {
res.addPropertySchemata(val, pName, pSchema) // this shallow-clones the content of the pSchema pointer
}
}
}
if len(o.Required) == 0 {
return
}
// Check required properties
for _, k := range o.Required {
v, ok := val[k]
if ok {
continue
}
_, isCreatedFromDefaults := createdFromDefaults[k]
if isCreatedFromDefaults {
continue
}
res.AddErrors(errors.Required(fmt.Sprintf("%s.%s", o.Path, k), o.In, v))
}
}
// TODO: succeededOnce is not used anywhere
func (o *objectValidator) validatePatternProperty(key string, value interface{}, result *Result) (bool, bool, []string) {
matched := false
succeededOnce := false
var patterns []string
for k, schema := range o.PatternProperties {
sch := schema
if match, _ := regexp.MatchString(k, key); match {
patterns = append(patterns, k)
matched = true
validator := NewSchemaValidator(&sch, o.Root, o.Path+"."+key, o.KnownFormats, o.Options.Options()...)
res := validator.Validate(value)
result.Merge(res)
}
if len(o.PatternProperties) == 0 {
return false, false, nil
}
// BUG(fredbi): can't get to here. Should remove dead code (commented out).
matched := false
succeededOnce := false
patterns := make([]string, 0, len(o.PatternProperties))
// if succeededOnce {
// result.Inc()
// }
schema := pools.poolOfSchemas.BorrowSchema()
defer func() {
pools.poolOfSchemas.RedeemSchema(schema)
}()
for k := range o.PatternProperties {
re, err := compileRegexp(k)
if err != nil {
continue
}
match := re.MatchString(key)
if !match {
continue
}
*schema = o.PatternProperties[k]
patterns = append(patterns, k)
matched = true
validator := newSchemaValidator(schema, o.Root, fmt.Sprintf("%s.%s", o.Path, key), o.KnownFormats, o.Options)
res := validator.Validate(value)
result.Merge(res)
}
return matched, succeededOnce, patterns
}
func (o *objectValidator) redeem() {
pools.poolOfObjectValidators.RedeemValidator(o)
}

View file

@ -31,6 +31,7 @@ type Opts struct {
// GET:/v1/{shelve} and GET:/v1/{book}, where the IDs are "shelve/*" and
// /"shelve/*/book/*" respectively.
StrictPathParamUniqueness bool
SkipSchemataResult bool
}
var (

366
vendor/github.com/go-openapi/validate/pools.go generated vendored Normal file
View file

@ -0,0 +1,366 @@
//go:build !validatedebug
package validate
import (
"sync"
"github.com/go-openapi/spec"
)
var pools allPools
func init() {
resetPools()
}
func resetPools() {
// NOTE: for testing purpose, we might want to reset pools after calling Validate twice.
// The pool is corrupted in that case: calling Put twice inserts a duplicate in the pool
// and further calls to Get are mishandled.
pools = allPools{
poolOfSchemaValidators: schemaValidatorsPool{
Pool: &sync.Pool{
New: func() any {
s := &SchemaValidator{}
return s
},
},
},
poolOfObjectValidators: objectValidatorsPool{
Pool: &sync.Pool{
New: func() any {
s := &objectValidator{}
return s
},
},
},
poolOfSliceValidators: sliceValidatorsPool{
Pool: &sync.Pool{
New: func() any {
s := &schemaSliceValidator{}
return s
},
},
},
poolOfItemsValidators: itemsValidatorsPool{
Pool: &sync.Pool{
New: func() any {
s := &itemsValidator{}
return s
},
},
},
poolOfBasicCommonValidators: basicCommonValidatorsPool{
Pool: &sync.Pool{
New: func() any {
s := &basicCommonValidator{}
return s
},
},
},
poolOfHeaderValidators: headerValidatorsPool{
Pool: &sync.Pool{
New: func() any {
s := &HeaderValidator{}
return s
},
},
},
poolOfParamValidators: paramValidatorsPool{
Pool: &sync.Pool{
New: func() any {
s := &ParamValidator{}
return s
},
},
},
poolOfBasicSliceValidators: basicSliceValidatorsPool{
Pool: &sync.Pool{
New: func() any {
s := &basicSliceValidator{}
return s
},
},
},
poolOfNumberValidators: numberValidatorsPool{
Pool: &sync.Pool{
New: func() any {
s := &numberValidator{}
return s
},
},
},
poolOfStringValidators: stringValidatorsPool{
Pool: &sync.Pool{
New: func() any {
s := &stringValidator{}
return s
},
},
},
poolOfSchemaPropsValidators: schemaPropsValidatorsPool{
Pool: &sync.Pool{
New: func() any {
s := &schemaPropsValidator{}
return s
},
},
},
poolOfFormatValidators: formatValidatorsPool{
Pool: &sync.Pool{
New: func() any {
s := &formatValidator{}
return s
},
},
},
poolOfTypeValidators: typeValidatorsPool{
Pool: &sync.Pool{
New: func() any {
s := &typeValidator{}
return s
},
},
},
poolOfSchemas: schemasPool{
Pool: &sync.Pool{
New: func() any {
s := &spec.Schema{}
return s
},
},
},
poolOfResults: resultsPool{
Pool: &sync.Pool{
New: func() any {
s := &Result{}
return s
},
},
},
}
}
type (
allPools struct {
// memory pools for all validator objects.
//
// Each pool can be borrowed from and redeemed to.
poolOfSchemaValidators schemaValidatorsPool
poolOfObjectValidators objectValidatorsPool
poolOfSliceValidators sliceValidatorsPool
poolOfItemsValidators itemsValidatorsPool
poolOfBasicCommonValidators basicCommonValidatorsPool
poolOfHeaderValidators headerValidatorsPool
poolOfParamValidators paramValidatorsPool
poolOfBasicSliceValidators basicSliceValidatorsPool
poolOfNumberValidators numberValidatorsPool
poolOfStringValidators stringValidatorsPool
poolOfSchemaPropsValidators schemaPropsValidatorsPool
poolOfFormatValidators formatValidatorsPool
poolOfTypeValidators typeValidatorsPool
poolOfSchemas schemasPool
poolOfResults resultsPool
}
schemaValidatorsPool struct {
*sync.Pool
}
objectValidatorsPool struct {
*sync.Pool
}
sliceValidatorsPool struct {
*sync.Pool
}
itemsValidatorsPool struct {
*sync.Pool
}
basicCommonValidatorsPool struct {
*sync.Pool
}
headerValidatorsPool struct {
*sync.Pool
}
paramValidatorsPool struct {
*sync.Pool
}
basicSliceValidatorsPool struct {
*sync.Pool
}
numberValidatorsPool struct {
*sync.Pool
}
stringValidatorsPool struct {
*sync.Pool
}
schemaPropsValidatorsPool struct {
*sync.Pool
}
formatValidatorsPool struct {
*sync.Pool
}
typeValidatorsPool struct {
*sync.Pool
}
schemasPool struct {
*sync.Pool
}
resultsPool struct {
*sync.Pool
}
)
func (p schemaValidatorsPool) BorrowValidator() *SchemaValidator {
return p.Get().(*SchemaValidator)
}
func (p schemaValidatorsPool) RedeemValidator(s *SchemaValidator) {
// NOTE: s might be nil. In that case, Put is a noop.
p.Put(s)
}
func (p objectValidatorsPool) BorrowValidator() *objectValidator {
return p.Get().(*objectValidator)
}
func (p objectValidatorsPool) RedeemValidator(s *objectValidator) {
p.Put(s)
}
func (p sliceValidatorsPool) BorrowValidator() *schemaSliceValidator {
return p.Get().(*schemaSliceValidator)
}
func (p sliceValidatorsPool) RedeemValidator(s *schemaSliceValidator) {
p.Put(s)
}
func (p itemsValidatorsPool) BorrowValidator() *itemsValidator {
return p.Get().(*itemsValidator)
}
func (p itemsValidatorsPool) RedeemValidator(s *itemsValidator) {
p.Put(s)
}
func (p basicCommonValidatorsPool) BorrowValidator() *basicCommonValidator {
return p.Get().(*basicCommonValidator)
}
func (p basicCommonValidatorsPool) RedeemValidator(s *basicCommonValidator) {
p.Put(s)
}
func (p headerValidatorsPool) BorrowValidator() *HeaderValidator {
return p.Get().(*HeaderValidator)
}
func (p headerValidatorsPool) RedeemValidator(s *HeaderValidator) {
p.Put(s)
}
func (p paramValidatorsPool) BorrowValidator() *ParamValidator {
return p.Get().(*ParamValidator)
}
func (p paramValidatorsPool) RedeemValidator(s *ParamValidator) {
p.Put(s)
}
func (p basicSliceValidatorsPool) BorrowValidator() *basicSliceValidator {
return p.Get().(*basicSliceValidator)
}
func (p basicSliceValidatorsPool) RedeemValidator(s *basicSliceValidator) {
p.Put(s)
}
func (p numberValidatorsPool) BorrowValidator() *numberValidator {
return p.Get().(*numberValidator)
}
func (p numberValidatorsPool) RedeemValidator(s *numberValidator) {
p.Put(s)
}
func (p stringValidatorsPool) BorrowValidator() *stringValidator {
return p.Get().(*stringValidator)
}
func (p stringValidatorsPool) RedeemValidator(s *stringValidator) {
p.Put(s)
}
func (p schemaPropsValidatorsPool) BorrowValidator() *schemaPropsValidator {
return p.Get().(*schemaPropsValidator)
}
func (p schemaPropsValidatorsPool) RedeemValidator(s *schemaPropsValidator) {
p.Put(s)
}
func (p formatValidatorsPool) BorrowValidator() *formatValidator {
return p.Get().(*formatValidator)
}
func (p formatValidatorsPool) RedeemValidator(s *formatValidator) {
p.Put(s)
}
func (p typeValidatorsPool) BorrowValidator() *typeValidator {
return p.Get().(*typeValidator)
}
func (p typeValidatorsPool) RedeemValidator(s *typeValidator) {
p.Put(s)
}
func (p schemasPool) BorrowSchema() *spec.Schema {
return p.Get().(*spec.Schema)
}
func (p schemasPool) RedeemSchema(s *spec.Schema) {
p.Put(s)
}
func (p resultsPool) BorrowResult() *Result {
return p.Get().(*Result).cleared()
}
func (p resultsPool) RedeemResult(s *Result) {
if s == emptyResult {
return
}
p.Put(s)
}

1012
vendor/github.com/go-openapi/validate/pools_debug.go generated vendored Normal file

File diff suppressed because it is too large Load diff

View file

@ -15,7 +15,7 @@
package validate
import (
"fmt"
stderrors "errors"
"reflect"
"strings"
@ -23,6 +23,8 @@ import (
"github.com/go-openapi/spec"
)
var emptyResult = &Result{MatchCount: 1}
// Result represents a validation result set, composed of
// errors and warnings.
//
@ -50,8 +52,10 @@ type Result struct {
// Schemata for slice items
itemSchemata []itemSchemata
cachedFieldSchemta map[FieldKey][]*spec.Schema
cachedItemSchemata map[ItemKey][]*spec.Schema
cachedFieldSchemata map[FieldKey][]*spec.Schema
cachedItemSchemata map[ItemKey][]*spec.Schema
wantsRedeemOnMerge bool
}
// FieldKey is a pair of an object and a field, usable as a key for a map.
@ -116,6 +120,9 @@ func (r *Result) Merge(others ...*Result) *Result {
}
r.mergeWithoutRootSchemata(other)
r.rootObjectSchemata.Append(other.rootObjectSchemata)
if other.wantsRedeemOnMerge {
pools.poolOfResults.RedeemResult(other)
}
}
return r
}
@ -133,8 +140,8 @@ func (r *Result) RootObjectSchemata() []*spec.Schema {
// FieldSchemata returns the schemata which apply to fields in objects.
func (r *Result) FieldSchemata() map[FieldKey][]*spec.Schema {
if r.cachedFieldSchemta != nil {
return r.cachedFieldSchemta
if r.cachedFieldSchemata != nil {
return r.cachedFieldSchemata
}
ret := make(map[FieldKey][]*spec.Schema, len(r.fieldSchemata))
@ -146,7 +153,8 @@ func (r *Result) FieldSchemata() map[FieldKey][]*spec.Schema {
ret[key] = append(ret[key], fs.schemata.multiple...)
}
}
r.cachedFieldSchemta = ret
r.cachedFieldSchemata = ret
return ret
}
@ -170,7 +178,7 @@ func (r *Result) ItemSchemata() map[ItemKey][]*spec.Schema {
}
func (r *Result) resetCaches() {
r.cachedFieldSchemta = nil
r.cachedFieldSchemata = nil
r.cachedItemSchemata = nil
}
@ -187,12 +195,16 @@ func (r *Result) mergeForField(obj map[string]interface{}, field string, other *
if r.fieldSchemata == nil {
r.fieldSchemata = make([]fieldSchemata, len(obj))
}
// clone other schemata, as other is about to be redeemed to the pool
r.fieldSchemata = append(r.fieldSchemata, fieldSchemata{
obj: obj,
field: field,
schemata: other.rootObjectSchemata,
schemata: other.rootObjectSchemata.Clone(),
})
}
if other.wantsRedeemOnMerge {
pools.poolOfResults.RedeemResult(other)
}
return r
}
@ -210,29 +222,38 @@ func (r *Result) mergeForSlice(slice reflect.Value, i int, other *Result) *Resul
if r.itemSchemata == nil {
r.itemSchemata = make([]itemSchemata, slice.Len())
}
// clone other schemata, as other is about to be redeemed to the pool
r.itemSchemata = append(r.itemSchemata, itemSchemata{
slice: slice,
index: i,
schemata: other.rootObjectSchemata,
schemata: other.rootObjectSchemata.Clone(),
})
}
if other.wantsRedeemOnMerge {
pools.poolOfResults.RedeemResult(other)
}
return r
}
// addRootObjectSchemata adds the given schemata for the root object of the result.
// The slice schemata might be reused. I.e. do not modify it after being added to a result.
//
// Since the slice schemata might be reused, it is shallow-cloned before saving it into the result.
func (r *Result) addRootObjectSchemata(s *spec.Schema) {
r.rootObjectSchemata.Append(schemata{one: s})
clone := *s
r.rootObjectSchemata.Append(schemata{one: &clone})
}
// addPropertySchemata adds the given schemata for the object and field.
// The slice schemata might be reused. I.e. do not modify it after being added to a result.
//
// Since the slice schemata might be reused, it is shallow-cloned before saving it into the result.
func (r *Result) addPropertySchemata(obj map[string]interface{}, fld string, schema *spec.Schema) {
if r.fieldSchemata == nil {
r.fieldSchemata = make([]fieldSchemata, 0, len(obj))
}
r.fieldSchemata = append(r.fieldSchemata, fieldSchemata{obj: obj, field: fld, schemata: schemata{one: schema}})
clone := *schema
r.fieldSchemata = append(r.fieldSchemata, fieldSchemata{obj: obj, field: fld, schemata: schemata{one: &clone}})
}
/*
@ -255,17 +276,21 @@ func (r *Result) mergeWithoutRootSchemata(other *Result) {
if other.fieldSchemata != nil {
if r.fieldSchemata == nil {
r.fieldSchemata = other.fieldSchemata
} else {
r.fieldSchemata = append(r.fieldSchemata, other.fieldSchemata...)
r.fieldSchemata = make([]fieldSchemata, 0, len(other.fieldSchemata))
}
for _, field := range other.fieldSchemata {
field.schemata = field.schemata.Clone()
r.fieldSchemata = append(r.fieldSchemata, field)
}
}
if other.itemSchemata != nil {
if r.itemSchemata == nil {
r.itemSchemata = other.itemSchemata
} else {
r.itemSchemata = append(r.itemSchemata, other.itemSchemata...)
r.itemSchemata = make([]itemSchemata, 0, len(other.itemSchemata))
}
for _, field := range other.itemSchemata {
field.schemata = field.schemata.Clone()
r.itemSchemata = append(r.itemSchemata, field)
}
}
}
@ -280,6 +305,9 @@ func (r *Result) MergeAsErrors(others ...*Result) *Result {
r.AddErrors(other.Errors...)
r.AddErrors(other.Warnings...)
r.MatchCount += other.MatchCount
if other.wantsRedeemOnMerge {
pools.poolOfResults.RedeemResult(other)
}
}
}
return r
@ -295,6 +323,9 @@ func (r *Result) MergeAsWarnings(others ...*Result) *Result {
r.AddWarnings(other.Errors...)
r.AddWarnings(other.Warnings...)
r.MatchCount += other.MatchCount
if other.wantsRedeemOnMerge {
pools.poolOfResults.RedeemResult(other)
}
}
}
return r
@ -356,16 +387,21 @@ func (r *Result) keepRelevantErrors() *Result {
strippedErrors := []error{}
for _, e := range r.Errors {
if strings.HasPrefix(e.Error(), "IMPORTANT!") {
strippedErrors = append(strippedErrors, fmt.Errorf(strings.TrimPrefix(e.Error(), "IMPORTANT!")))
strippedErrors = append(strippedErrors, stderrors.New(strings.TrimPrefix(e.Error(), "IMPORTANT!")))
}
}
strippedWarnings := []error{}
for _, e := range r.Warnings {
if strings.HasPrefix(e.Error(), "IMPORTANT!") {
strippedWarnings = append(strippedWarnings, fmt.Errorf(strings.TrimPrefix(e.Error(), "IMPORTANT!")))
strippedWarnings = append(strippedWarnings, stderrors.New(strings.TrimPrefix(e.Error(), "IMPORTANT!")))
}
}
strippedResult := new(Result)
var strippedResult *Result
if r.wantsRedeemOnMerge {
strippedResult = pools.poolOfResults.BorrowResult()
} else {
strippedResult = new(Result)
}
strippedResult.Errors = strippedErrors
strippedResult.Warnings = strippedWarnings
return strippedResult
@ -427,6 +463,27 @@ func (r *Result) AsError() error {
return errors.CompositeValidationError(r.Errors...)
}
func (r *Result) cleared() *Result {
// clear the Result to be reusable. Keep allocated capacity.
r.Errors = r.Errors[:0]
r.Warnings = r.Warnings[:0]
r.MatchCount = 0
r.data = nil
r.rootObjectSchemata.one = nil
r.rootObjectSchemata.multiple = r.rootObjectSchemata.multiple[:0]
r.fieldSchemata = r.fieldSchemata[:0]
r.itemSchemata = r.itemSchemata[:0]
for k := range r.cachedFieldSchemata {
delete(r.cachedFieldSchemata, k)
}
for k := range r.cachedItemSchemata {
delete(r.cachedItemSchemata, k)
}
r.wantsRedeemOnMerge = true // mark this result as eligible for redeem when merged into another
return r
}
// schemata is an arbitrary number of schemata. It does a distinction between zero,
// one and many schemata to avoid slice allocations.
type schemata struct {
@ -453,7 +510,7 @@ func (s *schemata) Slice() []*spec.Schema {
return s.multiple
}
// appendSchemata appends the schemata in other to s. It mutated s in-place.
// appendSchemata appends the schemata in other to s. It mutates s in-place.
func (s *schemata) Append(other schemata) {
if other.one == nil && len(other.multiple) == 0 {
return
@ -484,3 +541,23 @@ func (s *schemata) Append(other schemata) {
}
}
}
func (s schemata) Clone() schemata {
var clone schemata
if s.one != nil {
clone.one = new(spec.Schema)
*clone.one = *s.one
}
if len(s.multiple) > 0 {
clone.multiple = make([]*spec.Schema, len(s.multiple))
for idx := 0; idx < len(s.multiple); idx++ {
sp := new(spec.Schema)
*sp = *s.multiple[idx]
clone.multiple[idx] = sp
}
}
return clone
}

View file

@ -24,32 +24,32 @@ import (
"github.com/go-openapi/swag"
)
var (
specSchemaType = reflect.TypeOf(&spec.Schema{})
specParameterType = reflect.TypeOf(&spec.Parameter{})
specHeaderType = reflect.TypeOf(&spec.Header{})
// specItemsType = reflect.TypeOf(&spec.Items{})
)
// SchemaValidator validates data against a JSON schema
type SchemaValidator struct {
Path string
in string
Schema *spec.Schema
validators []valueValidator
validators [8]valueValidator
Root interface{}
KnownFormats strfmt.Registry
Options SchemaValidatorOptions
Options *SchemaValidatorOptions
}
// AgainstSchema validates the specified data against the provided schema, using a registry of supported formats.
//
// When no pre-parsed *spec.Schema structure is provided, it uses a JSON schema as default. See example.
func AgainstSchema(schema *spec.Schema, data interface{}, formats strfmt.Registry, options ...Option) error {
res := NewSchemaValidator(schema, nil, "", formats, options...).Validate(data)
res := NewSchemaValidator(schema, nil, "", formats,
append(options, WithRecycleValidators(true), withRecycleResults(true))...,
).Validate(data)
defer func() {
pools.poolOfResults.RedeemResult(res)
}()
if res.HasErrors() {
return errors.CompositeValidationError(res.Errors...)
}
return nil
}
@ -57,6 +57,15 @@ func AgainstSchema(schema *spec.Schema, data interface{}, formats strfmt.Registr
//
// Panics if the provided schema is invalid.
func NewSchemaValidator(schema *spec.Schema, rootSchema interface{}, root string, formats strfmt.Registry, options ...Option) *SchemaValidator {
opts := new(SchemaValidatorOptions)
for _, o := range options {
o(opts)
}
return newSchemaValidator(schema, rootSchema, root, formats, opts)
}
func newSchemaValidator(schema *spec.Schema, rootSchema interface{}, root string, formats strfmt.Registry, opts *SchemaValidatorOptions) *SchemaValidator {
if schema == nil {
return nil
}
@ -72,17 +81,26 @@ func NewSchemaValidator(schema *spec.Schema, rootSchema interface{}, root string
panic(msg)
}
}
s := SchemaValidator{
Path: root,
in: "body",
Schema: schema,
Root: rootSchema,
KnownFormats: formats,
Options: SchemaValidatorOptions{}}
for _, o := range options {
o(&s.Options)
if opts == nil {
opts = new(SchemaValidatorOptions)
}
s.validators = []valueValidator{
var s *SchemaValidator
if opts.recycleValidators {
s = pools.poolOfSchemaValidators.BorrowValidator()
} else {
s = new(SchemaValidator)
}
s.Path = root
s.in = "body"
s.Schema = schema
s.Root = rootSchema
s.Options = opts
s.KnownFormats = formats
s.validators = [8]valueValidator{
s.typeValidator(),
s.schemaPropsValidator(),
s.stringValidator(),
@ -92,7 +110,8 @@ func NewSchemaValidator(schema *spec.Schema, rootSchema interface{}, root string
s.commonValidator(),
s.objectValidator(),
}
return &s
return s
}
// SetPath sets the path for this schema valdiator
@ -108,17 +127,39 @@ func (s *SchemaValidator) Applies(source interface{}, _ reflect.Kind) bool {
// Validate validates the data against the schema
func (s *SchemaValidator) Validate(data interface{}) *Result {
result := &Result{data: data}
if s == nil {
return result
return emptyResult
}
if s.Schema != nil {
if s.Options.recycleValidators {
defer func() {
s.redeemChildren()
s.redeem() // one-time use validator
}()
}
var result *Result
if s.Options.recycleResult {
result = pools.poolOfResults.BorrowResult()
result.data = data
} else {
result = &Result{data: data}
}
if s.Schema != nil && !s.Options.skipSchemataResult {
result.addRootObjectSchemata(s.Schema)
}
if data == nil {
// early exit with minimal validation
result.Merge(s.validators[0].Validate(data)) // type validator
result.Merge(s.validators[6].Validate(data)) // common validator
if s.Options.recycleValidators {
s.validators[0] = nil
s.validators[6] = nil
}
return result
}
@ -147,6 +188,7 @@ func (s *SchemaValidator) Validate(data interface{}) *Result {
if erri != nil {
result.AddErrors(invalidTypeConversionMsg(s.Path, erri))
result.Inc()
return result
}
d = in
@ -155,6 +197,7 @@ func (s *SchemaValidator) Validate(data interface{}) *Result {
if errf != nil {
result.AddErrors(invalidTypeConversionMsg(s.Path, errf))
result.Inc()
return result
}
d = nf
@ -164,14 +207,26 @@ func (s *SchemaValidator) Validate(data interface{}) *Result {
kind = tpe.Kind()
}
for _, v := range s.validators {
for idx, v := range s.validators {
if !v.Applies(s.Schema, kind) {
debugLog("%T does not apply for %v", v, kind)
if s.Options.recycleValidators {
// Validate won't be called, so relinquish this validator
if redeemableChildren, ok := v.(interface{ redeemChildren() }); ok {
redeemableChildren.redeemChildren()
}
if redeemable, ok := v.(interface{ redeem() }); ok {
redeemable.redeem()
}
s.validators[idx] = nil // prevents further (unsafe) usage
}
continue
}
err := v.Validate(d)
result.Merge(err)
result.Merge(v.Validate(d))
if s.Options.recycleValidators {
s.validators[idx] = nil // prevents further (unsafe) usage
}
result.Inc()
}
result.Inc()
@ -180,81 +235,120 @@ func (s *SchemaValidator) Validate(data interface{}) *Result {
}
func (s *SchemaValidator) typeValidator() valueValidator {
return &typeValidator{Type: s.Schema.Type, Nullable: s.Schema.Nullable, Format: s.Schema.Format, In: s.in, Path: s.Path}
return newTypeValidator(
s.Path,
s.in,
s.Schema.Type,
s.Schema.Nullable,
s.Schema.Format,
s.Options,
)
}
func (s *SchemaValidator) commonValidator() valueValidator {
return &basicCommonValidator{
Path: s.Path,
In: s.in,
Enum: s.Schema.Enum,
}
return newBasicCommonValidator(
s.Path,
s.in,
s.Schema.Default,
s.Schema.Enum,
s.Options,
)
}
func (s *SchemaValidator) sliceValidator() valueValidator {
return &schemaSliceValidator{
Path: s.Path,
In: s.in,
MaxItems: s.Schema.MaxItems,
MinItems: s.Schema.MinItems,
UniqueItems: s.Schema.UniqueItems,
AdditionalItems: s.Schema.AdditionalItems,
Items: s.Schema.Items,
Root: s.Root,
KnownFormats: s.KnownFormats,
Options: s.Options,
}
return newSliceValidator(
s.Path,
s.in,
s.Schema.MaxItems,
s.Schema.MinItems,
s.Schema.UniqueItems,
s.Schema.AdditionalItems,
s.Schema.Items,
s.Root,
s.KnownFormats,
s.Options,
)
}
func (s *SchemaValidator) numberValidator() valueValidator {
return &numberValidator{
Path: s.Path,
In: s.in,
Default: s.Schema.Default,
MultipleOf: s.Schema.MultipleOf,
Maximum: s.Schema.Maximum,
ExclusiveMaximum: s.Schema.ExclusiveMaximum,
Minimum: s.Schema.Minimum,
ExclusiveMinimum: s.Schema.ExclusiveMinimum,
}
return newNumberValidator(
s.Path,
s.in,
s.Schema.Default,
s.Schema.MultipleOf,
s.Schema.Maximum,
s.Schema.ExclusiveMaximum,
s.Schema.Minimum,
s.Schema.ExclusiveMinimum,
"",
"",
s.Options,
)
}
func (s *SchemaValidator) stringValidator() valueValidator {
return &stringValidator{
Path: s.Path,
In: s.in,
MaxLength: s.Schema.MaxLength,
MinLength: s.Schema.MinLength,
Pattern: s.Schema.Pattern,
}
return newStringValidator(
s.Path,
s.in,
nil,
false,
false,
s.Schema.MaxLength,
s.Schema.MinLength,
s.Schema.Pattern,
s.Options,
)
}
func (s *SchemaValidator) formatValidator() valueValidator {
return &formatValidator{
Path: s.Path,
In: s.in,
Format: s.Schema.Format,
KnownFormats: s.KnownFormats,
}
return newFormatValidator(
s.Path,
s.in,
s.Schema.Format,
s.KnownFormats,
s.Options,
)
}
func (s *SchemaValidator) schemaPropsValidator() valueValidator {
sch := s.Schema
return newSchemaPropsValidator(s.Path, s.in, sch.AllOf, sch.OneOf, sch.AnyOf, sch.Not, sch.Dependencies, s.Root, s.KnownFormats, s.Options.Options()...)
return newSchemaPropsValidator(
s.Path, s.in, sch.AllOf, sch.OneOf, sch.AnyOf, sch.Not, sch.Dependencies, s.Root, s.KnownFormats,
s.Options,
)
}
func (s *SchemaValidator) objectValidator() valueValidator {
return &objectValidator{
Path: s.Path,
In: s.in,
MaxProperties: s.Schema.MaxProperties,
MinProperties: s.Schema.MinProperties,
Required: s.Schema.Required,
Properties: s.Schema.Properties,
AdditionalProperties: s.Schema.AdditionalProperties,
PatternProperties: s.Schema.PatternProperties,
Root: s.Root,
KnownFormats: s.KnownFormats,
Options: s.Options,
return newObjectValidator(
s.Path,
s.in,
s.Schema.MaxProperties,
s.Schema.MinProperties,
s.Schema.Required,
s.Schema.Properties,
s.Schema.AdditionalProperties,
s.Schema.PatternProperties,
s.Root,
s.KnownFormats,
s.Options,
)
}
func (s *SchemaValidator) redeem() {
pools.poolOfSchemaValidators.RedeemValidator(s)
}
func (s *SchemaValidator) redeemChildren() {
for i, validator := range s.validators {
if validator == nil {
continue
}
if redeemableChildren, ok := validator.(interface{ redeemChildren() }); ok {
redeemableChildren.redeemChildren()
}
if redeemable, ok := validator.(interface{ redeem() }); ok {
redeemable.redeem()
}
s.validators[i] = nil // free up allocated children if not in pool
}
}

View file

@ -18,6 +18,9 @@ package validate
type SchemaValidatorOptions struct {
EnableObjectArrayTypeCheck bool
EnableArrayMustHaveItemsCheck bool
recycleValidators bool
recycleResult bool
skipSchemataResult bool
}
// Option sets optional rules for schema validation
@ -45,10 +48,36 @@ func SwaggerSchema(enable bool) Option {
}
}
// Options returns current options
// WithRecycleValidators saves memory allocations and makes validators
// available for a single use of Validate() only.
//
// When a validator is recycled, called MUST not call the Validate() method twice.
func WithRecycleValidators(enable bool) Option {
return func(svo *SchemaValidatorOptions) {
svo.recycleValidators = enable
}
}
func withRecycleResults(enable bool) Option {
return func(svo *SchemaValidatorOptions) {
svo.recycleResult = enable
}
}
// WithSkipSchemataResult skips the deep audit payload stored in validation Result
func WithSkipSchemataResult(enable bool) Option {
return func(svo *SchemaValidatorOptions) {
svo.skipSchemataResult = enable
}
}
// Options returns the current set of options
func (svo SchemaValidatorOptions) Options() []Option {
return []Option{
EnableObjectArrayTypeCheck(svo.EnableObjectArrayTypeCheck),
EnableArrayMustHaveItemsCheck(svo.EnableArrayMustHaveItemsCheck),
WithRecycleValidators(svo.recycleValidators),
withRecycleResults(svo.recycleResult),
WithSkipSchemataResult(svo.skipSchemataResult),
}
}

View file

@ -30,211 +30,327 @@ type schemaPropsValidator struct {
AnyOf []spec.Schema
Not *spec.Schema
Dependencies spec.Dependencies
anyOfValidators []SchemaValidator
allOfValidators []SchemaValidator
oneOfValidators []SchemaValidator
anyOfValidators []*SchemaValidator
allOfValidators []*SchemaValidator
oneOfValidators []*SchemaValidator
notValidator *SchemaValidator
Root interface{}
KnownFormats strfmt.Registry
Options SchemaValidatorOptions
Options *SchemaValidatorOptions
}
func (s *schemaPropsValidator) SetPath(path string) {
s.Path = path
}
func newSchemaPropsValidator(path string, in string, allOf, oneOf, anyOf []spec.Schema, not *spec.Schema, deps spec.Dependencies, root interface{}, formats strfmt.Registry, options ...Option) *schemaPropsValidator {
anyValidators := make([]SchemaValidator, 0, len(anyOf))
for _, v := range anyOf {
v := v
anyValidators = append(anyValidators, *NewSchemaValidator(&v, root, path, formats, options...))
func newSchemaPropsValidator(
path string, in string, allOf, oneOf, anyOf []spec.Schema, not *spec.Schema, deps spec.Dependencies, root interface{}, formats strfmt.Registry,
opts *SchemaValidatorOptions) *schemaPropsValidator {
if opts == nil {
opts = new(SchemaValidatorOptions)
}
allValidators := make([]SchemaValidator, 0, len(allOf))
for _, v := range allOf {
v := v
allValidators = append(allValidators, *NewSchemaValidator(&v, root, path, formats, options...))
anyValidators := make([]*SchemaValidator, 0, len(anyOf))
for i := range anyOf {
anyValidators = append(anyValidators, newSchemaValidator(&anyOf[i], root, path, formats, opts))
}
oneValidators := make([]SchemaValidator, 0, len(oneOf))
for _, v := range oneOf {
v := v
oneValidators = append(oneValidators, *NewSchemaValidator(&v, root, path, formats, options...))
allValidators := make([]*SchemaValidator, 0, len(allOf))
for i := range allOf {
allValidators = append(allValidators, newSchemaValidator(&allOf[i], root, path, formats, opts))
}
oneValidators := make([]*SchemaValidator, 0, len(oneOf))
for i := range oneOf {
oneValidators = append(oneValidators, newSchemaValidator(&oneOf[i], root, path, formats, opts))
}
var notValidator *SchemaValidator
if not != nil {
notValidator = NewSchemaValidator(not, root, path, formats, options...)
notValidator = newSchemaValidator(not, root, path, formats, opts)
}
schOptions := &SchemaValidatorOptions{}
for _, o := range options {
o(schOptions)
}
return &schemaPropsValidator{
Path: path,
In: in,
AllOf: allOf,
OneOf: oneOf,
AnyOf: anyOf,
Not: not,
Dependencies: deps,
anyOfValidators: anyValidators,
allOfValidators: allValidators,
oneOfValidators: oneValidators,
notValidator: notValidator,
Root: root,
KnownFormats: formats,
Options: *schOptions,
var s *schemaPropsValidator
if opts.recycleValidators {
s = pools.poolOfSchemaPropsValidators.BorrowValidator()
} else {
s = new(schemaPropsValidator)
}
s.Path = path
s.In = in
s.AllOf = allOf
s.OneOf = oneOf
s.AnyOf = anyOf
s.Not = not
s.Dependencies = deps
s.anyOfValidators = anyValidators
s.allOfValidators = allValidators
s.oneOfValidators = oneValidators
s.notValidator = notValidator
s.Root = root
s.KnownFormats = formats
s.Options = opts
return s
}
func (s *schemaPropsValidator) Applies(source interface{}, kind reflect.Kind) bool {
r := reflect.TypeOf(source) == specSchemaType
debugLog("schema props validator for %q applies %t for %T (kind: %v)\n", s.Path, r, source, kind)
return r
func (s *schemaPropsValidator) Applies(source interface{}, _ reflect.Kind) bool {
_, isSchema := source.(*spec.Schema)
return isSchema
}
func (s *schemaPropsValidator) Validate(data interface{}) *Result {
mainResult := new(Result)
var mainResult *Result
if s.Options.recycleResult {
mainResult = pools.poolOfResults.BorrowResult()
} else {
mainResult = new(Result)
}
// Intermediary error results
// IMPORTANT! messages from underlying validators
keepResultAnyOf := new(Result)
keepResultOneOf := new(Result)
keepResultAllOf := new(Result)
var keepResultAnyOf, keepResultOneOf, keepResultAllOf *Result
if s.Options.recycleValidators {
defer func() {
s.redeemChildren()
s.redeem()
// results are redeemed when merged
}()
}
// Validates at least one in anyOf schemas
var firstSuccess *Result
if len(s.anyOfValidators) > 0 {
var bestFailures *Result
succeededOnce := false
for _, anyOfSchema := range s.anyOfValidators {
result := anyOfSchema.Validate(data)
// We keep inner IMPORTANT! errors no matter what MatchCount tells us
keepResultAnyOf.Merge(result.keepRelevantErrors())
if result.IsValid() {
bestFailures = nil
succeededOnce = true
if firstSuccess == nil {
firstSuccess = result
}
keepResultAnyOf = new(Result)
break
}
// MatchCount is used to select errors from the schema with most positive checks
if bestFailures == nil || result.MatchCount > bestFailures.MatchCount {
bestFailures = result
}
}
if !succeededOnce {
mainResult.AddErrors(mustValidateAtLeastOneSchemaMsg(s.Path))
}
if bestFailures != nil {
mainResult.Merge(bestFailures)
} else if firstSuccess != nil {
mainResult.Merge(firstSuccess)
}
keepResultAnyOf = pools.poolOfResults.BorrowResult()
s.validateAnyOf(data, mainResult, keepResultAnyOf)
}
// Validates exactly one in oneOf schemas
if len(s.oneOfValidators) > 0 {
var bestFailures *Result
var firstSuccess *Result
validated := 0
for _, oneOfSchema := range s.oneOfValidators {
result := oneOfSchema.Validate(data)
// We keep inner IMPORTANT! errors no matter what MatchCount tells us
keepResultOneOf.Merge(result.keepRelevantErrors())
if result.IsValid() {
validated++
bestFailures = nil
if firstSuccess == nil {
firstSuccess = result
}
keepResultOneOf = new(Result)
continue
}
// MatchCount is used to select errors from the schema with most positive checks
if validated == 0 && (bestFailures == nil || result.MatchCount > bestFailures.MatchCount) {
bestFailures = result
}
}
if validated != 1 {
var additionalMsg string
if validated == 0 {
additionalMsg = "Found none valid"
} else {
additionalMsg = fmt.Sprintf("Found %d valid alternatives", validated)
}
mainResult.AddErrors(mustValidateOnlyOneSchemaMsg(s.Path, additionalMsg))
if bestFailures != nil {
mainResult.Merge(bestFailures)
}
} else if firstSuccess != nil {
mainResult.Merge(firstSuccess)
}
keepResultOneOf = pools.poolOfResults.BorrowResult()
s.validateOneOf(data, mainResult, keepResultOneOf)
}
// Validates all of allOf schemas
if len(s.allOfValidators) > 0 {
validated := 0
for _, allOfSchema := range s.allOfValidators {
result := allOfSchema.Validate(data)
// We keep inner IMPORTANT! errors no matter what MatchCount tells us
keepResultAllOf.Merge(result.keepRelevantErrors())
// keepResultAllOf.Merge(result)
if result.IsValid() {
validated++
}
mainResult.Merge(result)
}
if validated != len(s.allOfValidators) {
additionalMsg := ""
if validated == 0 {
additionalMsg = ". None validated"
}
mainResult.AddErrors(mustValidateAllSchemasMsg(s.Path, additionalMsg))
}
keepResultAllOf = pools.poolOfResults.BorrowResult()
s.validateAllOf(data, mainResult, keepResultAllOf)
}
if s.notValidator != nil {
result := s.notValidator.Validate(data)
// We keep inner IMPORTANT! errors no matter what MatchCount tells us
if result.IsValid() {
mainResult.AddErrors(mustNotValidatechemaMsg(s.Path))
}
s.validateNot(data, mainResult)
}
if s.Dependencies != nil && len(s.Dependencies) > 0 && reflect.TypeOf(data).Kind() == reflect.Map {
val := data.(map[string]interface{})
for key := range val {
if dep, ok := s.Dependencies[key]; ok {
if dep.Schema != nil {
mainResult.Merge(NewSchemaValidator(dep.Schema, s.Root, s.Path+"."+key, s.KnownFormats, s.Options.Options()...).Validate(data))
continue
}
if len(dep.Property) > 0 {
for _, depKey := range dep.Property {
if _, ok := val[depKey]; !ok {
mainResult.AddErrors(hasADependencyMsg(s.Path, depKey))
}
}
}
}
}
s.validateDependencies(data, mainResult)
}
mainResult.Inc()
// In the end we retain best failures for schema validation
// plus, if any, composite errors which may explain special cases (tagged as IMPORTANT!).
return mainResult.Merge(keepResultAllOf, keepResultOneOf, keepResultAnyOf)
}
func (s *schemaPropsValidator) validateAnyOf(data interface{}, mainResult, keepResultAnyOf *Result) {
// Validates at least one in anyOf schemas
var bestFailures *Result
for i, anyOfSchema := range s.anyOfValidators {
result := anyOfSchema.Validate(data)
if s.Options.recycleValidators {
s.anyOfValidators[i] = nil
}
// We keep inner IMPORTANT! errors no matter what MatchCount tells us
keepResultAnyOf.Merge(result.keepRelevantErrors()) // merges (and redeems) a new instance of Result
if result.IsValid() {
if bestFailures != nil && bestFailures.wantsRedeemOnMerge {
pools.poolOfResults.RedeemResult(bestFailures)
}
_ = keepResultAnyOf.cleared()
mainResult.Merge(result)
return
}
// MatchCount is used to select errors from the schema with most positive checks
if bestFailures == nil || result.MatchCount > bestFailures.MatchCount {
if bestFailures != nil && bestFailures.wantsRedeemOnMerge {
pools.poolOfResults.RedeemResult(bestFailures)
}
bestFailures = result
continue
}
if result.wantsRedeemOnMerge {
pools.poolOfResults.RedeemResult(result) // this result is ditched
}
}
mainResult.AddErrors(mustValidateAtLeastOneSchemaMsg(s.Path))
mainResult.Merge(bestFailures)
}
func (s *schemaPropsValidator) validateOneOf(data interface{}, mainResult, keepResultOneOf *Result) {
// Validates exactly one in oneOf schemas
var (
firstSuccess, bestFailures *Result
validated int
)
for i, oneOfSchema := range s.oneOfValidators {
result := oneOfSchema.Validate(data)
if s.Options.recycleValidators {
s.oneOfValidators[i] = nil
}
// We keep inner IMPORTANT! errors no matter what MatchCount tells us
keepResultOneOf.Merge(result.keepRelevantErrors()) // merges (and redeems) a new instance of Result
if result.IsValid() {
validated++
_ = keepResultOneOf.cleared()
if firstSuccess == nil {
firstSuccess = result
} else if result.wantsRedeemOnMerge {
pools.poolOfResults.RedeemResult(result) // this result is ditched
}
continue
}
// MatchCount is used to select errors from the schema with most positive checks
if validated == 0 && (bestFailures == nil || result.MatchCount > bestFailures.MatchCount) {
if bestFailures != nil && bestFailures.wantsRedeemOnMerge {
pools.poolOfResults.RedeemResult(bestFailures)
}
bestFailures = result
} else if result.wantsRedeemOnMerge {
pools.poolOfResults.RedeemResult(result) // this result is ditched
}
}
switch validated {
case 0:
mainResult.AddErrors(mustValidateOnlyOneSchemaMsg(s.Path, "Found none valid"))
mainResult.Merge(bestFailures)
// firstSucess necessarily nil
case 1:
mainResult.Merge(firstSuccess)
if bestFailures != nil && bestFailures.wantsRedeemOnMerge {
pools.poolOfResults.RedeemResult(bestFailures)
}
default:
mainResult.AddErrors(mustValidateOnlyOneSchemaMsg(s.Path, fmt.Sprintf("Found %d valid alternatives", validated)))
mainResult.Merge(bestFailures)
if firstSuccess != nil && firstSuccess.wantsRedeemOnMerge {
pools.poolOfResults.RedeemResult(firstSuccess)
}
}
}
func (s *schemaPropsValidator) validateAllOf(data interface{}, mainResult, keepResultAllOf *Result) {
// Validates all of allOf schemas
var validated int
for i, allOfSchema := range s.allOfValidators {
result := allOfSchema.Validate(data)
if s.Options.recycleValidators {
s.allOfValidators[i] = nil
}
// We keep inner IMPORTANT! errors no matter what MatchCount tells us
keepResultAllOf.Merge(result.keepRelevantErrors())
if result.IsValid() {
validated++
}
mainResult.Merge(result)
}
switch validated {
case 0:
mainResult.AddErrors(mustValidateAllSchemasMsg(s.Path, ". None validated"))
case len(s.allOfValidators):
default:
mainResult.AddErrors(mustValidateAllSchemasMsg(s.Path, ""))
}
}
func (s *schemaPropsValidator) validateNot(data interface{}, mainResult *Result) {
result := s.notValidator.Validate(data)
if s.Options.recycleValidators {
s.notValidator = nil
}
// We keep inner IMPORTANT! errors no matter what MatchCount tells us
if result.IsValid() {
mainResult.AddErrors(mustNotValidatechemaMsg(s.Path))
}
if result.wantsRedeemOnMerge {
pools.poolOfResults.RedeemResult(result) // this result is ditched
}
}
func (s *schemaPropsValidator) validateDependencies(data interface{}, mainResult *Result) {
val := data.(map[string]interface{})
for key := range val {
dep, ok := s.Dependencies[key]
if !ok {
continue
}
if dep.Schema != nil {
mainResult.Merge(
newSchemaValidator(dep.Schema, s.Root, s.Path+"."+key, s.KnownFormats, s.Options).Validate(data),
)
continue
}
if len(dep.Property) > 0 {
for _, depKey := range dep.Property {
if _, ok := val[depKey]; !ok {
mainResult.AddErrors(hasADependencyMsg(s.Path, depKey))
}
}
}
}
}
func (s *schemaPropsValidator) redeem() {
pools.poolOfSchemaPropsValidators.RedeemValidator(s)
}
func (s *schemaPropsValidator) redeemChildren() {
for _, v := range s.anyOfValidators {
if v == nil {
continue
}
v.redeemChildren()
v.redeem()
}
s.anyOfValidators = nil
for _, v := range s.allOfValidators {
if v == nil {
continue
}
v.redeemChildren()
v.redeem()
}
s.allOfValidators = nil
for _, v := range s.oneOfValidators {
if v == nil {
continue
}
v.redeemChildren()
v.redeem()
}
s.oneOfValidators = nil
if s.notValidator != nil {
s.notValidator.redeemChildren()
s.notValidator.redeem()
s.notValidator = nil
}
}

View file

@ -32,7 +32,36 @@ type schemaSliceValidator struct {
Items *spec.SchemaOrArray
Root interface{}
KnownFormats strfmt.Registry
Options SchemaValidatorOptions
Options *SchemaValidatorOptions
}
func newSliceValidator(path, in string,
maxItems, minItems *int64, uniqueItems bool,
additionalItems *spec.SchemaOrBool, items *spec.SchemaOrArray,
root interface{}, formats strfmt.Registry, opts *SchemaValidatorOptions) *schemaSliceValidator {
if opts == nil {
opts = new(SchemaValidatorOptions)
}
var v *schemaSliceValidator
if opts.recycleValidators {
v = pools.poolOfSliceValidators.BorrowValidator()
} else {
v = new(schemaSliceValidator)
}
v.Path = path
v.In = in
v.MaxItems = maxItems
v.MinItems = minItems
v.UniqueItems = uniqueItems
v.AdditionalItems = additionalItems
v.Items = items
v.Root = root
v.KnownFormats = formats
v.Options = opts
return v
}
func (s *schemaSliceValidator) SetPath(path string) {
@ -46,7 +75,18 @@ func (s *schemaSliceValidator) Applies(source interface{}, kind reflect.Kind) bo
}
func (s *schemaSliceValidator) Validate(data interface{}) *Result {
result := new(Result)
if s.Options.recycleValidators {
defer func() {
s.redeem()
}()
}
var result *Result
if s.Options.recycleResult {
result = pools.poolOfResults.BorrowResult()
} else {
result = new(Result)
}
if data == nil {
return result
}
@ -54,8 +94,8 @@ func (s *schemaSliceValidator) Validate(data interface{}) *Result {
size := val.Len()
if s.Items != nil && s.Items.Schema != nil {
validator := NewSchemaValidator(s.Items.Schema, s.Root, s.Path, s.KnownFormats, s.Options.Options()...)
for i := 0; i < size; i++ {
validator := newSchemaValidator(s.Items.Schema, s.Root, s.Path, s.KnownFormats, s.Options)
validator.SetPath(fmt.Sprintf("%s.%d", s.Path, i))
value := val.Index(i)
result.mergeForSlice(val, i, validator.Validate(value.Interface()))
@ -66,10 +106,11 @@ func (s *schemaSliceValidator) Validate(data interface{}) *Result {
if s.Items != nil && len(s.Items.Schemas) > 0 {
itemsSize = len(s.Items.Schemas)
for i := 0; i < itemsSize; i++ {
validator := NewSchemaValidator(&s.Items.Schemas[i], s.Root, fmt.Sprintf("%s.%d", s.Path, i), s.KnownFormats, s.Options.Options()...)
if val.Len() <= i {
if size <= i {
break
}
validator := newSchemaValidator(&s.Items.Schemas[i], s.Root, fmt.Sprintf("%s.%d", s.Path, i), s.KnownFormats, s.Options)
result.mergeForSlice(val, i, validator.Validate(val.Index(i).Interface()))
}
}
@ -79,7 +120,7 @@ func (s *schemaSliceValidator) Validate(data interface{}) *Result {
}
if s.AdditionalItems.Schema != nil {
for i := itemsSize; i < size-itemsSize+1; i++ {
validator := NewSchemaValidator(s.AdditionalItems.Schema, s.Root, fmt.Sprintf("%s.%d", s.Path, i), s.KnownFormats, s.Options.Options()...)
validator := newSchemaValidator(s.AdditionalItems.Schema, s.Root, fmt.Sprintf("%s.%d", s.Path, i), s.KnownFormats, s.Options)
result.mergeForSlice(val, i, validator.Validate(val.Index(i).Interface()))
}
}
@ -103,3 +144,7 @@ func (s *schemaSliceValidator) Validate(data interface{}) *Result {
result.Inc()
return result
}
func (s *schemaSliceValidator) redeem() {
pools.poolOfSliceValidators.RedeemValidator(s)
}

View file

@ -15,6 +15,8 @@
package validate
import (
"bytes"
"encoding/gob"
"encoding/json"
"fmt"
"sort"
@ -26,6 +28,7 @@ import (
"github.com/go-openapi/loads"
"github.com/go-openapi/spec"
"github.com/go-openapi/strfmt"
"github.com/go-openapi/swag"
)
// Spec validates an OpenAPI 2.0 specification document.
@ -52,25 +55,38 @@ func Spec(doc *loads.Document, formats strfmt.Registry) error {
// SpecValidator validates a swagger 2.0 spec
type SpecValidator struct {
schema *spec.Schema // swagger 2.0 schema
spec *loads.Document
analyzer *analysis.Spec
expanded *loads.Document
KnownFormats strfmt.Registry
Options Opts // validation options
schema *spec.Schema // swagger 2.0 schema
spec *loads.Document
analyzer *analysis.Spec
expanded *loads.Document
KnownFormats strfmt.Registry
Options Opts // validation options
schemaOptions *SchemaValidatorOptions
}
// NewSpecValidator creates a new swagger spec validator instance
func NewSpecValidator(schema *spec.Schema, formats strfmt.Registry) *SpecValidator {
// schema options that apply to all called validators
schemaOptions := new(SchemaValidatorOptions)
for _, o := range []Option{
SwaggerSchema(true),
WithRecycleValidators(true),
// withRecycleResults(true),
} {
o(schemaOptions)
}
return &SpecValidator{
schema: schema,
KnownFormats: formats,
Options: defaultOpts,
schema: schema,
KnownFormats: formats,
Options: defaultOpts,
schemaOptions: schemaOptions,
}
}
// Validate validates the swagger spec
func (s *SpecValidator) Validate(data interface{}) (*Result, *Result) {
s.schemaOptions.skipSchemataResult = s.Options.SkipSchemataResult
var sd *loads.Document
errs, warnings := new(Result), new(Result)
@ -84,11 +100,8 @@ func (s *SpecValidator) Validate(data interface{}) (*Result, *Result) {
s.spec = sd
s.analyzer = analysis.New(sd.Spec())
// Swagger schema validator
schv := NewSchemaValidator(s.schema, nil, "", s.KnownFormats, SwaggerSchema(true))
var obj interface{}
// Raw spec unmarshalling errors
var obj interface{}
if err := json.Unmarshal(sd.Raw(), &obj); err != nil {
// NOTE: under normal conditions, the *load.Document has been already unmarshalled
// So this one is just a paranoid check on the behavior of the spec package
@ -102,6 +115,8 @@ func (s *SpecValidator) Validate(data interface{}) (*Result, *Result) {
warnings.AddErrors(errs.Warnings...)
}()
// Swagger schema validator
schv := newSchemaValidator(s.schema, nil, "", s.KnownFormats, s.schemaOptions)
errs.Merge(schv.Validate(obj)) // error -
// There may be a point in continuing to try and determine more accurate errors
if !s.Options.ContinueOnErrors && errs.HasErrors() {
@ -129,13 +144,13 @@ func (s *SpecValidator) Validate(data interface{}) (*Result, *Result) {
}
// Values provided as default MUST validate their schema
df := &defaultValidator{SpecValidator: s}
df := &defaultValidator{SpecValidator: s, schemaOptions: s.schemaOptions}
errs.Merge(df.Validate())
// Values provided as examples MUST validate their schema
// Value provided as examples in a response without schema generate a warning
// Known limitations: examples in responses for mime type not application/json are ignored (warning)
ex := &exampleValidator{SpecValidator: s}
ex := &exampleValidator{SpecValidator: s, schemaOptions: s.schemaOptions}
errs.Merge(ex.Validate())
errs.Merge(s.validateNonEmptyPathParamNames())
@ -147,22 +162,27 @@ func (s *SpecValidator) Validate(data interface{}) (*Result, *Result) {
}
func (s *SpecValidator) validateNonEmptyPathParamNames() *Result {
res := new(Result)
res := pools.poolOfResults.BorrowResult()
if s.spec.Spec().Paths == nil {
// There is no Paths object: error
res.AddErrors(noValidPathMsg())
} else {
if s.spec.Spec().Paths.Paths == nil {
// Paths may be empty: warning
res.AddWarnings(noValidPathMsg())
} else {
for k := range s.spec.Spec().Paths.Paths {
if strings.Contains(k, "{}") {
res.AddErrors(emptyPathParameterMsg(k))
}
}
return res
}
if s.spec.Spec().Paths.Paths == nil {
// Paths may be empty: warning
res.AddWarnings(noValidPathMsg())
return res
}
for k := range s.spec.Spec().Paths.Paths {
if strings.Contains(k, "{}") {
res.AddErrors(emptyPathParameterMsg(k))
}
}
return res
}
@ -176,7 +196,7 @@ func (s *SpecValidator) validateDuplicateOperationIDs() *Result {
// fallback on possible incomplete picture because of previous errors
analyzer = s.analyzer
}
res := new(Result)
res := pools.poolOfResults.BorrowResult()
known := make(map[string]int)
for _, v := range analyzer.OperationIDs() {
if v != "" {
@ -198,7 +218,7 @@ type dupProp struct {
func (s *SpecValidator) validateDuplicatePropertyNames() *Result {
// definition can't declare a property that's already defined by one of its ancestors
res := new(Result)
res := pools.poolOfResults.BorrowResult()
for k, sch := range s.spec.Spec().Definitions {
if len(sch.AllOf) == 0 {
continue
@ -247,7 +267,7 @@ func (s *SpecValidator) validateSchemaPropertyNames(nm string, sch spec.Schema,
schn := nm
schc := &sch
res := new(Result)
res := pools.poolOfResults.BorrowResult()
for schc.Ref.String() != "" {
// gather property names
@ -284,7 +304,7 @@ func (s *SpecValidator) validateSchemaPropertyNames(nm string, sch spec.Schema,
}
func (s *SpecValidator) validateCircularAncestry(nm string, sch spec.Schema, knowns map[string]struct{}) ([]string, *Result) {
res := new(Result)
res := pools.poolOfResults.BorrowResult()
if sch.Ref.String() == "" && len(sch.AllOf) == 0 { // Safeguard. We should not be able to actually get there
return nil, res
@ -334,7 +354,7 @@ func (s *SpecValidator) validateCircularAncestry(nm string, sch spec.Schema, kno
func (s *SpecValidator) validateItems() *Result {
// validate parameter, items, schema and response objects for presence of item if type is array
res := new(Result)
res := pools.poolOfResults.BorrowResult()
for method, pi := range s.analyzer.Operations() {
for path, op := range pi {
@ -393,7 +413,7 @@ func (s *SpecValidator) validateItems() *Result {
// Verifies constraints on array type
func (s *SpecValidator) validateSchemaItems(schema spec.Schema, prefix, opID string) *Result {
res := new(Result)
res := pools.poolOfResults.BorrowResult()
if !schema.Type.Contains(arrayType) {
return res
}
@ -417,7 +437,7 @@ func (s *SpecValidator) validateSchemaItems(schema spec.Schema, prefix, opID str
func (s *SpecValidator) validatePathParamPresence(path string, fromPath, fromOperation []string) *Result {
// Each defined operation path parameters must correspond to a named element in the API's path pattern.
// (For example, you cannot have a path parameter named id for the following path /pets/{petId} but you must have a path parameter named petId.)
res := new(Result)
res := pools.poolOfResults.BorrowResult()
for _, l := range fromPath {
var matched bool
for _, r := range fromOperation {
@ -473,7 +493,7 @@ func (s *SpecValidator) validateReferencedParameters() *Result {
if len(expected) == 0 {
return nil
}
result := new(Result)
result := pools.poolOfResults.BorrowResult()
for k := range expected {
result.AddWarnings(unusedParamMsg(k))
}
@ -498,7 +518,7 @@ func (s *SpecValidator) validateReferencedResponses() *Result {
if len(expected) == 0 {
return nil
}
result := new(Result)
result := pools.poolOfResults.BorrowResult()
for k := range expected {
result.AddWarnings(unusedResponseMsg(k))
}
@ -533,7 +553,7 @@ func (s *SpecValidator) validateReferencedDefinitions() *Result {
func (s *SpecValidator) validateRequiredDefinitions() *Result {
// Each property listed in the required array must be defined in the properties of the model
res := new(Result)
res := pools.poolOfResults.BorrowResult()
DEFINITIONS:
for d, schema := range s.spec.Spec().Definitions {
@ -552,7 +572,7 @@ DEFINITIONS:
func (s *SpecValidator) validateRequiredProperties(path, in string, v *spec.Schema) *Result {
// Takes care of recursive property definitions, which may be nested in additionalProperties schemas
res := new(Result)
res := pools.poolOfResults.BorrowResult()
propertyMatch := false
patternMatch := false
additionalPropertiesMatch := false
@ -618,7 +638,7 @@ func (s *SpecValidator) validateParameters() *Result {
// - parameters with pattern property must specify valid patterns
// - $ref in parameters must resolve
// - path param must be required
res := new(Result)
res := pools.poolOfResults.BorrowResult()
rexGarbledPathSegment := mustCompileRegexp(`.*[{}\s]+.*`)
for method, pi := range s.expandedAnalyzer().Operations() {
methodPaths := make(map[string]map[string]string)
@ -657,7 +677,23 @@ func (s *SpecValidator) validateParameters() *Result {
// TODO: should be done after param expansion
res.Merge(s.checkUniqueParams(path, method, op))
// pick the root schema from the swagger specification which describes a parameter
origSchema, ok := s.schema.Definitions["parameter"]
if !ok {
panic("unexpected swagger schema: missing #/definitions/parameter")
}
// clone it once to avoid expanding a global schema (e.g. swagger spec)
paramSchema, err := deepCloneSchema(origSchema)
if err != nil {
panic(fmt.Errorf("can't clone schema: %v", err))
}
for _, pr := range paramHelp.safeExpandedParamsFor(path, method, op.ID, res, s) {
// An expanded parameter must validate the Parameter schema (an unexpanded $ref always passes high-level schema validation)
schv := newSchemaValidator(&paramSchema, s.schema, fmt.Sprintf("%s.%s.parameters.%s", path, method, pr.Name), s.KnownFormats, s.schemaOptions)
obj := swag.ToDynamicJSON(pr)
res.Merge(schv.Validate(obj))
// Validate pattern regexp for parameters with a Pattern property
if _, err := compileRegexp(pr.Pattern); err != nil {
res.AddErrors(invalidPatternInParamMsg(op.ID, pr.Name, pr.Pattern))
@ -739,7 +775,7 @@ func (s *SpecValidator) validateParameters() *Result {
func (s *SpecValidator) validateReferencesValid() *Result {
// each reference must point to a valid object
res := new(Result)
res := pools.poolOfResults.BorrowResult()
for _, r := range s.analyzer.AllRefs() {
if !r.IsValidURI(s.spec.SpecFilePath()) { // Safeguard - spec should always yield a valid URI
res.AddErrors(invalidRefMsg(r.String()))
@ -765,7 +801,7 @@ func (s *SpecValidator) checkUniqueParams(path, method string, op *spec.Operatio
// However, there are some issues with such a factorization:
// - analysis does not seem to fully expand params
// - param keys may be altered by x-go-name
res := new(Result)
res := pools.poolOfResults.BorrowResult()
pnames := make(map[string]struct{})
if op.Parameters != nil { // Safeguard
@ -800,3 +836,17 @@ func (s *SpecValidator) expandedAnalyzer() *analysis.Spec {
}
return s.analyzer
}
func deepCloneSchema(src spec.Schema) (spec.Schema, error) {
var b bytes.Buffer
if err := gob.NewEncoder(&b).Encode(src); err != nil {
return spec.Schema{}, err
}
var dst spec.Schema
if err := gob.NewDecoder(&b).Decode(&dst); err != nil {
return spec.Schema{}, err
}
return dst, nil
}

View file

@ -187,6 +187,8 @@ const (
// UnusedResponseWarning ...
UnusedResponseWarning = "response %q is not used anywhere"
InvalidObject = "expected an object in %q.%s"
)
// Additional error codes
@ -347,6 +349,9 @@ func invalidParameterDefinitionAsSchemaMsg(path, method, operationID string) err
func parameterValidationTypeMismatchMsg(param, path, typ string) errors.Error {
return errors.New(errors.CompositeErrorCode, ParamValidationTypeMismatch, param, path, typ)
}
func invalidObjectMsg(path, in string) errors.Error {
return errors.New(errors.CompositeErrorCode, InvalidObject, path, in)
}
// disabled
//

View file

@ -25,11 +25,34 @@ import (
)
type typeValidator struct {
Path string
In string
Type spec.StringOrArray
Nullable bool
Format string
In string
Path string
Options *SchemaValidatorOptions
}
func newTypeValidator(path, in string, typ spec.StringOrArray, nullable bool, format string, opts *SchemaValidatorOptions) *typeValidator {
if opts == nil {
opts = new(SchemaValidatorOptions)
}
var t *typeValidator
if opts.recycleValidators {
t = pools.poolOfTypeValidators.BorrowValidator()
} else {
t = new(typeValidator)
}
t.Path = path
t.In = in
t.Type = typ
t.Nullable = nullable
t.Format = format
t.Options = opts
return t
}
func (t *typeValidator) schemaInfoForType(data interface{}) (string, string) {
@ -125,23 +148,33 @@ func (t *typeValidator) SetPath(path string) {
t.Path = path
}
func (t *typeValidator) Applies(source interface{}, kind reflect.Kind) bool {
func (t *typeValidator) Applies(source interface{}, _ reflect.Kind) bool {
// typeValidator applies to Schema, Parameter and Header objects
stpe := reflect.TypeOf(source)
r := (len(t.Type) > 0 || t.Format != "") && (stpe == specSchemaType || stpe == specParameterType || stpe == specHeaderType)
debugLog("type validator for %q applies %t for %T (kind: %v)\n", t.Path, r, source, kind)
return r
switch source.(type) {
case *spec.Schema:
case *spec.Parameter:
case *spec.Header:
default:
return false
}
return (len(t.Type) > 0 || t.Format != "")
}
func (t *typeValidator) Validate(data interface{}) *Result {
result := new(Result)
result.Inc()
if t.Options.recycleValidators {
defer func() {
t.redeem()
}()
}
if data == nil {
// nil or zero value for the passed structure require Type: null
if len(t.Type) > 0 && !t.Type.Contains(nullType) && !t.Nullable { // TODO: if a property is not required it also passes this
return errorHelp.sErr(errors.InvalidType(t.Path, t.In, strings.Join(t.Type, ","), nullType))
return errorHelp.sErr(errors.InvalidType(t.Path, t.In, strings.Join(t.Type, ","), nullType), t.Options.recycleResult)
}
return result
return emptyResult
}
// check if the type matches, should be used in every validator chain as first item
@ -151,8 +184,6 @@ func (t *typeValidator) Validate(data interface{}) *Result {
// infer schema type (JSON) and format from passed data type
schType, format := t.schemaInfoForType(data)
debugLog("path: %s, schType: %s, format: %s, expType: %s, expFmt: %s, kind: %s", t.Path, schType, format, t.Type, t.Format, val.Kind().String())
// check numerical types
// TODO: check unsigned ints
// TODO: check json.Number (see schema.go)
@ -163,15 +194,20 @@ func (t *typeValidator) Validate(data interface{}) *Result {
if kind != reflect.String && kind != reflect.Slice && t.Format != "" && !(t.Type.Contains(schType) || format == t.Format || isFloatInt || isIntFloat || isLowerInt || isLowerFloat) {
// TODO: test case
return errorHelp.sErr(errors.InvalidType(t.Path, t.In, t.Format, format))
return errorHelp.sErr(errors.InvalidType(t.Path, t.In, t.Format, format), t.Options.recycleResult)
}
if !(t.Type.Contains(numberType) || t.Type.Contains(integerType)) && t.Format != "" && (kind == reflect.String || kind == reflect.Slice) {
return result
return emptyResult
}
if !(t.Type.Contains(schType) || isFloatInt || isIntFloat) {
return errorHelp.sErr(errors.InvalidType(t.Path, t.In, strings.Join(t.Type, ","), schType))
return errorHelp.sErr(errors.InvalidType(t.Path, t.In, strings.Join(t.Type, ","), schType), t.Options.recycleResult)
}
return result
return emptyResult
}
func (t *typeValidator) redeem() {
pools.poolOfTypeValidators.RedeemValidator(t)
}

File diff suppressed because it is too large Load diff