450 lines
13 KiB
MQL5
450 lines
13 KiB
MQL5
|
//+------------------------------------------------------------------+
|
||
|
//| toyjson.mqh |
|
||
|
//| Copyright 2022, MetaQuotes Ltd. |
|
||
|
//| https://www.mql5.com |
|
||
|
//+------------------------------------------------------------------+
|
||
|
|
||
|
#include "MapArray.mqh"
|
||
|
#include "AutoPtr.mqh"
|
||
|
#include "Defines.mqh"
|
||
|
|
||
|
enum JS_TYPE
|
||
|
{
|
||
|
JS_OBJECT,
|
||
|
JS_ARRAY,
|
||
|
JS_PRIMITIVE,
|
||
|
JS_NULL // singleton object stub for undefined props
|
||
|
};
|
||
|
|
||
|
enum JS_AFFINITY // for primitive type
|
||
|
{
|
||
|
JS_NONE = 0,
|
||
|
JS_CONST = 1, // true, false, null
|
||
|
JS_INT = 2,
|
||
|
JS_FLOAT = 4,
|
||
|
JS_STRING = 8
|
||
|
};
|
||
|
|
||
|
//+------------------------------------------------------------------+
|
||
|
//| Main class to store single value (primitive, object, or array) |
|
||
|
//+------------------------------------------------------------------+
|
||
|
class JsValue
|
||
|
{
|
||
|
MapArray<string, int> properties;
|
||
|
AutoPtr<JsValue> objects[];
|
||
|
|
||
|
const static JsValue null;
|
||
|
static bool selfcall;
|
||
|
|
||
|
public:
|
||
|
const string s; // value is assigned once at construction
|
||
|
const JS_TYPE t; // type and
|
||
|
const JS_AFFINITY a; // affinity are detected
|
||
|
JsValue(const string _s): s(_s), t(JS_PRIMITIVE), a(detect()) { }
|
||
|
|
||
|
JsValue(const JS_TYPE _t = JS_OBJECT): t(_t), s(NULL), a(JS_NONE) { }
|
||
|
|
||
|
int size() const
|
||
|
{
|
||
|
return ArraySize(objects);
|
||
|
}
|
||
|
|
||
|
static JS_AFFINITY isNumeric(const ushort c)
|
||
|
{
|
||
|
if((c >= '0' && c <= '9') || c == '+' || c == '-') return JS_INT;
|
||
|
else if(c == '.' || c == 'e' || c == 'E') return JS_FLOAT;
|
||
|
else if(c == ' ' || c == '\r' || c == '\n' || c == '\t') return JS_NONE;
|
||
|
return JS_STRING;
|
||
|
}
|
||
|
|
||
|
JS_AFFINITY detect() const
|
||
|
{
|
||
|
if(s == "true" || s == "false" || s == "null") return JS_CONST;
|
||
|
JS_AFFINITY result = 0;
|
||
|
for(int i = 0; i < StringLen(s) && result < JS_STRING; ++i)
|
||
|
{
|
||
|
result |= isNumeric(s[i]);
|
||
|
}
|
||
|
return result;
|
||
|
}
|
||
|
|
||
|
string stringify() const
|
||
|
{
|
||
|
if(a >= JS_STRING)
|
||
|
{
|
||
|
string ss = s;
|
||
|
StringReplace(ss, "\"", "\\\"");
|
||
|
return "\"" + ss + "\"";
|
||
|
}
|
||
|
if(a >= JS_FLOAT) return s; // FIXME: apply specific accuracy
|
||
|
if(a >= JS_INT) return s;
|
||
|
return s != NULL ? s : "null";
|
||
|
}
|
||
|
|
||
|
template<typename T>
|
||
|
T get(const bool typechecks = true) const
|
||
|
{
|
||
|
if(typechecks)
|
||
|
{
|
||
|
if(t != JS_PRIMITIVE) return (T)NULL;
|
||
|
if(a >= JS_STRING && typename(T) != "string") return (T)NULL;
|
||
|
if(a == JS_CONST && typename(T) != "bool") return (T)NULL;
|
||
|
}
|
||
|
return (T)s;
|
||
|
}
|
||
|
|
||
|
template<typename T>
|
||
|
bool operator==(const T value)
|
||
|
{
|
||
|
if(t != JS_PRIMITIVE) return false;
|
||
|
if(a >= JS_STRING && typename(T) != "string") return false;
|
||
|
if(a == JS_CONST && typename(T) != "bool") return false;
|
||
|
T temp = this.get<T>(false);
|
||
|
return temp == value;
|
||
|
}
|
||
|
|
||
|
// object putters
|
||
|
|
||
|
template<typename T>
|
||
|
void put(const string key, const T value)
|
||
|
{
|
||
|
put(key, new JsValue((string)value));
|
||
|
}
|
||
|
|
||
|
void put(const string key, const double value, const int digits = 8)
|
||
|
{
|
||
|
put(key, new JsValue(DoubleToString(value, digits)));
|
||
|
}
|
||
|
|
||
|
void put(const string key, const JsValue *value)
|
||
|
{
|
||
|
if(t != JS_OBJECT && !selfcall)
|
||
|
{
|
||
|
PrintFormat("WARNING: Setting property '%s' for non-object", key);
|
||
|
}
|
||
|
int p = properties.find(key);
|
||
|
if(p == -1)
|
||
|
{
|
||
|
p = EXPAND(objects);
|
||
|
properties.put(key, p);
|
||
|
}
|
||
|
objects[p] = value;
|
||
|
}
|
||
|
|
||
|
// array putters
|
||
|
|
||
|
template<typename T>
|
||
|
void put(const T value)
|
||
|
{
|
||
|
put("[" + (string)ArraySize(objects) + "]", new JsValue((string)value));
|
||
|
}
|
||
|
|
||
|
void put(const double value, const int digits = 8)
|
||
|
{
|
||
|
put("[" + (string)ArraySize(objects) + "]", new JsValue(DoubleToString(value, digits)));
|
||
|
}
|
||
|
|
||
|
void put(const JsValue *value)
|
||
|
{
|
||
|
if(t != JS_ARRAY)
|
||
|
{
|
||
|
Print("WARNING: Setting indexed element for non-array: ", value.stringify());
|
||
|
}
|
||
|
selfcall = true;
|
||
|
put("[" + (string)ArraySize(objects) + "]", value);
|
||
|
selfcall = false;
|
||
|
}
|
||
|
|
||
|
// indexed access
|
||
|
|
||
|
JsValue *operator[](const string name)
|
||
|
{
|
||
|
const int p = properties.find(name);
|
||
|
if(p == -1) return (JsValue *)&null;
|
||
|
return objects[p][];
|
||
|
}
|
||
|
|
||
|
JsValue *operator[](const int i)
|
||
|
{
|
||
|
if(i < 0 || i >= ArraySize(objects)) return (JsValue *)&null;
|
||
|
return objects[i][];
|
||
|
}
|
||
|
|
||
|
// aux stuff
|
||
|
|
||
|
void print(const int maxlevel = INT_MAX) const
|
||
|
{
|
||
|
int level = 0;
|
||
|
print(level, maxlevel);
|
||
|
}
|
||
|
|
||
|
void print(int &level, const int maxlevel) const
|
||
|
{
|
||
|
const static string open[] = {"{", "[", "", "<null>"};
|
||
|
const static string close[] = {"}", "]", "", ""};
|
||
|
Print(StringFormat("%*s", level * 2, ""), open[t], s);
|
||
|
++level;
|
||
|
if(level < maxlevel)
|
||
|
{
|
||
|
const string padding = StringFormat("%*s", level * 2, "");
|
||
|
for(int i = 0; i < properties.getSize(); ++i)
|
||
|
{
|
||
|
if(objects[i][].s != NULL)
|
||
|
{
|
||
|
Print(padding, properties.getKey(i), " = ", objects[i][].s);
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
Print(padding, properties.getKey(i), " = ");
|
||
|
objects[i][].print(level, maxlevel);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
--level;
|
||
|
if(t < JS_PRIMITIVE) Print(StringFormat("%*s", level * 2, ""), close[t]);
|
||
|
}
|
||
|
|
||
|
void stringify(string &buffer) const
|
||
|
{
|
||
|
const static string open[] = {"{", "[", "", "null"};
|
||
|
const static string close[] = {"}", "]", "", ""};
|
||
|
StringAdd(buffer, open[t]);
|
||
|
for(int i = 0; i < ArraySize(objects); ++i)
|
||
|
{
|
||
|
if(i > 0) StringAdd(buffer, ", ");
|
||
|
if(t != JS_ARRAY) StringAdd(buffer, "\"" + properties.getKey(i) + "\" : ");
|
||
|
if(objects[i][].s != NULL)
|
||
|
{
|
||
|
StringAdd(buffer, objects[i][].stringify());
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
objects[i][].stringify(buffer);
|
||
|
}
|
||
|
}
|
||
|
StringAdd(buffer, close[t]);
|
||
|
}
|
||
|
};
|
||
|
|
||
|
/* Can't do this due to MQL5 limitation (pointers vs values mixture)
|
||
|
class JsObject: public JsValue
|
||
|
{
|
||
|
public:
|
||
|
JsObject() : JsValue(JS_OBJECT) { }
|
||
|
};
|
||
|
|
||
|
class JsArray: public JsValue
|
||
|
{
|
||
|
public:
|
||
|
JsArray() : JsValue(JS_ARRAY) { }
|
||
|
};
|
||
|
*/
|
||
|
|
||
|
//+------------------------------------------------------------------+
|
||
|
//| JSON parser class |
|
||
|
//+------------------------------------------------------------------+
|
||
|
class JsParser
|
||
|
{
|
||
|
int cursor;
|
||
|
string tokens[];
|
||
|
bool error;
|
||
|
|
||
|
void tokenize(const string &context)
|
||
|
{
|
||
|
string copy = context;
|
||
|
StringReplace(copy, "{", ShortToString(1));
|
||
|
StringReplace(copy, "}", ShortToString(1));
|
||
|
StringReplace(copy, "[", ShortToString(1));
|
||
|
StringReplace(copy, "]", ShortToString(1));
|
||
|
StringReplace(copy, ShortToString('"'), ShortToString(1));
|
||
|
StringReplace(copy, ",", ShortToString(1));
|
||
|
StringReplace(copy, ":", ShortToString(1));
|
||
|
StringSplit(copy, 1, tokens);
|
||
|
int position = 0;
|
||
|
for(int i = 0; i < ArraySize(tokens); ++i)
|
||
|
{
|
||
|
int step = i > 0 ? StringLen(tokens[i]) + 1 : StringLen(tokens[i]);
|
||
|
tokens[i] = (i > 0 ? ShortToString(context[position]) : ShortToString(1)) + tokens[i];
|
||
|
position += step;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
string parse_key()
|
||
|
{
|
||
|
if(tokens[cursor][0] == '"')
|
||
|
{
|
||
|
const string result = StringSubstr(tokens[cursor], 1);
|
||
|
if(tokens[++cursor][0] != '"')
|
||
|
{
|
||
|
PrintFormat("Closing '\"' expected, got: '%s' @ %d", tokens[cursor], cursor);
|
||
|
error = true;
|
||
|
return NULL;
|
||
|
}
|
||
|
++cursor;
|
||
|
return result;
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
PrintFormat("Opening '\"' expected, got: '%s' @ %d", tokens[cursor], cursor);
|
||
|
error = true;
|
||
|
return NULL;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
JsValue *parse_value()
|
||
|
{
|
||
|
if(tokens[cursor][0] == '"')
|
||
|
{
|
||
|
string v = StringSubstr(tokens[cursor], 1);
|
||
|
while(tokens[++cursor][0] != '"'
|
||
|
|| (tokens[cursor - 1][StringLen(tokens[cursor - 1]) - 1] == '\\'))
|
||
|
{
|
||
|
v += tokens[cursor];
|
||
|
}
|
||
|
if(v != NULL)
|
||
|
{
|
||
|
StringReplace(v, "\\\"", "\"");
|
||
|
++cursor;
|
||
|
return new JsValue(v);
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
PrintFormat("Value expected, got: '%s' @ %d", tokens[cursor], cursor);
|
||
|
error = true;
|
||
|
}
|
||
|
return NULL;
|
||
|
}
|
||
|
else if(tokens[cursor][0] == '{')
|
||
|
{
|
||
|
return parse_object();
|
||
|
}
|
||
|
else if(tokens[cursor][0] == '[')
|
||
|
{
|
||
|
return parse_array();
|
||
|
}
|
||
|
else // primitive, should be ":123.456", ":true"
|
||
|
{
|
||
|
return new JsValue(StringSubstr(tokens[cursor++], 1));
|
||
|
}
|
||
|
}
|
||
|
|
||
|
JsValue *parse_array()
|
||
|
{
|
||
|
JsValue *current = NULL;
|
||
|
if(tokens[cursor][0] == '[')
|
||
|
{
|
||
|
current = new JsValue(JS_ARRAY);
|
||
|
StringSetCharacter(tokens[cursor], 0, ','); // prevent reentrancy from parse_value
|
||
|
do
|
||
|
{
|
||
|
string t = tokens[cursor];
|
||
|
StringTrimRight(t);
|
||
|
if(StringLen(t) == 1) ++cursor;
|
||
|
if(tokens[cursor][0] == ']') break; // empty array
|
||
|
current.put(parse_value()); // parse_value increments cursor internally
|
||
|
|
||
|
if(tokens[cursor][0] != ']' && tokens[cursor][0] != ',')
|
||
|
{
|
||
|
PrintFormat("'],' expected, got: '%s' @ %d", tokens[cursor], cursor);
|
||
|
error = true;
|
||
|
return current;
|
||
|
}
|
||
|
}
|
||
|
while(tokens[cursor][0] != ']');
|
||
|
if(cursor < ArraySize(tokens)) ++cursor;
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
PrintFormat("'[' expected, got: '%s' @ %d", tokens[cursor], cursor);
|
||
|
error = true;
|
||
|
return NULL;
|
||
|
}
|
||
|
return current;
|
||
|
}
|
||
|
|
||
|
JsValue *parse_object()
|
||
|
{
|
||
|
JsValue *current = NULL;
|
||
|
if(tokens[cursor][0] == '{')
|
||
|
{
|
||
|
current = new JsValue(JS_OBJECT);
|
||
|
do
|
||
|
{
|
||
|
++cursor;
|
||
|
string key = parse_key(); // parse_key increments cursor internally
|
||
|
if(tokens[cursor][0] == '}') break; // empty object
|
||
|
if(tokens[cursor][0] != ':')
|
||
|
{
|
||
|
PrintFormat("':' expected, got: '%s' @ %d", tokens[cursor], cursor);
|
||
|
error = true;
|
||
|
return NULL;
|
||
|
}
|
||
|
string t = tokens[cursor]; // can be ":<whitespace>" (if a string or object is next) or ":<whitespace>value"
|
||
|
StringTrimRight(t);
|
||
|
if(StringLen(t) == 1) ++cursor;
|
||
|
current.put(key, parse_value()); // parse_value increments cursor internally
|
||
|
|
||
|
if(tokens[cursor][0] != '}' && tokens[cursor][0] != ',')
|
||
|
{
|
||
|
PrintFormat("'},' expected, got: '%s' @ %d", tokens[cursor], cursor);
|
||
|
error = true;
|
||
|
return current;
|
||
|
}
|
||
|
}
|
||
|
while(tokens[cursor][0] != '}');
|
||
|
if(cursor < ArraySize(tokens)) ++cursor;
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
PrintFormat("'{' expected, got: '%s' @ %d", tokens[cursor], cursor);
|
||
|
error = true;
|
||
|
return NULL;
|
||
|
}
|
||
|
return current;
|
||
|
}
|
||
|
|
||
|
public:
|
||
|
JsValue *parse(const string &text)
|
||
|
{
|
||
|
if(StringLen(text) < 2) return NULL;
|
||
|
|
||
|
cursor = 1; // skip start token (0x1)
|
||
|
error = false;
|
||
|
tokenize(text);
|
||
|
// printTokens();
|
||
|
return tokens[cursor][0] == '[' ? parse_array() : parse_object();
|
||
|
}
|
||
|
|
||
|
void printTokens(const bool context = false) const
|
||
|
{
|
||
|
if(context)
|
||
|
{
|
||
|
string line = "";
|
||
|
for(int i = cursor - 10; i <= cursor + 10; ++i)
|
||
|
{
|
||
|
if(i >= 0 && i < ArraySize(tokens))
|
||
|
{
|
||
|
line += "[" + (string)i + "]:`" + tokens[i] + "' ";
|
||
|
}
|
||
|
}
|
||
|
Print(line);
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
ArrayPrint(tokens);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
static JsValue *jsonify(const string text)
|
||
|
{
|
||
|
JsParser parser;
|
||
|
JsValue *result = parser.parse(text);
|
||
|
if(parser.error) parser.printTokens(true);
|
||
|
return result;
|
||
|
}
|
||
|
};
|
||
|
|
||
|
static bool JsValue::selfcall = false;
|
||
|
const static JsValue JsValue::null(JS_NULL);
|
||
|
//+------------------------------------------------------------------+
|