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 analysis
16
17import (
18	"fmt"
19	"reflect"
20
21	"github.com/go-openapi/spec"
22)
23
24// Mixin modifies the primary swagger spec by adding the paths and
25// definitions from the mixin specs. Top level parameters and
26// responses from the mixins are also carried over. Operation id
27// collisions are avoided by appending "Mixin<N>" but only if
28// needed.
29//
30// The following parts of primary are subject to merge, filling empty details
31//   - Info
32//   - BasePath
33//   - Host
34//   - ExternalDocs
35//
36// Consider calling FixEmptyResponseDescriptions() on the modified primary
37// if you read them from storage and they are valid to start with.
38//
39// Entries in "paths", "definitions", "parameters" and "responses" are
40// added to the primary in the order of the given mixins. If the entry
41// already exists in primary it is skipped with a warning message.
42//
43// The count of skipped entries (from collisions) is returned so any
44// deviation from the number expected can flag a warning in your build
45// scripts. Carefully review the collisions before accepting them;
46// consider renaming things if possible.
47//
48// No key normalization takes place (paths, type defs,
49// etc). Ensure they are canonical if your downstream tools do
50// key normalization of any form.
51//
52// Merging schemes (http, https), and consumers/producers do not account for
53// collisions.
54func Mixin(primary *spec.Swagger, mixins ...*spec.Swagger) []string {
55	skipped := make([]string, 0, len(mixins))
56	opIds := getOpIds(primary)
57	initPrimary(primary)
58
59	for i, m := range mixins {
60		skipped = append(skipped, mergeSwaggerProps(primary, m)...)
61
62		skipped = append(skipped, mergeConsumes(primary, m)...)
63
64		skipped = append(skipped, mergeProduces(primary, m)...)
65
66		skipped = append(skipped, mergeTags(primary, m)...)
67
68		skipped = append(skipped, mergeSchemes(primary, m)...)
69
70		skipped = append(skipped, mergeSecurityDefinitions(primary, m)...)
71
72		skipped = append(skipped, mergeSecurityRequirements(primary, m)...)
73
74		skipped = append(skipped, mergeDefinitions(primary, m)...)
75
76		// merging paths requires a map of operationIDs to work with
77		skipped = append(skipped, mergePaths(primary, m, opIds, i)...)
78
79		skipped = append(skipped, mergeParameters(primary, m)...)
80
81		skipped = append(skipped, mergeResponses(primary, m)...)
82	}
83
84	return skipped
85}
86
87// getOpIds extracts all the paths.<path>.operationIds from the given
88// spec and returns them as the keys in a map with 'true' values.
89func getOpIds(s *spec.Swagger) map[string]bool {
90	rv := make(map[string]bool)
91	if s.Paths == nil {
92		return rv
93	}
94
95	for _, v := range s.Paths.Paths {
96		piops := pathItemOps(v)
97
98		for _, op := range piops {
99			rv[op.ID] = true
100		}
101	}
102
103	return rv
104}
105
106func pathItemOps(p spec.PathItem) []*spec.Operation {
107	var rv []*spec.Operation
108	rv = appendOp(rv, p.Get)
109	rv = appendOp(rv, p.Put)
110	rv = appendOp(rv, p.Post)
111	rv = appendOp(rv, p.Delete)
112	rv = appendOp(rv, p.Head)
113	rv = appendOp(rv, p.Patch)
114
115	return rv
116}
117
118func appendOp(ops []*spec.Operation, op *spec.Operation) []*spec.Operation {
119	if op == nil {
120		return ops
121	}
122
123	return append(ops, op)
124}
125
126func mergeSecurityDefinitions(primary *spec.Swagger, m *spec.Swagger) (skipped []string) {
127	for k, v := range m.SecurityDefinitions {
128		if _, exists := primary.SecurityDefinitions[k]; exists {
129			warn := fmt.Sprintf(
130				"SecurityDefinitions entry '%v' already exists in primary or higher priority mixin, skipping\n", k)
131			skipped = append(skipped, warn)
132
133			continue
134		}
135
136		primary.SecurityDefinitions[k] = v
137	}
138
139	return
140}
141
142func mergeSecurityRequirements(primary *spec.Swagger, m *spec.Swagger) (skipped []string) {
143	for _, v := range m.Security {
144		found := false
145		for _, vv := range primary.Security {
146			if reflect.DeepEqual(v, vv) {
147				found = true
148
149				break
150			}
151		}
152
153		if found {
154			warn := fmt.Sprintf(
155				"Security requirement: '%v' already exists in primary or higher priority mixin, skipping\n", v)
156			skipped = append(skipped, warn)
157
158			continue
159		}
160		primary.Security = append(primary.Security, v)
161	}
162
163	return
164}
165
166func mergeDefinitions(primary *spec.Swagger, m *spec.Swagger) (skipped []string) {
167	for k, v := range m.Definitions {
168		// assume name collisions represent IDENTICAL type. careful.
169		if _, exists := primary.Definitions[k]; exists {
170			warn := fmt.Sprintf(
171				"definitions entry '%v' already exists in primary or higher priority mixin, skipping\n", k)
172			skipped = append(skipped, warn)
173
174			continue
175		}
176		primary.Definitions[k] = v
177	}
178
179	return
180}
181
182func mergePaths(primary *spec.Swagger, m *spec.Swagger, opIds map[string]bool, mixIndex int) (skipped []string) {
183	if m.Paths != nil {
184		for k, v := range m.Paths.Paths {
185			if _, exists := primary.Paths.Paths[k]; exists {
186				warn := fmt.Sprintf(
187					"paths entry '%v' already exists in primary or higher priority mixin, skipping\n", k)
188				skipped = append(skipped, warn)
189
190				continue
191			}
192
193			// Swagger requires that operationIds be
194			// unique within a spec. If we find a
195			// collision we append "Mixin0" to the
196			// operatoinId we are adding, where 0 is mixin
197			// index.  We assume that operationIds with
198			// all the proivded specs are already unique.
199			piops := pathItemOps(v)
200			for _, piop := range piops {
201				if opIds[piop.ID] {
202					piop.ID = fmt.Sprintf("%v%v%v", piop.ID, "Mixin", mixIndex)
203				}
204				opIds[piop.ID] = true
205			}
206			primary.Paths.Paths[k] = v
207		}
208	}
209
210	return
211}
212
213func mergeParameters(primary *spec.Swagger, m *spec.Swagger) (skipped []string) {
214	for k, v := range m.Parameters {
215		// could try to rename on conflict but would
216		// have to fix $refs in the mixin. Complain
217		// for now
218		if _, exists := primary.Parameters[k]; exists {
219			warn := fmt.Sprintf(
220				"top level parameters entry '%v' already exists in primary or higher priority mixin, skipping\n", k)
221			skipped = append(skipped, warn)
222
223			continue
224		}
225		primary.Parameters[k] = v
226	}
227
228	return
229}
230
231func mergeResponses(primary *spec.Swagger, m *spec.Swagger) (skipped []string) {
232	for k, v := range m.Responses {
233		// could try to rename on conflict but would
234		// have to fix $refs in the mixin. Complain
235		// for now
236		if _, exists := primary.Responses[k]; exists {
237			warn := fmt.Sprintf(
238				"top level responses entry '%v' already exists in primary or higher priority mixin, skipping\n", k)
239			skipped = append(skipped, warn)
240
241			continue
242		}
243		primary.Responses[k] = v
244	}
245
246	return skipped
247}
248
249func mergeConsumes(primary *spec.Swagger, m *spec.Swagger) []string {
250	for _, v := range m.Consumes {
251		found := false
252		for _, vv := range primary.Consumes {
253			if v == vv {
254				found = true
255
256				break
257			}
258		}
259
260		if found {
261			// no warning here: we just skip it
262			continue
263		}
264		primary.Consumes = append(primary.Consumes, v)
265	}
266
267	return []string{}
268}
269
270func mergeProduces(primary *spec.Swagger, m *spec.Swagger) []string {
271	for _, v := range m.Produces {
272		found := false
273		for _, vv := range primary.Produces {
274			if v == vv {
275				found = true
276
277				break
278			}
279		}
280
281		if found {
282			// no warning here: we just skip it
283			continue
284		}
285		primary.Produces = append(primary.Produces, v)
286	}
287
288	return []string{}
289}
290
291func mergeTags(primary *spec.Swagger, m *spec.Swagger) (skipped []string) {
292	for _, v := range m.Tags {
293		found := false
294		for _, vv := range primary.Tags {
295			if v.Name == vv.Name {
296				found = true
297
298				break
299			}
300		}
301
302		if found {
303			warn := fmt.Sprintf(
304				"top level tags entry with name '%v' already exists in primary or higher priority mixin, skipping\n",
305				v.Name,
306			)
307			skipped = append(skipped, warn)
308
309			continue
310		}
311
312		primary.Tags = append(primary.Tags, v)
313	}
314
315	return
316}
317
318func mergeSchemes(primary *spec.Swagger, m *spec.Swagger) []string {
319	for _, v := range m.Schemes {
320		found := false
321		for _, vv := range primary.Schemes {
322			if v == vv {
323				found = true
324
325				break
326			}
327		}
328
329		if found {
330			// no warning here: we just skip it
331			continue
332		}
333		primary.Schemes = append(primary.Schemes, v)
334	}
335
336	return []string{}
337}
338
339func mergeSwaggerProps(primary *spec.Swagger, m *spec.Swagger) []string {
340	var skipped, skippedInfo, skippedDocs []string
341
342	primary.Extensions, skipped = mergeExtensions(primary.Extensions, m.Extensions)
343
344	// merging details in swagger top properties
345	if primary.Host == "" {
346		primary.Host = m.Host
347	}
348
349	if primary.BasePath == "" {
350		primary.BasePath = m.BasePath
351	}
352
353	if primary.Info == nil {
354		primary.Info = m.Info
355	} else if m.Info != nil {
356		skippedInfo = mergeInfo(primary.Info, m.Info)
357		skipped = append(skipped, skippedInfo...)
358	}
359
360	if primary.ExternalDocs == nil {
361		primary.ExternalDocs = m.ExternalDocs
362	} else if m != nil {
363		skippedDocs = mergeExternalDocs(primary.ExternalDocs, m.ExternalDocs)
364		skipped = append(skipped, skippedDocs...)
365	}
366
367	return skipped
368}
369
370// nolint: unparam
371func mergeExternalDocs(primary *spec.ExternalDocumentation, m *spec.ExternalDocumentation) []string {
372	if primary.Description == "" {
373		primary.Description = m.Description
374	}
375
376	if primary.URL == "" {
377		primary.URL = m.URL
378	}
379
380	return nil
381}
382
383func mergeInfo(primary *spec.Info, m *spec.Info) []string {
384	var sk, skipped []string
385
386	primary.Extensions, sk = mergeExtensions(primary.Extensions, m.Extensions)
387	skipped = append(skipped, sk...)
388
389	if primary.Description == "" {
390		primary.Description = m.Description
391	}
392
393	if primary.Title == "" {
394		primary.Description = m.Description
395	}
396
397	if primary.TermsOfService == "" {
398		primary.TermsOfService = m.TermsOfService
399	}
400
401	if primary.Version == "" {
402		primary.Version = m.Version
403	}
404
405	if primary.Contact == nil {
406		primary.Contact = m.Contact
407	} else if m.Contact != nil {
408		var csk []string
409		primary.Contact.Extensions, csk = mergeExtensions(primary.Contact.Extensions, m.Contact.Extensions)
410		skipped = append(skipped, csk...)
411
412		if primary.Contact.Name == "" {
413			primary.Contact.Name = m.Contact.Name
414		}
415
416		if primary.Contact.URL == "" {
417			primary.Contact.URL = m.Contact.URL
418		}
419
420		if primary.Contact.Email == "" {
421			primary.Contact.Email = m.Contact.Email
422		}
423	}
424
425	if primary.License == nil {
426		primary.License = m.License
427	} else if m.License != nil {
428		var lsk []string
429		primary.License.Extensions, lsk = mergeExtensions(primary.License.Extensions, m.License.Extensions)
430		skipped = append(skipped, lsk...)
431
432		if primary.License.Name == "" {
433			primary.License.Name = m.License.Name
434		}
435
436		if primary.License.URL == "" {
437			primary.License.URL = m.License.URL
438		}
439	}
440
441	return skipped
442}
443
444func mergeExtensions(primary spec.Extensions, m spec.Extensions) (result spec.Extensions, skipped []string) {
445	if primary == nil {
446		result = m
447
448		return
449	}
450
451	if m == nil {
452		result = primary
453
454		return
455	}
456
457	result = primary
458	for k, v := range m {
459		if _, found := primary[k]; found {
460			skipped = append(skipped, k)
461
462			continue
463		}
464
465		primary[k] = v
466	}
467
468	return
469}
470
471func initPrimary(primary *spec.Swagger) {
472	if primary.SecurityDefinitions == nil {
473		primary.SecurityDefinitions = make(map[string]*spec.SecurityScheme)
474	}
475
476	if primary.Security == nil {
477		primary.Security = make([]map[string][]string, 0, 10)
478	}
479
480	if primary.Produces == nil {
481		primary.Produces = make([]string, 0, 10)
482	}
483
484	if primary.Consumes == nil {
485		primary.Consumes = make([]string, 0, 10)
486	}
487
488	if primary.Tags == nil {
489		primary.Tags = make([]spec.Tag, 0, 10)
490	}
491
492	if primary.Schemes == nil {
493		primary.Schemes = make([]string, 0, 10)
494	}
495
496	if primary.Paths == nil {
497		primary.Paths = &spec.Paths{Paths: make(map[string]spec.PathItem)}
498	}
499
500	if primary.Paths.Paths == nil {
501		primary.Paths.Paths = make(map[string]spec.PathItem)
502	}
503
504	if primary.Definitions == nil {
505		primary.Definitions = make(spec.Definitions)
506	}
507
508	if primary.Parameters == nil {
509		primary.Parameters = make(map[string]spec.Parameter)
510	}
511
512	if primary.Responses == nil {
513		primary.Responses = make(map[string]spec.Response)
514	}
515}
516