366 lines
12 KiB
C#
366 lines
12 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Text;
|
|
|
|
namespace Unosquare.Swan.Formatters {
|
|
/// <summary>
|
|
/// A very simple, light-weight JSON library written by Mario
|
|
/// to teach Geo how things are done
|
|
///
|
|
/// This is an useful helper for small tasks but it doesn't represent a full-featured
|
|
/// serializer such as the beloved Json.NET.
|
|
/// </summary>
|
|
public partial class Json {
|
|
/// <summary>
|
|
/// A simple JSON Deserializer.
|
|
/// </summary>
|
|
private class Deserializer {
|
|
#region State Variables
|
|
|
|
private readonly Object _result;
|
|
private readonly Dictionary<String, Object> _resultObject;
|
|
private readonly List<Object> _resultArray;
|
|
|
|
private readonly ReadState _state = ReadState.WaitingForRootOpen;
|
|
private readonly String _currentFieldName;
|
|
private readonly String _json;
|
|
|
|
private Int32 _index;
|
|
|
|
#endregion
|
|
|
|
private Deserializer(String json, Int32 startIndex) {
|
|
this._json = json;
|
|
|
|
for(this._index = startIndex; this._index < this._json.Length; this._index++) {
|
|
#region Wait for { or [
|
|
|
|
if(this._state == ReadState.WaitingForRootOpen) {
|
|
if(Char.IsWhiteSpace(this._json, this._index)) {
|
|
continue;
|
|
}
|
|
|
|
if(this._json[this._index] == OpenObjectChar) {
|
|
this._resultObject = new Dictionary<String, Object>();
|
|
this._state = ReadState.WaitingForField;
|
|
continue;
|
|
}
|
|
|
|
if(this._json[this._index] == OpenArrayChar) {
|
|
this._resultArray = new List<Object>();
|
|
this._state = ReadState.WaitingForValue;
|
|
continue;
|
|
}
|
|
|
|
throw this.CreateParserException($"'{OpenObjectChar}' or '{OpenArrayChar}'");
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Wait for opening field " (only applies for object results)
|
|
|
|
if(this._state == ReadState.WaitingForField) {
|
|
if(Char.IsWhiteSpace(this._json, this._index)) {
|
|
continue;
|
|
}
|
|
|
|
// Handle empty arrays and empty objects
|
|
if(this._resultObject != null && this._json[this._index] == CloseObjectChar
|
|
|| this._resultArray != null && this._json[this._index] == CloseArrayChar) {
|
|
this._result = this._resultObject ?? this._resultArray as Object;
|
|
return;
|
|
}
|
|
|
|
if(this._json[this._index] != StringQuotedChar) {
|
|
throw this.CreateParserException($"'{StringQuotedChar}'");
|
|
}
|
|
|
|
Int32 charCount = this.GetFieldNameCount();
|
|
|
|
this._currentFieldName = Unescape(this._json.SliceLength(this._index + 1, charCount));
|
|
this._index += charCount + 1;
|
|
this._state = ReadState.WaitingForColon;
|
|
continue;
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Wait for field-value separator : (only applies for object results
|
|
|
|
if(this._state == ReadState.WaitingForColon) {
|
|
if(Char.IsWhiteSpace(this._json, this._index)) {
|
|
continue;
|
|
}
|
|
|
|
if(this._json[this._index] != ValueSeparatorChar) {
|
|
throw this.CreateParserException($"'{ValueSeparatorChar}'");
|
|
}
|
|
|
|
this._state = ReadState.WaitingForValue;
|
|
continue;
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Wait for and Parse the value
|
|
|
|
if(this._state == ReadState.WaitingForValue) {
|
|
if(Char.IsWhiteSpace(this._json, this._index)) {
|
|
continue;
|
|
}
|
|
|
|
// Handle empty arrays and empty objects
|
|
if(this._resultObject != null && this._json[this._index] == CloseObjectChar
|
|
|| this._resultArray != null && this._json[this._index] == CloseArrayChar) {
|
|
this._result = this._resultObject ?? this._resultArray as Object;
|
|
return;
|
|
}
|
|
|
|
// determine the value based on what it starts with
|
|
switch(this._json[this._index]) {
|
|
case StringQuotedChar: // expect a string
|
|
this.ExtractStringQuoted();
|
|
break;
|
|
|
|
case OpenObjectChar: // expect object
|
|
case OpenArrayChar: // expect array
|
|
this.ExtractObject();
|
|
break;
|
|
|
|
case 't': // expect true
|
|
this.ExtractConstant(TrueLiteral, true);
|
|
break;
|
|
|
|
case 'f': // expect false
|
|
this.ExtractConstant(FalseLiteral, false);
|
|
break;
|
|
|
|
case 'n': // expect null
|
|
this.ExtractConstant(NullLiteral, null);
|
|
break;
|
|
|
|
default: // expect number
|
|
this.ExtractNumber();
|
|
break;
|
|
}
|
|
|
|
this._currentFieldName = null;
|
|
this._state = ReadState.WaitingForNextOrRootClose;
|
|
continue;
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Wait for closing ], } or an additional field or value ,
|
|
|
|
if(this._state != ReadState.WaitingForNextOrRootClose) {
|
|
continue;
|
|
}
|
|
|
|
if(Char.IsWhiteSpace(this._json, this._index)) {
|
|
continue;
|
|
}
|
|
|
|
if(this._json[this._index] == FieldSeparatorChar) {
|
|
if(this._resultObject != null) {
|
|
this._state = ReadState.WaitingForField;
|
|
this._currentFieldName = null;
|
|
continue;
|
|
}
|
|
|
|
this._state = ReadState.WaitingForValue;
|
|
continue;
|
|
}
|
|
|
|
if(this._resultObject != null && this._json[this._index] == CloseObjectChar ||
|
|
this._resultArray != null && this._json[this._index] == CloseArrayChar) {
|
|
this._result = this._resultObject ?? this._resultArray as Object;
|
|
return;
|
|
}
|
|
|
|
throw this.CreateParserException($"'{FieldSeparatorChar}' '{CloseObjectChar}' or '{CloseArrayChar}'");
|
|
|
|
#endregion
|
|
}
|
|
}
|
|
|
|
internal static Object DeserializeInternal(String json) => new Deserializer(json, 0)._result;
|
|
|
|
private static String Unescape(String str) {
|
|
// check if we need to unescape at all
|
|
if(str.IndexOf(StringEscapeChar) < 0) {
|
|
return str;
|
|
}
|
|
|
|
StringBuilder builder = new StringBuilder(str.Length);
|
|
for(Int32 i = 0; i < str.Length; i++) {
|
|
if(str[i] != StringEscapeChar) {
|
|
_ = builder.Append(str[i]);
|
|
continue;
|
|
}
|
|
|
|
if(i + 1 > str.Length - 1) {
|
|
break;
|
|
}
|
|
|
|
// escape sequence begins here
|
|
switch(str[i + 1]) {
|
|
case 'u':
|
|
i = ExtractEscapeSequence(str, i, builder);
|
|
break;
|
|
case 'b':
|
|
_ = builder.Append('\b');
|
|
i += 1;
|
|
break;
|
|
case 't':
|
|
_ = builder.Append('\t');
|
|
i += 1;
|
|
break;
|
|
case 'n':
|
|
_ = builder.Append('\n');
|
|
i += 1;
|
|
break;
|
|
case 'f':
|
|
_ = builder.Append('\f');
|
|
i += 1;
|
|
break;
|
|
case 'r':
|
|
_ = builder.Append('\r');
|
|
i += 1;
|
|
break;
|
|
default:
|
|
_ = builder.Append(str[i + 1]);
|
|
i += 1;
|
|
break;
|
|
}
|
|
}
|
|
|
|
return builder.ToString();
|
|
}
|
|
|
|
private static Int32 ExtractEscapeSequence(String str, Int32 i, StringBuilder builder) {
|
|
Int32 startIndex = i + 2;
|
|
Int32 endIndex = i + 5;
|
|
if(endIndex > str.Length - 1) {
|
|
_ = builder.Append(str[i + 1]);
|
|
i += 1;
|
|
return i;
|
|
}
|
|
|
|
Byte[] hexCode = str.Slice(startIndex, endIndex).ConvertHexadecimalToBytes();
|
|
_ = builder.Append(Encoding.BigEndianUnicode.GetChars(hexCode));
|
|
i += 5;
|
|
return i;
|
|
}
|
|
|
|
private Int32 GetFieldNameCount() {
|
|
Int32 charCount = 0;
|
|
for(Int32 j = this._index + 1; j < this._json.Length; j++) {
|
|
if(this._json[j] == StringQuotedChar && this._json[j - 1] != StringEscapeChar) {
|
|
break;
|
|
}
|
|
|
|
charCount++;
|
|
}
|
|
|
|
return charCount;
|
|
}
|
|
|
|
private void ExtractObject() {
|
|
// Extract and set the value
|
|
Deserializer deserializer = new Deserializer(this._json, this._index);
|
|
|
|
if(this._currentFieldName != null) {
|
|
this._resultObject[this._currentFieldName] = deserializer._result;
|
|
} else {
|
|
this._resultArray.Add(deserializer._result);
|
|
}
|
|
|
|
this._index = deserializer._index;
|
|
}
|
|
|
|
private void ExtractNumber() {
|
|
Int32 charCount = 0;
|
|
for(Int32 j = this._index; j < this._json.Length; j++) {
|
|
if(Char.IsWhiteSpace(this._json[j]) || this._json[j] == FieldSeparatorChar
|
|
|| this._resultObject != null && this._json[j] == CloseObjectChar
|
|
|| this._resultArray != null && this._json[j] == CloseArrayChar) {
|
|
break;
|
|
}
|
|
|
|
charCount++;
|
|
}
|
|
|
|
// Extract and set the value
|
|
String stringValue = this._json.SliceLength(this._index, charCount);
|
|
|
|
if(Decimal.TryParse(stringValue, out Decimal value) == false) {
|
|
throw this.CreateParserException("[number]");
|
|
}
|
|
|
|
if(this._currentFieldName != null) {
|
|
this._resultObject[this._currentFieldName] = value;
|
|
} else {
|
|
this._resultArray.Add(value);
|
|
}
|
|
|
|
this._index += charCount - 1;
|
|
}
|
|
|
|
private void ExtractConstant(String boolValue, Boolean? value) {
|
|
if(!this._json.SliceLength(this._index, boolValue.Length).Equals(boolValue)) {
|
|
throw this.CreateParserException($"'{ValueSeparatorChar}'");
|
|
}
|
|
|
|
// Extract and set the value
|
|
if(this._currentFieldName != null) {
|
|
this._resultObject[this._currentFieldName] = value;
|
|
} else {
|
|
this._resultArray.Add(value);
|
|
}
|
|
|
|
this._index += boolValue.Length - 1;
|
|
}
|
|
|
|
private void ExtractStringQuoted() {
|
|
Int32 charCount = 0;
|
|
Boolean escapeCharFound = false;
|
|
for(Int32 j = this._index + 1; j < this._json.Length; j++) {
|
|
if(this._json[j] == StringQuotedChar && !escapeCharFound) {
|
|
break;
|
|
}
|
|
|
|
escapeCharFound = this._json[j] == StringEscapeChar && !escapeCharFound;
|
|
charCount++;
|
|
}
|
|
|
|
// Extract and set the value
|
|
String value = Unescape(this._json.SliceLength(this._index + 1, charCount));
|
|
if(this._currentFieldName != null) {
|
|
this._resultObject[this._currentFieldName] = value;
|
|
} else {
|
|
this._resultArray.Add(value);
|
|
}
|
|
|
|
this._index += charCount + 1;
|
|
}
|
|
|
|
private FormatException CreateParserException(String expected) {
|
|
Tuple<Int32, Int32> textPosition = this._json.TextPositionAt(this._index);
|
|
return new FormatException(
|
|
$"Parser error (Line {textPosition.Item1}, Col {textPosition.Item2}, State {this._state}): Expected {expected} but got '{this._json[this._index]}'.");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Defines the different JSON read states.
|
|
/// </summary>
|
|
private enum ReadState {
|
|
WaitingForRootOpen,
|
|
WaitingForField,
|
|
WaitingForColon,
|
|
WaitingForValue,
|
|
WaitingForNextOrRootClose,
|
|
}
|
|
}
|
|
}
|
|
} |