1 /**
2  * Authors: Szabo Bogdan <szabobogdan@yahoo.com>
3  * Date: 9 8, 2015
4  * License: Subject to the terms of the MIT license, as written in the included LICENSE.txt file.
5  * Copyright: Public Domain
6  */
7 module openapi.validation;
8 
9 import openapi.definitions;
10 import openapi.exceptions;
11 
12 import vibe.http.server;
13 import vibe.data.json;
14 
15 import std.conv;
16 import std.datetime;
17 import std.regex;
18 import std.stdio;
19 import std.algorithm.iteration;
20 import std.algorithm.comparison;
21 import std.algorithm.searching;
22 import std.array;
23 import std.traits;
24 import vibe.utils.dictionarylist;
25 
26 bool isValid(Json value, string type, string format = "") {
27   if(type == "object")
28     return value.type == Json.Type.Object;
29 
30   if(type == "array")
31     return value.type == Json.Type.Array;
32 
33   return value.to!string.isValid(type, format);
34 }
35 
36 bool isValid(string value, string type, string format = "") {
37   if(format == "undefined") {
38     format = "";
39   }
40 
41   try {
42     if(type == "integer" && format == "int32") {
43       value.to!int;
44       return true;
45     }
46 
47     if(type == "integer" && format == "int64") {
48       value.to!long;
49       return true;
50     }
51 
52     if(type == "integer" && format == "") {
53       return value.isValid(type, "int32") || value.isValid(type, "int64");
54     }
55 
56     if(type == "number" && format == "float") {
57       value.to!float;
58       return true;
59     }
60 
61     if(type == "number" && format == "double") {
62       value.to!double;
63       return true;
64     }
65 
66     if(type == "number" && format == "") {
67       return value.isValid(type, "double") || value.isValid(type, "float");
68     }
69 
70     if(type == "boolean") {
71       value.to!bool;
72       return true;
73     }
74 
75     if(type == "string" && format == "") {
76       return true;
77     }
78 
79     if(type == "string" && format == "password") {
80       return true;
81     }
82 
83     if(type == "string" && format == "binary") {
84       return true;
85     }
86   } catch(ConvOverflowException e) {
87     return false;
88   } catch(ConvException e) {
89     return false;
90   }
91 
92   try {
93     if(type == "string" && format == "date-time") {
94       SysTime.fromISOExtString(value);
95       return true;
96     }
97 
98     if(type == "string" && format == "date") {
99       Date.fromISOExtString(value);
100       return true;
101     }
102 
103     if(type == "string" && format == "time") {
104       TimeOfDay.fromISOExtString(value);
105       return true;
106     }
107   } catch(Exception e) {
108     return false;
109   }
110 
111   if(type == "string" && format == "byte") {
112     auto ctr = ctRegex!(`^([A-Za-z0-9]*\n{0,1})*`);
113     auto matches = value.matchAll(ctr);
114 
115     if(matches.post != "" || matches.pre != "") {
116       return false;
117     }
118 
119     return true;
120   }
121 
122   return false;
123 }
124 
125 @("it should validate integers")
126 unittest {
127   assert("0".isValid("integer", "int32"));
128   assert("2147483647".isValid("integer", "int32"));
129   assert("-2147483648".isValid("integer", "int32"));
130   assert(!"2147483648".isValid("integer", "int32"));
131   assert(!"-2147483649".isValid("integer", "int32"));
132   assert(!"text".isValid("integer", "int32"));
133 
134   assert("0".isValid("integer", "int64"));
135   assert("9223372036854775807".isValid("integer", "int64"));
136   assert("-9223372036854775807".isValid("integer", "int64"));
137   assert(!"9223372036854775808".isValid("integer", "int64"));
138   assert(!"9223372036854775808".isValid("integer", "int64"));
139   assert(!"text".isValid("integer", "int64"));
140 }
141 
142 @("it should validate numbers")
143 unittest {
144   assert("0".isValid("number", "float"));
145   assert("0.5".isValid("number", "float"));
146   assert("-0.5".isValid("number", "float"));
147   assert(!"0,5".isValid("number", "float"));
148   assert(!"-0,5".isValid("number", "float"));
149   assert(!"text".isValid("number", "float"));
150 
151   assert("0".isValid("number", "double"));
152   assert("0.5".isValid("number", "double"));
153   assert("-0.5".isValid("number", "double"));
154   assert(!"0,5".isValid("number", "double"));
155   assert(!"-0,5".isValid("number", "double"));
156   assert(!"text".isValid("number", "double"));
157 }
158 
159 @("it should validate boolean")
160 unittest {
161   assert(!"0".isValid("boolean"));
162   assert(!"1".isValid("boolean"));
163   assert(!"2".isValid("boolean"));
164   assert(!"-1".isValid("boolean"));
165   assert("true".isValid("boolean"));
166   assert("false".isValid("boolean"));
167   assert(!"text".isValid("boolean"));
168 }
169 
170 @("it should validate strings")
171 unittest {
172   assert("text".isValid("string"));
173 
174   assert("text".isValid("string", "password"));
175 
176   assert("text".isValid("string", "binary"));
177 
178   assert(!"text".isValid("string", "date-time"));
179   assert("2000-01-01T00:00:00Z".isValid("string", "date-time"));
180 
181   assert("2000-01-01".isValid("string", "date"));
182   assert(!"text".isValid("string", "date"));
183 
184   assert("00:00:00".isValid("string", "time"));
185   assert(!"text".isValid("string", "time"));
186 
187   assert("ASDASDasd0123\nadma21".isValid("string", "byte"));
188   assert(!"!@#".isValid("string", "byte"));
189 }
190 
191 private string[] keys(T)(T list) {
192   string[] keyList;
193 
194   foreach(string key, val; list)
195     keyList ~= key;
196 
197   return keyList;
198 }
199 
200 Components getOpenApiOperation(HTTPServerRequest, OpenApi) {
201   return Components();
202 }
203 
204 void validateExistence(ParameterIn in_)(HTTPServerRequest request, OpenApi definition) {
205 
206 }
207 
208 Path matchedPath(OpenApi definition, string) {
209   return Path();
210 }
211 
212 void validateValues(ParameterIn in_)(HTTPServerRequest request, OpenApi definition) {
213   
214 }
215 
216 void validateAgainstSchema(Json value, Json schema) {
217   if("required" in schema) {
218     foreach(field; schema["required"]) {
219       if(field.to!string !in value) {
220         throw new OpenApiParameterException("Missing `" ~ field.to!string ~ "` parameter.");
221       }
222     }
223   }
224 
225   if(value.type == Json.Type.object) {
226     foreach(string key, subValue; value) {
227       if(key !in schema["properties"]) {
228         throw new OpenApiParameterException("Extra `"~key~"` parameter found.");
229       }
230 
231       string format = "format" in schema["properties"][key] ? schema["properties"][key]["format"].to!string : "";
232 
233       if(!subValue.isValid(schema["properties"][key]["type"].to!string, format)) {
234         throw new OpenApiValidationException("Invalid `" ~ key ~ "` schema value.");
235       }
236 
237       subValue.validateAgainstSchema(schema["properties"][key]);
238     }
239   }
240 }
241 
242 void validateBody(HTTPServerRequest request, OpenApi definition) {
243 
244 }
245 
246 void validate(ParameterIn in_)(HTTPServerRequest request, OpenApi definition) {
247   request.validateExistence!in_(definition);
248   request.validateValues!in_(definition);
249 }