MQL5Book/Include/toyjson.mqh

450 lines
13 KiB
MQL5
Raw Permalink Normal View History

2025-05-30 16:09:41 +02:00
//+------------------------------------------------------------------+
//| 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);
//+------------------------------------------------------------------+