alertmanager/vendor/github.com/go-openapi/validate/result.go
Max Leonard Inden f1b920bcc9
api: Implement OpenAPI generated Alertmanager API V2
The current Alertmanager API v1 is undocumented and written by hand.
This patch introduces a new Alertmanager API - v2. The API is fully
generated via an OpenAPI 2.0 [1] specification (see
`api/v2/openapi.yaml`) with the exception of the http handlers itself.

Pros:
- Generated server code
- Ability to generate clients in all major languages
  (Go, Java, JS, Python, Ruby, Haskell, *elm* [3] ...)
    - Strict contract (OpenAPI spec) between server and clients.
    - Instant feedback on frontend-breaking changes, due to strictly
      typed frontend language elm.
- Generated documentation (See Alertmanager online Swagger UI [4])

Cons:
- Dependency on open api ecosystem including go-swagger [2]

In addition this patch includes the following changes.

- README.md: Add API section

- test: Duplicate acceptance test to API v1 & API v2 version

  The Alertmanager acceptance test framework has a decent test coverage
  on the Alertmanager API. Introducing the Alertmanager API v2 does not go
  hand in hand with deprecating API v1. They should live alongside each
  other for a couple of minor Alertmanager versions.

  Instead of porting the acceptance test framework to use the new API v2,
  this patch duplicates the acceptance tests, one using the API v1, the
  other API v2.

  Once API v1 is removed we can simply remove `test/with_api_v1` and bring
  `test/with_api_v2` to `test/`.

[1]
https://github.com/OAI/OpenAPI-Specification/blob/master/versions/2.0.md

[2] https://github.com/go-swagger/go-swagger/

[3] https://github.com/ahultgren/swagger-elm

[4]
http://petstore.swagger.io/?url=https://raw.githubusercontent.com/mxinden/alertmanager/apiv2/api/v2/openapi.yaml

Signed-off-by: Max Leonard Inden <IndenML@gmail.com>
2018-09-04 13:38:34 +02:00

485 lines
13 KiB
Go

// Copyright 2015 go-swagger maintainers
//
// 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 validate
import (
"fmt"
"reflect"
"strings"
"github.com/go-openapi/errors"
"github.com/go-openapi/spec"
)
// Result represents a validation result set, composed of
// errors and warnings.
//
// It is used to keep track of all detected errors and warnings during
// the validation of a specification.
//
// Matchcount is used to determine
// which errors are relevant in the case of AnyOf, OneOf
// schema validation. Results from the validation branch
// with most matches get eventually selected.
//
// TODO: keep path of key originating the error
type Result struct {
Errors []error
Warnings []error
MatchCount int
// the object data
data interface{}
// Schemata for the root object
rootObjectSchemata schemata
// Schemata for object fields
fieldSchemata []fieldSchemata
// Schemata for slice items
itemSchemata []itemSchemata
cachedFieldSchemta map[FieldKey][]*spec.Schema
cachedItemSchemata map[ItemKey][]*spec.Schema
}
// FieldKey is a pair of an object and a field, usable as a key for a map.
type FieldKey struct {
object reflect.Value // actually a map[string]interface{}, but the latter cannot be a key
field string
}
// ItemKey is a pair of a slice and an index, usable as a key for a map.
type ItemKey struct {
slice reflect.Value // actually a []interface{}, but the latter cannot be a key
index int
}
// NewFieldKey returns a pair of an object and field usable as a key of a map.
func NewFieldKey(obj map[string]interface{}, field string) FieldKey {
return FieldKey{object: reflect.ValueOf(obj), field: field}
}
// Object returns the underlying object of this key.
func (fk *FieldKey) Object() map[string]interface{} {
return fk.object.Interface().(map[string]interface{})
}
// Field returns the underlying field of this key.
func (fk *FieldKey) Field() string {
return fk.field
}
// NewItemKey returns a pair of a slice and index usable as a key of a map.
func NewItemKey(slice interface{}, i int) ItemKey {
return ItemKey{slice: reflect.ValueOf(slice), index: i}
}
// Slice returns the underlying slice of this key.
func (ik *ItemKey) Slice() []interface{} {
return ik.slice.Interface().([]interface{})
}
// Index returns the underlying index of this key.
func (ik *ItemKey) Index() int {
return ik.index
}
type fieldSchemata struct {
obj map[string]interface{}
field string
schemata schemata
}
type itemSchemata struct {
slice reflect.Value
index int
schemata schemata
}
// Merge merges this result with the other one(s), preserving match counts etc.
func (r *Result) Merge(others ...*Result) *Result {
for _, other := range others {
if other == nil {
continue
}
r.mergeWithoutRootSchemata(other)
r.rootObjectSchemata.Append(other.rootObjectSchemata)
}
return r
}
// Data returns the original data object used for validation. Mutating this renders
// the result invalid.
func (r *Result) Data() interface{} {
return r.data
}
// RootObjectSchemata returns the schemata which apply to the root object.
func (r *Result) RootObjectSchemata() []*spec.Schema {
return r.rootObjectSchemata.Slice()
}
// 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
}
ret := make(map[FieldKey][]*spec.Schema, len(r.fieldSchemata))
for _, fs := range r.fieldSchemata {
key := NewFieldKey(fs.obj, fs.field)
if fs.schemata.one != nil {
ret[key] = append(ret[key], fs.schemata.one)
} else if len(fs.schemata.multiple) > 0 {
ret[key] = append(ret[key], fs.schemata.multiple...)
}
}
r.cachedFieldSchemta = ret
return ret
}
// ItemSchemata returns the schemata which apply to items in slices.
func (r *Result) ItemSchemata() map[ItemKey][]*spec.Schema {
if r.cachedItemSchemata != nil {
return r.cachedItemSchemata
}
ret := make(map[ItemKey][]*spec.Schema, len(r.itemSchemata))
for _, ss := range r.itemSchemata {
key := NewItemKey(ss.slice, ss.index)
if ss.schemata.one != nil {
ret[key] = append(ret[key], ss.schemata.one)
} else if len(ss.schemata.multiple) > 0 {
ret[key] = append(ret[key], ss.schemata.multiple...)
}
}
r.cachedItemSchemata = ret
return ret
}
func (r *Result) resetCaches() {
r.cachedFieldSchemta = nil
r.cachedItemSchemata = nil
}
// mergeForField merges other into r, assigning other's root schemata to the given Object and field name.
func (r *Result) mergeForField(obj map[string]interface{}, field string, other *Result) *Result {
if other == nil {
return r
}
r.mergeWithoutRootSchemata(other)
if other.rootObjectSchemata.Len() > 0 {
if r.fieldSchemata == nil {
r.fieldSchemata = make([]fieldSchemata, len(obj))
}
r.fieldSchemata = append(r.fieldSchemata, fieldSchemata{
obj: obj,
field: field,
schemata: other.rootObjectSchemata,
})
}
return r
}
// mergeForSlice merges other into r, assigning other's root schemata to the given slice and index.
func (r *Result) mergeForSlice(slice reflect.Value, i int, other *Result) *Result {
if other == nil {
return r
}
r.mergeWithoutRootSchemata(other)
if other.rootObjectSchemata.Len() > 0 {
if r.itemSchemata == nil {
r.itemSchemata = make([]itemSchemata, slice.Len())
}
r.itemSchemata = append(r.itemSchemata, itemSchemata{
slice: slice,
index: i,
schemata: other.rootObjectSchemata,
})
}
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.
func (r *Result) addRootObjectSchemata(s *spec.Schema) {
r.rootObjectSchemata.Append(schemata{one: s})
}
// 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.
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}})
}
// addSliceSchemata adds the given schemata for the slice and index.
// The slice schemata might be reused. I.e. do not modify it after being added to a result.
func (r *Result) addSliceSchemata(slice reflect.Value, i int, schema *spec.Schema) {
if r.itemSchemata == nil {
r.itemSchemata = make([]itemSchemata, 0, slice.Len())
}
r.itemSchemata = append(r.itemSchemata, itemSchemata{slice: slice, index: i, schemata: schemata{one: schema}})
}
// mergeWithoutRootSchemata merges other into r, ignoring the rootObject schemata.
func (r *Result) mergeWithoutRootSchemata(other *Result) {
r.resetCaches()
r.AddErrors(other.Errors...)
r.AddWarnings(other.Warnings...)
r.MatchCount += other.MatchCount
if other.fieldSchemata != nil {
if r.fieldSchemata == nil {
r.fieldSchemata = other.fieldSchemata
} else {
for _, x := range other.fieldSchemata {
r.fieldSchemata = append(r.fieldSchemata, x)
}
}
}
if other.itemSchemata != nil {
if r.itemSchemata == nil {
r.itemSchemata = other.itemSchemata
} else {
for _, x := range other.itemSchemata {
r.itemSchemata = append(r.itemSchemata, x)
}
}
}
}
// MergeAsErrors merges this result with the other one(s), preserving match counts etc.
//
// Warnings from input are merged as Errors in the returned merged Result.
func (r *Result) MergeAsErrors(others ...*Result) *Result {
for _, other := range others {
if other != nil {
r.resetCaches()
r.AddErrors(other.Errors...)
r.AddErrors(other.Warnings...)
r.MatchCount += other.MatchCount
}
}
return r
}
// MergeAsWarnings merges this result with the other one(s), preserving match counts etc.
//
// Errors from input are merged as Warnings in the returned merged Result.
func (r *Result) MergeAsWarnings(others ...*Result) *Result {
for _, other := range others {
if other != nil {
r.resetCaches()
r.AddWarnings(other.Errors...)
r.AddWarnings(other.Warnings...)
r.MatchCount += other.MatchCount
}
}
return r
}
// AddErrors adds errors to this validation result (if not already reported).
//
// Since the same check may be passed several times while exploring the
// spec structure (via $ref, ...) reported messages are kept
// unique.
func (r *Result) AddErrors(errors ...error) {
for _, e := range errors {
found := false
if e != nil {
for _, isReported := range r.Errors {
if e.Error() == isReported.Error() {
found = true
break
}
}
if !found {
r.Errors = append(r.Errors, e)
}
}
}
}
// AddWarnings adds warnings to this validation result (if not already reported).
func (r *Result) AddWarnings(warnings ...error) {
for _, e := range warnings {
found := false
if e != nil {
for _, isReported := range r.Warnings {
if e.Error() == isReported.Error() {
found = true
break
}
}
if !found {
r.Warnings = append(r.Warnings, e)
}
}
}
}
func (r *Result) keepRelevantErrors() *Result {
// TODO: this one is going to disapear...
// keepRelevantErrors strips a result from standard errors and keeps
// the ones which are supposedly more accurate.
//
// The original result remains unaffected (creates a new instance of Result).
// This method is used to work around the "matchCount" filter which would otherwise
// strip our result from some accurate error reporting from lower level validators.
//
// NOTE: this implementation with a placeholder (IMPORTANT!) is neither clean nor
// very efficient. On the other hand, relying on go-openapi/errors to manipulate
// codes would require to change a lot here. So, for the moment, let's go with
// placeholders.
strippedErrors := []error{}
for _, e := range r.Errors {
if strings.HasPrefix(e.Error(), "IMPORTANT!") {
strippedErrors = append(strippedErrors, fmt.Errorf(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!")))
}
}
strippedResult := new(Result)
strippedResult.Errors = strippedErrors
strippedResult.Warnings = strippedWarnings
return strippedResult
}
// IsValid returns true when this result is valid.
//
// Returns true on a nil *Result.
func (r *Result) IsValid() bool {
if r == nil {
return true
}
return len(r.Errors) == 0
}
// HasErrors returns true when this result is invalid.
//
// Returns false on a nil *Result.
func (r *Result) HasErrors() bool {
if r == nil {
return false
}
return !r.IsValid()
}
// HasWarnings returns true when this result contains warnings.
//
// Returns false on a nil *Result.
func (r *Result) HasWarnings() bool {
if r == nil {
return false
}
return len(r.Warnings) > 0
}
// HasErrorsOrWarnings returns true when this result contains
// either errors or warnings.
//
// Returns false on a nil *Result.
func (r *Result) HasErrorsOrWarnings() bool {
if r == nil {
return false
}
return len(r.Errors) > 0 || len(r.Warnings) > 0
}
// Inc increments the match count
func (r *Result) Inc() {
r.MatchCount++
}
// AsError renders this result as an error interface
//
// TODO: reporting / pretty print with path ordered and indented
func (r *Result) AsError() error {
if r.IsValid() {
return nil
}
return errors.CompositeValidationError(r.Errors...)
}
// schemata is an arbitrary number of schemata. It does a distinction between zero,
// one and many schemata to avoid slice allocations.
type schemata struct {
// one is set if there is exactly one schema. In that case multiple must be nil.
one *spec.Schema
// multiple is an arbitrary number of schemas. If it is set, one must be nil.
multiple []*spec.Schema
}
func (s *schemata) Len() int {
if s.one != nil {
return 1
}
return len(s.multiple)
}
func (s *schemata) Slice() []*spec.Schema {
if s == nil {
return nil
}
if s.one != nil {
return []*spec.Schema{s.one}
}
return s.multiple
}
// appendSchemata appends the schemata in other to s. It mutated s in-place.
func (s *schemata) Append(other schemata) {
if other.one == nil && len(other.multiple) == 0 {
return
}
if s.one == nil && len(s.multiple) == 0 {
*s = other
return
}
if s.one != nil {
if other.one != nil {
s.multiple = []*spec.Schema{s.one, other.one}
} else {
t := make([]*spec.Schema, 0, 1+len(other.multiple))
s.multiple = append(append(t, s.one), other.multiple...)
}
s.one = nil
} else {
if other.one != nil {
s.multiple = append(s.multiple, other.one)
} else {
if cap(s.multiple) >= len(s.multiple)+len(other.multiple) {
s.multiple = append(s.multiple, other.multiple...)
} else {
t := make([]*spec.Schema, 0, len(s.multiple)+len(other.multiple))
s.multiple = append(append(t, s.multiple...), other.multiple...)
}
}
}
}