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

@ -13,7 +13,7 @@ import (
"sync"
"time"
"github.com/google/go-github/v57/github"
"github.com/google/go-github/v60/github"
)
const (

View file

@ -70,3 +70,5 @@ benchstat <(go test -benchtime 500ms -count 15 -bench 'Sum64$')
- [VictoriaMetrics](https://github.com/VictoriaMetrics/VictoriaMetrics)
- [FreeCache](https://github.com/coocood/freecache)
- [FastCache](https://github.com/VictoriaMetrics/fastcache)
- [Ristretto](https://github.com/dgraph-io/ristretto)
- [Badger](https://github.com/dgraph-io/badger)

View file

@ -19,10 +19,13 @@ const (
// Store the primes in an array as well.
//
// The consts are used when possible in Go code to avoid MOVs but we need a
// contiguous array of the assembly code.
// contiguous array for the assembly code.
var primes = [...]uint64{prime1, prime2, prime3, prime4, prime5}
// Digest implements hash.Hash64.
//
// Note that a zero-valued Digest is not ready to receive writes.
// Call Reset or create a Digest using New before calling other methods.
type Digest struct {
v1 uint64
v2 uint64
@ -33,19 +36,31 @@ type Digest struct {
n int // how much of mem is used
}
// New creates a new Digest that computes the 64-bit xxHash algorithm.
// New creates a new Digest with a zero seed.
func New() *Digest {
return NewWithSeed(0)
}
// NewWithSeed creates a new Digest with the given seed.
func NewWithSeed(seed uint64) *Digest {
var d Digest
d.Reset()
d.ResetWithSeed(seed)
return &d
}
// Reset clears the Digest's state so that it can be reused.
// It uses a seed value of zero.
func (d *Digest) Reset() {
d.v1 = primes[0] + prime2
d.v2 = prime2
d.v3 = 0
d.v4 = -primes[0]
d.ResetWithSeed(0)
}
// ResetWithSeed clears the Digest's state so that it can be reused.
// It uses the given seed to initialize the state.
func (d *Digest) ResetWithSeed(seed uint64) {
d.v1 = seed + prime1 + prime2
d.v2 = seed + prime2
d.v3 = seed
d.v4 = seed - prime1
d.total = 0
d.n = 0
}

View file

@ -6,7 +6,7 @@
package xxhash
// Sum64 computes the 64-bit xxHash digest of b.
// Sum64 computes the 64-bit xxHash digest of b with a zero seed.
//
//go:noescape
func Sum64(b []byte) uint64

View file

@ -3,7 +3,7 @@
package xxhash
// Sum64 computes the 64-bit xxHash digest of b.
// Sum64 computes the 64-bit xxHash digest of b with a zero seed.
func Sum64(b []byte) uint64 {
// A simpler version would be
// d := New()

View file

@ -5,7 +5,7 @@
package xxhash
// Sum64String computes the 64-bit xxHash digest of s.
// Sum64String computes the 64-bit xxHash digest of s with a zero seed.
func Sum64String(s string) uint64 {
return Sum64([]byte(s))
}

View file

@ -33,7 +33,7 @@ import (
//
// See https://github.com/golang/go/issues/42739 for discussion.
// Sum64String computes the 64-bit xxHash digest of s.
// Sum64String computes the 64-bit xxHash digest of s with a zero seed.
// It may be faster than Sum64([]byte(s)) by avoiding a copy.
func Sum64String(s string) uint64 {
b := *(*[]byte)(unsafe.Pointer(&sliceHeader{s, len(s)}))

View file

@ -91,11 +91,12 @@ logr design but also left out some parts and changed others:
| Adding a name to a logger | `WithName` | no API |
| Modify verbosity of log entries in a call chain | `V` | no API |
| Grouping of key/value pairs | not supported | `WithGroup`, `GroupValue` |
| Pass context for extracting additional values | no API | API variants like `InfoCtx` |
The high-level slog API is explicitly meant to be one of many different APIs
that can be layered on top of a shared `slog.Handler`. logr is one such
alternative API, with [interoperability](#slog-interoperability) provided by the [`slogr`](slogr)
package.
alternative API, with [interoperability](#slog-interoperability) provided by
some conversion functions.
### Inspiration
@ -145,24 +146,24 @@ There are implementations for the following logging libraries:
## slog interoperability
Interoperability goes both ways, using the `logr.Logger` API with a `slog.Handler`
and using the `slog.Logger` API with a `logr.LogSink`. [slogr](./slogr) provides `NewLogr` and
`NewSlogHandler` API calls to convert between a `logr.Logger` and a `slog.Handler`.
and using the `slog.Logger` API with a `logr.LogSink`. `FromSlogHandler` and
`ToSlogHandler` convert between a `logr.Logger` and a `slog.Handler`.
As usual, `slog.New` can be used to wrap such a `slog.Handler` in the high-level
slog API. `slogr` itself leaves that to the caller.
slog API.
## Using a `logr.Sink` as backend for slog
### Using a `logr.LogSink` as backend for slog
Ideally, a logr sink implementation should support both logr and slog by
implementing both the normal logr interface(s) and `slogr.SlogSink`. Because
implementing both the normal logr interface(s) and `SlogSink`. Because
of a conflict in the parameters of the common `Enabled` method, it is [not
possible to implement both slog.Handler and logr.Sink in the same
type](https://github.com/golang/go/issues/59110).
If both are supported, log calls can go from the high-level APIs to the backend
without the need to convert parameters. `NewLogr` and `NewSlogHandler` can
without the need to convert parameters. `FromSlogHandler` and `ToSlogHandler` can
convert back and forth without adding additional wrappers, with one exception:
when `Logger.V` was used to adjust the verbosity for a `slog.Handler`, then
`NewSlogHandler` has to use a wrapper which adjusts the verbosity for future
`ToSlogHandler` has to use a wrapper which adjusts the verbosity for future
log calls.
Such an implementation should also support values that implement specific
@ -187,13 +188,13 @@ Not supporting slog has several drawbacks:
These drawbacks are severe enough that applications using a mixture of slog and
logr should switch to a different backend.
## Using a `slog.Handler` as backend for logr
### Using a `slog.Handler` as backend for logr
Using a plain `slog.Handler` without support for logr works better than the
other direction:
- All logr verbosity levels can be mapped 1:1 to their corresponding slog level
by negating them.
- Stack unwinding is done by the `slogr.SlogSink` and the resulting program
- Stack unwinding is done by the `SlogSink` and the resulting program
counter is passed to the `slog.Handler`.
- Names added via `Logger.WithName` are gathered and recorded in an additional
attribute with `logger` as key and the names separated by slash as value.
@ -205,27 +206,39 @@ ideally support both `logr.Marshaler` and `slog.Valuer`. If compatibility
with logr implementations without slog support is not important, then
`slog.Valuer` is sufficient.
## Context support for slog
### Context support for slog
Storing a logger in a `context.Context` is not supported by
slog. `logr.NewContext` and `logr.FromContext` can be used with slog like this
to fill this gap:
slog. `NewContextWithSlogLogger` and `FromContextAsSlogLogger` can be
used to fill this gap. They store and retrieve a `slog.Logger` pointer
under the same context key that is also used by `NewContext` and
`FromContext` for `logr.Logger` value.
func HandlerFromContext(ctx context.Context) slog.Handler {
logger, err := logr.FromContext(ctx)
if err == nil {
return slogr.NewSlogHandler(logger)
}
return slog.Default().Handler()
}
When `NewContextWithSlogLogger` is followed by `FromContext`, the latter will
automatically convert the `slog.Logger` to a
`logr.Logger`. `FromContextAsSlogLogger` does the same for the other direction.
func ContextWithHandler(ctx context.Context, handler slog.Handler) context.Context {
return logr.NewContext(ctx, slogr.NewLogr(handler))
}
With this approach, binaries which use either slog or logr are as efficient as
possible with no unnecessary allocations. This is also why the API stores a
`slog.Logger` pointer: when storing a `slog.Handler`, creating a `slog.Logger`
on retrieval would need to allocate one.
The downside is that storing and retrieving a `slog.Handler` needs more
allocations compared to using a `logr.Logger`. Therefore the recommendation is
to use the `logr.Logger` API in code which uses contextual logging.
The downside is that switching back and forth needs more allocations. Because
logr is the API that is already in use by different packages, in particular
Kubernetes, the recommendation is to use the `logr.Logger` API in code which
uses contextual logging.
An alternative to adding values to a logger and storing that logger in the
context is to store the values in the context and to configure a logging
backend to extract those values when emitting log entries. This only works when
log calls are passed the context, which is not supported by the logr API.
With the slog API, it is possible, but not
required. https://github.com/veqryn/slog-context is a package for slog which
provides additional support code for this approach. It also contains wrappers
for the context functions in logr, so developers who prefer to not use the logr
APIs directly can use those instead and the resulting code will still be
interoperable with logr.
## FAQ

33
vendor/github.com/go-logr/logr/context.go generated vendored Normal file
View file

@ -0,0 +1,33 @@
/*
Copyright 2023 The logr Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package logr
// contextKey is how we find Loggers in a context.Context. With Go < 1.21,
// the value is always a Logger value. With Go >= 1.21, the value can be a
// Logger value or a slog.Logger pointer.
type contextKey struct{}
// notFoundError exists to carry an IsNotFound method.
type notFoundError struct{}
func (notFoundError) Error() string {
return "no logr.Logger was present"
}
func (notFoundError) IsNotFound() bool {
return true
}

49
vendor/github.com/go-logr/logr/context_noslog.go generated vendored Normal file
View file

@ -0,0 +1,49 @@
//go:build !go1.21
// +build !go1.21
/*
Copyright 2019 The logr Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package logr
import (
"context"
)
// FromContext returns a Logger from ctx or an error if no Logger is found.
func FromContext(ctx context.Context) (Logger, error) {
if v, ok := ctx.Value(contextKey{}).(Logger); ok {
return v, nil
}
return Logger{}, notFoundError{}
}
// FromContextOrDiscard returns a Logger from ctx. If no Logger is found, this
// returns a Logger that discards all log messages.
func FromContextOrDiscard(ctx context.Context) Logger {
if v, ok := ctx.Value(contextKey{}).(Logger); ok {
return v
}
return Discard()
}
// NewContext returns a new Context, derived from ctx, which carries the
// provided Logger.
func NewContext(ctx context.Context, logger Logger) context.Context {
return context.WithValue(ctx, contextKey{}, logger)
}

83
vendor/github.com/go-logr/logr/context_slog.go generated vendored Normal file
View file

@ -0,0 +1,83 @@
//go:build go1.21
// +build go1.21
/*
Copyright 2019 The logr Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package logr
import (
"context"
"fmt"
"log/slog"
)
// FromContext returns a Logger from ctx or an error if no Logger is found.
func FromContext(ctx context.Context) (Logger, error) {
v := ctx.Value(contextKey{})
if v == nil {
return Logger{}, notFoundError{}
}
switch v := v.(type) {
case Logger:
return v, nil
case *slog.Logger:
return FromSlogHandler(v.Handler()), nil
default:
// Not reached.
panic(fmt.Sprintf("unexpected value type for logr context key: %T", v))
}
}
// FromContextAsSlogLogger returns a slog.Logger from ctx or nil if no such Logger is found.
func FromContextAsSlogLogger(ctx context.Context) *slog.Logger {
v := ctx.Value(contextKey{})
if v == nil {
return nil
}
switch v := v.(type) {
case Logger:
return slog.New(ToSlogHandler(v))
case *slog.Logger:
return v
default:
// Not reached.
panic(fmt.Sprintf("unexpected value type for logr context key: %T", v))
}
}
// FromContextOrDiscard returns a Logger from ctx. If no Logger is found, this
// returns a Logger that discards all log messages.
func FromContextOrDiscard(ctx context.Context) Logger {
if logger, err := FromContext(ctx); err == nil {
return logger
}
return Discard()
}
// NewContext returns a new Context, derived from ctx, which carries the
// provided Logger.
func NewContext(ctx context.Context, logger Logger) context.Context {
return context.WithValue(ctx, contextKey{}, logger)
}
// NewContextWithSlogLogger returns a new Context, derived from ctx, which carries the
// provided slog.Logger.
func NewContextWithSlogLogger(ctx context.Context, logger *slog.Logger) context.Context {
return context.WithValue(ctx, contextKey{}, logger)
}

View file

@ -100,6 +100,11 @@ type Options struct {
// details, see docs for Go's time.Layout.
TimestampFormat string
// LogInfoLevel tells funcr what key to use to log the info level.
// If not specified, the info level will be logged as "level".
// If this is set to "", the info level will not be logged at all.
LogInfoLevel *string
// Verbosity tells funcr which V logs to produce. Higher values enable
// more logs. Info logs at or below this level will be written, while logs
// above this level will be discarded.
@ -213,6 +218,10 @@ func newFormatter(opts Options, outfmt outputFormat) Formatter {
if opts.MaxLogDepth == 0 {
opts.MaxLogDepth = defaultMaxLogDepth
}
if opts.LogInfoLevel == nil {
opts.LogInfoLevel = new(string)
*opts.LogInfoLevel = "level"
}
f := Formatter{
outputFormat: outfmt,
prefix: "",
@ -227,12 +236,15 @@ func newFormatter(opts Options, outfmt outputFormat) Formatter {
// implementation. It should be constructed with NewFormatter. Some of
// its methods directly implement logr.LogSink.
type Formatter struct {
outputFormat outputFormat
prefix string
values []any
valuesStr string
depth int
opts *Options
outputFormat outputFormat
prefix string
values []any
valuesStr string
parentValuesStr string
depth int
opts *Options
group string // for slog groups
groupDepth int
}
// outputFormat indicates which outputFormat to use.
@ -253,33 +265,62 @@ func (f Formatter) render(builtins, args []any) string {
// Empirically bytes.Buffer is faster than strings.Builder for this.
buf := bytes.NewBuffer(make([]byte, 0, 1024))
if f.outputFormat == outputJSON {
buf.WriteByte('{')
buf.WriteByte('{') // for the whole line
}
vals := builtins
if hook := f.opts.RenderBuiltinsHook; hook != nil {
vals = hook(f.sanitize(vals))
}
f.flatten(buf, vals, false, false) // keys are ours, no need to escape
continuing := len(builtins) > 0
if len(f.valuesStr) > 0 {
if f.parentValuesStr != "" {
if continuing {
if f.outputFormat == outputJSON {
buf.WriteByte(',')
} else {
buf.WriteByte(' ')
}
buf.WriteByte(f.comma())
}
buf.WriteString(f.parentValuesStr)
continuing = true
buf.WriteString(f.valuesStr)
}
groupDepth := f.groupDepth
if f.group != "" {
if f.valuesStr != "" || len(args) != 0 {
if continuing {
buf.WriteByte(f.comma())
}
buf.WriteString(f.quoted(f.group, true)) // escape user-provided keys
buf.WriteByte(f.colon())
buf.WriteByte('{') // for the group
continuing = false
} else {
// The group was empty
groupDepth--
}
}
if f.valuesStr != "" {
if continuing {
buf.WriteByte(f.comma())
}
buf.WriteString(f.valuesStr)
continuing = true
}
vals = args
if hook := f.opts.RenderArgsHook; hook != nil {
vals = hook(f.sanitize(vals))
}
f.flatten(buf, vals, continuing, true) // escape user-provided keys
if f.outputFormat == outputJSON {
buf.WriteByte('}')
for i := 0; i < groupDepth; i++ {
buf.WriteByte('}') // for the groups
}
if f.outputFormat == outputJSON {
buf.WriteByte('}') // for the whole line
}
return buf.String()
}
@ -298,9 +339,16 @@ func (f Formatter) flatten(buf *bytes.Buffer, kvList []any, continuing bool, esc
if len(kvList)%2 != 0 {
kvList = append(kvList, noValue)
}
copied := false
for i := 0; i < len(kvList); i += 2 {
k, ok := kvList[i].(string)
if !ok {
if !copied {
newList := make([]any, len(kvList))
copy(newList, kvList)
kvList = newList
copied = true
}
k = f.nonStringKey(kvList[i])
kvList[i] = k
}
@ -308,7 +356,7 @@ func (f Formatter) flatten(buf *bytes.Buffer, kvList []any, continuing bool, esc
if i > 0 || continuing {
if f.outputFormat == outputJSON {
buf.WriteByte(',')
buf.WriteByte(f.comma())
} else {
// In theory the format could be something we don't understand. In
// practice, we control it, so it won't be.
@ -316,24 +364,35 @@ func (f Formatter) flatten(buf *bytes.Buffer, kvList []any, continuing bool, esc
}
}
if escapeKeys {
buf.WriteString(prettyString(k))
} else {
// this is faster
buf.WriteByte('"')
buf.WriteString(k)
buf.WriteByte('"')
}
if f.outputFormat == outputJSON {
buf.WriteByte(':')
} else {
buf.WriteByte('=')
}
buf.WriteString(f.quoted(k, escapeKeys))
buf.WriteByte(f.colon())
buf.WriteString(f.pretty(v))
}
return kvList
}
func (f Formatter) quoted(str string, escape bool) string {
if escape {
return prettyString(str)
}
// this is faster
return `"` + str + `"`
}
func (f Formatter) comma() byte {
if f.outputFormat == outputJSON {
return ','
}
return ' '
}
func (f Formatter) colon() byte {
if f.outputFormat == outputJSON {
return ':'
}
return '='
}
func (f Formatter) pretty(value any) string {
return f.prettyWithFlags(value, 0, 0)
}
@ -407,12 +466,12 @@ func (f Formatter) prettyWithFlags(value any, flags uint32, depth int) string {
}
for i := 0; i < len(v); i += 2 {
if i > 0 {
buf.WriteByte(',')
buf.WriteByte(f.comma())
}
k, _ := v[i].(string) // sanitize() above means no need to check success
// arbitrary keys might need escaping
buf.WriteString(prettyString(k))
buf.WriteByte(':')
buf.WriteByte(f.colon())
buf.WriteString(f.prettyWithFlags(v[i+1], 0, depth+1))
}
if flags&flagRawStruct == 0 {
@ -481,7 +540,7 @@ func (f Formatter) prettyWithFlags(value any, flags uint32, depth int) string {
continue
}
if printComma {
buf.WriteByte(',')
buf.WriteByte(f.comma())
}
printComma = true // if we got here, we are rendering a field
if fld.Anonymous && fld.Type.Kind() == reflect.Struct && name == "" {
@ -492,10 +551,8 @@ func (f Formatter) prettyWithFlags(value any, flags uint32, depth int) string {
name = fld.Name
}
// field names can't contain characters which need escaping
buf.WriteByte('"')
buf.WriteString(name)
buf.WriteByte('"')
buf.WriteByte(':')
buf.WriteString(f.quoted(name, false))
buf.WriteByte(f.colon())
buf.WriteString(f.prettyWithFlags(v.Field(i).Interface(), 0, depth+1))
}
if flags&flagRawStruct == 0 {
@ -520,7 +577,7 @@ func (f Formatter) prettyWithFlags(value any, flags uint32, depth int) string {
buf.WriteByte('[')
for i := 0; i < v.Len(); i++ {
if i > 0 {
buf.WriteByte(',')
buf.WriteByte(f.comma())
}
e := v.Index(i)
buf.WriteString(f.prettyWithFlags(e.Interface(), 0, depth+1))
@ -534,7 +591,7 @@ func (f Formatter) prettyWithFlags(value any, flags uint32, depth int) string {
i := 0
for it.Next() {
if i > 0 {
buf.WriteByte(',')
buf.WriteByte(f.comma())
}
// If a map key supports TextMarshaler, use it.
keystr := ""
@ -556,7 +613,7 @@ func (f Formatter) prettyWithFlags(value any, flags uint32, depth int) string {
}
}
buf.WriteString(keystr)
buf.WriteByte(':')
buf.WriteByte(f.colon())
buf.WriteString(f.prettyWithFlags(it.Value().Interface(), 0, depth+1))
i++
}
@ -706,6 +763,53 @@ func (f Formatter) sanitize(kvList []any) []any {
return kvList
}
// startGroup opens a new group scope (basically a sub-struct), which locks all
// the current saved values and starts them anew. This is needed to satisfy
// slog.
func (f *Formatter) startGroup(group string) {
// Unnamed groups are just inlined.
if group == "" {
return
}
// Any saved values can no longer be changed.
buf := bytes.NewBuffer(make([]byte, 0, 1024))
continuing := false
if f.parentValuesStr != "" {
buf.WriteString(f.parentValuesStr)
continuing = true
}
if f.group != "" && f.valuesStr != "" {
if continuing {
buf.WriteByte(f.comma())
}
buf.WriteString(f.quoted(f.group, true)) // escape user-provided keys
buf.WriteByte(f.colon())
buf.WriteByte('{') // for the group
continuing = false
}
if f.valuesStr != "" {
if continuing {
buf.WriteByte(f.comma())
}
buf.WriteString(f.valuesStr)
}
// NOTE: We don't close the scope here - that's done later, when a log line
// is actually rendered (because we have N scopes to close).
f.parentValuesStr = buf.String()
// Start collecting new values.
f.group = group
f.groupDepth++
f.valuesStr = ""
f.values = nil
}
// Init configures this Formatter from runtime info, such as the call depth
// imposed by logr itself.
// Note that this receiver is a pointer, so depth can be saved.
@ -740,7 +844,10 @@ func (f Formatter) FormatInfo(level int, msg string, kvList []any) (prefix, args
if policy := f.opts.LogCaller; policy == All || policy == Info {
args = append(args, "caller", f.caller())
}
args = append(args, "level", level, "msg", msg)
if key := *f.opts.LogInfoLevel; key != "" {
args = append(args, key, level)
}
args = append(args, "msg", msg)
return prefix, f.render(args, kvList)
}

105
vendor/github.com/go-logr/logr/funcr/slogsink.go generated vendored Normal file
View file

@ -0,0 +1,105 @@
//go:build go1.21
// +build go1.21
/*
Copyright 2023 The logr Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package funcr
import (
"context"
"log/slog"
"github.com/go-logr/logr"
)
var _ logr.SlogSink = &fnlogger{}
const extraSlogSinkDepth = 3 // 2 for slog, 1 for SlogSink
func (l fnlogger) Handle(_ context.Context, record slog.Record) error {
kvList := make([]any, 0, 2*record.NumAttrs())
record.Attrs(func(attr slog.Attr) bool {
kvList = attrToKVs(attr, kvList)
return true
})
if record.Level >= slog.LevelError {
l.WithCallDepth(extraSlogSinkDepth).Error(nil, record.Message, kvList...)
} else {
level := l.levelFromSlog(record.Level)
l.WithCallDepth(extraSlogSinkDepth).Info(level, record.Message, kvList...)
}
return nil
}
func (l fnlogger) WithAttrs(attrs []slog.Attr) logr.SlogSink {
kvList := make([]any, 0, 2*len(attrs))
for _, attr := range attrs {
kvList = attrToKVs(attr, kvList)
}
l.AddValues(kvList)
return &l
}
func (l fnlogger) WithGroup(name string) logr.SlogSink {
l.startGroup(name)
return &l
}
// attrToKVs appends a slog.Attr to a logr-style kvList. It handle slog Groups
// and other details of slog.
func attrToKVs(attr slog.Attr, kvList []any) []any {
attrVal := attr.Value.Resolve()
if attrVal.Kind() == slog.KindGroup {
groupVal := attrVal.Group()
grpKVs := make([]any, 0, 2*len(groupVal))
for _, attr := range groupVal {
grpKVs = attrToKVs(attr, grpKVs)
}
if attr.Key == "" {
// slog says we have to inline these
kvList = append(kvList, grpKVs...)
} else {
kvList = append(kvList, attr.Key, PseudoStruct(grpKVs))
}
} else if attr.Key != "" {
kvList = append(kvList, attr.Key, attrVal.Any())
}
return kvList
}
// levelFromSlog adjusts the level by the logger's verbosity and negates it.
// It ensures that the result is >= 0. This is necessary because the result is
// passed to a LogSink and that API did not historically document whether
// levels could be negative or what that meant.
//
// Some example usage:
//
// logrV0 := getMyLogger()
// logrV2 := logrV0.V(2)
// slogV2 := slog.New(logr.ToSlogHandler(logrV2))
// slogV2.Debug("msg") // =~ logrV2.V(4) =~ logrV0.V(6)
// slogV2.Info("msg") // =~ logrV2.V(0) =~ logrV0.V(2)
// slogv2.Warn("msg") // =~ logrV2.V(-4) =~ logrV0.V(0)
func (l fnlogger) levelFromSlog(level slog.Level) int {
result := -level
if result < 0 {
result = 0 // because LogSink doesn't expect negative V levels
}
return int(result)
}

View file

@ -207,10 +207,6 @@ limitations under the License.
// those.
package logr
import (
"context"
)
// New returns a new Logger instance. This is primarily used by libraries
// implementing LogSink, rather than end users. Passing a nil sink will create
// a Logger which discards all log lines.
@ -410,45 +406,6 @@ func (l Logger) IsZero() bool {
return l.sink == nil
}
// contextKey is how we find Loggers in a context.Context.
type contextKey struct{}
// FromContext returns a Logger from ctx or an error if no Logger is found.
func FromContext(ctx context.Context) (Logger, error) {
if v, ok := ctx.Value(contextKey{}).(Logger); ok {
return v, nil
}
return Logger{}, notFoundError{}
}
// notFoundError exists to carry an IsNotFound method.
type notFoundError struct{}
func (notFoundError) Error() string {
return "no logr.Logger was present"
}
func (notFoundError) IsNotFound() bool {
return true
}
// FromContextOrDiscard returns a Logger from ctx. If no Logger is found, this
// returns a Logger that discards all log messages.
func FromContextOrDiscard(ctx context.Context) Logger {
if v, ok := ctx.Value(contextKey{}).(Logger); ok {
return v
}
return Discard()
}
// NewContext returns a new Context, derived from ctx, which carries the
// provided Logger.
func NewContext(ctx context.Context, logger Logger) context.Context {
return context.WithValue(ctx, contextKey{}, logger)
}
// RuntimeInfo holds information that the logr "core" library knows which
// LogSinks might want to know.
type RuntimeInfo struct {

192
vendor/github.com/go-logr/logr/sloghandler.go generated vendored Normal file
View file

@ -0,0 +1,192 @@
//go:build go1.21
// +build go1.21
/*
Copyright 2023 The logr Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package logr
import (
"context"
"log/slog"
)
type slogHandler struct {
// May be nil, in which case all logs get discarded.
sink LogSink
// Non-nil if sink is non-nil and implements SlogSink.
slogSink SlogSink
// groupPrefix collects values from WithGroup calls. It gets added as
// prefix to value keys when handling a log record.
groupPrefix string
// levelBias can be set when constructing the handler to influence the
// slog.Level of log records. A positive levelBias reduces the
// slog.Level value. slog has no API to influence this value after the
// handler got created, so it can only be set indirectly through
// Logger.V.
levelBias slog.Level
}
var _ slog.Handler = &slogHandler{}
// groupSeparator is used to concatenate WithGroup names and attribute keys.
const groupSeparator = "."
// GetLevel is used for black box unit testing.
func (l *slogHandler) GetLevel() slog.Level {
return l.levelBias
}
func (l *slogHandler) Enabled(_ context.Context, level slog.Level) bool {
return l.sink != nil && (level >= slog.LevelError || l.sink.Enabled(l.levelFromSlog(level)))
}
func (l *slogHandler) Handle(ctx context.Context, record slog.Record) error {
if l.slogSink != nil {
// Only adjust verbosity level of log entries < slog.LevelError.
if record.Level < slog.LevelError {
record.Level -= l.levelBias
}
return l.slogSink.Handle(ctx, record)
}
// No need to check for nil sink here because Handle will only be called
// when Enabled returned true.
kvList := make([]any, 0, 2*record.NumAttrs())
record.Attrs(func(attr slog.Attr) bool {
kvList = attrToKVs(attr, l.groupPrefix, kvList)
return true
})
if record.Level >= slog.LevelError {
l.sinkWithCallDepth().Error(nil, record.Message, kvList...)
} else {
level := l.levelFromSlog(record.Level)
l.sinkWithCallDepth().Info(level, record.Message, kvList...)
}
return nil
}
// sinkWithCallDepth adjusts the stack unwinding so that when Error or Info
// are called by Handle, code in slog gets skipped.
//
// This offset currently (Go 1.21.0) works for calls through
// slog.New(ToSlogHandler(...)). There's no guarantee that the call
// chain won't change. Wrapping the handler will also break unwinding. It's
// still better than not adjusting at all....
//
// This cannot be done when constructing the handler because FromSlogHandler needs
// access to the original sink without this adjustment. A second copy would
// work, but then WithAttrs would have to be called for both of them.
func (l *slogHandler) sinkWithCallDepth() LogSink {
if sink, ok := l.sink.(CallDepthLogSink); ok {
return sink.WithCallDepth(2)
}
return l.sink
}
func (l *slogHandler) WithAttrs(attrs []slog.Attr) slog.Handler {
if l.sink == nil || len(attrs) == 0 {
return l
}
clone := *l
if l.slogSink != nil {
clone.slogSink = l.slogSink.WithAttrs(attrs)
clone.sink = clone.slogSink
} else {
kvList := make([]any, 0, 2*len(attrs))
for _, attr := range attrs {
kvList = attrToKVs(attr, l.groupPrefix, kvList)
}
clone.sink = l.sink.WithValues(kvList...)
}
return &clone
}
func (l *slogHandler) WithGroup(name string) slog.Handler {
if l.sink == nil {
return l
}
if name == "" {
// slog says to inline empty groups
return l
}
clone := *l
if l.slogSink != nil {
clone.slogSink = l.slogSink.WithGroup(name)
clone.sink = clone.slogSink
} else {
clone.groupPrefix = addPrefix(clone.groupPrefix, name)
}
return &clone
}
// attrToKVs appends a slog.Attr to a logr-style kvList. It handle slog Groups
// and other details of slog.
func attrToKVs(attr slog.Attr, groupPrefix string, kvList []any) []any {
attrVal := attr.Value.Resolve()
if attrVal.Kind() == slog.KindGroup {
groupVal := attrVal.Group()
grpKVs := make([]any, 0, 2*len(groupVal))
prefix := groupPrefix
if attr.Key != "" {
prefix = addPrefix(groupPrefix, attr.Key)
}
for _, attr := range groupVal {
grpKVs = attrToKVs(attr, prefix, grpKVs)
}
kvList = append(kvList, grpKVs...)
} else if attr.Key != "" {
kvList = append(kvList, addPrefix(groupPrefix, attr.Key), attrVal.Any())
}
return kvList
}
func addPrefix(prefix, name string) string {
if prefix == "" {
return name
}
if name == "" {
return prefix
}
return prefix + groupSeparator + name
}
// levelFromSlog adjusts the level by the logger's verbosity and negates it.
// It ensures that the result is >= 0. This is necessary because the result is
// passed to a LogSink and that API did not historically document whether
// levels could be negative or what that meant.
//
// Some example usage:
//
// logrV0 := getMyLogger()
// logrV2 := logrV0.V(2)
// slogV2 := slog.New(logr.ToSlogHandler(logrV2))
// slogV2.Debug("msg") // =~ logrV2.V(4) =~ logrV0.V(6)
// slogV2.Info("msg") // =~ logrV2.V(0) =~ logrV0.V(2)
// slogv2.Warn("msg") // =~ logrV2.V(-4) =~ logrV0.V(0)
func (l *slogHandler) levelFromSlog(level slog.Level) int {
result := -level
result += l.levelBias // in case the original Logger had a V level
if result < 0 {
result = 0 // because LogSink doesn't expect negative V levels
}
return int(result)
}

100
vendor/github.com/go-logr/logr/slogr.go generated vendored Normal file
View file

@ -0,0 +1,100 @@
//go:build go1.21
// +build go1.21
/*
Copyright 2023 The logr Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package logr
import (
"context"
"log/slog"
)
// FromSlogHandler returns a Logger which writes to the slog.Handler.
//
// The logr verbosity level is mapped to slog levels such that V(0) becomes
// slog.LevelInfo and V(4) becomes slog.LevelDebug.
func FromSlogHandler(handler slog.Handler) Logger {
if handler, ok := handler.(*slogHandler); ok {
if handler.sink == nil {
return Discard()
}
return New(handler.sink).V(int(handler.levelBias))
}
return New(&slogSink{handler: handler})
}
// ToSlogHandler returns a slog.Handler which writes to the same sink as the Logger.
//
// The returned logger writes all records with level >= slog.LevelError as
// error log entries with LogSink.Error, regardless of the verbosity level of
// the Logger:
//
// logger := <some Logger with 0 as verbosity level>
// slog.New(ToSlogHandler(logger.V(10))).Error(...) -> logSink.Error(...)
//
// The level of all other records gets reduced by the verbosity
// level of the Logger and the result is negated. If it happens
// to be negative, then it gets replaced by zero because a LogSink
// is not expected to handled negative levels:
//
// slog.New(ToSlogHandler(logger)).Debug(...) -> logger.GetSink().Info(level=4, ...)
// slog.New(ToSlogHandler(logger)).Warning(...) -> logger.GetSink().Info(level=0, ...)
// slog.New(ToSlogHandler(logger)).Info(...) -> logger.GetSink().Info(level=0, ...)
// slog.New(ToSlogHandler(logger.V(4))).Info(...) -> logger.GetSink().Info(level=4, ...)
func ToSlogHandler(logger Logger) slog.Handler {
if sink, ok := logger.GetSink().(*slogSink); ok && logger.GetV() == 0 {
return sink.handler
}
handler := &slogHandler{sink: logger.GetSink(), levelBias: slog.Level(logger.GetV())}
if slogSink, ok := handler.sink.(SlogSink); ok {
handler.slogSink = slogSink
}
return handler
}
// SlogSink is an optional interface that a LogSink can implement to support
// logging through the slog.Logger or slog.Handler APIs better. It then should
// also support special slog values like slog.Group. When used as a
// slog.Handler, the advantages are:
//
// - stack unwinding gets avoided in favor of logging the pre-recorded PC,
// as intended by slog
// - proper grouping of key/value pairs via WithGroup
// - verbosity levels > slog.LevelInfo can be recorded
// - less overhead
//
// Both APIs (Logger and slog.Logger/Handler) then are supported equally
// well. Developers can pick whatever API suits them better and/or mix
// packages which use either API in the same binary with a common logging
// implementation.
//
// This interface is necessary because the type implementing the LogSink
// interface cannot also implement the slog.Handler interface due to the
// different prototype of the common Enabled method.
//
// An implementation could support both interfaces in two different types, but then
// additional interfaces would be needed to convert between those types in FromSlogHandler
// and ToSlogHandler.
type SlogSink interface {
LogSink
Handle(ctx context.Context, record slog.Record) error
WithAttrs(attrs []slog.Attr) SlogSink
WithGroup(name string) SlogSink
}

120
vendor/github.com/go-logr/logr/slogsink.go generated vendored Normal file
View file

@ -0,0 +1,120 @@
//go:build go1.21
// +build go1.21
/*
Copyright 2023 The logr Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package logr
import (
"context"
"log/slog"
"runtime"
"time"
)
var (
_ LogSink = &slogSink{}
_ CallDepthLogSink = &slogSink{}
_ Underlier = &slogSink{}
)
// Underlier is implemented by the LogSink returned by NewFromLogHandler.
type Underlier interface {
// GetUnderlying returns the Handler used by the LogSink.
GetUnderlying() slog.Handler
}
const (
// nameKey is used to log the `WithName` values as an additional attribute.
nameKey = "logger"
// errKey is used to log the error parameter of Error as an additional attribute.
errKey = "err"
)
type slogSink struct {
callDepth int
name string
handler slog.Handler
}
func (l *slogSink) Init(info RuntimeInfo) {
l.callDepth = info.CallDepth
}
func (l *slogSink) GetUnderlying() slog.Handler {
return l.handler
}
func (l *slogSink) WithCallDepth(depth int) LogSink {
newLogger := *l
newLogger.callDepth += depth
return &newLogger
}
func (l *slogSink) Enabled(level int) bool {
return l.handler.Enabled(context.Background(), slog.Level(-level))
}
func (l *slogSink) Info(level int, msg string, kvList ...interface{}) {
l.log(nil, msg, slog.Level(-level), kvList...)
}
func (l *slogSink) Error(err error, msg string, kvList ...interface{}) {
l.log(err, msg, slog.LevelError, kvList...)
}
func (l *slogSink) log(err error, msg string, level slog.Level, kvList ...interface{}) {
var pcs [1]uintptr
// skip runtime.Callers, this function, Info/Error, and all helper functions above that.
runtime.Callers(3+l.callDepth, pcs[:])
record := slog.NewRecord(time.Now(), level, msg, pcs[0])
if l.name != "" {
record.AddAttrs(slog.String(nameKey, l.name))
}
if err != nil {
record.AddAttrs(slog.Any(errKey, err))
}
record.Add(kvList...)
_ = l.handler.Handle(context.Background(), record)
}
func (l slogSink) WithName(name string) LogSink {
if l.name != "" {
l.name += "/"
}
l.name += name
return &l
}
func (l slogSink) WithValues(kvList ...interface{}) LogSink {
l.handler = l.handler.WithAttrs(kvListToAttrs(kvList...))
return &l
}
func kvListToAttrs(kvList ...interface{}) []slog.Attr {
// We don't need the record itself, only its Add method.
record := slog.NewRecord(time.Time{}, 0, "", 0)
record.Add(kvList...)
attrs := make([]slog.Attr, 0, record.NumAttrs())
record.Attrs(func(attr slog.Attr) bool {
attrs = append(attrs, attr)
return true
})
return attrs
}

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

View file

@ -13,6 +13,7 @@
Aaron Hopkins <go-sql-driver at die.net>
Achille Roussel <achille.roussel at gmail.com>
Aidan <aidan.liu at pingcap.com>
Alex Snast <alexsn at fb.com>
Alexey Palazhchenko <alexey.palazhchenko at gmail.com>
Andrew Reid <andrew.reid at tixtrack.com>
@ -20,12 +21,14 @@ Animesh Ray <mail.rayanimesh at gmail.com>
Arne Hormann <arnehormann at gmail.com>
Ariel Mashraki <ariel at mashraki.co.il>
Asta Xie <xiemengjun at gmail.com>
Brian Hendriks <brian at dolthub.com>
Bulat Gaifullin <gaifullinbf at gmail.com>
Caine Jette <jette at alum.mit.edu>
Carlos Nieto <jose.carlos at menteslibres.net>
Chris Kirkland <chriskirkland at github.com>
Chris Moos <chris at tech9computers.com>
Craig Wilson <craiggwilson at gmail.com>
Daemonxiao <735462752 at qq.com>
Daniel Montoya <dsmontoyam at gmail.com>
Daniel Nichter <nil at codenode.com>
Daniël van Eeden <git at myname.nl>
@ -33,9 +36,11 @@ Dave Protasowski <dprotaso at gmail.com>
DisposaBoy <disposaboy at dby.me>
Egor Smolyakov <egorsmkv at gmail.com>
Erwan Martin <hello at erwan.io>
Evan Elias <evan at skeema.net>
Evan Shaw <evan at vendhq.com>
Frederick Mayle <frederickmayle at gmail.com>
Gustavo Kristic <gkristic at gmail.com>
Gusted <postmaster at gusted.xyz>
Hajime Nakagami <nakagami at gmail.com>
Hanno Braun <mail at hannobraun.com>
Henri Yandell <flamefew at gmail.com>
@ -47,8 +52,11 @@ INADA Naoki <songofacandy at gmail.com>
Jacek Szwec <szwec.jacek at gmail.com>
James Harr <james.harr at gmail.com>
Janek Vedock <janekvedock at comcast.net>
Jason Ng <oblitorum at gmail.com>
Jean-Yves Pellé <jy at pelle.link>
Jeff Hodges <jeff at somethingsimilar.com>
Jeffrey Charles <jeffreycharles at gmail.com>
Jennifer Purevsuren <jennifer at dolthub.com>
Jerome Meyer <jxmeyer at gmail.com>
Jiajia Zhong <zhong2plus at gmail.com>
Jian Zhen <zhenjl at gmail.com>
@ -74,9 +82,11 @@ Maciej Zimnoch <maciej.zimnoch at codilime.com>
Michael Woolnough <michael.woolnough at gmail.com>
Nathanial Murphy <nathanial.murphy at gmail.com>
Nicola Peduzzi <thenikso at gmail.com>
Oliver Bone <owbone at github.com>
Olivier Mengué <dolmen at cpan.org>
oscarzhao <oscarzhaosl at gmail.com>
Paul Bonser <misterpib at gmail.com>
Paulius Lozys <pauliuslozys at gmail.com>
Peter Schultz <peter.schultz at classmarkets.com>
Phil Porada <philporada at gmail.com>
Rebecca Chin <rchin at pivotal.io>
@ -95,6 +105,7 @@ Stan Putrya <root.vagner at gmail.com>
Stanley Gunawan <gunawan.stanley at gmail.com>
Steven Hartland <steven.hartland at multiplay.co.uk>
Tan Jinhua <312841925 at qq.com>
Tetsuro Aoki <t.aoki1130 at gmail.com>
Thomas Wodarek <wodarekwebpage at gmail.com>
Tim Ruffles <timruffles at gmail.com>
Tom Jenkinson <tom at tjenkinson.me>
@ -104,6 +115,7 @@ Xiangyu Hu <xiangyu.hu at outlook.com>
Xiaobing Jiang <s7v7nislands at gmail.com>
Xiuming Chen <cc at cxm.cc>
Xuehong Chan <chanxuehong at gmail.com>
Zhang Xiang <angwerzx at 126.com>
Zhenye Xie <xiezhenye at gmail.com>
Zhixin Wen <john.wenzhixin at gmail.com>
Ziheng Lyu <zihenglv at gmail.com>
@ -113,14 +125,18 @@ Ziheng Lyu <zihenglv at gmail.com>
Barracuda Networks, Inc.
Counting Ltd.
DigitalOcean Inc.
Dolthub Inc.
dyves labs AG
Facebook Inc.
GitHub Inc.
Google Inc.
InfoSum Ltd.
Keybase Inc.
Microsoft Corp.
Multiplay Ltd.
Percona LLC
PingCAP Inc.
Pivotal Inc.
Shattered Silicon Ltd.
Stripe Inc.
Zendesk Inc.

View file

@ -1,3 +1,45 @@
## Version 1.8.1 (2024-03-26)
Bugfixes:
- fix race condition when context is canceled in [#1562](https://github.com/go-sql-driver/mysql/pull/1562) and [#1570](https://github.com/go-sql-driver/mysql/pull/1570)
## Version 1.8.0 (2024-03-09)
Major Changes:
- Use `SET NAMES charset COLLATE collation`. by @methane in [#1437](https://github.com/go-sql-driver/mysql/pull/1437)
- Older go-mysql-driver used `collation_id` in the handshake packet. But it caused collation mismatch in some situation.
- If you don't specify charset nor collation, go-mysql-driver sends `SET NAMES utf8mb4` for new connection. This uses server's default collation for utf8mb4.
- If you specify charset, go-mysql-driver sends `SET NAMES <charset>`. This uses the server's default collation for `<charset>`.
- If you specify collation and/or charset, go-mysql-driver sends `SET NAMES charset COLLATE collation`.
- PathEscape dbname in DSN. by @methane in [#1432](https://github.com/go-sql-driver/mysql/pull/1432)
- This is backward incompatible in rare case. Check your DSN.
- Drop Go 1.13-17 support by @methane in [#1420](https://github.com/go-sql-driver/mysql/pull/1420)
- Use Go 1.18+
- Parse numbers on text protocol too by @methane in [#1452](https://github.com/go-sql-driver/mysql/pull/1452)
- When text protocol is used, go-mysql-driver passed bare `[]byte` to database/sql for avoid unnecessary allocation and conversion.
- If user specified `*any` to `Scan()`, database/sql passed the `[]byte` into the target variable.
- This confused users because most user doesn't know when text/binary protocol used.
- go-mysql-driver 1.8 converts integer/float values into int64/double even in text protocol. This doesn't increase allocation compared to `[]byte` and conversion cost is negatable.
- New options start using the Functional Option Pattern to avoid increasing technical debt in the Config object. Future version may introduce Functional Option for existing options, but not for now.
- Make TimeTruncate functional option by @methane in [1552](https://github.com/go-sql-driver/mysql/pull/1552)
- Add BeforeConnect callback to configuration object by @ItalyPaleAle in [#1469](https://github.com/go-sql-driver/mysql/pull/1469)
Other changes:
- Adding DeregisterDialContext to prevent memory leaks with dialers we don't need anymore by @jypelle in https://github.com/go-sql-driver/mysql/pull/1422
- Make logger configurable per connection by @frozenbonito in https://github.com/go-sql-driver/mysql/pull/1408
- Fix ColumnType.DatabaseTypeName for mediumint unsigned by @evanelias in https://github.com/go-sql-driver/mysql/pull/1428
- Add connection attributes by @Daemonxiao in https://github.com/go-sql-driver/mysql/pull/1389
- Stop `ColumnTypeScanType()` from returning `sql.RawBytes` by @methane in https://github.com/go-sql-driver/mysql/pull/1424
- Exec() now provides access to status of multiple statements. by @mherr-google in https://github.com/go-sql-driver/mysql/pull/1309
- Allow to change (or disable) the default driver name for registration by @dolmen in https://github.com/go-sql-driver/mysql/pull/1499
- Add default connection attribute '_server_host' by @oblitorum in https://github.com/go-sql-driver/mysql/pull/1506
- QueryUnescape DSN ConnectionAttribute value by @zhangyangyu in https://github.com/go-sql-driver/mysql/pull/1470
- Add client_ed25519 authentication by @Gusted in https://github.com/go-sql-driver/mysql/pull/1518
## Version 1.7.1 (2023-04-25)
Changes:
@ -162,7 +204,7 @@ New Features:
- Enable microsecond resolution on TIME, DATETIME and TIMESTAMP (#249)
- Support for returning table alias on Columns() (#289, #359, #382)
- Placeholder interpolation, can be actived with the DSN parameter `interpolateParams=true` (#309, #318, #490)
- Placeholder interpolation, can be activated with the DSN parameter `interpolateParams=true` (#309, #318, #490)
- Support for uint64 parameters with high bit set (#332, #345)
- Cleartext authentication plugin support (#327)
- Exported ParseDSN function and the Config struct (#403, #419, #429)
@ -206,7 +248,7 @@ Changes:
- Also exported the MySQLWarning type
- mysqlConn.Close returns the first error encountered instead of ignoring all errors
- writePacket() automatically writes the packet size to the header
- readPacket() uses an iterative approach instead of the recursive approach to merge splitted packets
- readPacket() uses an iterative approach instead of the recursive approach to merge split packets
New Features:
@ -254,7 +296,7 @@ Bugfixes:
- Fixed MySQL 4.1 support: MySQL 4.1 sends packets with lengths which differ from the specification
- Convert to DB timezone when inserting `time.Time`
- Splitted packets (more than 16MB) are now merged correctly
- Split packets (more than 16MB) are now merged correctly
- Fixed false positive `io.EOF` errors when the data was fully read
- Avoid panics on reuse of closed connections
- Fixed empty string producing false nil values

View file

@ -40,15 +40,23 @@ A MySQL-Driver for Go's [database/sql](https://golang.org/pkg/database/sql/) pac
* Optional placeholder interpolation
## Requirements
* Go 1.13 or higher. We aim to support the 3 latest versions of Go.
* MySQL (4.1+), MariaDB, Percona Server, Google CloudSQL or Sphinx (2.2.3+)
* Go 1.19 or higher. We aim to support the 3 latest versions of Go.
* MySQL (5.7+) and MariaDB (10.3+) are supported.
* [TiDB](https://github.com/pingcap/tidb) is supported by PingCAP.
* Do not ask questions about TiDB in our issue tracker or forum.
* [Document](https://docs.pingcap.com/tidb/v6.1/dev-guide-sample-application-golang)
* [Forum](https://ask.pingcap.com/)
* go-mysql would work with Percona Server, Google CloudSQL or Sphinx (2.2.3+).
* Maintainers won't support them. Do not expect issues are investigated and resolved by maintainers.
* Investigate issues yourself and please send a pull request to fix it.
---------------------------------------
## Installation
Simple install the package to your [$GOPATH](https://github.com/golang/go/wiki/GOPATH "GOPATH") with the [go tool](https://golang.org/cmd/go/ "go command") from shell:
```bash
$ go get -u github.com/go-sql-driver/mysql
go get -u github.com/go-sql-driver/mysql
```
Make sure [Git is installed](https://git-scm.com/downloads) on your machine and in your system's `PATH`.
@ -114,6 +122,12 @@ This has the same effect as an empty DSN string:
```
`dbname` is escaped by [PathEscape()](https://pkg.go.dev/net/url#PathEscape) since v1.8.0. If your database name is `dbname/withslash`, it becomes:
```
/dbname%2Fwithslash
```
Alternatively, [Config.FormatDSN](https://godoc.org/github.com/go-sql-driver/mysql#Config.FormatDSN) can be used to create a DSN string by filling a struct.
#### Password
@ -121,7 +135,7 @@ Passwords can consist of any character. Escaping is **not** necessary.
#### Protocol
See [net.Dial](https://golang.org/pkg/net/#Dial) for more information which networks are available.
In general you should use an Unix domain socket if available and TCP otherwise for best performance.
In general you should use a Unix domain socket if available and TCP otherwise for best performance.
#### Address
For TCP and UDP networks, addresses have the form `host[:port]`.
@ -145,7 +159,7 @@ Default: false
```
`allowAllFiles=true` disables the file allowlist for `LOAD DATA LOCAL INFILE` and allows *all* files.
[*Might be insecure!*](http://dev.mysql.com/doc/refman/5.7/en/load-data-local.html)
[*Might be insecure!*](https://dev.mysql.com/doc/refman/8.0/en/load-data.html#load-data-local)
##### `allowCleartextPasswords`
@ -194,10 +208,9 @@ Valid Values: <name>
Default: none
```
Sets the charset used for client-server interaction (`"SET NAMES <value>"`). If multiple charsets are set (separated by a comma), the following charset is used if setting the charset failes. This enables for example support for `utf8mb4` ([introduced in MySQL 5.5.3](http://dev.mysql.com/doc/refman/5.5/en/charset-unicode-utf8mb4.html)) with fallback to `utf8` for older servers (`charset=utf8mb4,utf8`).
Sets the charset used for client-server interaction (`"SET NAMES <value>"`). If multiple charsets are set (separated by a comma), the following charset is used if setting the charset fails. This enables for example support for `utf8mb4` ([introduced in MySQL 5.5.3](http://dev.mysql.com/doc/refman/5.5/en/charset-unicode-utf8mb4.html)) with fallback to `utf8` for older servers (`charset=utf8mb4,utf8`).
Usage of the `charset` parameter is discouraged because it issues additional queries to the server.
Unless you need the fallback behavior, please use `collation` instead.
See also [Unicode Support](#unicode-support).
##### `checkConnLiveness`
@ -226,6 +239,7 @@ The default collation (`utf8mb4_general_ci`) is supported from MySQL 5.5. You s
Collations for charset "ucs2", "utf16", "utf16le", and "utf32" can not be used ([ref](https://dev.mysql.com/doc/refman/5.7/en/charset-connection.html#charset-connection-impermissible-client-charset)).
See also [Unicode Support](#unicode-support).
##### `clientFoundRows`
@ -279,6 +293,15 @@ Note that this sets the location for time.Time values but does not change MySQL'
Please keep in mind, that param values must be [url.QueryEscape](https://golang.org/pkg/net/url/#QueryEscape)'ed. Alternatively you can manually replace the `/` with `%2F`. For example `US/Pacific` would be `loc=US%2FPacific`.
##### `timeTruncate`
```
Type: duration
Default: 0
```
[Truncate time values](https://pkg.go.dev/time#Duration.Truncate) to the specified duration. The value must be a decimal number with a unit suffix (*"ms"*, *"s"*, *"m"*, *"h"*), such as *"30s"*, *"0.5m"* or *"1m30s"*.
##### `maxAllowedPacket`
```
Type: decimal number
@ -295,9 +318,25 @@ Valid Values: true, false
Default: false
```
Allow multiple statements in one query. While this allows batch queries, it also greatly increases the risk of SQL injections. Only the result of the first query is returned, all other results are silently discarded.
Allow multiple statements in one query. This can be used to bach multiple queries. Use [Rows.NextResultSet()](https://pkg.go.dev/database/sql#Rows.NextResultSet) to get result of the second and subsequent queries.
When `multiStatements` is used, `?` parameters must only be used in the first statement.
When `multiStatements` is used, `?` parameters must only be used in the first statement. [interpolateParams](#interpolateparams) can be used to avoid this limitation unless prepared statement is used explicitly.
It's possible to access the last inserted ID and number of affected rows for multiple statements by using `sql.Conn.Raw()` and the `mysql.Result`. For example:
```go
conn, _ := db.Conn(ctx)
conn.Raw(func(conn any) error {
ex := conn.(driver.Execer)
res, err := ex.Exec(`
UPDATE point SET x = 1 WHERE y = 2;
UPDATE point SET x = 2 WHERE y = 3;
`, nil)
// Both slices have 2 elements.
log.Print(res.(mysql.Result).AllRowsAffected())
log.Print(res.(mysql.Result).AllLastInsertIds())
})
```
##### `parseTime`
@ -393,6 +432,15 @@ Default: 0
I/O write timeout. The value must be a decimal number with a unit suffix (*"ms"*, *"s"*, *"m"*, *"h"*), such as *"30s"*, *"0.5m"* or *"1m30s"*.
##### `connectionAttributes`
```
Type: comma-delimited string of user-defined "key:value" pairs
Valid Values: (<name1>:<value1>,<name2>:<value2>,...)
Default: none
```
[Connection attributes](https://dev.mysql.com/doc/refman/8.0/en/performance-schema-connection-attribute-tables.html) are key-value pairs that application programs can pass to the server at connect time.
##### System Variables
@ -465,7 +513,7 @@ user:password@/
The connection pool is managed by Go's database/sql package. For details on how to configure the size of the pool and how long connections stay in the pool see `*DB.SetMaxOpenConns`, `*DB.SetMaxIdleConns`, and `*DB.SetConnMaxLifetime` in the [database/sql documentation](https://golang.org/pkg/database/sql/). The read, write, and dial timeouts for each individual connection are configured with the DSN parameters [`readTimeout`](#readtimeout), [`writeTimeout`](#writetimeout), and [`timeout`](#timeout), respectively.
## `ColumnType` Support
This driver supports the [`ColumnType` interface](https://golang.org/pkg/database/sql/#ColumnType) introduced in Go 1.8, with the exception of [`ColumnType.Length()`](https://golang.org/pkg/database/sql/#ColumnType.Length), which is currently not supported. All Unsigned database type names will be returned `UNSIGNED ` with `INT`, `TINYINT`, `SMALLINT`, `BIGINT`.
This driver supports the [`ColumnType` interface](https://golang.org/pkg/database/sql/#ColumnType) introduced in Go 1.8, with the exception of [`ColumnType.Length()`](https://golang.org/pkg/database/sql/#ColumnType.Length), which is currently not supported. All Unsigned database type names will be returned `UNSIGNED ` with `INT`, `TINYINT`, `SMALLINT`, `MEDIUMINT`, `BIGINT`.
## `context.Context` Support
Go 1.8 added `database/sql` support for `context.Context`. This driver supports query timeouts and cancellation via contexts.
@ -478,7 +526,7 @@ For this feature you need direct access to the package. Therefore you must chang
import "github.com/go-sql-driver/mysql"
```
Files must be explicitly allowed by registering them with `mysql.RegisterLocalFile(filepath)` (recommended) or the allowlist check must be deactivated by using the DSN parameter `allowAllFiles=true` ([*Might be insecure!*](http://dev.mysql.com/doc/refman/5.7/en/load-data-local.html)).
Files must be explicitly allowed by registering them with `mysql.RegisterLocalFile(filepath)` (recommended) or the allowlist check must be deactivated by using the DSN parameter `allowAllFiles=true` ([*Might be insecure!*](https://dev.mysql.com/doc/refman/8.0/en/load-data.html#load-data-local)).
To use a `io.Reader` a handler function must be registered with `mysql.RegisterReaderHandler(name, handler)` which returns a `io.Reader` or `io.ReadCloser`. The Reader is available with the filepath `Reader::<name>` then. Choose different names for different handlers and `DeregisterReaderHandler` when you don't need it anymore.
@ -496,9 +544,11 @@ However, many want to scan MySQL `DATE` and `DATETIME` values into `time.Time` v
### Unicode support
Since version 1.5 Go-MySQL-Driver automatically uses the collation ` utf8mb4_general_ci` by default.
Other collations / charsets can be set using the [`collation`](#collation) DSN parameter.
Other charsets / collations can be set using the [`charset`](#charset) or [`collation`](#collation) DSN parameter.
Version 1.0 of the driver recommended adding `&charset=utf8` (alias for `SET NAMES utf8`) to the DSN to enable proper UTF-8 support. This is not necessary anymore. The [`collation`](#collation) parameter should be preferred to set another collation / charset than the default.
- When only the `charset` is specified, the `SET NAMES <charset>` query is sent and the server's default collation is used.
- When both the `charset` and `collation` are specified, the `SET NAMES <charset> COLLATE <collation>` query is sent.
- When only the `collation` is specified, the collation is specified in the protocol handshake and the `SET NAMES` query is not sent. This can save one roundtrip, but note that the server may ignore the specified collation silently and use the server's default charset/collation instead.
See http://dev.mysql.com/doc/refman/8.0/en/charset-unicode.html for more details on MySQL's Unicode support.

View file

@ -13,10 +13,13 @@ import (
"crypto/rsa"
"crypto/sha1"
"crypto/sha256"
"crypto/sha512"
"crypto/x509"
"encoding/pem"
"fmt"
"sync"
"filippo.io/edwards25519"
)
// server pub keys registry
@ -33,7 +36,7 @@ var (
// Note: The provided rsa.PublicKey instance is exclusively owned by the driver
// after registering it and may not be modified.
//
// data, err := ioutil.ReadFile("mykey.pem")
// data, err := os.ReadFile("mykey.pem")
// if err != nil {
// log.Fatal(err)
// }
@ -225,6 +228,44 @@ func encryptPassword(password string, seed []byte, pub *rsa.PublicKey) ([]byte,
return rsa.EncryptOAEP(sha1, rand.Reader, pub, plain, nil)
}
// authEd25519 does ed25519 authentication used by MariaDB.
func authEd25519(scramble []byte, password string) ([]byte, error) {
// Derived from https://github.com/MariaDB/server/blob/d8e6bb00888b1f82c031938f4c8ac5d97f6874c3/plugin/auth_ed25519/ref10/sign.c
// Code style is from https://cs.opensource.google/go/go/+/refs/tags/go1.21.5:src/crypto/ed25519/ed25519.go;l=207
h := sha512.Sum512([]byte(password))
s, err := edwards25519.NewScalar().SetBytesWithClamping(h[:32])
if err != nil {
return nil, err
}
A := (&edwards25519.Point{}).ScalarBaseMult(s)
mh := sha512.New()
mh.Write(h[32:])
mh.Write(scramble)
messageDigest := mh.Sum(nil)
r, err := edwards25519.NewScalar().SetUniformBytes(messageDigest)
if err != nil {
return nil, err
}
R := (&edwards25519.Point{}).ScalarBaseMult(r)
kh := sha512.New()
kh.Write(R.Bytes())
kh.Write(A.Bytes())
kh.Write(scramble)
hramDigest := kh.Sum(nil)
k, err := edwards25519.NewScalar().SetUniformBytes(hramDigest)
if err != nil {
return nil, err
}
S := k.MultiplyAdd(k, s, r)
return append(R.Bytes(), S.Bytes()...), nil
}
func (mc *mysqlConn) sendEncryptedPassword(seed []byte, pub *rsa.PublicKey) error {
enc, err := encryptPassword(mc.cfg.Passwd, seed, pub)
if err != nil {
@ -290,8 +331,14 @@ func (mc *mysqlConn) auth(authData []byte, plugin string) ([]byte, error) {
enc, err := encryptPassword(mc.cfg.Passwd, authData, pubKey)
return enc, err
case "client_ed25519":
if len(authData) != 32 {
return nil, ErrMalformPkt
}
return authEd25519(authData, mc.cfg.Passwd)
default:
errLog.Print("unknown auth plugin:", plugin)
mc.log("unknown auth plugin:", plugin)
return nil, ErrUnknownPlugin
}
}
@ -338,7 +385,7 @@ func (mc *mysqlConn) handleAuthResult(oldAuthData []byte, plugin string) error {
switch plugin {
// https://insidemysql.com/preparing-your-community-connector-for-mysql-8-part-2-sha256/
// https://dev.mysql.com/blog-archive/preparing-your-community-connector-for-mysql-8-part-2-sha256/
case "caching_sha2_password":
switch len(authData) {
case 0:
@ -346,7 +393,7 @@ func (mc *mysqlConn) handleAuthResult(oldAuthData []byte, plugin string) error {
case 1:
switch authData[0] {
case cachingSha2PasswordFastAuthSuccess:
if err = mc.readResultOK(); err == nil {
if err = mc.resultUnchanged().readResultOK(); err == nil {
return nil // auth successful
}
@ -376,13 +423,13 @@ func (mc *mysqlConn) handleAuthResult(oldAuthData []byte, plugin string) error {
}
if data[0] != iAuthMoreData {
return fmt.Errorf("unexpect resp from server for caching_sha2_password perform full authentication")
return fmt.Errorf("unexpected resp from server for caching_sha2_password, perform full authentication")
}
// parse public key
block, rest := pem.Decode(data[1:])
if block == nil {
return fmt.Errorf("No Pem data found, data: %s", rest)
return fmt.Errorf("no pem data found, data: %s", rest)
}
pkix, err := x509.ParsePKIXPublicKey(block.Bytes)
if err != nil {
@ -397,7 +444,7 @@ func (mc *mysqlConn) handleAuthResult(oldAuthData []byte, plugin string) error {
return err
}
}
return mc.readResultOK()
return mc.resultUnchanged().readResultOK()
default:
return ErrMalformPkt
@ -426,7 +473,7 @@ func (mc *mysqlConn) handleAuthResult(oldAuthData []byte, plugin string) error {
if err != nil {
return err
}
return mc.readResultOK()
return mc.resultUnchanged().readResultOK()
}
default:

View file

@ -9,7 +9,7 @@
package mysql
const defaultCollation = "utf8mb4_general_ci"
const binaryCollation = "binary"
const binaryCollationID = 63
// A list of available collations mapped to the internal ID.
// To update this map use the following MySQL query:

View file

@ -23,10 +23,10 @@ import (
type mysqlConn struct {
buf buffer
netConn net.Conn
rawConn net.Conn // underlying connection when netConn is TLS connection.
affectedRows uint64
insertId uint64
rawConn net.Conn // underlying connection when netConn is TLS connection.
result mysqlResult // managed by clearResult() and handleOkPacket().
cfg *Config
connector *connector
maxAllowedPacket int
maxWriteSize int
writeTimeout time.Duration
@ -34,7 +34,6 @@ type mysqlConn struct {
status statusFlag
sequence uint8
parseTime bool
reset bool // set when the Go SQL package calls ResetSession
// for context support (Go 1.8+)
watching bool
@ -45,17 +44,27 @@ type mysqlConn struct {
closed atomicBool // set when conn is closed, before closech is closed
}
// Helper function to call per-connection logger.
func (mc *mysqlConn) log(v ...any) {
mc.cfg.Logger.Print(v...)
}
// Handles parameters set in DSN after the connection is established
func (mc *mysqlConn) handleParams() (err error) {
var cmdSet strings.Builder
for param, val := range mc.cfg.Params {
switch param {
// Charset: character_set_connection, character_set_client, character_set_results
case "charset":
charsets := strings.Split(val, ",")
for i := range charsets {
for _, cs := range charsets {
// ignore errors here - a charset may not exist
err = mc.exec("SET NAMES " + charsets[i])
if mc.cfg.Collation != "" {
err = mc.exec("SET NAMES " + cs + " COLLATE " + mc.cfg.Collation)
} else {
err = mc.exec("SET NAMES " + cs)
}
if err == nil {
break
}
@ -68,7 +77,7 @@ func (mc *mysqlConn) handleParams() (err error) {
default:
if cmdSet.Len() == 0 {
// Heuristic: 29 chars for each other key=value to reduce reallocations
cmdSet.Grow(4 + len(param) + 1 + len(val) + 30*(len(mc.cfg.Params)-1))
cmdSet.Grow(4 + len(param) + 3 + len(val) + 30*(len(mc.cfg.Params)-1))
cmdSet.WriteString("SET ")
} else {
cmdSet.WriteString(", ")
@ -105,7 +114,7 @@ func (mc *mysqlConn) Begin() (driver.Tx, error) {
func (mc *mysqlConn) begin(readOnly bool) (driver.Tx, error) {
if mc.closed.Load() {
errLog.Print(ErrInvalidConn)
mc.log(ErrInvalidConn)
return nil, driver.ErrBadConn
}
var q string
@ -128,7 +137,7 @@ func (mc *mysqlConn) Close() (err error) {
}
mc.cleanup()
mc.clearResult()
return
}
@ -143,12 +152,16 @@ func (mc *mysqlConn) cleanup() {
// Makes cleanup idempotent
close(mc.closech)
if mc.netConn == nil {
conn := mc.rawConn
if conn == nil {
return
}
if err := mc.netConn.Close(); err != nil {
errLog.Print(err)
if err := conn.Close(); err != nil {
mc.log(err)
}
// This function can be called from multiple goroutines.
// So we can not mc.clearResult() here.
// Caller should do it if they are in safe goroutine.
}
func (mc *mysqlConn) error() error {
@ -163,14 +176,14 @@ func (mc *mysqlConn) error() error {
func (mc *mysqlConn) Prepare(query string) (driver.Stmt, error) {
if mc.closed.Load() {
errLog.Print(ErrInvalidConn)
mc.log(ErrInvalidConn)
return nil, driver.ErrBadConn
}
// Send command
err := mc.writeCommandPacketStr(comStmtPrepare, query)
if err != nil {
// STMT_PREPARE is safe to retry. So we can return ErrBadConn here.
errLog.Print(err)
mc.log(err)
return nil, driver.ErrBadConn
}
@ -204,7 +217,7 @@ func (mc *mysqlConn) interpolateParams(query string, args []driver.Value) (strin
buf, err := mc.buf.takeCompleteBuffer()
if err != nil {
// can not take the buffer. Something must be wrong with the connection
errLog.Print(err)
mc.log(err)
return "", ErrInvalidConn
}
buf = buf[:0]
@ -246,7 +259,7 @@ func (mc *mysqlConn) interpolateParams(query string, args []driver.Value) (strin
buf = append(buf, "'0000-00-00'"...)
} else {
buf = append(buf, '\'')
buf, err = appendDateTime(buf, v.In(mc.cfg.Loc))
buf, err = appendDateTime(buf, v.In(mc.cfg.Loc), mc.cfg.timeTruncate)
if err != nil {
return "", err
}
@ -296,7 +309,7 @@ func (mc *mysqlConn) interpolateParams(query string, args []driver.Value) (strin
func (mc *mysqlConn) Exec(query string, args []driver.Value) (driver.Result, error) {
if mc.closed.Load() {
errLog.Print(ErrInvalidConn)
mc.log(ErrInvalidConn)
return nil, driver.ErrBadConn
}
if len(args) != 0 {
@ -310,28 +323,25 @@ func (mc *mysqlConn) Exec(query string, args []driver.Value) (driver.Result, err
}
query = prepared
}
mc.affectedRows = 0
mc.insertId = 0
err := mc.exec(query)
if err == nil {
return &mysqlResult{
affectedRows: int64(mc.affectedRows),
insertId: int64(mc.insertId),
}, err
copied := mc.result
return &copied, err
}
return nil, mc.markBadConn(err)
}
// Internal function to execute commands
func (mc *mysqlConn) exec(query string) error {
handleOk := mc.clearResult()
// Send command
if err := mc.writeCommandPacketStr(comQuery, query); err != nil {
return mc.markBadConn(err)
}
// Read Result
resLen, err := mc.readResultSetHeaderPacket()
resLen, err := handleOk.readResultSetHeaderPacket()
if err != nil {
return err
}
@ -348,7 +358,7 @@ func (mc *mysqlConn) exec(query string) error {
}
}
return mc.discardResults()
return handleOk.discardResults()
}
func (mc *mysqlConn) Query(query string, args []driver.Value) (driver.Rows, error) {
@ -356,8 +366,10 @@ func (mc *mysqlConn) Query(query string, args []driver.Value) (driver.Rows, erro
}
func (mc *mysqlConn) query(query string, args []driver.Value) (*textRows, error) {
handleOk := mc.clearResult()
if mc.closed.Load() {
errLog.Print(ErrInvalidConn)
mc.log(ErrInvalidConn)
return nil, driver.ErrBadConn
}
if len(args) != 0 {
@ -376,7 +388,7 @@ func (mc *mysqlConn) query(query string, args []driver.Value) (*textRows, error)
if err == nil {
// Read Result
var resLen int
resLen, err = mc.readResultSetHeaderPacket()
resLen, err = handleOk.readResultSetHeaderPacket()
if err == nil {
rows := new(textRows)
rows.mc = mc
@ -404,12 +416,13 @@ func (mc *mysqlConn) query(query string, args []driver.Value) (*textRows, error)
// The returned byte slice is only valid until the next read
func (mc *mysqlConn) getSystemVar(name string) ([]byte, error) {
// Send command
handleOk := mc.clearResult()
if err := mc.writeCommandPacketStr(comQuery, "SELECT @@"+name); err != nil {
return nil, err
}
// Read Result
resLen, err := mc.readResultSetHeaderPacket()
resLen, err := handleOk.readResultSetHeaderPacket()
if err == nil {
rows := new(textRows)
rows.mc = mc
@ -451,7 +464,7 @@ func (mc *mysqlConn) finish() {
// Ping implements driver.Pinger interface
func (mc *mysqlConn) Ping(ctx context.Context) (err error) {
if mc.closed.Load() {
errLog.Print(ErrInvalidConn)
mc.log(ErrInvalidConn)
return driver.ErrBadConn
}
@ -460,11 +473,12 @@ func (mc *mysqlConn) Ping(ctx context.Context) (err error) {
}
defer mc.finish()
handleOk := mc.clearResult()
if err = mc.writeCommandPacket(comPing); err != nil {
return mc.markBadConn(err)
}
return mc.readResultOK()
return handleOk.readResultOK()
}
// BeginTx implements driver.ConnBeginTx interface
@ -639,7 +653,31 @@ func (mc *mysqlConn) ResetSession(ctx context.Context) error {
if mc.closed.Load() {
return driver.ErrBadConn
}
mc.reset = true
// Perform a stale connection check. We only perform this check for
// the first query on a connection that has been checked out of the
// connection pool: a fresh connection from the pool is more likely
// to be stale, and it has not performed any previous writes that
// could cause data corruption, so it's safe to return ErrBadConn
// if the check fails.
if mc.cfg.CheckConnLiveness {
conn := mc.netConn
if mc.rawConn != nil {
conn = mc.rawConn
}
var err error
if mc.cfg.ReadTimeout != 0 {
err = conn.SetReadDeadline(time.Now().Add(mc.cfg.ReadTimeout))
}
if err == nil {
err = connCheck(conn)
}
if err != nil {
mc.log("closing bad idle connection: ", err)
return driver.ErrBadConn
}
}
return nil
}

View file

@ -12,10 +12,53 @@ import (
"context"
"database/sql/driver"
"net"
"os"
"strconv"
"strings"
)
type connector struct {
cfg *Config // immutable private copy.
cfg *Config // immutable private copy.
encodedAttributes string // Encoded connection attributes.
}
func encodeConnectionAttributes(cfg *Config) string {
connAttrsBuf := make([]byte, 0)
// default connection attributes
connAttrsBuf = appendLengthEncodedString(connAttrsBuf, connAttrClientName)
connAttrsBuf = appendLengthEncodedString(connAttrsBuf, connAttrClientNameValue)
connAttrsBuf = appendLengthEncodedString(connAttrsBuf, connAttrOS)
connAttrsBuf = appendLengthEncodedString(connAttrsBuf, connAttrOSValue)
connAttrsBuf = appendLengthEncodedString(connAttrsBuf, connAttrPlatform)
connAttrsBuf = appendLengthEncodedString(connAttrsBuf, connAttrPlatformValue)
connAttrsBuf = appendLengthEncodedString(connAttrsBuf, connAttrPid)
connAttrsBuf = appendLengthEncodedString(connAttrsBuf, strconv.Itoa(os.Getpid()))
serverHost, _, _ := net.SplitHostPort(cfg.Addr)
if serverHost != "" {
connAttrsBuf = appendLengthEncodedString(connAttrsBuf, connAttrServerHost)
connAttrsBuf = appendLengthEncodedString(connAttrsBuf, serverHost)
}
// user-defined connection attributes
for _, connAttr := range strings.Split(cfg.ConnectionAttributes, ",") {
k, v, found := strings.Cut(connAttr, ":")
if !found {
continue
}
connAttrsBuf = appendLengthEncodedString(connAttrsBuf, k)
connAttrsBuf = appendLengthEncodedString(connAttrsBuf, v)
}
return string(connAttrsBuf)
}
func newConnector(cfg *Config) *connector {
encodedAttributes := encodeConnectionAttributes(cfg)
return &connector{
cfg: cfg,
encodedAttributes: encodedAttributes,
}
}
// Connect implements driver.Connector interface.
@ -23,12 +66,23 @@ type connector struct {
func (c *connector) Connect(ctx context.Context) (driver.Conn, error) {
var err error
// Invoke beforeConnect if present, with a copy of the configuration
cfg := c.cfg
if c.cfg.beforeConnect != nil {
cfg = c.cfg.Clone()
err = c.cfg.beforeConnect(ctx, cfg)
if err != nil {
return nil, err
}
}
// New mysqlConn
mc := &mysqlConn{
maxAllowedPacket: maxPacketSize,
maxWriteSize: maxPacketSize - 1,
closech: make(chan struct{}),
cfg: c.cfg,
cfg: cfg,
connector: c,
}
mc.parseTime = mc.cfg.ParseTime
@ -48,18 +102,15 @@ func (c *connector) Connect(ctx context.Context) (driver.Conn, error) {
nd := net.Dialer{Timeout: mc.cfg.Timeout}
mc.netConn, err = nd.DialContext(ctx, mc.cfg.Net, mc.cfg.Addr)
}
if err != nil {
return nil, err
}
mc.rawConn = mc.netConn
// Enable TCP Keepalives on TCP connections
if tc, ok := mc.netConn.(*net.TCPConn); ok {
if err := tc.SetKeepAlive(true); err != nil {
// Don't send COM_QUIT before handshake.
mc.netConn.Close()
mc.netConn = nil
return nil, err
c.cfg.Logger.Print(err)
}
}
@ -92,7 +143,7 @@ func (c *connector) Connect(ctx context.Context) (driver.Conn, error) {
authResp, err := mc.auth(authData, plugin)
if err != nil {
// try the default auth plugin, if using the requested plugin failed
errLog.Print("could not use requested auth plugin '"+plugin+"': ", err.Error())
c.cfg.Logger.Print("could not use requested auth plugin '"+plugin+"': ", err.Error())
plugin = defaultAuthPlugin
authResp, err = mc.auth(authData, plugin)
if err != nil {

View file

@ -8,12 +8,25 @@
package mysql
import "runtime"
const (
defaultAuthPlugin = "mysql_native_password"
defaultMaxAllowedPacket = 64 << 20 // 64 MiB. See https://github.com/go-sql-driver/mysql/issues/1355
minProtocolVersion = 10
maxPacketSize = 1<<24 - 1
timeFormat = "2006-01-02 15:04:05.999999"
// Connection attributes
// See https://dev.mysql.com/doc/refman/8.0/en/performance-schema-connection-attribute-tables.html#performance-schema-connection-attributes-available
connAttrClientName = "_client_name"
connAttrClientNameValue = "Go-MySQL-Driver"
connAttrOS = "_os"
connAttrOSValue = runtime.GOOS
connAttrPlatform = "_platform"
connAttrPlatformValue = runtime.GOARCH
connAttrPid = "_pid"
connAttrServerHost = "_server_host"
)
// MySQL constants documentation:

View file

@ -55,6 +55,15 @@ func RegisterDialContext(net string, dial DialContextFunc) {
dials[net] = dial
}
// DeregisterDialContext removes the custom dial function registered with the given net.
func DeregisterDialContext(net string) {
dialsLock.Lock()
defer dialsLock.Unlock()
if dials != nil {
delete(dials, net)
}
}
// RegisterDial registers a custom dial function. It can then be used by the
// network address mynet(addr), where mynet is the registered new network.
// addr is passed as a parameter to the dial function.
@ -74,14 +83,18 @@ func (d MySQLDriver) Open(dsn string) (driver.Conn, error) {
if err != nil {
return nil, err
}
c := &connector{
cfg: cfg,
}
c := newConnector(cfg)
return c.Connect(context.Background())
}
// This variable can be replaced with -ldflags like below:
// go build "-ldflags=-X github.com/go-sql-driver/mysql.driverName=custom"
var driverName = "mysql"
func init() {
sql.Register("mysql", &MySQLDriver{})
if driverName != "" {
sql.Register(driverName, &MySQLDriver{})
}
}
// NewConnector returns new driver.Connector.
@ -92,7 +105,7 @@ func NewConnector(cfg *Config) (driver.Connector, error) {
if err := cfg.normalize(); err != nil {
return nil, err
}
return &connector{cfg: cfg}, nil
return newConnector(cfg), nil
}
// OpenConnector implements driver.DriverContext.
@ -101,7 +114,5 @@ func (d MySQLDriver) OpenConnector(dsn string) (driver.Connector, error) {
if err != nil {
return nil, err
}
return &connector{
cfg: cfg,
}, nil
return newConnector(cfg), nil
}

View file

@ -10,6 +10,7 @@ package mysql
import (
"bytes"
"context"
"crypto/rsa"
"crypto/tls"
"errors"
@ -34,22 +35,27 @@ var (
// If a new Config is created instead of being parsed from a DSN string,
// the NewConfig function should be used, which sets default values.
type Config struct {
User string // Username
Passwd string // Password (requires User)
Net string // Network type
Addr string // Network address (requires Net)
DBName string // Database name
Params map[string]string // Connection parameters
Collation string // Connection collation
Loc *time.Location // Location for time.Time values
MaxAllowedPacket int // Max packet size allowed
ServerPubKey string // Server public key name
pubKey *rsa.PublicKey // Server public key
TLSConfig string // TLS configuration name
TLS *tls.Config // TLS configuration, its priority is higher than TLSConfig
Timeout time.Duration // Dial timeout
ReadTimeout time.Duration // I/O read timeout
WriteTimeout time.Duration // I/O write timeout
// non boolean fields
User string // Username
Passwd string // Password (requires User)
Net string // Network (e.g. "tcp", "tcp6", "unix". default: "tcp")
Addr string // Address (default: "127.0.0.1:3306" for "tcp" and "/tmp/mysql.sock" for "unix")
DBName string // Database name
Params map[string]string // Connection parameters
ConnectionAttributes string // Connection Attributes, comma-delimited string of user-defined "key:value" pairs
Collation string // Connection collation
Loc *time.Location // Location for time.Time values
MaxAllowedPacket int // Max packet size allowed
ServerPubKey string // Server public key name
TLSConfig string // TLS configuration name
TLS *tls.Config // TLS configuration, its priority is higher than TLSConfig
Timeout time.Duration // Dial timeout
ReadTimeout time.Duration // I/O read timeout
WriteTimeout time.Duration // I/O write timeout
Logger Logger // Logger
// boolean fields
AllowAllFiles bool // Allow all files to be used with LOAD DATA LOCAL INFILE
AllowCleartextPasswords bool // Allows the cleartext client side plugin
@ -63,17 +69,57 @@ type Config struct {
MultiStatements bool // Allow multiple statements in one query
ParseTime bool // Parse time values to time.Time
RejectReadOnly bool // Reject read-only connections
// unexported fields. new options should be come here
beforeConnect func(context.Context, *Config) error // Invoked before a connection is established
pubKey *rsa.PublicKey // Server public key
timeTruncate time.Duration // Truncate time.Time values to the specified duration
}
// Functional Options Pattern
// https://dave.cheney.net/2014/10/17/functional-options-for-friendly-apis
type Option func(*Config) error
// NewConfig creates a new Config and sets default values.
func NewConfig() *Config {
return &Config{
Collation: defaultCollation,
cfg := &Config{
Loc: time.UTC,
MaxAllowedPacket: defaultMaxAllowedPacket,
Logger: defaultLogger,
AllowNativePasswords: true,
CheckConnLiveness: true,
}
return cfg
}
// Apply applies the given options to the Config object.
func (c *Config) Apply(opts ...Option) error {
for _, opt := range opts {
err := opt(c)
if err != nil {
return err
}
}
return nil
}
// TimeTruncate sets the time duration to truncate time.Time values in
// query parameters.
func TimeTruncate(d time.Duration) Option {
return func(cfg *Config) error {
cfg.timeTruncate = d
return nil
}
}
// BeforeConnect sets the function to be invoked before a connection is established.
func BeforeConnect(fn func(context.Context, *Config) error) Option {
return func(cfg *Config) error {
cfg.beforeConnect = fn
return nil
}
}
func (cfg *Config) Clone() *Config {
@ -97,7 +143,7 @@ func (cfg *Config) Clone() *Config {
}
func (cfg *Config) normalize() error {
if cfg.InterpolateParams && unsafeCollations[cfg.Collation] {
if cfg.InterpolateParams && cfg.Collation != "" && unsafeCollations[cfg.Collation] {
return errInvalidDSNUnsafeCollation
}
@ -153,6 +199,10 @@ func (cfg *Config) normalize() error {
}
}
if cfg.Logger == nil {
cfg.Logger = defaultLogger
}
return nil
}
@ -171,6 +221,8 @@ func writeDSNParam(buf *bytes.Buffer, hasParam *bool, name, value string) {
// FormatDSN formats the given Config into a DSN string which can be passed to
// the driver.
//
// Note: use [NewConnector] and [database/sql.OpenDB] to open a connection from a [*Config].
func (cfg *Config) FormatDSN() string {
var buf bytes.Buffer
@ -196,7 +248,7 @@ func (cfg *Config) FormatDSN() string {
// /dbname
buf.WriteByte('/')
buf.WriteString(cfg.DBName)
buf.WriteString(url.PathEscape(cfg.DBName))
// [?param1=value1&...&paramN=valueN]
hasParam := false
@ -230,7 +282,7 @@ func (cfg *Config) FormatDSN() string {
writeDSNParam(&buf, &hasParam, "clientFoundRows", "true")
}
if col := cfg.Collation; col != defaultCollation && len(col) > 0 {
if col := cfg.Collation; col != "" {
writeDSNParam(&buf, &hasParam, "collation", col)
}
@ -254,6 +306,10 @@ func (cfg *Config) FormatDSN() string {
writeDSNParam(&buf, &hasParam, "parseTime", "true")
}
if cfg.timeTruncate > 0 {
writeDSNParam(&buf, &hasParam, "timeTruncate", cfg.timeTruncate.String())
}
if cfg.ReadTimeout > 0 {
writeDSNParam(&buf, &hasParam, "readTimeout", cfg.ReadTimeout.String())
}
@ -358,7 +414,11 @@ func ParseDSN(dsn string) (cfg *Config, err error) {
break
}
}
cfg.DBName = dsn[i+1 : j]
dbname := dsn[i+1 : j]
if cfg.DBName, err = url.PathUnescape(dbname); err != nil {
return nil, fmt.Errorf("invalid dbname %q: %w", dbname, err)
}
break
}
@ -378,13 +438,13 @@ func ParseDSN(dsn string) (cfg *Config, err error) {
// Values must be url.QueryEscape'ed
func parseDSNParams(cfg *Config, params string) (err error) {
for _, v := range strings.Split(params, "&") {
param := strings.SplitN(v, "=", 2)
if len(param) != 2 {
key, value, found := strings.Cut(v, "=")
if !found {
continue
}
// cfg params
switch value := param[1]; param[0] {
switch key {
// Disable INFILE allowlist / enable all files
case "allowAllFiles":
var isBool bool
@ -490,6 +550,13 @@ func parseDSNParams(cfg *Config, params string) (err error) {
return errors.New("invalid bool value: " + value)
}
// time.Time truncation
case "timeTruncate":
cfg.timeTruncate, err = time.ParseDuration(value)
if err != nil {
return fmt.Errorf("invalid timeTruncate value: %v, error: %w", value, err)
}
// I/O read Timeout
case "readTimeout":
cfg.ReadTimeout, err = time.ParseDuration(value)
@ -554,13 +621,22 @@ func parseDSNParams(cfg *Config, params string) (err error) {
if err != nil {
return
}
// Connection attributes
case "connectionAttributes":
connectionAttributes, err := url.QueryUnescape(value)
if err != nil {
return fmt.Errorf("invalid connectionAttributes value: %v", err)
}
cfg.ConnectionAttributes = connectionAttributes
default:
// lazy init
if cfg.Params == nil {
cfg.Params = make(map[string]string)
}
if cfg.Params[param[0]], err = url.QueryUnescape(value); err != nil {
if cfg.Params[key], err = url.QueryUnescape(value); err != nil {
return
}
}

View file

@ -21,7 +21,7 @@ var (
ErrMalformPkt = errors.New("malformed packet")
ErrNoTLS = errors.New("TLS requested but server does not support TLS")
ErrCleartextPassword = errors.New("this user requires clear text authentication. If you still want to use it, please add 'allowCleartextPasswords=1' to your DSN")
ErrNativePassword = errors.New("this user requires mysql native password authentication.")
ErrNativePassword = errors.New("this user requires mysql native password authentication")
ErrOldPassword = errors.New("this user requires old password authentication. If you still want to use it, please add 'allowOldPasswords=1' to your DSN. See also https://github.com/go-sql-driver/mysql/wiki/old_passwords")
ErrUnknownPlugin = errors.New("this authentication plugin is not supported")
ErrOldProtocol = errors.New("MySQL server does not support required protocol 41+")
@ -37,20 +37,26 @@ var (
errBadConnNoWrite = errors.New("bad connection")
)
var errLog = Logger(log.New(os.Stderr, "[mysql] ", log.Ldate|log.Ltime|log.Lshortfile))
var defaultLogger = Logger(log.New(os.Stderr, "[mysql] ", log.Ldate|log.Ltime|log.Lshortfile))
// Logger is used to log critical error messages.
type Logger interface {
Print(v ...interface{})
Print(v ...any)
}
// SetLogger is used to set the logger for critical errors.
// NopLogger is a nop implementation of the Logger interface.
type NopLogger struct{}
// Print implements Logger interface.
func (nl *NopLogger) Print(_ ...any) {}
// SetLogger is used to set the default logger for critical errors.
// The initial logger is os.Stderr.
func SetLogger(logger Logger) error {
if logger == nil {
return errors.New("logger is nil")
}
errLog = logger
defaultLogger = logger
return nil
}

View file

@ -18,7 +18,7 @@ func (mf *mysqlField) typeDatabaseName() string {
case fieldTypeBit:
return "BIT"
case fieldTypeBLOB:
if mf.charSet != collations[binaryCollation] {
if mf.charSet != binaryCollationID {
return "TEXT"
}
return "BLOB"
@ -37,6 +37,9 @@ func (mf *mysqlField) typeDatabaseName() string {
case fieldTypeGeometry:
return "GEOMETRY"
case fieldTypeInt24:
if mf.flags&flagUnsigned != 0 {
return "UNSIGNED MEDIUMINT"
}
return "MEDIUMINT"
case fieldTypeJSON:
return "JSON"
@ -46,7 +49,7 @@ func (mf *mysqlField) typeDatabaseName() string {
}
return "INT"
case fieldTypeLongBLOB:
if mf.charSet != collations[binaryCollation] {
if mf.charSet != binaryCollationID {
return "LONGTEXT"
}
return "LONGBLOB"
@ -56,7 +59,7 @@ func (mf *mysqlField) typeDatabaseName() string {
}
return "BIGINT"
case fieldTypeMediumBLOB:
if mf.charSet != collations[binaryCollation] {
if mf.charSet != binaryCollationID {
return "MEDIUMTEXT"
}
return "MEDIUMBLOB"
@ -74,7 +77,12 @@ func (mf *mysqlField) typeDatabaseName() string {
}
return "SMALLINT"
case fieldTypeString:
if mf.charSet == collations[binaryCollation] {
if mf.flags&flagEnum != 0 {
return "ENUM"
} else if mf.flags&flagSet != 0 {
return "SET"
}
if mf.charSet == binaryCollationID {
return "BINARY"
}
return "CHAR"
@ -88,17 +96,17 @@ func (mf *mysqlField) typeDatabaseName() string {
}
return "TINYINT"
case fieldTypeTinyBLOB:
if mf.charSet != collations[binaryCollation] {
if mf.charSet != binaryCollationID {
return "TINYTEXT"
}
return "TINYBLOB"
case fieldTypeVarChar:
if mf.charSet == collations[binaryCollation] {
if mf.charSet == binaryCollationID {
return "VARBINARY"
}
return "VARCHAR"
case fieldTypeVarString:
if mf.charSet == collations[binaryCollation] {
if mf.charSet == binaryCollationID {
return "VARBINARY"
}
return "VARCHAR"
@ -110,21 +118,23 @@ func (mf *mysqlField) typeDatabaseName() string {
}
var (
scanTypeFloat32 = reflect.TypeOf(float32(0))
scanTypeFloat64 = reflect.TypeOf(float64(0))
scanTypeInt8 = reflect.TypeOf(int8(0))
scanTypeInt16 = reflect.TypeOf(int16(0))
scanTypeInt32 = reflect.TypeOf(int32(0))
scanTypeInt64 = reflect.TypeOf(int64(0))
scanTypeNullFloat = reflect.TypeOf(sql.NullFloat64{})
scanTypeNullInt = reflect.TypeOf(sql.NullInt64{})
scanTypeNullTime = reflect.TypeOf(sql.NullTime{})
scanTypeUint8 = reflect.TypeOf(uint8(0))
scanTypeUint16 = reflect.TypeOf(uint16(0))
scanTypeUint32 = reflect.TypeOf(uint32(0))
scanTypeUint64 = reflect.TypeOf(uint64(0))
scanTypeRawBytes = reflect.TypeOf(sql.RawBytes{})
scanTypeUnknown = reflect.TypeOf(new(interface{}))
scanTypeFloat32 = reflect.TypeOf(float32(0))
scanTypeFloat64 = reflect.TypeOf(float64(0))
scanTypeInt8 = reflect.TypeOf(int8(0))
scanTypeInt16 = reflect.TypeOf(int16(0))
scanTypeInt32 = reflect.TypeOf(int32(0))
scanTypeInt64 = reflect.TypeOf(int64(0))
scanTypeNullFloat = reflect.TypeOf(sql.NullFloat64{})
scanTypeNullInt = reflect.TypeOf(sql.NullInt64{})
scanTypeNullTime = reflect.TypeOf(sql.NullTime{})
scanTypeUint8 = reflect.TypeOf(uint8(0))
scanTypeUint16 = reflect.TypeOf(uint16(0))
scanTypeUint32 = reflect.TypeOf(uint32(0))
scanTypeUint64 = reflect.TypeOf(uint64(0))
scanTypeString = reflect.TypeOf("")
scanTypeNullString = reflect.TypeOf(sql.NullString{})
scanTypeBytes = reflect.TypeOf([]byte{})
scanTypeUnknown = reflect.TypeOf(new(any))
)
type mysqlField struct {
@ -187,12 +197,18 @@ func (mf *mysqlField) scanType() reflect.Type {
}
return scanTypeNullFloat
case fieldTypeBit, fieldTypeTinyBLOB, fieldTypeMediumBLOB, fieldTypeLongBLOB,
fieldTypeBLOB, fieldTypeVarString, fieldTypeString, fieldTypeGeometry:
if mf.charSet == binaryCollationID {
return scanTypeBytes
}
fallthrough
case fieldTypeDecimal, fieldTypeNewDecimal, fieldTypeVarChar,
fieldTypeBit, fieldTypeEnum, fieldTypeSet, fieldTypeTinyBLOB,
fieldTypeMediumBLOB, fieldTypeLongBLOB, fieldTypeBLOB,
fieldTypeVarString, fieldTypeString, fieldTypeGeometry, fieldTypeJSON,
fieldTypeTime:
return scanTypeRawBytes
fieldTypeEnum, fieldTypeSet, fieldTypeJSON, fieldTypeTime:
if mf.flags&flagNotNULL != 0 {
return scanTypeString
}
return scanTypeNullString
case fieldTypeDate, fieldTypeNewDate,
fieldTypeTimestamp, fieldTypeDateTime:

View file

@ -1,25 +0,0 @@
// Go MySQL Driver - A MySQL-Driver for Go's database/sql package.
//
// Copyright 2020 The Go-MySQL-Driver Authors. All rights reserved.
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
// You can obtain one at http://mozilla.org/MPL/2.0/.
//go:build gofuzz
// +build gofuzz
package mysql
import (
"database/sql"
)
func Fuzz(data []byte) int {
db, err := sql.Open("mysql", string(data))
if err != nil {
return 0
}
db.Close()
return 1
}

View file

@ -93,7 +93,7 @@ func deferredClose(err *error, closer io.Closer) {
const defaultPacketSize = 16 * 1024 // 16KB is small enough for disk readahead and large enough for TCP
func (mc *mysqlConn) handleInFileRequest(name string) (err error) {
func (mc *okHandler) handleInFileRequest(name string) (err error) {
var rdr io.Reader
var data []byte
packetSize := defaultPacketSize
@ -116,10 +116,10 @@ func (mc *mysqlConn) handleInFileRequest(name string) (err error) {
defer deferredClose(&err, cl)
}
} else {
err = fmt.Errorf("Reader '%s' is <nil>", name)
err = fmt.Errorf("reader '%s' is <nil>", name)
}
} else {
err = fmt.Errorf("Reader '%s' is not registered", name)
err = fmt.Errorf("reader '%s' is not registered", name)
}
} else { // File
name = strings.Trim(name, `"`)
@ -154,7 +154,7 @@ func (mc *mysqlConn) handleInFileRequest(name string) (err error) {
for err == nil {
n, err = rdr.Read(data[4:])
if n > 0 {
if ioErr := mc.writePacket(data[:4+n]); ioErr != nil {
if ioErr := mc.conn().writePacket(data[:4+n]); ioErr != nil {
return ioErr
}
}
@ -168,7 +168,7 @@ func (mc *mysqlConn) handleInFileRequest(name string) (err error) {
if data == nil {
data = make([]byte, 4)
}
if ioErr := mc.writePacket(data[:4]); ioErr != nil {
if ioErr := mc.conn().writePacket(data[:4]); ioErr != nil {
return ioErr
}
@ -177,6 +177,6 @@ func (mc *mysqlConn) handleInFileRequest(name string) (err error) {
return mc.readResultOK()
}
mc.readPacket()
mc.conn().readPacket()
return err
}

View file

@ -38,7 +38,7 @@ type NullTime sql.NullTime
// Scan implements the Scanner interface.
// The value type must be time.Time or string / []byte (formatted time-string),
// otherwise Scan fails.
func (nt *NullTime) Scan(value interface{}) (err error) {
func (nt *NullTime) Scan(value any) (err error) {
if value == nil {
nt.Time, nt.Valid = time.Time{}, false
return
@ -59,7 +59,7 @@ func (nt *NullTime) Scan(value interface{}) (err error) {
}
nt.Valid = false
return fmt.Errorf("Can't convert %T to time.Time", value)
return fmt.Errorf("can't convert %T to time.Time", value)
}
// Value implements the driver Valuer interface.

View file

@ -14,10 +14,10 @@ import (
"database/sql/driver"
"encoding/binary"
"encoding/json"
"errors"
"fmt"
"io"
"math"
"strconv"
"time"
)
@ -34,7 +34,7 @@ func (mc *mysqlConn) readPacket() ([]byte, error) {
if cerr := mc.canceled.Value(); cerr != nil {
return nil, cerr
}
errLog.Print(err)
mc.log(err)
mc.Close()
return nil, ErrInvalidConn
}
@ -44,6 +44,7 @@ func (mc *mysqlConn) readPacket() ([]byte, error) {
// check packet sync [8 bit]
if data[3] != mc.sequence {
mc.Close()
if data[3] > mc.sequence {
return nil, ErrPktSyncMul
}
@ -56,7 +57,7 @@ func (mc *mysqlConn) readPacket() ([]byte, error) {
if pktLen == 0 {
// there was no previous packet
if prevData == nil {
errLog.Print(ErrMalformPkt)
mc.log(ErrMalformPkt)
mc.Close()
return nil, ErrInvalidConn
}
@ -70,7 +71,7 @@ func (mc *mysqlConn) readPacket() ([]byte, error) {
if cerr := mc.canceled.Value(); cerr != nil {
return nil, cerr
}
errLog.Print(err)
mc.log(err)
mc.Close()
return nil, ErrInvalidConn
}
@ -97,34 +98,6 @@ func (mc *mysqlConn) writePacket(data []byte) error {
return ErrPktTooLarge
}
// Perform a stale connection check. We only perform this check for
// the first query on a connection that has been checked out of the
// connection pool: a fresh connection from the pool is more likely
// to be stale, and it has not performed any previous writes that
// could cause data corruption, so it's safe to return ErrBadConn
// if the check fails.
if mc.reset {
mc.reset = false
conn := mc.netConn
if mc.rawConn != nil {
conn = mc.rawConn
}
var err error
if mc.cfg.CheckConnLiveness {
if mc.cfg.ReadTimeout != 0 {
err = conn.SetReadDeadline(time.Now().Add(mc.cfg.ReadTimeout))
}
if err == nil {
err = connCheck(conn)
}
}
if err != nil {
errLog.Print("closing bad idle connection: ", err)
mc.Close()
return driver.ErrBadConn
}
}
for {
var size int
if pktLen >= maxPacketSize {
@ -161,7 +134,7 @@ func (mc *mysqlConn) writePacket(data []byte) error {
// Handle error
if err == nil { // n != len(data)
mc.cleanup()
errLog.Print(ErrMalformPkt)
mc.log(ErrMalformPkt)
} else {
if cerr := mc.canceled.Value(); cerr != nil {
return cerr
@ -171,7 +144,7 @@ func (mc *mysqlConn) writePacket(data []byte) error {
return errBadConnNoWrite
}
mc.cleanup()
errLog.Print(err)
mc.log(err)
}
return ErrInvalidConn
}
@ -239,7 +212,7 @@ func (mc *mysqlConn) readHandshakePacket() (data []byte, plugin string, err erro
// reserved (all [00]) [10 bytes]
pos += 1 + 2 + 2 + 1 + 10
// second part of the password cipher [mininum 13 bytes],
// second part of the password cipher [minimum 13 bytes],
// where len=MAX(13, length of auth-plugin-data - 8)
//
// The web documentation is ambiguous about the length. However,
@ -285,6 +258,7 @@ func (mc *mysqlConn) writeHandshakeResponsePacket(authResp []byte, plugin string
clientLocalFiles |
clientPluginAuth |
clientMultiResults |
clientConnectAttrs |
mc.flags&clientLongFlag
if mc.cfg.ClientFoundRows {
@ -318,11 +292,17 @@ func (mc *mysqlConn) writeHandshakeResponsePacket(authResp []byte, plugin string
pktLen += n + 1
}
// encode length of the connection attributes
var connAttrsLEIBuf [9]byte
connAttrsLen := len(mc.connector.encodedAttributes)
connAttrsLEI := appendLengthEncodedInteger(connAttrsLEIBuf[:0], uint64(connAttrsLen))
pktLen += len(connAttrsLEI) + len(mc.connector.encodedAttributes)
// Calculate packet length and get buffer with that size
data, err := mc.buf.takeSmallBuffer(pktLen + 4)
data, err := mc.buf.takeBuffer(pktLen + 4)
if err != nil {
// cannot take the buffer. Something must be wrong with the connection
errLog.Print(err)
mc.log(err)
return errBadConnNoWrite
}
@ -338,14 +318,18 @@ func (mc *mysqlConn) writeHandshakeResponsePacket(authResp []byte, plugin string
data[10] = 0x00
data[11] = 0x00
// Charset [1 byte]
// Collation ID [1 byte]
cname := mc.cfg.Collation
if cname == "" {
cname = defaultCollation
}
var found bool
data[12], found = collations[mc.cfg.Collation]
data[12], found = collations[cname]
if !found {
// Note possibility for false negatives:
// could be triggered although the collation is valid if the
// collations map does not contain entries the server supports.
return errors.New("unknown collation")
return fmt.Errorf("unknown collation: %q", cname)
}
// Filler [23 bytes] (all 0x00)
@ -367,7 +351,6 @@ func (mc *mysqlConn) writeHandshakeResponsePacket(authResp []byte, plugin string
if err := tlsConn.Handshake(); err != nil {
return err
}
mc.rawConn = mc.netConn
mc.netConn = tlsConn
mc.buf.nc = tlsConn
}
@ -394,6 +377,10 @@ func (mc *mysqlConn) writeHandshakeResponsePacket(authResp []byte, plugin string
data[pos] = 0x00
pos++
// Connection Attributes
pos += copy(data[pos:], connAttrsLEI)
pos += copy(data[pos:], []byte(mc.connector.encodedAttributes))
// Send Auth packet
return mc.writePacket(data[:pos])
}
@ -404,7 +391,7 @@ func (mc *mysqlConn) writeAuthSwitchPacket(authData []byte) error {
data, err := mc.buf.takeSmallBuffer(pktLen)
if err != nil {
// cannot take the buffer. Something must be wrong with the connection
errLog.Print(err)
mc.log(err)
return errBadConnNoWrite
}
@ -424,7 +411,7 @@ func (mc *mysqlConn) writeCommandPacket(command byte) error {
data, err := mc.buf.takeSmallBuffer(4 + 1)
if err != nil {
// cannot take the buffer. Something must be wrong with the connection
errLog.Print(err)
mc.log(err)
return errBadConnNoWrite
}
@ -443,7 +430,7 @@ func (mc *mysqlConn) writeCommandPacketStr(command byte, arg string) error {
data, err := mc.buf.takeBuffer(pktLen + 4)
if err != nil {
// cannot take the buffer. Something must be wrong with the connection
errLog.Print(err)
mc.log(err)
return errBadConnNoWrite
}
@ -464,7 +451,7 @@ func (mc *mysqlConn) writeCommandPacketUint32(command byte, arg uint32) error {
data, err := mc.buf.takeSmallBuffer(4 + 1 + 4)
if err != nil {
// cannot take the buffer. Something must be wrong with the connection
errLog.Print(err)
mc.log(err)
return errBadConnNoWrite
}
@ -495,7 +482,9 @@ func (mc *mysqlConn) readAuthResult() ([]byte, string, error) {
switch data[0] {
case iOK:
return nil, "", mc.handleOkPacket(data)
// resultUnchanged, since auth happens before any queries or
// commands have been executed.
return nil, "", mc.resultUnchanged().handleOkPacket(data)
case iAuthMoreData:
return data[1:], "", err
@ -518,9 +507,9 @@ func (mc *mysqlConn) readAuthResult() ([]byte, string, error) {
}
}
// Returns error if Packet is not an 'Result OK'-Packet
func (mc *mysqlConn) readResultOK() error {
data, err := mc.readPacket()
// Returns error if Packet is not a 'Result OK'-Packet
func (mc *okHandler) readResultOK() error {
data, err := mc.conn().readPacket()
if err != nil {
return err
}
@ -528,13 +517,17 @@ func (mc *mysqlConn) readResultOK() error {
if data[0] == iOK {
return mc.handleOkPacket(data)
}
return mc.handleErrorPacket(data)
return mc.conn().handleErrorPacket(data)
}
// Result Set Header Packet
// http://dev.mysql.com/doc/internals/en/com-query-response.html#packet-ProtocolText::Resultset
func (mc *mysqlConn) readResultSetHeaderPacket() (int, error) {
data, err := mc.readPacket()
func (mc *okHandler) readResultSetHeaderPacket() (int, error) {
// handleOkPacket replaces both values; other cases leave the values unchanged.
mc.result.affectedRows = append(mc.result.affectedRows, 0)
mc.result.insertIds = append(mc.result.insertIds, 0)
data, err := mc.conn().readPacket()
if err == nil {
switch data[0] {
@ -542,19 +535,16 @@ func (mc *mysqlConn) readResultSetHeaderPacket() (int, error) {
return 0, mc.handleOkPacket(data)
case iERR:
return 0, mc.handleErrorPacket(data)
return 0, mc.conn().handleErrorPacket(data)
case iLocalInFile:
return 0, mc.handleInFileRequest(string(data[1:]))
}
// column count
num, _, n := readLengthEncodedInteger(data)
if n-len(data) == 0 {
return int(num), nil
}
return 0, ErrMalformPkt
num, _, _ := readLengthEncodedInteger(data)
// ignore remaining data in the packet. see #1478.
return int(num), nil
}
return 0, err
}
@ -607,18 +597,61 @@ func readStatus(b []byte) statusFlag {
return statusFlag(b[0]) | statusFlag(b[1])<<8
}
// Returns an instance of okHandler for codepaths where mysqlConn.result doesn't
// need to be cleared first (e.g. during authentication, or while additional
// resultsets are being fetched.)
func (mc *mysqlConn) resultUnchanged() *okHandler {
return (*okHandler)(mc)
}
// okHandler represents the state of the connection when mysqlConn.result has
// been prepared for processing of OK packets.
//
// To correctly populate mysqlConn.result (updated by handleOkPacket()), all
// callpaths must either:
//
// 1. first clear it using clearResult(), or
// 2. confirm that they don't need to (by calling resultUnchanged()).
//
// Both return an instance of type *okHandler.
type okHandler mysqlConn
// Exposes the underlying type's methods.
func (mc *okHandler) conn() *mysqlConn {
return (*mysqlConn)(mc)
}
// clearResult clears the connection's stored affectedRows and insertIds
// fields.
//
// It returns a handler that can process OK responses.
func (mc *mysqlConn) clearResult() *okHandler {
mc.result = mysqlResult{}
return (*okHandler)(mc)
}
// Ok Packet
// http://dev.mysql.com/doc/internals/en/generic-response-packets.html#packet-OK_Packet
func (mc *mysqlConn) handleOkPacket(data []byte) error {
func (mc *okHandler) handleOkPacket(data []byte) error {
var n, m int
var affectedRows, insertId uint64
// 0x00 [1 byte]
// Affected rows [Length Coded Binary]
mc.affectedRows, _, n = readLengthEncodedInteger(data[1:])
affectedRows, _, n = readLengthEncodedInteger(data[1:])
// Insert id [Length Coded Binary]
mc.insertId, _, m = readLengthEncodedInteger(data[1+n:])
insertId, _, m = readLengthEncodedInteger(data[1+n:])
// Update for the current statement result (only used by
// readResultSetHeaderPacket).
if len(mc.result.affectedRows) > 0 {
mc.result.affectedRows[len(mc.result.affectedRows)-1] = int64(affectedRows)
}
if len(mc.result.insertIds) > 0 {
mc.result.insertIds[len(mc.result.insertIds)-1] = int64(insertId)
}
// server_status [2 bytes]
mc.status = readStatus(data[1+n+m : 1+n+m+2])
@ -769,7 +802,8 @@ func (rows *textRows) readRow(dest []driver.Value) error {
for i := range dest {
// Read bytes and convert to string
dest[i], isNull, n, err = readLengthEncodedString(data[pos:])
var buf []byte
buf, isNull, n, err = readLengthEncodedString(data[pos:])
pos += n
if err != nil {
@ -781,19 +815,40 @@ func (rows *textRows) readRow(dest []driver.Value) error {
continue
}
if !mc.parseTime {
continue
}
// Parse time field
switch rows.rs.columns[i].fieldType {
case fieldTypeTimestamp,
fieldTypeDateTime,
fieldTypeDate,
fieldTypeNewDate:
if dest[i], err = parseDateTime(dest[i].([]byte), mc.cfg.Loc); err != nil {
return err
if mc.parseTime {
dest[i], err = parseDateTime(buf, mc.cfg.Loc)
} else {
dest[i] = buf
}
case fieldTypeTiny, fieldTypeShort, fieldTypeInt24, fieldTypeYear, fieldTypeLong:
dest[i], err = strconv.ParseInt(string(buf), 10, 64)
case fieldTypeLongLong:
if rows.rs.columns[i].flags&flagUnsigned != 0 {
dest[i], err = strconv.ParseUint(string(buf), 10, 64)
} else {
dest[i], err = strconv.ParseInt(string(buf), 10, 64)
}
case fieldTypeFloat:
var d float64
d, err = strconv.ParseFloat(string(buf), 32)
dest[i] = float32(d)
case fieldTypeDouble:
dest[i], err = strconv.ParseFloat(string(buf), 64)
default:
dest[i] = buf
}
if err != nil {
return err
}
}
@ -938,7 +993,7 @@ func (stmt *mysqlStmt) writeExecutePacket(args []driver.Value) error {
}
if err != nil {
// cannot take the buffer. Something must be wrong with the connection
errLog.Print(err)
mc.log(err)
return errBadConnNoWrite
}
@ -1116,7 +1171,7 @@ func (stmt *mysqlStmt) writeExecutePacket(args []driver.Value) error {
if v.IsZero() {
b = append(b, "0000-00-00"...)
} else {
b, err = appendDateTime(b, v.In(mc.cfg.Loc))
b, err = appendDateTime(b, v.In(mc.cfg.Loc), mc.cfg.timeTruncate)
if err != nil {
return err
}
@ -1137,7 +1192,7 @@ func (stmt *mysqlStmt) writeExecutePacket(args []driver.Value) error {
if valuesCap != cap(paramValues) {
data = append(data[:pos], paramValues...)
if err = mc.buf.store(data); err != nil {
errLog.Print(err)
mc.log(err)
return errBadConnNoWrite
}
}
@ -1149,7 +1204,9 @@ func (stmt *mysqlStmt) writeExecutePacket(args []driver.Value) error {
return mc.writePacket(data)
}
func (mc *mysqlConn) discardResults() error {
// For each remaining resultset in the stream, discards its rows and updates
// mc.affectedRows and mc.insertIds.
func (mc *okHandler) discardResults() error {
for mc.status&statusMoreResultsExists != 0 {
resLen, err := mc.readResultSetHeaderPacket()
if err != nil {
@ -1157,11 +1214,11 @@ func (mc *mysqlConn) discardResults() error {
}
if resLen > 0 {
// columns
if err := mc.readUntilEOF(); err != nil {
if err := mc.conn().readUntilEOF(); err != nil {
return err
}
// rows
if err := mc.readUntilEOF(); err != nil {
if err := mc.conn().readUntilEOF(); err != nil {
return err
}
}

View file

@ -8,15 +8,43 @@
package mysql
import "database/sql/driver"
// Result exposes data not available through *connection.Result.
//
// This is accessible by executing statements using sql.Conn.Raw() and
// downcasting the returned result:
//
// res, err := rawConn.Exec(...)
// res.(mysql.Result).AllRowsAffected()
type Result interface {
driver.Result
// AllRowsAffected returns a slice containing the affected rows for each
// executed statement.
AllRowsAffected() []int64
// AllLastInsertIds returns a slice containing the last inserted ID for each
// executed statement.
AllLastInsertIds() []int64
}
type mysqlResult struct {
affectedRows int64
insertId int64
// One entry in both slices is created for every executed statement result.
affectedRows []int64
insertIds []int64
}
func (res *mysqlResult) LastInsertId() (int64, error) {
return res.insertId, nil
return res.insertIds[len(res.insertIds)-1], nil
}
func (res *mysqlResult) RowsAffected() (int64, error) {
return res.affectedRows, nil
return res.affectedRows[len(res.affectedRows)-1], nil
}
func (res *mysqlResult) AllLastInsertIds() []int64 {
return append([]int64{}, res.insertIds...) // defensive copy
}
func (res *mysqlResult) AllRowsAffected() []int64 {
return append([]int64{}, res.affectedRows...) // defensive copy
}

View file

@ -123,7 +123,8 @@ func (rows *mysqlRows) Close() (err error) {
err = mc.readUntilEOF()
}
if err == nil {
if err = mc.discardResults(); err != nil {
handleOk := mc.clearResult()
if err = handleOk.discardResults(); err != nil {
return err
}
}
@ -160,7 +161,15 @@ func (rows *mysqlRows) nextResultSet() (int, error) {
return 0, io.EOF
}
rows.rs = resultSet{}
return rows.mc.readResultSetHeaderPacket()
// rows.mc.affectedRows and rows.mc.insertIds accumulate on each call to
// nextResultSet.
resLen, err := rows.mc.resultUnchanged().readResultSetHeaderPacket()
if err != nil {
// Clean up about multi-results flag
rows.rs.done = true
rows.mc.status = rows.mc.status & (^statusMoreResultsExists)
}
return resLen, err
}
func (rows *mysqlRows) nextNotEmptyResultSet() (int, error) {

View file

@ -51,7 +51,7 @@ func (stmt *mysqlStmt) CheckNamedValue(nv *driver.NamedValue) (err error) {
func (stmt *mysqlStmt) Exec(args []driver.Value) (driver.Result, error) {
if stmt.mc.closed.Load() {
errLog.Print(ErrInvalidConn)
stmt.mc.log(ErrInvalidConn)
return nil, driver.ErrBadConn
}
// Send command
@ -61,12 +61,10 @@ func (stmt *mysqlStmt) Exec(args []driver.Value) (driver.Result, error) {
}
mc := stmt.mc
mc.affectedRows = 0
mc.insertId = 0
handleOk := stmt.mc.clearResult()
// Read Result
resLen, err := mc.readResultSetHeaderPacket()
resLen, err := handleOk.readResultSetHeaderPacket()
if err != nil {
return nil, err
}
@ -83,14 +81,12 @@ func (stmt *mysqlStmt) Exec(args []driver.Value) (driver.Result, error) {
}
}
if err := mc.discardResults(); err != nil {
if err := handleOk.discardResults(); err != nil {
return nil, err
}
return &mysqlResult{
affectedRows: int64(mc.affectedRows),
insertId: int64(mc.insertId),
}, nil
copied := mc.result
return &copied, nil
}
func (stmt *mysqlStmt) Query(args []driver.Value) (driver.Rows, error) {
@ -99,7 +95,7 @@ func (stmt *mysqlStmt) Query(args []driver.Value) (driver.Rows, error) {
func (stmt *mysqlStmt) query(args []driver.Value) (*binaryRows, error) {
if stmt.mc.closed.Load() {
errLog.Print(ErrInvalidConn)
stmt.mc.log(ErrInvalidConn)
return nil, driver.ErrBadConn
}
// Send command
@ -111,7 +107,8 @@ func (stmt *mysqlStmt) query(args []driver.Value) (*binaryRows, error) {
mc := stmt.mc
// Read Result
resLen, err := mc.readResultSetHeaderPacket()
handleOk := stmt.mc.clearResult()
resLen, err := handleOk.readResultSetHeaderPacket()
if err != nil {
return nil, err
}
@ -144,7 +141,7 @@ type converter struct{}
// implementation does not. This function should be kept in sync with
// database/sql/driver defaultConverter.ConvertValue() except for that
// deliberate difference.
func (c converter) ConvertValue(v interface{}) (driver.Value, error) {
func (c converter) ConvertValue(v any) (driver.Value, error) {
if driver.IsValue(v) {
return v, nil
}

View file

@ -36,7 +36,7 @@ var (
// registering it.
//
// rootCertPool := x509.NewCertPool()
// pem, err := ioutil.ReadFile("/path/ca-cert.pem")
// pem, err := os.ReadFile("/path/ca-cert.pem")
// if err != nil {
// log.Fatal(err)
// }
@ -265,7 +265,11 @@ func parseBinaryDateTime(num uint64, data []byte, loc *time.Location) (driver.Va
return nil, fmt.Errorf("invalid DATETIME packet length %d", num)
}
func appendDateTime(buf []byte, t time.Time) ([]byte, error) {
func appendDateTime(buf []byte, t time.Time, timeTruncate time.Duration) ([]byte, error) {
if timeTruncate > 0 {
t = t.Truncate(timeTruncate)
}
year, month, day := t.Date()
hour, min, sec := t.Clock()
nsec := t.Nanosecond()
@ -616,6 +620,11 @@ func appendLengthEncodedInteger(b []byte, n uint64) []byte {
byte(n>>32), byte(n>>40), byte(n>>48), byte(n>>56))
}
func appendLengthEncodedString(b []byte, s string) []byte {
b = appendLengthEncodedInteger(b, uint64(len(s)))
return append(b, s...)
}
// reserveBuffer checks cap(buf) and expand buffer to len(buf) + appendSize.
// If cap(buf) is not enough, reallocate new buffer.
func reserveBuffer(buf []byte, appendSize int) []byte {

View file

@ -62,7 +62,7 @@ func (m *SigningMethodECDSA) Verify(signingString string, sig []byte, key interf
case *ecdsa.PublicKey:
ecdsaKey = k
default:
return newError("ECDSA verify expects *ecsda.PublicKey", ErrInvalidKeyType)
return newError("ECDSA verify expects *ecdsa.PublicKey", ErrInvalidKeyType)
}
if len(sig) != 2*m.KeySize {
@ -96,7 +96,7 @@ func (m *SigningMethodECDSA) Sign(signingString string, key interface{}) ([]byte
case *ecdsa.PrivateKey:
ecdsaKey = k
default:
return nil, newError("ECDSA sign expects *ecsda.PrivateKey", ErrInvalidKeyType)
return nil, newError("ECDSA sign expects *ecdsa.PrivateKey", ErrInvalidKeyType)
}
// Create the hasher

Some files were not shown because too many files have changed in this diff Show more