@ahamove/kql
Version:
181 lines (180 loc) • 6.99 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
const P = require("parsimmon");
// Turn escaped characters into real ones (e.g. "\\n" becomes "\n").
function interpretEscapes(str) {
let escapes = {
b: "\b",
f: "\f",
n: "\n",
r: "\r",
t: "\t",
};
return str.replace(/\\(u[0-9a-fA-F]{4}|[^u])/, (_, escape) => {
let type = escape.charAt(0);
let hex = escape.slice(1);
if (type === "u") {
return String.fromCharCode(parseInt(hex, 16));
}
if (escapes.hasOwnProperty(type)) {
return escapes[type];
}
return type;
});
}
// Use the JSON standard's definition of whitespace rather than Parsimmon's.
const whitespace = P.regexp(/\s*/m);
const optional = P.of(null);
const comment = P.regexp(/--.*/).or(optional);
// JSON is pretty relaxed about whitespace, so let's make it easy to ignore
// after most text.
function token(parser) {
return parser.skip(whitespace).skip(comment);
}
// Several parsers are just strings with optional whitespace.
function word(str) {
return token(comment).then(P.string(str)).thru(token);
}
const kql = P.createLanguage({
// This is the main entry point of the parser: a full JSON value.
value: (r) => P.alt(r.object, r.array, r.string, r.number, r.null, r.true, r.false, r.fn, r.constants, r.args).thru((parser) => whitespace.then(parser)),
// The basic tokens in JSON, with optional whitespace afterward.
lbrace: () => word("{"),
rbrace: () => word("}"),
lbracket: () => word("["),
rbracket: () => word("]"),
lParenthesis: () => word("("),
rParenthesis: () => word(")"),
comma: () => word(","),
colon: () => word(":"),
dot: () => word("."),
and: () => word("and"),
comment: () => token(comment),
// `.result` is like `.map` but it takes a value instead of a function, and
// always returns the same value.
null: () => word("null").result(null),
true: () => word("true").result(true),
false: () => word("false").result(false),
// Regexp based parsers should generally be named for better error reporting.
string: () => token(P.regexp(/(["'])((?:\\.|.)*?)(["'])/, 2))
.map(interpretEscapes)
.desc("string"),
text: () => {
return token(P.regexp(/[a-zA-Z0-9-_]*/))
.map(interpretEscapes)
.desc("string");
},
constants: () => {
return P.alt(word("current_bounds"), word("current_points"), word("current_features"));
},
number: () => token(P.regexp(/-?(0|[1-9][0-9]*)([.][0-9]+)?([eE][+-]?[0-9]+)?/))
.map(Number)
.desc("number"),
// Array parsing is just ignoring brackets and commas and parsing as many nested
// JSON documents as possible. Notice that we're using the parser `json` we just
// defined above. Arrays and objects in the JSON grammar are recursive because
// they can contain any other JSON document within them.
array: (r) => P.alt(r.lbracket, r.lParenthesis)
.then(r.value.sepBy(r.comma))
.skip(P.alt(r.rbracket, r.rParenthesis)),
args: (r) => {
return P.alt(word("{{"))
.then(r.value)
.skip(P.alt(word("}}")));
},
// Object parsing is a little trickier because we have to collect all the key-
// value pairs in order as length-2 arrays, then manually copy them into an
// object.
pair: (r) => P.seq(r.string.skip(r.colon), r.value),
operator: () => {
return P.alt(word("="), word(">"), word(">="), word("<"), word("<="), word("in"), word("is"));
},
object: (r) => {
return r.lbrace
.then(r.pair.sepBy(r.comma))
.skip(r.rbrace)
.map((pairs) => {
let object = {};
pairs.forEach((pair) => {
let [key, value] = pair;
object[key] = value;
});
return object;
});
},
fn: (r) => {
return P.seqObj(["fnName", r.text], ["arguments", r.value.sepBy(r.comma).wrap(r.lParenthesis, r.rParenthesis)]);
},
databaseName: (r) => {
return P.alt(word("tile38"), word("metabase"), word("url"), word("t"), word("m"), word("u"));
},
asAlias: (r) => {
return word("as").then(r.text).or(optional);
},
key: (r) => {
return P.seqObj(["alias", r.text.skip(r.dot).or(optional)], ["key", r.text]);
},
pairCondition: (r) => {
return r.comment.then(P.seqObj(["left", r.key], ["operator", r.operator], ["right", r.value]));
},
where: (r) => {
return word("where").then(r.pairCondition.sepBy(r.and));
},
joinCondition: (r) => {
return P.seqObj(["left", r.key], word("="), ["right", r.key]);
},
join: (r) => {
return word("join")
.skip(r.databaseName)
.skip(word("on"))
.then(r.joinCondition);
},
limit: (r) => {
return word("limit").then(r.number);
},
select: (r) => {
return P.seqObj(["command", word("select")], word("*"), word("from"), [
"database",
P.seqObj(["name", r.databaseName], ["alias", r.asAlias]),
], ["join", r.join.or(optional)], ["where", r.where.or(optional)], ["limit", r.limit.or(optional)])
.skip(r.comment)
.thru((parser) => whitespace.then(parser));
},
parameters: (r) => {
return r.lParenthesis
.then(r.pairCondition.sepBy(r.comma))
.skip(r.rParenthesis);
},
fetch: (r) => {
return P.seqObj(["command", word("fetch")], [
"database",
P.seqObj(["name", r.databaseName], ["alias", r.asAlias]),
], ["parameters", r.parameters])
.skip(r.comment)
.thru((parser) => whitespace.then(parser));
},
create: (r) => {
return P.seqObj(["command", word("create")], ["type", r.text], ["alias", r.asAlias], ["parameters", r.parameters.or(optional)])
.skip(r.comment)
.thru((parser) => whitespace.then(parser));
},
drop: (r) => {
return P.seqObj(["command", word("drop")], ["type", r.text], ["alias", r.asAlias], ["parameters", r.parameters.or(optional)])
.skip(r.comment)
.thru((parser) => whitespace.then(parser));
},
get: (r) => {
return P.seqObj(["command", word("get")], ["type", r.text], ["alias", r.asAlias], ["parameters", r.parameters.or(optional)])
.skip(r.comment)
.thru((parser) => whitespace.then(parser));
},
update: (r) => {
return P.seqObj(["command", word("update")], ["type", r.text], ["alias", r.asAlias], ["parameters", r.parameters.or(optional)])
.skip(r.comment)
.thru((parser) => whitespace.then(parser));
},
parser: (r) => {
return P.alt(r.fetch, r.select, r.create, r.drop, r.get, r.update);
},
});
exports.default = kql;