1//! @file json.cpp
2//! @author ryftchen
3//! @brief The definitions (json) in the utility module.
4//! @version 0.1.0
5//! @copyright Copyright (c) 2022-2025 ryftchen. All rights reserved.
6
7#include "json.hpp"
8
9#include <charconv>
10#include <cmath>
11
12namespace utility::json
13{
14//! @brief Function version number.
15//! @return version number (major.minor.patch)
16const char* version() noexcept
17{
18 static const char* const ver = "0.1.0";
19 return ver;
20}
21
22//! @brief Parse the next data in JSON.
23//! @param fmt - formatted string
24//! @param offset - data offset
25//! @return JSON data
26static JSON parseNext(const std::string_view fmt, std::size_t& offset);
27
28//! @brief Escape for JSON.
29//! @param fmt - formatted string
30//! @return string after escape
31static std::string jsonEscape(const std::string_view fmt)
32{
33 std::string output{};
34 for (const auto c : fmt)
35 {
36 switch (c)
37 {
38 case '\"':
39 output += "\\\"";
40 break;
41 case '\\':
42 output += "\\\\";
43 break;
44 case '\b':
45 output += "\\b";
46 break;
47 case '\f':
48 output += "\\f";
49 break;
50 case '\n':
51 output += "\\n";
52 break;
53 case '\r':
54 output += "\\r";
55 break;
56 case '\t':
57 output += "\\t";
58 break;
59 default:
60 output += c;
61 break;
62 }
63 }
64 return output;
65}
66
67//! @brief Consume whitespace.
68//! @param fmt - formatted string
69//! @param offset - data offset
70static void consumeWhitespace(const std::string_view fmt, std::size_t& offset)
71{
72 while ((offset < fmt.length()) && std::isspace(fmt.at(pos: offset)))
73 {
74 ++offset;
75 }
76
77 if (offset == fmt.length())
78 {
79 --offset;
80 }
81}
82
83//! @brief Parse object in JSON.
84//! @param fmt - formatted string
85//! @param offset - data offset
86//! @return JSON object
87static JSON parseObject(const std::string_view fmt, std::size_t& offset)
88{
89 auto object = JSON::make(type: JSON::Type::object);
90 ++offset;
91 consumeWhitespace(fmt, offset);
92 if (fmt.at(pos: offset) == '}')
93 {
94 ++offset;
95 return object;
96 }
97
98 for (;;)
99 {
100 const auto key = parseNext(fmt, offset);
101 consumeWhitespace(fmt, offset);
102 if (fmt.at(pos: offset) != ':')
103 {
104 throw std::runtime_error{"For JSON object, expected ':', found '" + std::string{fmt.at(pos: offset)} + "'."};
105 }
106 consumeWhitespace(fmt, offset&: ++offset);
107 const auto val = parseNext(fmt, offset);
108 object[key.asString()] = val;
109
110 consumeWhitespace(fmt, offset);
111 if (fmt.at(pos: offset) == ',')
112 {
113 ++offset;
114 continue;
115 }
116 if (fmt.at(pos: offset) == '}')
117 {
118 ++offset;
119 break;
120 }
121 throw std::runtime_error{"For JSON object, expected ',' or '}', found '" + std::string{fmt.at(pos: offset)} + "'."};
122 }
123 return object;
124}
125
126//! @brief Parse array in JSON.
127//! @param fmt - formatted string
128//! @param offset - data offset
129//! @return JSON array
130static JSON parseArray(const std::string_view fmt, std::size_t& offset)
131{
132 auto array = JSON::make(type: JSON::Type::array);
133 ++offset;
134 consumeWhitespace(fmt, offset);
135 if (fmt.at(pos: offset) == ']')
136 {
137 ++offset;
138 return array;
139 }
140
141 for (std::size_t index = 0;;)
142 {
143 array[index++] = parseNext(fmt, offset);
144 consumeWhitespace(fmt, offset);
145 if (fmt.at(pos: offset) == ',')
146 {
147 ++offset;
148 continue;
149 }
150 if (fmt.at(pos: offset) == ']')
151 {
152 ++offset;
153 break;
154 }
155 throw std::runtime_error{"For JSON array, expected ',' or ']', found '" + std::string{fmt.at(pos: offset)} + "'."};
156 }
157 return array;
158}
159
160//! @brief Check whether the target character is hexadecimal.
161//! @param c - target character
162//! @return be hexadecimal or not
163static bool isHexCharacter(const char c)
164{
165 return ((c >= '0') && (c <= '9')) || ((c >= 'a') && (c <= 'f')) || ((c >= 'A') && (c <= 'F'));
166}
167
168//! @brief Parse string in JSON.
169//! @param fmt - formatted string
170//! @param offset - data offset
171//! @return JSON string
172static JSON parseString(const std::string_view fmt, std::size_t& offset)
173{
174 std::string val{};
175 for (char c = fmt.at(pos: ++offset); c != '\"'; c = fmt.at(pos: ++offset))
176 {
177 if (c == '\\')
178 {
179 switch (fmt.at(pos: ++offset))
180 {
181 case '\"':
182 val += '\"';
183 break;
184 case '\\':
185 val += '\\';
186 break;
187 case '/':
188 val += '/';
189 break;
190 case 'b':
191 val += '\b';
192 break;
193 case 'f':
194 val += '\f';
195 break;
196 case 'n':
197 val += '\n';
198 break;
199 case 'r':
200 val += '\r';
201 break;
202 case 't':
203 val += '\t';
204 break;
205 case 'u':
206 val += "\\u";
207 for (std::uint8_t i = 1; i <= 4; ++i)
208 {
209 c = fmt.at(pos: offset + i);
210 if (!isHexCharacter(c))
211 {
212 throw std::runtime_error{
213 "For JSON string, expected hex character in unicode escape, found '" + std::string{c}
214 + "'."};
215 }
216 val += c;
217 }
218 offset += 4;
219 break;
220 default:
221 val += '\\';
222 break;
223 }
224 }
225 else
226 {
227 val += c;
228 }
229
230 if ((offset + 1) == fmt.length())
231 {
232 throw std::runtime_error{"JSON syntax error, at \"" + val + "\" and after."};
233 }
234 }
235 ++offset;
236 return val;
237}
238
239//! @brief Extract the exponent.
240//! @param fmt - formatted string
241//! @param offset - data offset
242//! @return exponent string
243static std::string extractExponent(const std::string_view fmt, std::size_t& offset)
244{
245 char c = fmt.at(pos: offset);
246 std::string expStr{};
247 if (c == '-')
248 {
249 ++offset;
250 expStr += '-';
251 }
252 else if (c == '+')
253 {
254 ++offset;
255 }
256
257 while (offset < fmt.length())
258 {
259 c = fmt.at(pos: offset++);
260 if ((c >= '0') && (c <= '9'))
261 {
262 expStr += c;
263 }
264 else if (!std::isspace(c) && (c != ',') && (c != ']') && (c != '}'))
265 {
266 throw std::runtime_error{
267 "For JSON number, expected a number for exponent, found '" + std::string{c} + "'."};
268 }
269 else
270 {
271 break;
272 }
273 }
274 return expStr;
275}
276
277//! @brief Parse number in JSON.
278//! @param fmt - formatted string
279//! @param offset - data offset
280//! @return JSON number
281static JSON parseNumber(const std::string_view fmt, std::size_t& offset)
282{
283 std::string val{};
284 char c = '\0';
285 bool isFloating = false;
286 while (offset < fmt.length())
287 {
288 c = fmt.at(pos: offset++);
289 if (((c >= '0') && (c <= '9')) || (c == '-'))
290 {
291 val += c;
292 }
293 else if (c == '.')
294 {
295 val += c;
296 isFloating = true;
297 }
298 else
299 {
300 break;
301 }
302 }
303
304 long long exp = 0;
305 std::string expStr{};
306 if ((c == 'E') || (c == 'e'))
307 {
308 expStr = extractExponent(fmt, offset);
309 exp = std::stol(str: expStr);
310 }
311 else if (!std::isspace(c) && (c != ',') && (c != ']') && (c != '}'))
312 {
313 throw std::runtime_error{"For JSON number, unexpected character '" + std::string{c} + "'."};
314 }
315 --offset;
316
317 JSON number{};
318 if (constexpr std::uint8_t base = 10; isFloating)
319 {
320 number = std::stod(str: val) * std::pow(x: base, y: exp);
321 }
322 else if (!expStr.empty())
323 {
324 number = std::stol(str: val) * std::pow(x: base, y: exp);
325 }
326 else
327 {
328 number = std::stol(str: val);
329 }
330 return number;
331}
332
333//! @brief Parse boolean in JSON.
334//! @param fmt - formatted string
335//! @param offset - data offset
336//! @return JSON boolean
337static JSON parseBoolean(const std::string_view fmt, std::size_t& offset)
338{
339 constexpr std::string_view trueLit = "true";
340 constexpr std::string_view falseLit = "false";
341 JSON boolean{};
342 if (fmt.substr(pos: offset, n: trueLit.length()) == trueLit)
343 {
344 boolean = true;
345 }
346 else if (fmt.substr(pos: offset, n: falseLit.length()) == falseLit)
347 {
348 boolean = false;
349 }
350 else
351 {
352 throw std::runtime_error{
353 "For JSON boolean, expected \"" + std::string{trueLit} + "\" or \"" + std::string{falseLit} + "\", found \""
354 + std::string{fmt.substr(pos: offset, n: falseLit.length())} + "\"."};
355 }
356 offset += (boolean.asBoolean() ? trueLit.length() : falseLit.length());
357 return boolean;
358}
359
360//! @brief Parse null in JSON.
361//! @param fmt - formatted string
362//! @param offset - data offset
363//! @return JSON null
364static JSON parseNull(const std::string_view fmt, std::size_t& offset)
365{
366 constexpr std::string_view nullLit = "null";
367 if (fmt.substr(pos: offset, n: nullLit.length()) != nullLit)
368 {
369 throw std::runtime_error{
370 "For JSON null, expected \"" + std::string{nullLit} + "\", found \""
371 + std::string{fmt.substr(pos: offset, n: nullLit.length())} + "\"."};
372 }
373 offset += nullLit.length();
374 return {};
375}
376
377static JSON parseNext(const std::string_view fmt, std::size_t& offset)
378{
379 consumeWhitespace(fmt, offset);
380 const char c = fmt.at(pos: offset);
381 switch (c)
382 {
383 case '{':
384 return parseObject(fmt, offset);
385 case '[':
386 return parseArray(fmt, offset);
387 case '\"':
388 return parseString(fmt, offset);
389 case 't':
390 [[fallthrough]];
391 case 'f':
392 return parseBoolean(fmt, offset);
393 case 'n':
394 return parseNull(fmt, offset);
395 default:
396 if (((c >= '0') && (c <= '9')) || (c == '-'))
397 {
398 return parseNumber(fmt, offset);
399 }
400 break;
401 }
402 throw std::runtime_error{"JSON syntax error, unknown starting character '" + std::string{c} + "'."};
403}
404
405JSON::JSON(const Type type)
406{
407 switch (type)
408 {
409 case Type::null:
410 ensureType<Null>();
411 break;
412 case Type::object:
413 ensureType<Object>();
414 break;
415 case Type::array:
416 ensureType<Array>();
417 break;
418 case Type::string:
419 ensureType<String>();
420 break;
421 case Type::floating:
422 ensureType<Floating>();
423 break;
424 case Type::integral:
425 ensureType<Integral>();
426 break;
427 case Type::boolean:
428 ensureType<Boolean>();
429 break;
430 default:
431 break;
432 }
433}
434
435JSON::JSON(const std::initializer_list<JSON>& list)
436{
437 ensureType<Object>();
438 for (const auto* iterator = list.begin(); iterator != list.end(); std::advance(i&: iterator, n: 2))
439 {
440 operator[](key: iterator->asString()) = *std::next(x: iterator);
441 }
442}
443
444JSON::JSON(JSON&& json) noexcept : data{std::move(json.data)}
445{
446 json.data = Data{};
447}
448
449JSON& JSON::operator=(JSON&& json) noexcept
450{
451 if (&json != this)
452 {
453 std::swap(a&: data, b&: json.data);
454 }
455 return *this;
456}
457
458JSON JSON::make(const Type type)
459{
460 return JSON(type);
461}
462
463JSON JSON::load(const std::string_view fmt)
464{
465 std::size_t offset = 0;
466 auto object = parseNext(fmt, offset);
467 if ((offset + 1) <= fmt.length())
468 {
469 throw std::runtime_error{"JSON syntax error, expected 'EOF' (" + std::string{fmt} + ")."};
470 }
471 return object;
472}
473
474JSON& JSON::operator[](const std::string& key)
475{
476 ensureType<Object>();
477 return getData<Object>()->operator[](k: key);
478}
479
480JSON& JSON::operator[](std::size_t index)
481{
482 ensureType<Array>();
483 const auto& arrayVal = getData<Array>();
484 if (index >= arrayVal->size())
485 {
486 arrayVal->resize(new_size: index + 1);
487 }
488 return arrayVal->operator[](n: index);
489}
490
491JSON& JSON::at(const std::string& key)
492{
493 return operator[](key);
494}
495
496const JSON& JSON::at(const std::string& key) const
497{
498 return getData<Object>()->at(k: key);
499}
500
501JSON& JSON::at(std::size_t index)
502{
503 return operator[](index);
504}
505
506const JSON& JSON::at(std::size_t index) const
507{
508 return getData<Array>()->at(n: index);
509}
510
511int JSON::length() const
512{
513 return holdsData<Array>() ? static_cast<int>(getData<Array>()->size()) : -1;
514}
515
516int JSON::size() const
517{
518 return std::visit(
519 visitor: ValueVisitor{
520 [](const Data::ObjectPtr& val) { return static_cast<int>(val->size()); },
521 [](const Data::ArrayPtr& val) { return static_cast<int>(val->size()); },
522 [](const auto& /*val*/) { return -1; }},
523 variants: data.value);
524}
525
526bool JSON::hasKey(const std::string& key) const
527{
528 return holdsData<Object>() && getData<Object>()->contains(x: key);
529}
530
531bool JSON::isNullType() const
532{
533 return holdsData<Null>();
534}
535
536bool JSON::isObjectType() const
537{
538 return holdsData<Object>();
539}
540
541bool JSON::isArrayType() const
542{
543 return holdsData<Array>();
544}
545
546bool JSON::isStringType() const
547{
548 return holdsData<String>();
549}
550
551bool JSON::isFloatingType() const
552{
553 return holdsData<Floating>();
554}
555
556bool JSON::isIntegralType() const
557{
558 return holdsData<Integral>();
559}
560
561bool JSON::isBooleanType() const
562{
563 return holdsData<Boolean>();
564}
565
566JSON::String JSON::asString() const
567{
568 return std::visit(
569 visitor: ValueVisitor{
570 [](const Data::StringPtr& val) -> String { return jsonEscape(fmt: *val); },
571 [this](const Data::ObjectPtr& /*val*/) -> String { return dumpMinified(); },
572 [this](const Data::ArrayPtr& /*val*/) -> String { return dumpMinified(); },
573 [](const Floating& val) -> String { return std::to_string(val: val); },
574 [](const Integral& val) -> String { return std::to_string(val: val); },
575 [](const Boolean& val) -> String { return val ? "true" : "false"; },
576 [](const Null& /*val*/) -> String { return "null"; },
577 [](const auto& /*val*/) -> String { return {}; }},
578 variants: data.value);
579}
580
581JSON::String JSON::asUnescapedString() const
582{
583 return std::visit(
584 visitor: ValueVisitor{
585 [](const Data::StringPtr& val) -> String { return *val; },
586 [this](const Data::ObjectPtr& /*val*/) -> String { return dumpMinified(); },
587 [this](const Data::ArrayPtr& /*val*/) -> String { return dumpMinified(); },
588 [](const Floating& val) -> String { return std::to_string(val: val); },
589 [](const Integral& val) -> String { return std::to_string(val: val); },
590 [](const Boolean& val) -> String { return val ? "true" : "false"; },
591 [](const Null& /*val*/) -> String { return "null"; },
592 [](const auto& /*val*/) -> String { return {}; }},
593 variants: data.value);
594}
595
596JSON::Floating JSON::asFloating() const
597{
598 return std::visit(
599 visitor: ValueVisitor{
600 [](const Floating& val) -> Floating { return val; },
601 [](const Integral& val) -> Floating { return val; },
602 [](const Boolean& val) -> Floating { return val; },
603 [](const Data::StringPtr& val) -> Floating
604 {
605 double parsed = 0.0;
606 try
607 {
608 parsed = std::stod(str: *val);
609 }
610 catch (const std::exception& err)
611 {
612 throw std::logic_error{
613 "Failed to convert the string value to floating in JSON, " + std::string{err.what()} + '.'};
614 }
615 return parsed;
616 },
617 [](const auto& /*val*/) -> Floating
618 { throw std::logic_error{"Failed to convert the value to floating in JSON."}; }},
619 variants: data.value);
620}
621
622JSON::Integral JSON::asIntegral() const
623{
624 return std::visit(
625 visitor: ValueVisitor{
626 [](const Integral& val) -> Integral { return val; },
627 [](const Boolean& val) -> Integral { return val; },
628 [](const Floating& val) -> Integral { return val; },
629 [](const Data::StringPtr& val) -> Integral
630 {
631 long long parsed = 0;
632 if (const auto result = std::from_chars(first: val->c_str(), last: val->c_str() + val->size(), value&: parsed);
633 result.ec == std::errc{})
634 {
635 return parsed;
636 }
637 throw std::logic_error{"Failed to convert the string value to integral in JSON."};
638 },
639 [](const auto& /*val*/) -> Integral
640 { throw std::logic_error{"Failed to convert the value to integral in JSON."}; }},
641 variants: data.value);
642}
643
644JSON::Boolean JSON::asBoolean() const
645{
646 return std::visit(
647 visitor: ValueVisitor{
648 [](const Boolean& val) -> Boolean { return val; },
649 [](const Floating& val) -> Boolean { return val; },
650 [](const Integral& val) -> Boolean { return val; },
651 [](const Data::StringPtr& val) -> Boolean
652 {
653 if (val->find(s: "true") != std::string::npos)
654 {
655 return true;
656 }
657 if (val->find(s: "false") != std::string::npos)
658 {
659 return false;
660 }
661 int parsed = 0;
662 if (const auto result = std::from_chars(first: val->c_str(), last: val->c_str() + val->size(), value&: parsed);
663 result.ec == std::errc{})
664 {
665 return parsed;
666 }
667 throw std::logic_error{"Failed to convert the string value to boolean in JSON."};
668 },
669 [](const auto& /*val*/) -> Boolean
670 { throw std::logic_error{"Failed to convert the value to boolean in JSON."}; }},
671 variants: data.value);
672}
673
674JSON::JSONWrapper<JSON::Object> JSON::objectRange()
675{
676 return holdsData<Object>() ? JSONWrapper<Object>{getData<Object>().get()} : JSONWrapper<Object>{nullptr};
677}
678
679JSON::JSONWrapper<JSON::Array> JSON::arrayRange()
680{
681 return holdsData<Array>() ? JSONWrapper<Array>{getData<Array>().get()} : JSONWrapper<Array>{nullptr};
682}
683
684JSON::JSONConstWrapper<JSON::Object> JSON::objectRange() const
685{
686 return holdsData<Object>() ? JSONConstWrapper<Object>{getData<Object>().get()} : JSONConstWrapper<Object>{nullptr};
687}
688
689JSON::JSONConstWrapper<JSON::Array> JSON::arrayRange() const
690{
691 return holdsData<Array>() ? JSONConstWrapper<Array>{getData<Array>().get()} : JSONConstWrapper<Array>{nullptr};
692}
693
694std::string JSON::dump(const std::uint32_t depth, const std::string_view tab) const
695{
696 return std::visit(
697 visitor: ValueVisitor{
698 [](const Null& /*val*/) -> std::string { return "null"; },
699 [depth, &tab](const Data::ObjectPtr& val) -> std::string
700 {
701 std::string pad{};
702 for (std::uint32_t i = 0; i < depth; ++i)
703 {
704 pad += tab;
705 }
706 std::string s("{\n");
707 for (bool skip = true; const auto& p : *val)
708 {
709 if (!skip)
710 {
711 s += ",\n";
712 }
713 s += (pad + '\"' + p.first + "\": " + p.second.dump(depth: depth + 1, tab));
714 skip = false;
715 }
716 s += ('\n' + pad.erase(pos: 0, n: tab.length()) + '}');
717 return s;
718 },
719 [depth, &tab](const Data::ArrayPtr& val) -> std::string
720 {
721 std::string s("[");
722 for (bool skip = true; const auto& p : *val)
723 {
724 if (!skip)
725 {
726 s += ", ";
727 }
728 s += p.dump(depth: depth + 1, tab);
729 skip = false;
730 }
731 s += ']';
732 return s;
733 },
734 [](const Data::StringPtr& val) -> std::string { return '\"' + jsonEscape(fmt: *val) + '\"'; },
735 [](const Floating& val) -> std::string { return std::to_string(val: val); },
736 [](const Integral& val) -> std::string { return std::to_string(val: val); },
737 [](const Boolean& val) -> std::string { return val ? "true" : "false"; },
738 [](const auto& /*val*/) -> std::string { return {}; }},
739 variants: data.value);
740}
741
742std::string JSON::dumpMinified() const
743{
744 return std::visit(
745 visitor: ValueVisitor{
746 [](const Null& /*val*/) -> std::string { return "null"; },
747 [](const Data::ObjectPtr& val) -> std::string
748 {
749 std::string s("{");
750 for (bool skip = true; const auto& p : *val)
751 {
752 if (!skip)
753 {
754 s += ',';
755 }
756 s += ('\"' + p.first + "\":" + p.second.dumpMinified());
757 skip = false;
758 }
759 s += '}';
760 return s;
761 },
762 [](const Data::ArrayPtr& val) -> std::string
763 {
764 std::string s("[");
765 for (bool skip = true; const auto& p : *val)
766 {
767 if (!skip)
768 {
769 s += ',';
770 }
771 s += p.dumpMinified();
772 skip = false;
773 }
774 s += ']';
775 return s;
776 },
777 [](const Data::StringPtr& val) -> std::string { return '\"' + jsonEscape(fmt: *val) + '\"'; },
778 [](const Floating& val) -> std::string { return std::to_string(val: val); },
779 [](const Integral& val) -> std::string { return std::to_string(val: val); },
780 [](const Boolean& val) -> std::string { return val ? "true" : "false"; },
781 [](const auto& /*val*/) -> std::string { return {}; }},
782 variants: data.value);
783}
784
785//! @brief The operator (<<) overloading of the JSON class.
786//! @param os - output stream object
787//! @param json - specific JSON object
788//! @return reference of the output stream object
789std::ostream& operator<<(std::ostream& os, const JSON& json)
790{
791 os << json.dump();
792 return os;
793}
794
795//! @brief Make an JSON array.
796//! @return JSON array
797JSON array()
798{
799 return JSON::make(type: JSON::Type::array);
800}
801
802//! @brief Make an JSON object.
803//! @return JSON object
804JSON object()
805{
806 return JSON::make(type: JSON::Type::object);
807}
808} // namespace utility::json
809