1package codescan
2
3import (
4	"errors"
5	"strconv"
6	"strings"
7
8	"github.com/go-openapi/spec"
9)
10
11const (
12	// ParamDescriptionKey indicates the tag used to define a parameter description in swagger:route
13	ParamDescriptionKey = "description"
14	// ParamNameKey indicates the tag used to define a parameter name in swagger:route
15	ParamNameKey = "name"
16	// ParamInKey indicates the tag used to define a parameter location in swagger:route
17	ParamInKey = "in"
18	// ParamRequiredKey indicates the tag used to declare whether a parameter is required in swagger:route
19	ParamRequiredKey = "required"
20	// ParamTypeKey indicates the tag used to define the parameter type in swagger:route
21	ParamTypeKey = "type"
22	// ParamAllowEmptyKey indicates the tag used to indicate whether a parameter allows empty values in swagger:route
23	ParamAllowEmptyKey = "allowempty"
24
25	// SchemaMinKey indicates the tag used to indicate the minimum value allowed for this type in swagger:route
26	SchemaMinKey = "min"
27	// SchemaMaxKey indicates the tag used to indicate the maximum value allowed for this type in swagger:route
28	SchemaMaxKey = "max"
29	// SchemaEnumKey indicates the tag used to specify the allowed values for this type in swagger:route
30	SchemaEnumKey = "enum"
31	// SchemaFormatKey indicates the expected format for this field in swagger:route
32	SchemaFormatKey = "format"
33	// SchemaDefaultKey indicates the default value for this field in swagger:route
34	SchemaDefaultKey = "default"
35	// SchemaMinLenKey indicates the minimum length this field in swagger:route
36	SchemaMinLenKey = "minlength"
37	// SchemaMaxLenKey indicates the minimum length this field in swagger:route
38	SchemaMaxLenKey = "maxlength"
39
40	// TypeArray is the identifier for an array type in swagger:route
41	TypeArray = "array"
42	// TypeNumber is the identifier for a number type in swagger:route
43	TypeNumber = "number"
44	// TypeInteger is the identifier for an integer type in swagger:route
45	TypeInteger = "integer"
46	// TypeBoolean is the identifier for a boolean type in swagger:route
47	TypeBoolean = "boolean"
48	// TypeBool is the identifier for a boolean type in swagger:route
49	TypeBool = "bool"
50	// TypeObject is the identifier for an object type in swagger:route
51	TypeObject = "object"
52	// TypeString is the identifier for a string type in swagger:route
53	TypeString = "string"
54)
55
56var (
57	validIn    = []string{"path", "query", "header", "body", "form"}
58	basicTypes = []string{TypeInteger, TypeNumber, TypeString, TypeBoolean, TypeBool, TypeArray}
59)
60
61func newSetParams(params []*spec.Parameter, setter func([]*spec.Parameter)) *setOpParams {
62	return &setOpParams{
63		set:        setter,
64		parameters: params,
65	}
66}
67
68type setOpParams struct {
69	set        func([]*spec.Parameter)
70	parameters []*spec.Parameter
71}
72
73func (s *setOpParams) Matches(line string) bool {
74	return rxParameters.MatchString(line)
75}
76
77func (s *setOpParams) Parse(lines []string) error {
78	if len(lines) == 0 || (len(lines) == 1 && len(lines[0]) == 0) {
79		return nil
80	}
81
82	var current *spec.Parameter
83	var extraData map[string]string
84
85	for _, line := range lines {
86		l := strings.TrimSpace(line)
87
88		if strings.HasPrefix(l, "+") {
89			s.finalizeParam(current, extraData)
90			current = new(spec.Parameter)
91			extraData = make(map[string]string)
92			l = strings.TrimPrefix(l, "+")
93		}
94
95		kv := strings.SplitN(l, ":", 2)
96
97		if len(kv) <= 1 {
98			continue
99		}
100
101		key := strings.ToLower(strings.TrimSpace(kv[0]))
102		value := strings.TrimSpace(kv[1])
103
104		if current == nil {
105			return errors.New("invalid route/operation schema provided")
106		}
107
108		switch key {
109		case ParamDescriptionKey:
110			current.Description = value
111		case ParamNameKey:
112			current.Name = value
113		case ParamInKey:
114			v := strings.ToLower(value)
115			if contains(validIn, v) {
116				current.In = v
117			}
118		case ParamRequiredKey:
119			if v, err := strconv.ParseBool(value); err == nil {
120				current.Required = v
121			}
122		case ParamTypeKey:
123			if current.Schema == nil {
124				current.Schema = new(spec.Schema)
125			}
126			if contains(basicTypes, value) {
127				current.Type = strings.ToLower(value)
128				if current.Type == TypeBool {
129					current.Type = TypeBoolean
130				}
131			} else if ref, err := spec.NewRef("#/definitions/" + value); err == nil {
132				current.Type = TypeObject
133				current.Schema.Ref = ref
134			}
135			current.Schema.Type = spec.StringOrArray{current.Type}
136		case ParamAllowEmptyKey:
137			if v, err := strconv.ParseBool(value); err == nil {
138				current.AllowEmptyValue = v
139			}
140		default:
141			extraData[key] = value
142		}
143	}
144
145	s.finalizeParam(current, extraData)
146	s.set(s.parameters)
147	return nil
148}
149
150func (s *setOpParams) finalizeParam(param *spec.Parameter, data map[string]string) {
151	if param == nil {
152		return
153	}
154
155	processSchema(data, param)
156	s.parameters = append(s.parameters, param)
157}
158
159func processSchema(data map[string]string, param *spec.Parameter) {
160	if param.Schema == nil {
161		return
162	}
163
164	var enumValues []string
165
166	for key, value := range data {
167		switch key {
168		case SchemaMinKey:
169			if t := getType(param.Schema); t == TypeNumber || t == TypeInteger {
170				v, _ := strconv.ParseFloat(value, 64)
171				param.Schema.Minimum = &v
172			}
173		case SchemaMaxKey:
174			if t := getType(param.Schema); t == TypeNumber || t == TypeInteger {
175				v, _ := strconv.ParseFloat(value, 64)
176				param.Schema.Maximum = &v
177			}
178		case SchemaMinLenKey:
179			if getType(param.Schema) == TypeArray {
180				v, _ := strconv.ParseInt(value, 10, 64)
181				param.Schema.MinLength = &v
182			}
183		case SchemaMaxLenKey:
184			if getType(param.Schema) == TypeArray {
185				v, _ := strconv.ParseInt(value, 10, 64)
186				param.Schema.MaxLength = &v
187			}
188		case SchemaEnumKey:
189			enumValues = strings.Split(value, ",")
190		case SchemaFormatKey:
191			param.Schema.Format = value
192		case SchemaDefaultKey:
193			param.Schema.Default = convert(param.Type, value)
194		}
195	}
196
197	if param.Description != "" {
198		param.Schema.Description = param.Description
199	}
200
201	convertEnum(param.Schema, enumValues)
202}
203
204func convertEnum(schema *spec.Schema, enumValues []string) {
205	if len(enumValues) == 0 {
206		return
207	}
208
209	var finalEnum []interface{}
210	for _, v := range enumValues {
211		finalEnum = append(finalEnum, convert(schema.Type[0], strings.TrimSpace(v)))
212	}
213	schema.Enum = finalEnum
214}
215
216func convert(typeStr, valueStr string) interface{} {
217	switch typeStr {
218	case TypeInteger:
219		fallthrough
220	case TypeNumber:
221		if num, err := strconv.ParseFloat(valueStr, 64); err == nil {
222			return num
223		}
224	case TypeBoolean:
225		fallthrough
226	case TypeBool:
227		if b, err := strconv.ParseBool(valueStr); err == nil {
228			return b
229		}
230	}
231	return valueStr
232}
233
234func getType(schema *spec.Schema) string {
235	if len(schema.Type) == 0 {
236		return ""
237	}
238	return schema.Type[0]
239}
240
241func contains(arr []string, obj string) bool {
242	for _, v := range arr {
243		if v == obj {
244			return true
245		}
246	}
247	return false
248}
249