1 /** 2 * Copyright © Yurai Web Framework 2021 3 * License: MIT (https://github.com/YuraiWeb/yurai/blob/main/LICENSE) 4 * Author: Jacob Jensen (bausshf) 5 */ 6 module yurai.json.parser; 7 8 import std.traits : isSomeString, isScalarType; 9 import std.range : zip, sequence, stride; 10 import std.string : format; 11 import std.uni : isWhite; 12 13 import yurai.core.conv; 14 15 import yurai.json.jsonobject; 16 import yurai.json.jsonobjectmember; 17 import yurai.json.jsontype; 18 19 Json!S parseJson(S = string)(S jsonString) 20 if (isSomeString!S) 21 { 22 Json!S json; 23 S[] errorMessages; 24 25 if (parseJsonSafe(jsonString, json, errorMessages)) 26 { 27 return json; 28 } 29 30 return null; 31 } 32 33 bool parseJsonSafe(S = string)(S jsonString, out Json!S json, out S[] errorMessages) 34 if (isSomeString!S) 35 { 36 json = null; 37 errorMessages = []; 38 39 S[] tokens; 40 if (!parseJsonTokens(jsonString, tokens, errorMessages)) 41 { 42 return false; 43 } 44 45 auto parsedJson = new Json!S; 46 auto scanner = new JsonTokenScanner!S; 47 scanner.tokens = tokens; 48 errorMessages = []; 49 50 if (!recursiveScanning(scanner, parsedJson, errorMessages)) 51 { 52 return false; 53 } 54 55 json = parsedJson; 56 return true; 57 } 58 59 private: 60 bool recursiveScanning(S = string)(JsonTokenScanner!S scanner, Json!S json, ref S[] errorMessages) 61 if (isSomeString!S) 62 { 63 if (!scanner.has) 64 { 65 errorMessages ~= "Partial json parsed. (L: %d, I: %d)".format(scanner.length, scanner.index); 66 return false; 67 } 68 69 switch (scanner.current) 70 { 71 case "null": 72 json.jsonType = JsonType.jsonNull; 73 return true; 74 case "{": 75 return scanObject(scanner, json, errorMessages); 76 case "[": 77 return scanArray(scanner, json, errorMessages); 78 case "true": 79 case "false": 80 return scanBoolean(scanner, json, errorMessages); 81 default: 82 if (scanner.current.length >= 2 && scanner.current[0] == '"' && scanner.current[$-1] == '"') 83 { 84 return scanString(scanner, json, errorMessages); 85 } 86 else if (scanner.current.canParseNumeric) 87 { 88 return scanNumber(scanner, json, errorMessages); 89 } 90 else 91 { 92 errorMessages ~= "Unexpected token: %s (%d)".format(scanner.current, scanner.index); 93 return false; 94 } 95 } 96 } 97 98 bool scanArray(S = string)(JsonTokenScanner!S scanner, Json!S json, ref S[] errorMessages) 99 { 100 scanner.moveNext(); 101 102 if (scanner.current == "]") 103 { 104 json.jsonType = JsonType.jsonArray; 105 } 106 else 107 { 108 while (scanner.current != "]") 109 { 110 auto valueJson = new Json!S; 111 112 if (!recursiveScanning(scanner, valueJson, errorMessages)) 113 { 114 return false; 115 } 116 117 json.addItem(valueJson); 118 119 scanner.moveNext(); 120 121 if (scanner.current == ",") 122 { 123 scanner.moveNext(); 124 continue; 125 } 126 else if (scanner.current == "]") 127 { 128 break; 129 } 130 else 131 { 132 errorMessages ~= "Unexpected token: %s (%d)".format(scanner.current, scanner.index); 133 return false; 134 } 135 } 136 } 137 138 return json.jsonType == JsonType.jsonArray; 139 } 140 141 bool scanNumber(S = string)(JsonTokenScanner!S scanner, Json!S json, ref S[] errorMessages) 142 { 143 double number; 144 if (!tryParse(scanner.current, number)) 145 { 146 errorMessages ~= "Failed to convert token ('%s') to numeric value. (%d)".format(scanner.current, scanner.index); 147 return false; 148 } 149 150 json.setNumber(number); 151 152 return true; 153 } 154 155 bool scanString(S = string)(JsonTokenScanner!S scanner, Json!S json, ref S[] errorMessages) 156 { 157 if (scanner.current.length == 2) 158 { 159 json.setText(""); 160 } 161 else 162 { 163 json.setText(scanner.current[1 .. $-1]); 164 } 165 166 return true; 167 } 168 169 bool scanObject(S = string)(JsonTokenScanner!S scanner, Json!S json, ref S[] errorMessages) 170 { 171 scanner.moveNext(); 172 173 if (scanner.current == "}") 174 { 175 json.jsonType = JsonType.jsonObject; 176 return true; 177 } 178 179 if (scanner.current.length < 3 && scanner.current[0] != '"' && scanner.current[$-1] != '"') 180 { 181 errorMessages ~= "Invalid key found for object. '%s' (%d)".format(scanner.current, scanner.index); 182 return false; 183 } 184 185 while (scanner.has) 186 { 187 auto key = scanner.current[1 .. $-1]; 188 189 if (scanner.moveNext() != ":") 190 { 191 errorMessages ~= "Expected '%s' but found '%s' (%d)".format(":", scanner.current, scanner.index); 192 return false; 193 } 194 195 auto entryJson = new Json!S; 196 197 scanner.moveNext(); 198 if (!recursiveScanning(scanner, entryJson, errorMessages)) 199 { 200 return false; 201 } 202 203 json.addMember(key, entryJson); 204 205 scanner.moveNext(); 206 207 if (scanner.current == ",") 208 { 209 scanner.moveNext(); 210 continue; 211 } 212 else if (scanner.current == "}") 213 { 214 break; 215 } 216 else 217 { 218 errorMessages ~= "Unexpected token: %s (%d)".format(scanner.current, scanner.index); 219 return false; 220 } 221 } 222 223 return true; 224 } 225 226 bool scanBoolean(S = string)(JsonTokenScanner!S scanner, Json!S json, ref S[] errorMessages) 227 { 228 bool booleanValue; 229 if (!tryParse(scanner.current, booleanValue)) 230 { 231 errorMessages ~= "Failed to convert token ('%s') to boolean value. (%d)".format(scanner.current, scanner.index); 232 return false; 233 } 234 235 json.setBoolean(booleanValue); 236 237 return true; 238 } 239 240 final class JsonTokenScanner(S = string) 241 if (isSomeString!S) 242 { 243 S[] tokens; 244 ptrdiff_t index; 245 S _current; 246 247 @property size_t length() 248 { 249 if (!tokens || !tokens.length) 250 { 251 return 0; 252 } 253 254 auto remaining = cast(ptrdiff_t)tokens.length - index; 255 256 return remaining > 0 ? remaining : 0; 257 } 258 259 @property S current() 260 { 261 if (!_current) 262 { 263 if (index >= tokens.length) 264 { 265 return null; 266 } 267 268 _current = tokens[index]; 269 } 270 271 return _current; 272 } 273 274 @property bool has() 275 { 276 return current !is null; 277 } 278 279 bool peekIs(bool delegate(S) fun) 280 { 281 return fun(peek()); 282 } 283 284 bool peekIs(bool function(S) fun) 285 { 286 return fun(peek()); 287 } 288 289 S peek() 290 { 291 if (index >= (tokens.length - 1)) 292 { 293 return null; 294 } 295 296 return tokens[index + 1]; 297 } 298 299 S moveNext() 300 { 301 index++; 302 303 _current = null; 304 305 return current; 306 } 307 308 S moveBack() 309 { 310 index--; 311 312 if (index < 0) 313 { 314 index = 0; 315 } 316 317 _current = null; 318 return current; 319 } 320 } 321 322 bool parseJsonTokens(S = string)(S text, out S[] tokens, out S[] errorMessages) 323 if (isSomeString!S) 324 { 325 tokens = []; 326 errorMessages = []; 327 328 bool escapeNext; 329 bool inString; 330 331 S currentToken; 332 333 foreach (i, c; zip(sequence!"n", text.stride(1))) 334 { 335 if (escapeNext && inString) 336 { 337 switch (c) 338 { 339 case '"': 340 case '\\': 341 currentToken ~= c; 342 break; 343 344 case 'b': currentToken ~= '\b'; break; 345 case 'f': currentToken ~= '\f'; break; 346 case 'n': currentToken ~= '\n'; break; 347 case 'r': currentToken ~= '\r'; break; 348 case 't': currentToken ~= '\t'; break; 349 case 'u': currentToken ~= "\\u"; break; 350 351 default: 352 errorMessages ~= "Expected escape character but found '%s'. (%d)".format(c, i); 353 return false; 354 } 355 356 escapeNext = false; 357 } 358 else if (inString) 359 { 360 if (c == '\\') 361 { 362 escapeNext = true; 363 } 364 else if (c == '"') 365 { 366 currentToken ~= c; 367 inString = false; 368 369 if (currentToken && currentToken.length) 370 { 371 tokens ~= currentToken; 372 currentToken = null; 373 } 374 } 375 else if (c == '\n' || c == '\r') 376 { 377 errorMessages ~= "Unexpected newline or carrot return. (%d)".format(i); 378 return false; 379 } 380 else 381 { 382 currentToken ~= c; 383 } 384 } 385 else if (c == '"') 386 { 387 if (currentToken && currentToken.length) 388 { 389 tokens ~= currentToken; 390 currentToken = null; 391 } 392 393 inString = true; 394 currentToken ~= c; 395 } 396 else if (c == '{' || c == '}' || c == '[' || c == ']' || c == ',' || c == ':') 397 { 398 if (currentToken && currentToken.length) 399 { 400 tokens ~= currentToken; 401 currentToken = null; 402 } 403 404 currentToken ~= c; 405 tokens ~= currentToken; 406 currentToken = null; 407 } 408 else 409 { 410 if (!c.isWhite) 411 { 412 currentToken ~= c; 413 } 414 else 415 { 416 if (currentToken && currentToken.length) 417 { 418 tokens ~= currentToken; 419 currentToken = null; 420 } 421 } 422 } 423 } 424 425 if (currentToken && currentToken.length) 426 { 427 tokens ~= currentToken; 428 } 429 430 return true; 431 }