1// Copyright 2015 go-swagger maintainers
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//    http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15package validate
16
17import (
18	"encoding/json"
19	"fmt"
20	"sort"
21	"strings"
22
23	"github.com/go-openapi/analysis"
24	"github.com/go-openapi/errors"
25	"github.com/go-openapi/jsonpointer"
26	"github.com/go-openapi/loads"
27	"github.com/go-openapi/spec"
28	"github.com/go-openapi/strfmt"
29)
30
31// Spec validates an OpenAPI 2.0 specification document.
32//
33// Returns an error flattening in a single standard error, all validation messages.
34//
35//  - TODO: $ref should not have siblings
36//  - TODO: make sure documentation reflects all checks and warnings
37//  - TODO: check on discriminators
38//  - TODO: explicit message on unsupported keywords (better than "forbidden property"...)
39//  - TODO: full list of unresolved refs
40//  - TODO: validate numeric constraints (issue#581): this should be handled like defaults and examples
41//  - TODO: option to determine if we validate for go-swagger or in a more general context
42//  - TODO: check on required properties to support anyOf, allOf, oneOf
43//
44// NOTE: SecurityScopes are maps: no need to check uniqueness
45//
46func Spec(doc *loads.Document, formats strfmt.Registry) error {
47	errs, _ /*warns*/ := NewSpecValidator(doc.Schema(), formats).Validate(doc)
48	if errs.HasErrors() {
49		return errors.CompositeValidationError(errs.Errors...)
50	}
51	return nil
52}
53
54// SpecValidator validates a swagger 2.0 spec
55type SpecValidator struct {
56	schema       *spec.Schema // swagger 2.0 schema
57	spec         *loads.Document
58	analyzer     *analysis.Spec
59	expanded     *loads.Document
60	KnownFormats strfmt.Registry
61	Options      Opts // validation options
62}
63
64// NewSpecValidator creates a new swagger spec validator instance
65func NewSpecValidator(schema *spec.Schema, formats strfmt.Registry) *SpecValidator {
66	return &SpecValidator{
67		schema:       schema,
68		KnownFormats: formats,
69		Options:      defaultOpts,
70	}
71}
72
73// Validate validates the swagger spec
74func (s *SpecValidator) Validate(data interface{}) (errs *Result, warnings *Result) {
75	var sd *loads.Document
76	errs = new(Result)
77
78	switch v := data.(type) {
79	case *loads.Document:
80		sd = v
81	}
82	if sd == nil {
83		errs.AddErrors(invalidDocumentMsg())
84		return
85	}
86	s.spec = sd
87	s.analyzer = analysis.New(sd.Spec())
88
89	warnings = new(Result)
90
91	// Swagger schema validator
92	schv := NewSchemaValidator(s.schema, nil, "", s.KnownFormats)
93	var obj interface{}
94
95	// Raw spec unmarshalling errors
96	if err := json.Unmarshal(sd.Raw(), &obj); err != nil {
97		// NOTE: under normal conditions, the *load.Document has been already unmarshalled
98		// So this one is just a paranoid check on the behavior of the spec package
99		panic(InvalidDocumentError)
100	}
101
102	defer func() {
103		// errs holds all errors and warnings,
104		// warnings only warnings
105		errs.MergeAsWarnings(warnings)
106		warnings.AddErrors(errs.Warnings...)
107	}()
108
109	errs.Merge(schv.Validate(obj)) // error -
110	// There may be a point in continuing to try and determine more accurate errors
111	if !s.Options.ContinueOnErrors && errs.HasErrors() {
112		return // no point in continuing
113	}
114
115	errs.Merge(s.validateReferencesValid()) // error -
116	// There may be a point in continuing to try and determine more accurate errors
117	if !s.Options.ContinueOnErrors && errs.HasErrors() {
118		return // no point in continuing
119	}
120
121	errs.Merge(s.validateDuplicateOperationIDs())
122	errs.Merge(s.validateDuplicatePropertyNames()) // error -
123	errs.Merge(s.validateParameters())             // error -
124	errs.Merge(s.validateItems())                  // error -
125
126	// Properties in required definition MUST validate their schema
127	// Properties SHOULD NOT be declared as both required and readOnly (warning)
128	errs.Merge(s.validateRequiredDefinitions()) // error and warning
129
130	// There may be a point in continuing to try and determine more accurate errors
131	if !s.Options.ContinueOnErrors && errs.HasErrors() {
132		return // no point in continuing
133	}
134
135	// Values provided as default MUST validate their schema
136	df := &defaultValidator{SpecValidator: s}
137	errs.Merge(df.Validate())
138
139	// Values provided as examples MUST validate their schema
140	// Value provided as examples in a response without schema generate a warning
141	// Known limitations: examples in responses for mime type not application/json are ignored (warning)
142	ex := &exampleValidator{SpecValidator: s}
143	errs.Merge(ex.Validate())
144
145	errs.Merge(s.validateNonEmptyPathParamNames())
146
147	//errs.Merge(s.validateRefNoSibling()) // warning only
148	errs.Merge(s.validateReferenced()) // warning only
149
150	return
151}
152
153func (s *SpecValidator) validateNonEmptyPathParamNames() *Result {
154	res := new(Result)
155	if s.spec.Spec().Paths == nil {
156		// There is no Paths object: error
157		res.AddErrors(noValidPathMsg())
158	} else {
159		if s.spec.Spec().Paths.Paths == nil {
160			// Paths may be empty: warning
161			res.AddWarnings(noValidPathMsg())
162		} else {
163			for k := range s.spec.Spec().Paths.Paths {
164				if strings.Contains(k, "{}") {
165					res.AddErrors(emptyPathParameterMsg(k))
166				}
167			}
168		}
169	}
170	return res
171}
172
173func (s *SpecValidator) validateDuplicateOperationIDs() *Result {
174	// OperationID, if specified, must be unique across the board
175	res := new(Result)
176	known := make(map[string]int)
177	for _, v := range s.analyzer.OperationIDs() {
178		if v != "" {
179			known[v]++
180		}
181	}
182	for k, v := range known {
183		if v > 1 {
184			res.AddErrors(nonUniqueOperationIDMsg(k, v))
185		}
186	}
187	return res
188}
189
190type dupProp struct {
191	Name       string
192	Definition string
193}
194
195func (s *SpecValidator) validateDuplicatePropertyNames() *Result {
196	// definition can't declare a property that's already defined by one of its ancestors
197	res := new(Result)
198	for k, sch := range s.spec.Spec().Definitions {
199		if len(sch.AllOf) == 0 {
200			continue
201		}
202
203		knownanc := map[string]struct{}{
204			"#/definitions/" + k: {},
205		}
206
207		ancs, rec := s.validateCircularAncestry(k, sch, knownanc)
208		if rec != nil && (rec.HasErrors() || !rec.HasWarnings()) {
209			res.Merge(rec)
210		}
211		if len(ancs) > 0 {
212			res.AddErrors(circularAncestryDefinitionMsg(k, ancs))
213			return res
214		}
215
216		knowns := make(map[string]struct{})
217		dups, rep := s.validateSchemaPropertyNames(k, sch, knowns)
218		if rep != nil && (rep.HasErrors() || rep.HasWarnings()) {
219			res.Merge(rep)
220		}
221		if len(dups) > 0 {
222			var pns []string
223			for _, v := range dups {
224				pns = append(pns, v.Definition+"."+v.Name)
225			}
226			res.AddErrors(duplicatePropertiesMsg(k, pns))
227		}
228
229	}
230	return res
231}
232
233func (s *SpecValidator) resolveRef(ref *spec.Ref) (*spec.Schema, error) {
234	if s.spec.SpecFilePath() != "" {
235		return spec.ResolveRefWithBase(s.spec.Spec(), ref, &spec.ExpandOptions{RelativeBase: s.spec.SpecFilePath()})
236	}
237	// NOTE: it looks like with the new spec resolver, this code is now unrecheable
238	return spec.ResolveRef(s.spec.Spec(), ref)
239}
240
241func (s *SpecValidator) validateSchemaPropertyNames(nm string, sch spec.Schema, knowns map[string]struct{}) ([]dupProp, *Result) {
242	var dups []dupProp
243
244	schn := nm
245	schc := &sch
246	res := new(Result)
247
248	for schc.Ref.String() != "" {
249		// gather property names
250		reso, err := s.resolveRef(&schc.Ref)
251		if err != nil {
252			errorHelp.addPointerError(res, err, schc.Ref.String(), nm)
253			return dups, res
254		}
255		schc = reso
256		schn = sch.Ref.String()
257	}
258
259	if len(schc.AllOf) > 0 {
260		for _, chld := range schc.AllOf {
261			dup, rep := s.validateSchemaPropertyNames(schn, chld, knowns)
262			if rep != nil && (rep.HasErrors() || rep.HasWarnings()) {
263				res.Merge(rep)
264			}
265			dups = append(dups, dup...)
266		}
267		return dups, res
268	}
269
270	for k := range schc.Properties {
271		_, ok := knowns[k]
272		if ok {
273			dups = append(dups, dupProp{Name: k, Definition: schn})
274		} else {
275			knowns[k] = struct{}{}
276		}
277	}
278
279	return dups, res
280}
281
282func (s *SpecValidator) validateCircularAncestry(nm string, sch spec.Schema, knowns map[string]struct{}) ([]string, *Result) {
283	res := new(Result)
284
285	if sch.Ref.String() == "" && len(sch.AllOf) == 0 { // Safeguard. We should not be able to actually get there
286		return nil, res
287	}
288	var ancs []string
289
290	schn := nm
291	schc := &sch
292
293	for schc.Ref.String() != "" {
294		reso, err := s.resolveRef(&schc.Ref)
295		if err != nil {
296			errorHelp.addPointerError(res, err, schc.Ref.String(), nm)
297			return ancs, res
298		}
299		schc = reso
300		schn = sch.Ref.String()
301	}
302
303	if schn != nm && schn != "" {
304		if _, ok := knowns[schn]; ok {
305			ancs = append(ancs, schn)
306		}
307		knowns[schn] = struct{}{}
308
309		if len(ancs) > 0 {
310			return ancs, res
311		}
312	}
313
314	if len(schc.AllOf) > 0 {
315		for _, chld := range schc.AllOf {
316			if chld.Ref.String() != "" || len(chld.AllOf) > 0 {
317				anc, rec := s.validateCircularAncestry(schn, chld, knowns)
318				if rec != nil && (rec.HasErrors() || !rec.HasWarnings()) {
319					res.Merge(rec)
320				}
321				ancs = append(ancs, anc...)
322				if len(ancs) > 0 {
323					return ancs, res
324				}
325			}
326		}
327	}
328	return ancs, res
329}
330
331func (s *SpecValidator) validateItems() *Result {
332	// validate parameter, items, schema and response objects for presence of item if type is array
333	res := new(Result)
334
335	for method, pi := range s.analyzer.Operations() {
336		for path, op := range pi {
337			for _, param := range paramHelp.safeExpandedParamsFor(path, method, op.ID, res, s) {
338
339				if param.TypeName() == "array" && param.ItemsTypeName() == "" {
340					res.AddErrors(arrayInParamRequiresItemsMsg(param.Name, op.ID))
341					continue
342				}
343				if param.In != "body" {
344					if param.Items != nil {
345						items := param.Items
346						for items.TypeName() == "array" {
347							if items.ItemsTypeName() == "" {
348								res.AddErrors(arrayInParamRequiresItemsMsg(param.Name, op.ID))
349								break
350							}
351							items = items.Items
352						}
353					}
354				} else {
355					// In: body
356					if param.Schema != nil {
357						res.Merge(s.validateSchemaItems(*param.Schema, fmt.Sprintf("body param %q", param.Name), op.ID))
358					}
359				}
360			}
361
362			var responses []spec.Response
363			if op.Responses != nil {
364				if op.Responses.Default != nil {
365					responses = append(responses, *op.Responses.Default)
366				}
367				if op.Responses.StatusCodeResponses != nil {
368					for _, v := range op.Responses.StatusCodeResponses {
369						responses = append(responses, v)
370					}
371				}
372			}
373
374			for _, resp := range responses {
375				// Response headers with array
376				for hn, hv := range resp.Headers {
377					if hv.TypeName() == "array" && hv.ItemsTypeName() == "" {
378						res.AddErrors(arrayInHeaderRequiresItemsMsg(hn, op.ID))
379					}
380				}
381				if resp.Schema != nil {
382					res.Merge(s.validateSchemaItems(*resp.Schema, "response body", op.ID))
383				}
384			}
385		}
386	}
387	return res
388}
389
390// Verifies constraints on array type
391func (s *SpecValidator) validateSchemaItems(schema spec.Schema, prefix, opID string) *Result {
392	res := new(Result)
393	if !schema.Type.Contains("array") {
394		return res
395	}
396
397	if schema.Items == nil || schema.Items.Len() == 0 {
398		res.AddErrors(arrayRequiresItemsMsg(prefix, opID))
399		return res
400	}
401
402	if schema.Items.Schema != nil {
403		schema = *schema.Items.Schema
404		if _, err := compileRegexp(schema.Pattern); err != nil {
405			res.AddErrors(invalidItemsPatternMsg(prefix, opID, schema.Pattern))
406		}
407
408		res.Merge(s.validateSchemaItems(schema, prefix, opID))
409	}
410	return res
411}
412
413func (s *SpecValidator) validatePathParamPresence(path string, fromPath, fromOperation []string) *Result {
414	// Each defined operation path parameters must correspond to a named element in the API's path pattern.
415	// (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.)
416	res := new(Result)
417	for _, l := range fromPath {
418		var matched bool
419		for _, r := range fromOperation {
420			if l == "{"+r+"}" {
421				matched = true
422				break
423			}
424		}
425		if !matched {
426			res.AddErrors(noParameterInPathMsg(l))
427		}
428	}
429
430	for _, p := range fromOperation {
431		var matched bool
432		for _, r := range fromPath {
433			if "{"+p+"}" == r {
434				matched = true
435				break
436			}
437		}
438		if !matched {
439			res.AddErrors(pathParamNotInPathMsg(path, p))
440		}
441	}
442
443	return res
444}
445
446func (s *SpecValidator) validateReferenced() *Result {
447	var res Result
448	res.MergeAsWarnings(s.validateReferencedParameters())
449	res.MergeAsWarnings(s.validateReferencedResponses())
450	res.MergeAsWarnings(s.validateReferencedDefinitions())
451	return &res
452}
453
454func (s *SpecValidator) validateReferencedParameters() *Result {
455	// Each referenceable definition should have references.
456	params := s.spec.Spec().Parameters
457	if len(params) == 0 {
458		return nil
459	}
460
461	expected := make(map[string]struct{})
462	for k := range params {
463		expected["#/parameters/"+jsonpointer.Escape(k)] = struct{}{}
464	}
465	for _, k := range s.analyzer.AllParameterReferences() {
466		if _, ok := expected[k]; ok {
467			delete(expected, k)
468		}
469	}
470
471	if len(expected) == 0 {
472		return nil
473	}
474	result := new(Result)
475	for k := range expected {
476		result.AddWarnings(unusedParamMsg(k))
477	}
478	return result
479}
480
481func (s *SpecValidator) validateReferencedResponses() *Result {
482	// Each referenceable definition should have references.
483	responses := s.spec.Spec().Responses
484	if len(responses) == 0 {
485		return nil
486	}
487
488	expected := make(map[string]struct{})
489	for k := range responses {
490		expected["#/responses/"+jsonpointer.Escape(k)] = struct{}{}
491	}
492	for _, k := range s.analyzer.AllResponseReferences() {
493		if _, ok := expected[k]; ok {
494			delete(expected, k)
495		}
496	}
497
498	if len(expected) == 0 {
499		return nil
500	}
501	result := new(Result)
502	for k := range expected {
503		result.AddWarnings(unusedResponseMsg(k))
504	}
505	return result
506}
507
508func (s *SpecValidator) validateReferencedDefinitions() *Result {
509	// Each referenceable definition must have references.
510	defs := s.spec.Spec().Definitions
511	if len(defs) == 0 {
512		return nil
513	}
514
515	expected := make(map[string]struct{})
516	for k := range defs {
517		expected["#/definitions/"+jsonpointer.Escape(k)] = struct{}{}
518	}
519	for _, k := range s.analyzer.AllDefinitionReferences() {
520		if _, ok := expected[k]; ok {
521			delete(expected, k)
522		}
523	}
524
525	if len(expected) == 0 {
526		return nil
527	}
528
529	result := new(Result)
530	for k := range expected {
531		result.AddWarnings(unusedDefinitionMsg(k))
532	}
533	return result
534}
535
536func (s *SpecValidator) validateRequiredDefinitions() *Result {
537	// Each property listed in the required array must be defined in the properties of the model
538	res := new(Result)
539
540DEFINITIONS:
541	for d, schema := range s.spec.Spec().Definitions {
542		if schema.Required != nil { // Safeguard
543			for _, pn := range schema.Required {
544				red := s.validateRequiredProperties(pn, d, &schema)
545				res.Merge(red)
546				if !red.IsValid() && !s.Options.ContinueOnErrors {
547					break DEFINITIONS // there is an error, let's stop that bleeding
548				}
549			}
550		}
551	}
552	return res
553}
554
555func (s *SpecValidator) validateRequiredProperties(path, in string, v *spec.Schema) *Result {
556	// Takes care of recursive property definitions, which may be nested in additionalProperties schemas
557	res := new(Result)
558	propertyMatch := false
559	patternMatch := false
560	additionalPropertiesMatch := false
561	isReadOnly := false
562
563	// Regular properties
564	if _, ok := v.Properties[path]; ok {
565		propertyMatch = true
566		isReadOnly = v.Properties[path].ReadOnly
567	}
568
569	// NOTE: patternProperties are not supported in swagger. Even though, we continue validation here
570	// We check all defined patterns: if one regexp is invalid, croaks an error
571	for pp, pv := range v.PatternProperties {
572		re, err := compileRegexp(pp)
573		if err != nil {
574			res.AddErrors(invalidPatternMsg(pp, in))
575		} else if re.MatchString(path) {
576			patternMatch = true
577			if !propertyMatch {
578				isReadOnly = pv.ReadOnly
579			}
580		}
581	}
582
583	if !(propertyMatch || patternMatch) {
584		if v.AdditionalProperties != nil {
585			if v.AdditionalProperties.Allows && v.AdditionalProperties.Schema == nil {
586				additionalPropertiesMatch = true
587			} else if v.AdditionalProperties.Schema != nil {
588				// additionalProperties as schema are upported in swagger
589				// recursively validates additionalProperties schema
590				// TODO : anyOf, allOf, oneOf like in schemaPropsValidator
591				red := s.validateRequiredProperties(path, in, v.AdditionalProperties.Schema)
592				if red.IsValid() {
593					additionalPropertiesMatch = true
594					if !propertyMatch && !patternMatch {
595						isReadOnly = v.AdditionalProperties.Schema.ReadOnly
596					}
597				}
598				res.Merge(red)
599			}
600		}
601	}
602
603	if !(propertyMatch || patternMatch || additionalPropertiesMatch) {
604		res.AddErrors(requiredButNotDefinedMsg(path, in))
605	}
606
607	if isReadOnly {
608		res.AddWarnings(readOnlyAndRequiredMsg(in, path))
609	}
610	return res
611}
612
613func (s *SpecValidator) validateParameters() *Result {
614	// - for each method, path is unique, regardless of path parameters
615	//   e.g. GET:/petstore/{id}, GET:/petstore/{pet}, GET:/petstore are
616	//   considered duplicate paths
617	// - each parameter should have a unique `name` and `type` combination
618	// - each operation should have only 1 parameter of type body
619	// - there must be at most 1 parameter in body
620	// - parameters with pattern property must specify valid patterns
621	// - $ref in parameters must resolve
622	// - path param must be required
623	res := new(Result)
624	rexGarbledPathSegment := mustCompileRegexp(`.*[{}\s]+.*`)
625	for method, pi := range s.analyzer.Operations() {
626		methodPaths := make(map[string]map[string]string)
627		if pi != nil { // Safeguard
628			for path, op := range pi {
629				pathToAdd := pathHelp.stripParametersInPath(path)
630
631				// Warn on garbled path afer param stripping
632				if rexGarbledPathSegment.MatchString(pathToAdd) {
633					res.AddWarnings(pathStrippedParamGarbledMsg(pathToAdd))
634				}
635
636				// Check uniqueness of stripped paths
637				if _, found := methodPaths[method][pathToAdd]; found {
638
639					// Sort names for stable, testable output
640					if strings.Compare(path, methodPaths[method][pathToAdd]) < 0 {
641						res.AddErrors(pathOverlapMsg(path, methodPaths[method][pathToAdd]))
642					} else {
643						res.AddErrors(pathOverlapMsg(methodPaths[method][pathToAdd], path))
644					}
645				} else {
646					if _, found := methodPaths[method]; !found {
647						methodPaths[method] = map[string]string{}
648					}
649					methodPaths[method][pathToAdd] = path //Original non stripped path
650
651				}
652
653				var bodyParams []string
654				var paramNames []string
655				var hasForm, hasBody bool
656
657				// Check parameters names uniqueness for operation
658				// TODO: should be done after param expansion
659				res.Merge(s.checkUniqueParams(path, method, op))
660
661				for _, pr := range paramHelp.safeExpandedParamsFor(path, method, op.ID, res, s) {
662					// Validate pattern regexp for parameters with a Pattern property
663					if _, err := compileRegexp(pr.Pattern); err != nil {
664						res.AddErrors(invalidPatternInParamMsg(op.ID, pr.Name, pr.Pattern))
665					}
666
667					// There must be at most one parameter in body: list them all
668					if pr.In == "body" {
669						bodyParams = append(bodyParams, fmt.Sprintf("%q", pr.Name))
670						hasBody = true
671					}
672
673					if pr.In == "path" {
674						paramNames = append(paramNames, pr.Name)
675						// Path declared in path must have the required: true property
676						if !pr.Required {
677							res.AddErrors(pathParamRequiredMsg(op.ID, pr.Name))
678						}
679					}
680
681					if pr.In == "formData" {
682						hasForm = true
683					}
684				}
685
686				// In:formData and In:body are mutually exclusive
687				if hasBody && hasForm {
688					res.AddErrors(bothFormDataAndBodyMsg(op.ID))
689				}
690				// There must be at most one body param
691				// Accurately report situations when more than 1 body param is declared (possibly unnamed)
692				if len(bodyParams) > 1 {
693					sort.Strings(bodyParams)
694					res.AddErrors(multipleBodyParamMsg(op.ID, bodyParams))
695				}
696
697				// Check uniqueness of parameters in path
698				paramsInPath := pathHelp.extractPathParams(path)
699				for i, p := range paramsInPath {
700					for j, q := range paramsInPath {
701						if p == q && i > j {
702							res.AddErrors(pathParamNotUniqueMsg(path, p, q))
703							break
704						}
705					}
706				}
707
708				// Warns about possible malformed params in path
709				rexGarbledParam := mustCompileRegexp(`{.*[{}\s]+.*}`)
710				for _, p := range paramsInPath {
711					if rexGarbledParam.MatchString(p) {
712						res.AddWarnings(pathParamGarbledMsg(path, p))
713					}
714				}
715
716				// Match params from path vs params from params section
717				res.Merge(s.validatePathParamPresence(path, paramsInPath, paramNames))
718			}
719		}
720	}
721	return res
722}
723
724func (s *SpecValidator) validateReferencesValid() *Result {
725	// each reference must point to a valid object
726	res := new(Result)
727	for _, r := range s.analyzer.AllRefs() {
728		if !r.IsValidURI(s.spec.SpecFilePath()) { // Safeguard - spec should always yield a valid URI
729			res.AddErrors(invalidRefMsg(r.String()))
730		}
731	}
732	if !res.HasErrors() {
733		// NOTE: with default settings, loads.Document.Expanded()
734		// stops on first error. Anyhow, the expand option to continue
735		// on errors fails to report errors at all.
736		exp, err := s.spec.Expanded()
737		if err != nil {
738			res.AddErrors(unresolvedReferencesMsg(err))
739		}
740		s.expanded = exp
741	}
742	return res
743}
744
745func (s *SpecValidator) checkUniqueParams(path, method string, op *spec.Operation) *Result {
746	// Check for duplicate parameters declaration in param section.
747	// Each parameter should have a unique `name` and `type` combination
748	// NOTE: this could be factorized in analysis (when constructing the params map)
749	// However, there are some issues with such a factorization:
750	// - analysis does not seem to fully expand params
751	// - param keys may be altered by x-go-name
752	res := new(Result)
753	pnames := make(map[string]struct{})
754
755	if op.Parameters != nil { // Safeguard
756		for _, ppr := range op.Parameters {
757			var ok bool
758			pr, red := paramHelp.resolveParam(path, method, op.ID, &ppr, s)
759			res.Merge(red)
760
761			if pr != nil && pr.Name != "" { // params with empty name does no participate the check
762				key := fmt.Sprintf("%s#%s", pr.In, pr.Name)
763
764				if _, ok = pnames[key]; ok {
765					res.AddErrors(duplicateParamNameMsg(pr.In, pr.Name, op.ID))
766				}
767				pnames[key] = struct{}{}
768			}
769		}
770	}
771	return res
772}
773
774// SetContinueOnErrors sets the ContinueOnErrors option for this validator.
775func (s *SpecValidator) SetContinueOnErrors(c bool) {
776	s.Options.ContinueOnErrors = c
777}
778