Created
January 24, 2019 15:34
-
-
Save ctigeek/7e725f431f444d4797e989faac7976a9 to your computer and use it in GitHub Desktop.
Tjson Deserializer....
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// written by Steven Swenson | |
// Released under the MIT open source license... | |
// No warranty at all. use at your own risk. | |
public static class Tjson | |
{ | |
//https://gist.github.com/ctigeek/4950c4b07a50a0791f012858e3bb2214 | |
private static readonly Splitter.Preserver QuotePreserver = new Splitter.Preserver { Start = '"', End = '"', Escape = '\\' }; | |
private static readonly Splitter.Preserver CurlyBoiPreserver = new Splitter.Preserver { Start = '{', End = '}', Escape = '\\', Recursive = true, SubPreserver = QuotePreserver }; | |
private static readonly Splitter.Preserver BracketPreserver = new Splitter.Preserver { Start = '[', End = ']', Escape = '\\', Recursive = true, SubPreserver = QuotePreserver }; | |
public static T Deserialize<T>(string literal) where T: new() | |
{ | |
var obj = new T(); | |
return (T) Deserialize(literal, typeof(T), obj); | |
} | |
public static object Deserialize(string literal, Type type) | |
{ | |
var obj = Activator.CreateInstance(type.Assembly.FullName, type.FullName).Unwrap(); | |
return Deserialize(literal, type, obj); | |
} | |
private static object Deserialize(string literal, Type type, object obj) | |
{ | |
var nvps = ParseObjectIntoStrings(literal.Trim()); | |
var props = type.GetProperties(); | |
var fields = type.GetFields(); | |
foreach (var part in nvps) | |
{ | |
var nvp = ParseNvpIntoStrings(part); | |
var prop1 = props.FirstOrDefault(p => p.Name == nvp.Name); | |
if (prop1 != null) | |
{ | |
prop1.SetValue(obj, CreateNetObject(nvp, prop1.PropertyType)); | |
} | |
else | |
{ | |
var fld1 = fields.FirstOrDefault(f => f.Name == nvp.Name); | |
if (fld1 != null) | |
{ | |
fld1.SetValue(obj, CreateNetObject(nvp, fld1.FieldType)); | |
} | |
} | |
} | |
return obj; | |
} | |
private static object CreateNetObject(NVP nvp, Type propType) | |
{ | |
if (nvp.ValueType == TjsonType.TjsonObject) | |
{ | |
var subObject = Deserialize(nvp.Value, propType); | |
return subObject; | |
} | |
if (nvp.ValueType == TjsonType.TjsonSeries) | |
{ | |
var array = ParseArray(nvp.Series, nvp.Value, propType); | |
return array; | |
} | |
var value = TurnValueIntoObject(nvp); | |
return value; | |
} | |
private static object TurnValueIntoObject(NVP nvp) | |
{ | |
switch (nvp.ValueType) | |
{ | |
case TjsonType.TjsonString: | |
return nvp.Value; | |
case TjsonType.TjsonBool: | |
return bool.Parse(nvp.Value); | |
case TjsonType.TjsonTime: | |
return ParseRfc3339DateTime(nvp.Value); | |
case TjsonType.TjsonInt: | |
var lng = long.Parse(nvp.Value); | |
if (lng <= int.MaxValue) | |
{ | |
return (int)lng; | |
} | |
return lng; | |
case TjsonType.TjsonUint: | |
var ulng = ulong.Parse(nvp.Value); | |
if (ulng <= uint.MaxValue) | |
{ | |
return (uint)ulng; | |
} | |
return ulng; | |
case TjsonType.TjsonFloat: | |
return float.Parse(nvp.Value); | |
case TjsonType.TjsonObject: | |
return nvp.Value; | |
case TjsonType.TjsonBase16: | |
return Encoding.ASCII.GetBytes(nvp.Value); | |
case TjsonType.TjsonBase32: | |
return DecodeBase32(nvp.Value); | |
case TjsonType.TjsonBase64: | |
return Convert.FromBase64String(nvp.Value); | |
default: | |
throw new Exception("This can never happen."); | |
} | |
} | |
private static object ParseArray(Series series, string value, Type arrayType) | |
{ | |
if (value[0] != '[' || value[value.Length - 1] != ']') | |
{ | |
throw new Exception("Invalid tjson: array must start and end with brackets."); | |
} | |
var noBrackets = value.Substring(1, value.Length - 2).Trim(); | |
bool shouldRemoveQuotes = ShouldRemoveQuotes(series.ElementType); | |
var arrayParts = Splitter.Split(noBrackets, ',', '\0', QuotePreserver, CurlyBoiPreserver, BracketPreserver) | |
.Select(e => shouldRemoveQuotes ? RemoveQuotes(e.Trim()) : e.Trim()).ToArray(); | |
//todo: verify set has unique elements | |
var array = Activator.CreateInstance(arrayType, arrayParts.Length) as Array; | |
int arrayIndex = 0; | |
foreach (var part in arrayParts) | |
{ | |
if (series.ElementType == TjsonType.TjsonSeries) | |
{ | |
var subarray = ParseArray(series.SubSeries, part, arrayType.GetElementType()); | |
array.SetValue(subarray, arrayIndex); | |
} | |
else if (series.ElementType == TjsonType.TjsonObject) | |
{ | |
var obj = Deserialize(part, arrayType.GetElementType()); | |
array.SetValue(obj, arrayIndex); | |
} | |
else | |
{ | |
var obj = TurnValueIntoObject(new NVP { Value = part, ValueType = series.ElementType }); | |
array.SetValue(obj, arrayIndex); | |
} | |
arrayIndex++; | |
} | |
return array; | |
} | |
private static byte[] DecodeBase32(string input) | |
{ | |
string ValidBase32Chars = "QAZ2WSX3EDC4RFV5TGB6YHN7UJM8K9LP"; | |
input = input.TrimEnd('='); //remove padding characters | |
int byteCount = input.Length * 5 / 8; //this must be TRUNCATED | |
byte[] returnArray = new byte[byteCount]; | |
byte curByte = 0, bitsRemaining = 8; | |
int mask = 0, arrayIndex = 0; | |
foreach (char c in input) | |
{ | |
int cValue = (int)c; | |
//65-90 == uppercase letters | |
if (cValue < 91 && cValue > 64) | |
{ | |
cValue = cValue - 65; | |
} | |
//50-55 == numbers 2-7 | |
if (cValue < 56 && cValue > 49) | |
{ | |
cValue = cValue - 24; | |
} | |
//97-122 == lowercase letters | |
if (cValue < 123 && cValue > 96) | |
{ | |
cValue = cValue - 97; | |
} | |
if (bitsRemaining > 5) | |
{ | |
mask = cValue << (bitsRemaining - 5); | |
curByte = (byte)(curByte | mask); | |
bitsRemaining -= 5; | |
} | |
else | |
{ | |
mask = cValue >> (5 - bitsRemaining); | |
curByte = (byte)(curByte | mask); | |
returnArray[arrayIndex++] = curByte; | |
curByte = (byte)(cValue << (3 + bitsRemaining)); | |
bitsRemaining += 3; | |
} | |
} | |
//if we didn't end with a full byte | |
if (arrayIndex != byteCount) | |
{ | |
returnArray[arrayIndex] = curByte; | |
} | |
return returnArray; | |
} | |
private static readonly string[] Rfc3339DateTimePatterns = new[] | |
{ | |
"yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fffffffK", | |
"yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'ffffffK", | |
"yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fffffK", | |
"yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'ffffK", | |
"yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fffK", | |
"yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'ffK", | |
"yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fK", | |
"yyyy'-'MM'-'dd'T'HH':'mm':'ssK", | |
"yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fffffffK" | |
}; | |
private static DateTime ParseRfc3339DateTime(string value) | |
{ | |
if (DateTime.TryParseExact(value, Rfc3339DateTimePatterns, DateTimeFormatInfo.InvariantInfo, DateTimeStyles.AdjustToUniversal, out var parseResult)) | |
{ | |
parseResult = DateTime.SpecifyKind(parseResult, DateTimeKind.Utc); | |
return parseResult; | |
} | |
throw new Exception("Invalid TJSON. DateTimes must be in Rfc3339 format. This is invalid: `" + value + "`."); | |
} | |
private static NVP ParseNvpIntoStrings(string nvpString) | |
{ | |
var results = Splitter.Split(nvpString, ':', '\0', QuotePreserver, BracketPreserver, CurlyBoiPreserver) | |
.Select(e => e.Trim()).ToArray(); | |
if (results.Length != 2) | |
{ | |
throw new Exception("Invalid JSON document. Error parsing for ':'."); | |
} | |
var nameWithoutQuotes = results[0].Substring(1, results[0].Length - 2); | |
var nameTypeParts = nameWithoutQuotes.Split(':'); | |
if (nameTypeParts.Length != 2) | |
{ | |
throw new Exception("Invalid TJSON document. Name must include type."); | |
} | |
var nvp = new NVP | |
{ | |
Name = nameTypeParts[0].Replace("-", "_"), | |
Value = results[1], | |
ValueType = GetTjsonType(nameTypeParts[1]) | |
}; | |
if (nvp.ValueType == TjsonType.TjsonSeries) | |
{ | |
nvp.Series = ParseSeriesTypes(nameTypeParts[1]); | |
} | |
if (ShouldRemoveQuotes(nvp.ValueType)) | |
{ | |
nvp.Value = RemoveQuotes(nvp.Value); | |
} | |
return nvp; | |
} | |
private static Series ParseSeriesTypes(string typeId) | |
{ | |
var series = new Series(); | |
if (typeId[0] == 'A') | |
{ | |
series.SeriesType = TjsonSeriesType.TjsonArray; | |
} | |
else if (typeId[0] == 'S') | |
{ | |
series.SeriesType = TjsonSeriesType.TjsonSet; | |
} | |
else | |
{ | |
throw new Exception("Unknown series type."); | |
} | |
var removeAO = typeId.Substring(1); | |
if (!(removeAO.StartsWith("<") && removeAO.EndsWith(">"))) | |
{ | |
throw new Exception("Invalid TJSON: Incorrect type identifier:" + typeId); | |
} | |
if (removeAO == "<>") | |
{ | |
series.ElementType = TjsonType.TjsonObject; | |
return series; | |
} | |
var removeChops = removeAO.Substring(1, removeAO.Length - 2); //removeChops will now either be a simple type, an object, or another series.... | |
series.ElementType = GetTjsonType(removeChops); | |
if (series.ElementType == TjsonType.TjsonSeries) | |
{ | |
series.SubSeries = ParseSeriesTypes(removeChops); | |
} | |
return series; | |
} | |
private static TjsonType GetTjsonType(string typeId) | |
{ | |
if (typeId.StartsWith("A<") || typeId.StartsWith("S<")) | |
{ | |
return TjsonType.TjsonSeries; | |
} | |
switch (typeId) | |
{ | |
case "s": | |
return TjsonType.TjsonString; | |
case "O": | |
return TjsonType.TjsonObject; | |
case "d": | |
return TjsonType.TjsonBase64; | |
case "d16": | |
return TjsonType.TjsonBase16; | |
case "d32": | |
return TjsonType.TjsonBase32; | |
case "d64": | |
return TjsonType.TjsonBase64; | |
case "i": | |
return TjsonType.TjsonInt; | |
case "u": | |
return TjsonType.TjsonUint; | |
case "f": | |
return TjsonType.TjsonFloat; | |
case "b": | |
return TjsonType.TjsonBool; | |
case "t": | |
return TjsonType.TjsonTime; | |
default: | |
throw new Exception("Unknown TJSON type: " + typeId); | |
} | |
} | |
private static string[] ParseObjectIntoStrings(string literal) | |
{ | |
if (literal[0] != '{' || literal[literal.Length - 1] != '}') | |
{ | |
throw new Exception("Invalid JSON object. Missing opening or closing bracket."); | |
} | |
literal = literal.Substring(1, literal.Length - 2); | |
var results = Splitter.Split(literal, ',', '\0', QuotePreserver, BracketPreserver, CurlyBoiPreserver) | |
.Select(e => e.Trim()).ToArray(); | |
return results; | |
} | |
private static bool ShouldRemoveQuotes(TjsonType tjsonType) | |
{ | |
return (tjsonType == TjsonType.TjsonString || tjsonType == TjsonType.TjsonBase16 || | |
tjsonType == TjsonType.TjsonBase32 || tjsonType == TjsonType.TjsonBase64 || | |
tjsonType == TjsonType.TjsonFloat || tjsonType == TjsonType.TjsonInt || | |
tjsonType == TjsonType.TjsonUint || tjsonType == TjsonType.TjsonTime); | |
} | |
private static string RemoveQuotes(string value) | |
{ | |
if (value[0] == '"' && value[value.Length - 1] == '"') | |
{ | |
return value.Substring(1, value.Length - 2); | |
} | |
throw new Exception("Invalid TJSON: The following value must be wrapped in quotes: \r\n" + value); | |
} | |
private enum TjsonType | |
{ | |
TjsonString, | |
TjsonObject, | |
TjsonSeries, | |
TjsonBase16, | |
TjsonBase32, | |
TjsonBase64, | |
TjsonInt, | |
TjsonUint, | |
TjsonFloat, | |
TjsonBool, | |
TjsonTime | |
} | |
private enum TjsonSeriesType | |
{ | |
TjsonArray, | |
TjsonSet | |
} | |
//Dead code... | |
private static Type GetTypeFromTjsonType(TjsonType tjsonType) | |
{ | |
switch (tjsonType) | |
{ | |
case TjsonType.TjsonString: | |
return typeof(string); | |
case TjsonType.TjsonObject: | |
return typeof(object); | |
case TjsonType.TjsonBool: | |
return typeof(bool); | |
case TjsonType.TjsonTime: | |
return typeof(DateTime); | |
case TjsonType.TjsonBase16: | |
case TjsonType.TjsonBase32: | |
case TjsonType.TjsonBase64: | |
return typeof(byte[]); | |
case TjsonType.TjsonInt: | |
return typeof(long); | |
case TjsonType.TjsonUint: | |
return typeof(ulong); | |
case TjsonType.TjsonFloat: | |
return typeof(float); | |
default: | |
throw new Exception("ermagawd"); | |
} | |
} | |
private class NVP | |
{ | |
public string Name; | |
public string Value; | |
public TjsonType ValueType; | |
public Series Series; | |
} | |
private class Series | |
{ | |
//we might need the hard type here too to track during recursive calls | |
public TjsonSeriesType SeriesType; | |
public TjsonType ElementType; | |
public Series SubSeries; | |
} | |
} | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment