-
-
Save HamedFathi/1dab4887572270917516e0016001cec1 to your computer and use it in GitHub Desktop.
JSON parser and stringifier implemented in C# (Recursive Descent Parser)
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
using System; | |
using System.Collections.Generic; | |
using System.IO; | |
using System.Linq; | |
using System.Text; | |
using System.Text.RegularExpressions; | |
namespace MiniWebFramework | |
{ | |
public enum JSONValueType | |
{ | |
NULL, | |
TRUE, | |
FALSE, | |
NUMBER, | |
STRING, | |
ARRAY, | |
OBJECT | |
} | |
public class JSONValue | |
{ | |
public JSONValueType type { get; private set; } | |
public object value { get; private set; } | |
public JSONValue(JSONValueType t, object v) | |
{ | |
type = t; | |
value = v; | |
} | |
} | |
public static class JSON | |
{ | |
private static readonly char[] positive_digits = new char[] { '1', '2', '3', '4', '5', '6', '7', '8', '9' }; | |
private static readonly char[] all_digits = new char[] { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9' }; | |
private static readonly char[] white_space = new char[] { ' ', '\t', '\n', '\r' }; | |
private static readonly char[] string_breakers = new char[] { '"', '\\', '\r', '\n', '\t', '\b', '\f'}; | |
private static readonly char[] hexdigit = new char[] { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', | |
'a', 'b', 'c', 'd', 'e', 'f', | |
'A', 'B', 'C', 'D', 'E', 'F' | |
}; | |
private class KVPair | |
{ | |
public string key; | |
public JSONValue value; | |
} | |
private class ParseContext | |
{ | |
private static readonly char EOT = '\x04'; | |
private char[] data; | |
int position; | |
int cap; | |
public ParseContext(char[] _data) | |
{ | |
data = _data; | |
cap = _data.Length; | |
position = 0; | |
} | |
public bool isEOT() | |
{ | |
return !(position < cap); | |
} | |
public char Peek() | |
{ | |
if(position < cap) | |
return data[position]; | |
return EOT; | |
} | |
public char Read() | |
{ | |
if (position < cap) | |
return data[position++]; | |
return EOT; | |
} | |
public int Tell() | |
{ | |
return position; | |
} | |
public void Seek(int p) | |
{ | |
position = p; | |
} | |
public string StringFrom(int start) | |
{ | |
int strlen = position - start; | |
char[] segment = new char[strlen]; | |
for(var i = 0; i < strlen; i++) | |
{ | |
segment[i] = data[start + i]; | |
} | |
return new string(segment); | |
} | |
} | |
private static bool parse_literal(ParseContext p, string literal) | |
{ | |
bool isMatch = true; | |
int current = p.Tell(); | |
foreach(var c in literal) | |
{ | |
if(c != p.Read()) | |
{ | |
isMatch = false; | |
break; | |
} | |
} | |
if (!isMatch) | |
p.Seek(current); | |
return isMatch; | |
} | |
private static bool parse_char(ParseContext p, char c) | |
{ | |
if(c == p.Peek()) | |
{ | |
p.Read(); | |
return true; | |
} | |
return false; | |
} | |
private static bool parse_any(ParseContext p, char[] options) | |
{ | |
bool isMatch = false; | |
char next_char = p.Peek(); | |
for(var i = 0; i < options.Length; i++) | |
{ | |
if(options[i] == next_char) | |
{ | |
isMatch = true; | |
p.Read(); | |
break; | |
} | |
} | |
return isMatch; | |
} | |
private static bool parse_ws(ParseContext p) | |
{ | |
int num_matches = 0; | |
while(parse_any(p, white_space)) | |
{ | |
num_matches++; | |
} | |
return num_matches > 0; | |
} | |
private static JSONValue parse_json_null(ParseContext p) | |
{ | |
if (parse_literal(p, "null")) | |
return new JSONValue(JSONValueType.NULL, null); | |
return null; | |
} | |
private static JSONValue parse_json_true(ParseContext p) | |
{ | |
if (parse_literal(p, "true")) | |
return new JSONValue(JSONValueType.TRUE, null); | |
return null; | |
} | |
private static JSONValue parse_json_false(ParseContext p) | |
{ | |
if (parse_literal(p, "false")) | |
return new JSONValue(JSONValueType.FALSE, null); | |
return null; | |
} | |
private static JSONValue parse_json_number(ParseContext p) | |
{ | |
int start = p.Tell(); | |
parse_char(p, '-'); | |
if(!parse_int(p)) | |
{ | |
p.Seek(start); | |
return null; | |
} | |
parse_frac(p); | |
parse_exponent(p); | |
return new JSONValue(JSONValueType.NUMBER, Convert.ToDouble(p.StringFrom(start))); | |
} | |
private static bool parse_exponent(ParseContext p) | |
{ | |
int start = p.Tell(); | |
if (!parse_any(p, new char[] { 'e', 'E' })) | |
return false; | |
parse_any(p, new char[] { '-', '+' }); | |
if (!parse_any(p, all_digits)) | |
{ | |
p.Seek(start); | |
return false; | |
} | |
while(parse_any(p, all_digits)) | |
{ | |
} | |
return true; | |
} | |
private static bool parse_frac(ParseContext p) | |
{ | |
int start = p.Tell(); | |
if (!parse_char(p, '.')) | |
return false; | |
if (!parse_any(p, all_digits)) | |
{ | |
p.Seek(start); | |
return false; | |
} | |
while(parse_any(p, all_digits)) | |
{ | |
} | |
return true; | |
} | |
private static bool parse_int(ParseContext p) | |
{ | |
if(parse_char(p, '0')) | |
{ | |
return true; | |
} | |
if(!parse_any(p, positive_digits)) | |
{ | |
return false; | |
} | |
while(parse_any(p, all_digits)) | |
{ | |
} | |
return true; | |
} | |
private static string parse_string_token(ParseContext p) | |
{ | |
int start = p.Tell(); | |
if (!parse_char(p, '\"')) | |
return null; | |
while (parse_normal_char(p) || parse_escaped_char(p)) | |
{ | |
continue; | |
} | |
if(p.Peek() != '"') | |
{ | |
p.Seek(start); | |
return null; | |
} | |
var result = p.StringFrom(start + 1); // +1 skip initial double quote | |
p.Read(); // consume ending double quote | |
return Regex.Unescape(result); | |
} | |
private static JSONValue parse_json_string(ParseContext p) | |
{ | |
string s; | |
if ((s = parse_string_token(p)) != null) | |
return new JSONValue(JSONValueType.STRING, s); | |
else | |
return null; | |
} | |
private static bool parse_escaped_char(ParseContext p) | |
{ | |
int start = p.Tell(); | |
if (!parse_char(p, '\\')) | |
return false; | |
if (p.isEOT()) | |
{ | |
p.Seek(start); | |
return false; | |
} | |
var c = p.Read(); | |
if (c != 'u') | |
return true; | |
if( | |
!parse_any(p, hexdigit) || | |
!parse_any(p, hexdigit) || | |
!parse_any(p, hexdigit) || | |
!parse_any(p, hexdigit) | |
) | |
{ | |
p.Seek(start); | |
return false; | |
} | |
return true; | |
} | |
private static bool parse_normal_char(ParseContext p) | |
{ | |
var c = p.Peek(); | |
if (!shouldescape(c)) | |
{ | |
p.Read(); | |
return true; | |
} else | |
{ | |
return false; | |
} | |
} | |
private static JSONValue parse_json_array(ParseContext p) | |
{ | |
int start = p.Tell(); | |
if (!parse_char(p, '[')) | |
return null; | |
parse_ws(p); // skip optional space | |
List<JSONValue> elems = parse_elems(p); | |
if (!parse_char(p, ']')) | |
{ | |
p.Seek(start); | |
return null; | |
} | |
return new JSONValue(JSONValueType.ARRAY, elems); | |
} | |
private static List<JSONValue> parse_elems(ParseContext p) | |
{ | |
var elems = new List<JSONValue>(); | |
var elem = parse_jsonvalue(p); | |
while(elem != null) | |
{ | |
elems.Add(elem); | |
if (!parse_char(p, ',')) | |
break; | |
elem = parse_jsonvalue(p); | |
} | |
return elems; | |
} | |
private static JSONValue parse_json_object(ParseContext p) | |
{ | |
int start = p.Tell(); | |
if (!parse_char(p, '{')) | |
return null; | |
parse_ws(p); // skip optional space | |
Dictionary<string, JSONValue> elems = parse_kvpairs(p); | |
if (!parse_char(p, '}')) | |
{ | |
p.Seek(start); | |
return null; | |
} | |
return new JSONValue(JSONValueType.OBJECT, elems); | |
} | |
private static Dictionary<string, JSONValue> parse_kvpairs(ParseContext p) | |
{ | |
KVPair pair; | |
Dictionary<string, JSONValue> pairs = new Dictionary<string, JSONValue>(); | |
while((pair = parse_kvpair(p)) != null) | |
{ | |
pairs[pair.key] = pair.value; | |
parse_ws(p); // skip space if any | |
if (!parse_char(p, ',')) | |
break; | |
} | |
return pairs; | |
} | |
private static KVPair parse_kvpair(ParseContext p) | |
{ | |
int start = p.Tell(); | |
KVPair pair = new KVPair(); | |
parse_ws(p); // skip white space | |
string s = parse_string_token(p); | |
if (s == null) | |
return null; | |
parse_ws(p); // skip whitespace | |
if(!parse_char(p, ':')) | |
{ | |
p.Seek(start); | |
return null; | |
} | |
JSONValue v = parse_jsonvalue(p); | |
if(v == null) | |
{ | |
p.Seek(start); | |
return null; | |
} | |
return new KVPair() | |
{ | |
key = s, | |
value = v | |
}; | |
} | |
private static JSONValue parse_jsonvalue(ParseContext p) | |
{ | |
JSONValue v; | |
parse_ws(p); | |
if ( | |
(v = parse_json_null(p) ) != null || | |
(v = parse_json_true(p) ) != null || | |
(v = parse_json_false(p) ) != null || | |
(v = parse_json_number(p)) != null || | |
(v = parse_json_string(p)) != null || | |
(v = parse_json_array(p) ) != null || | |
(v = parse_json_object(p)) != null | |
) | |
{ | |
} | |
parse_ws(p); | |
return v; | |
} | |
public static JSONValue parse(string s) | |
{ | |
return parse(s.ToCharArray()); | |
} | |
public static JSONValue parse(char[] s) | |
{ | |
ParseContext p = new ParseContext(s); | |
JSONValue v = parse_jsonvalue(p); | |
if (p.isEOT()) | |
return v; | |
else | |
return null; | |
} | |
public static string stringify() | |
{ | |
return "null"; | |
} | |
public static string stringify(bool b) | |
{ | |
if (b) | |
return "true"; | |
else | |
return "false"; | |
} | |
private static bool shouldescape(char c) | |
{ | |
return !(c >= ' ' && c != '"' && c != '\\'); | |
} | |
private static string escape(string s) | |
{ | |
using (var ms = new MemoryStream()) | |
using (var wr = new StreamWriter(ms)) | |
{ | |
foreach(char c in s) | |
{ | |
if(shouldescape(c)) | |
{ | |
int x = c; | |
wr.Write("\\u"); | |
wr.Write(x.ToString("X4")); | |
} else | |
{ | |
wr.Write(c); | |
} | |
} | |
wr.Flush(); | |
ms.Seek(0, SeekOrigin.Begin); | |
return Encoding.UTF8.GetString(ms.ToArray()); | |
} | |
} | |
public static string stringify(string s) | |
{ | |
return String.Format("\"{0}\"",escape(s)); | |
} | |
public static string stringify(double d) | |
{ | |
return d.ToString(); | |
} | |
public static string stringify(List<object> l) | |
{ | |
List<string> parts = new List<string>(); | |
foreach(var o in l) | |
{ | |
if (o == null) | |
{ | |
parts.Add("null"); | |
continue; | |
} | |
Type t = o.GetType(); | |
do | |
{ | |
if(t == typeof(bool)) | |
{ | |
if ((bool)o) | |
parts.Add("true"); | |
else | |
parts.Add("false"); | |
break; | |
} | |
if (t == typeof(string)) | |
{ | |
parts.Add(stringify((string)o)); | |
break; | |
} | |
if (t == typeof(int) || t == typeof(double) || t == typeof(float)) | |
{ | |
parts.Add(stringify(Convert.ToDouble(o))); | |
break; | |
} | |
if (t == typeof(List<object>)) | |
{ | |
parts.Add(stringify((List<object>)o)); | |
break; | |
} | |
if (t == typeof(Dictionary<string, object>)) | |
{ | |
parts.Add(stringify((Dictionary<string, object>)o)); | |
break; | |
} | |
throw new InvalidDataException(); | |
} while (false); | |
} | |
return "[" + String.Join(",", parts.ToArray()) + "]"; | |
} | |
public static string stringify(Dictionary<string, object> o) | |
{ | |
List<string> parts = new List<string>(); | |
foreach(KeyValuePair<string, object> kv in o) | |
{ | |
if (kv.Value == null) | |
{ | |
parts.Add(String.Format("{0}:null", stringify(kv.Key))); | |
continue; | |
} | |
Type t = kv.Value.GetType(); | |
do | |
{ | |
if (t == typeof(bool)) | |
{ | |
if ((bool)kv.Value) | |
parts.Add(String.Format("{0}:true", stringify(kv.Key))); | |
else | |
parts.Add(String.Format("{0}:false", stringify(kv.Key))); | |
break; | |
} | |
if (t == typeof(string)) | |
{ | |
parts.Add(String.Format("{0}:{1}", stringify(kv.Key), stringify((string)kv.Value))); | |
break; | |
} | |
if (t == typeof(int) || t == typeof(double) || t == typeof(float)) | |
{ | |
parts.Add(String.Format("{0}:{1}", stringify(kv.Key), stringify(Convert.ToDouble(kv.Value)))); | |
break; | |
} | |
if (t == typeof(List<object>)) | |
{ | |
parts.Add(String.Format("{0}:{1}", stringify(kv.Key), stringify((List<object>)kv.Value))); | |
break; | |
} | |
if (t == typeof(Dictionary<string, object>)) | |
{ | |
parts.Add(String.Format("{0}:{1}", stringify(kv.Key), stringify((Dictionary<string, object>)kv.Value))); | |
break; | |
} | |
throw new InvalidDataException(); | |
} while (false); | |
} | |
return "{" + String.Join(",", parts.ToArray()) + "}"; | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment