odatafy-parser
Version:
generate odatafy ast from odata query parameters
402 lines (324 loc) • 19.6 kB
JavaScript
"use strict";
/*
* parser for $filter-Expressions
* NOTE: STRING MUST BE URL-DECODED EXACTLY ONCE
*/
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.rawParser = exports.filterGrammar = void 0;
const peggy_1 = __importDefault(require("peggy"));
const filterAstPostProc_1 = __importDefault(require("../processing/filterAstPostProc"));
const filterExpressionPreProc_1 = __importDefault(require("../processing/filterExpressionPreProc"));
const errors_1 = require("../types/errors");
const utils_1 = require("../utils");
//import fs from "fs";
//import path from "path"
//const grammar = fs.readFileSync(path.join(__dirname, "filterParser.pegjs")).toString();
exports.filterGrammar = `
{
function filterExprHelper(left, right){
if (right) {
return {
nodeType: 'OperatorNode',
op: right.op,
left: left,
right: right.value
}
} else {
return left;
}
}
function constantNodeHelper(type, value) {
return {
nodeType: "ConstantNode",
type: type,
value: value
}
}
function removeEmpty(obj) {
return Object.fromEntries(Object.entries(obj).filter(([_, v]) => v != null));
}
}
boolCommonExpr = commonExpr
//basic types
UINT = $DIGIT+
// INT covers value ranges of sbyte, byte, int16, int32, int64
INT = value:$(SIGN? DIGIT+)
/*
* 4. Expressions
*/
//TODO ADD rootExpr/firstMemberExpr/functionExpr
//ggf methodCall auf boolMethodCalls restricten
commonExpr = (
andOrExpr
/ booleanValue
)
//{if(opAndRight) {return {left:left, op: opAndRight.op, right: opAndRight.right}} return left}
part = arrayOrObject / negateExpr / methodCallExpr / value:$mathExpr {return {type: 'mathExpr', value: value}} / primitiveLiteral / qualifiedEnumTypeName / memberExpr/ odataIdentifier/ enum
boolExpr = compareExpr / hasExpr / inExpr / methodCallExpr / notExpr
notExpr = "not" RWS right:compareExpr {return {nodeType: "OperatorNode", op: "not", right: right}} / "not" RWS OPEN BWS right:commonExpr BWS CLOSE {return {nodeType: "OperatorNode", op: "not", right: right}}
memberExpr = head:$odataIdentifier tail:( "/" @$odataIdentifier)+ {return {nodeType:"SymbolNode", type: "MemberExpression", value: [head, ...tail]}}
methodCallExpr = noArgFunctionCallExpr
/ oneArgFunctionCallExpr
/ twoArgFunctionCallExpr
/ substringMethodCallExpr
/ caseMethodCallExpr
/ castExpr
/ isofExpr
twoArgFunctionCallExpr = func:twoArgFunc OPEN BWS arg1:part BWS COMMA BWS arg2:part BWS CLOSE {return {nodeType: 'FuncNode2Args', func:func, args:[arg1, arg2]}}
twoArgFunc = "concat" / "contains" / "endswith" / "indexof" / "matchesPattern" / "startswith" / "geo.distance" / "geo.intersects" / "hassubset" / "hassubsequence"
oneArgFunctionCallExpr = func:oneArgFunc OPEN BWS arg1:part BWS CLOSE {return {nodeType: 'FuncNode1Args', func:func, args:[arg1]}}
oneArgFunc = "length" / "tolower"/"toupper"/"trim"/"year"/"month"/"day"/"hour"/"minute"/"second"/"fractionalseconds"/"totalseconds"/"date"/"time"/"totaloffsetminutes"/"round"/"floor"/"ceiling"/"geo.length"
noArgFunctionCallExpr = func:noArgFunc OPEN BWS CLOSE {return {nodeType: 'FuncNode0Args', func:func}}
noArgFunc = "mindatetime" / "maxdatetime" / "now"
//FuncNodeVarArgs
substringMethodCallExpr = "substring" OPEN BWS part1:part BWS COMMA BWS part2:part BWS part3:( COMMA BWS BWS )? CLOSE {return{nodeType: "FuncNodeVarArgs", func: "substring", args:part3?[part1,part2,part3]:[part1,part2]}}
caseMethodCallExpr = "case" OPEN BWS head:(cond:boolCommonExpr BWS COLON BWS value:part BWS {return{cond:cond, value:value}})
tail:( COMMA BWS cond:boolCommonExpr BWS COLON BWS value:part BWS {return{cond:cond, value:value}})* CLOSE {return{nodeType: "FuncNodeCase", args:[head, ...tail]}}
parenExpr = OPEN BWS commonExpr:commonExpr BWS CLOSE {return commonExpr}
listExpr = OPEN BWS head:primitiveLiteral BWS tail:( COMMA BWS BWS )* CLOSE {return [head, ...tail]}
//unary minus and plus are not needed according to odata ABNF
mathOp = "add" / "sub" / "mul" / "divby" / "div" / "mod"
mathExpr = ((primitiveLiteral / odataIdentifier / methodCallExpr / OPEN BWS mathExpr2 BWS CLOSE ) RWS mathOp RWS mathExpr2) / OPEN BWS mathExpr2 BWS CLOSE
mathExpr2 = (primitiveLiteral / odataIdentifier / methodCallExpr / OPEN BWS mathExpr2 BWS CLOSE ) (RWS mathOp RWS mathExpr2)?
andOrExpr = OPEN BWS left:commonExpr BWS CLOSE right:(RWS op:("and"/"or") RWS value:commonExpr BWS {return {op:op, value:value}})? {return filterExprHelper(left, right)}
/ BWS left:boolExpr right:(RWS op:("and"/"or") RWS value:commonExpr BWS {return {op:op, value:value}})? {return filterExprHelper(left, right)}
compareExpr = left:part RWS op:compareOp RWS right:part {return {nodeType: "OperatorNode", op: op, left: left, right:right }}
compareOp = "eq" / "ne" / "lt" / "le" / "gt" / "ge"
inExpr = left:part RWS "in" RWS right:( value:listExpr {return {type: "list", value:value}} / qualifiedEnumTypeName / odataIdentifier ) {return {nodeType: "OperatorNode", op: "in", left:left, right:right }}
hasExpr = left:part RWS "has" RWS right:enum {return {nodeType: "OperatorNode", op: "has", left:left, right:right }}
negateExpr = "-" BWS right:commonExpr {return {op: "-", value:right}}
//FuncNodeVarArgs
isofExpr = "isof" OPEN BWS part:( BWS COMMA BWS )? typeName:optionallyQualifiedTypeName BWS CLOSE { return {nodeType: "FuncNodeVarArgs", func: "isof", args:part?[part,typeName]:[typeName] }}
castExpr = "cast" OPEN BWS part:( BWS COMMA BWS )? typeName:optionallyQualifiedTypeName BWS CLOSE { return {nodeType: "FuncNodeVarArgs", func: "cast", args:part?[part,typeName]:[typeName] }}
/*
* 5. JSON format for queries
*/
arrayOrObject = value:$array {return constantNodeHelper("Array",JSON.parse(value))} / value:$object {return constantNodeHelper("Object",JSON.parse(value))}
array = begin_array
( valueInUrl ( value_separator valueInUrl )* )?
end_array
object = begin_object
( member ( value_separator member )* )?
end_object
member = stringInUrl name_separator valueInUrl
begin_object = BWS ( "{" / "%7B" ) BWS
end_object = BWS ( "}" / "%7D" )
begin_array = BWS ( "[" / "%5B" ) BWS
end_array = BWS ( "]" / "%5D" )
name_separator = BWS COLON BWS
value_separator = BWS COMMA BWS
quotation_mark = DQUOTE / "%22"
valueInUrl = stringInUrl / arrayOrObject / primitiveLiteral
stringInUrl = quotation_mark charInJSON* quotation_mark
charInJSON = [^"]
/*
* 6. Names and Identifiers
*/
optionallyQualifiedTypeName = singleQualifiedTypeName
/ 'Collection' OPEN value:$singleQualifiedTypeName CLOSE {return{ nodeType: "SymbolNode", type: "Collection", value: value}}
/ odataIdentifier
/ 'Collection' OPEN value:$odataIdentifier CLOSE {return{ nodeType: "SymbolNode", type: "Collection", value: value}}
singleQualifiedTypeName = value:$(odataIdentifier ("." odataIdentifier)+ / primitiveTypeName) {return{ nodeType: "SymbolNode", type: "TypeName", value: value }}
primitiveTypeName = 'Edm.' ( 'Binary'
/ 'Boolean'
/ 'Byte'
/ 'Date'
/ 'DateTimeOffset'
/ 'Decimal'
/ 'Double'
/ 'Duration'
/ 'Guid'
/ 'Int16'
/ 'Int32'
/ 'Int64'
/ 'ObjectId'
/ 'SByte'
/ 'Single'
/ 'Stream'
/ 'String'
/ 'TimeOfDay'
/ abstractSpatialTypeName ( concreteSpatialTypeName )?
)
abstractSpatialTypeName = 'Geography'
/ 'Geometry'
concreteSpatialTypeName = 'Collection'
/ 'LineString'
/ 'MultiLineString'
/ 'MultiPoint'
/ 'MultiPolygon'
/ 'Point'
/ 'Polygon'
odataIdentifier = value:$(identifierLeadingCharacter identifierCharacter*) {return{nodeType: "SymbolNode",type: "Identifier", value: value }}
identifierLeadingCharacter = ALPHA / "_"
identifierCharacter = ALPHA / "_" / DIGIT
qualifiedEnumTypeName = value:$(odataIdentifier ("." odataIdentifier)+) {return {nodeType: "ConstantNode",type: 'EnumTypeName', value:value}}
namespace = namespacePart ( "." namespacePart )*
namespacePart = odataIdentifier
enumerationTypeName = odataIdentifier
/*
* 7. Literal Data Values
*/
// INT covers value ranges of sbyte, byte, int16, int32, int64
//left out geography and geometry stuff bcs probably not relevant
primitiveLiteral = nullValue {return constantNodeHelper("Null", null)}
/ booleanValue
/ value:$guidValue {return constantNodeHelper("GUID",value)}
/ value:$dateTimeOffsetValueInUrl {return constantNodeHelper("DateTimeOffsetValueInUrl",value)}
/ value:$dateValue {return constantNodeHelper("DateValue",value)}
/ value:$timeOfDayValueInUrl {return constantNodeHelper("TimeOfDayValueInUrl",value)}
/ value:$decimalValue {return {nodeType: "ConstantNode", type: "Decimal", value:parseFloat(value)}}
/ value:$INT {return{nodeType: "ConstantNode", type: "Integer", value:parseInt(value)}}
/ value:$duration {return constantNodeHelper("Duration", value)}
/ value:string {return{nodeType: "ConstantNode", type: "String", value: value}}
/ enum
/ value:$binary {return constantNodeHelper("Binary",value)}
/ geographyLiteral
/ geometryLiteral
geographyLiteral = value:(geographyCollection
/ geographyLineString
/ geographyMultiLineString
/ geographyMultiPoint
/ geographyMultiPolygon
/ geographyPoint
/ geographyPolygon) {return {nodeType: "ConstantSpatialNode", abstractSpatialType: "Geography", value: value}}
geometryLiteral = value:(geometryCollection
/ geometryLineString
/ geometryMultiLineString
/ geometryMultiPoint
/ geometryMultiPolygon
/ geometryPoint
/ geometryPolygon ) {return {nodeType: "ConstantSpatialNode", abstractSpatialType: "Geometry", value: value}}
nullValue = 'null'
booleanValue = value:$("true" / "false") {return {nodeType: "ConstantNode", type: "Boolean", value: value === "true" ? true : false}}
//peggyjs does not support specific repetition
guidValue = HEXDIG HEXDIG HEXDIG HEXDIG HEXDIG HEXDIG HEXDIG HEXDIG "-" HEXDIG HEXDIG HEXDIG HEXDIG "-" HEXDIG HEXDIG HEXDIG HEXDIG "-" HEXDIG HEXDIG HEXDIG HEXDIG "-" HEXDIG HEXDIG HEXDIG HEXDIG HEXDIG HEXDIG HEXDIG HEXDIG HEXDIG HEXDIG HEXDIG HEXDIG
dateTimeOffsetValueInUrl = year "-" month "-" day "T" timeOfDayValueInUrl ( "Z" / SIGN hour COLON minute )
dateValue = year "-" month "-" day
timeOfDayValueInUrl = hour COLON minute ( COLON second ( "." fractionalSeconds )? )?
year = ( "-" )? ( "0" DIGIT+ / oneToNine DIGIT+ )
month = "0" oneToNine
/ "1" ( "0" / "1" / "2" )
day = "0" oneToNine
/ ( "1" / "2" ) DIGIT
/ "3" ( "0" / "1" )
hour = ( "0" / "1" ) DIGIT
/ "2" ( "0" / "1" / "2" / "3" )
minute = zeroToFiftyNine
second = zeroToFiftyNine / "60"
oneToNine = "1" / "2" / "3" / "4" / "5" / "6" / "7" / "8" / "9"
zeroToFiftyNine = ( "0" / "1" / "2" / "3" / "4" / "5" ) DIGIT
fractionalSeconds = DIGIT+
//decimalValue also covers doubleValue, singleValue
decimalValue = ( SIGN )? DIGIT+ ( "." DIGIT+ ) ( "e" ( SIGN )? DIGIT+ )? / nanInfinity
nanInfinity = 'NaN' / '-INF' / 'INF'
string = SQUOTE string:$( SQUOTE_in_string / [^'] )* SQUOTE {return string}
SQUOTE_in_string = SQUOTE SQUOTE
duration = ( "duration" )? SQUOTE durationValue SQUOTE
durationValue = ( SIGN )? "P" ( DIGIT+ "D" )? ( "T" ( DIGIT+ "H" )? ( DIGIT+ "M" )? ( DIGIT+ ( "." DIGIT+ )? "S" )? )?
enum = enumTypeName:$( qualifiedEnumTypeName )? SQUOTE enumValue:$enumValue SQUOTE {return enumTypeName ? {nodeType: 'EnumValueNode', type: 'EnumValue', enumTypeName: enumTypeName, enumValue: enumValue} : {nodeType: 'EnumValueNode', type: 'EnumValue', enumValue: enumValue}}
enumValue = singleEnumValue ( COMMA singleEnumValue )*
singleEnumValue = enumerationMember / enumMemberValue
enumMemberValue = INT
enumerationMember = odataIdentifier
binary = "binary" SQUOTE binaryValue SQUOTE
binaryValue = (base64char base64char base64char base64char)* ( base64b16 / base64b8 )?
base64b16 = base64char base64char ( 'A' / 'E' / 'I' / 'M' / 'Q' / 'U' / 'Y' / 'c' / 'g' / 'k' / 'o' / 's' / 'w' / '0' / '4' / '8' ) ( "=" )?
base64b8 = base64char ( 'A' / 'Q' / 'g' / 'w' ) ( "==" )?
base64char = ALPHA / DIGIT / "-" / "_"
geographyCollection = geographyPrefix SQUOTE node:fullCollectionLiteral SQUOTE {return node}
fullCollectionLiteral = srid: sridLiteral node:collectionLiteral {return {...srid, ...node}}
collectionLiteral = "Collection(" head:geoLiteral tail:( COMMA @geoLiteral )* CLOSE {return {nodeType: "CollectionNode",collection: [head, ...tail]}}
geoLiteral = collectionLiteral
/ lineStringLiteral
/ multiPointLiteral
/ multiLineStringLiteral
/ multiPolygonLiteral
/ pointLiteral
/ polygonLiteral
geographyLineString = geographyPrefix SQUOTE node:fullLineStringLiteral SQUOTE {return node}
fullLineStringLiteral = srid:sridLiteral node:lineStringLiteral {return {...node, ...srid}}
lineStringLiteral = "LineString" node:lineStringData {return node}
lineStringData = OPEN positions:(head:positionLiteral tail:( COMMA @positionLiteral )* {return [head, ...tail]}) CLOSE {return {nodeType: "LineStringNode", positions: positions}}
geographyMultiLineString = geographyPrefix SQUOTE node:fullMultiLineStringLiteral SQUOTE {return node}
fullMultiLineStringLiteral = srid:sridLiteral node:multiLineStringLiteral {return {...srid, ...node}}
multiLineStringLiteral = "MultiLineString(" lineStrings:( head:lineStringData tail:( COMMA @lineStringData )* {return [head, ...tail]} )? CLOSE {return {nodeType: "MultiLineStringNode", lineStrings: lineStrings}}
geographyMultiPoint = geographyPrefix SQUOTE node:fullMultiPointLiteral SQUOTE {return node}
fullMultiPointLiteral = srid:sridLiteral node:multiPointLiteral {return {...srid, ...node}}
multiPointLiteral = "MultiPoint(" points:( head:pointData tail:( COMMA @pointData )* {return [head, ...tail]})? CLOSE {return {nodeType: "MultiPointNode", points: points ? points: []}}
geographyMultiPolygon = geographyPrefix SQUOTE node:fullMultiPolygonLiteral SQUOTE {return node}
fullMultiPolygonLiteral = srid:sridLiteral node:multiPolygonLiteral {return {...srid, ...node}}
multiPolygonLiteral = "MultiPolygon(" polygons:( head:polygonData tail:( COMMA @polygonData )* {return [head, ...tail]} )? CLOSE {return {nodeType: "MultiPolygonNode", polygons: polygons? polygons: []}}
geographyPoint = geographyPrefix SQUOTE node:fullPointLiteral SQUOTE {return node}
fullPointLiteral = srid:sridLiteral node:pointLiteral {return {...srid, ...node}}
sridLiteral = "SRID" EQ srid:$DIGIT+ SEMI {return {srid: parseInt(srid)}}
pointLiteral ="Point" point:pointData {return {nodeType: "PointNode", point: point}}
pointData = OPEN pos:positionLiteral CLOSE {return pos}
positionLiteral = lon:$(decimalValue/INT) SP lat:$(decimalValue/INT) alt:( SP @$(decimalValue/INT) )? lrm:( SP @$(decimalValue/INT) )? {return {lon:Number(lon), lat:Number(lat), alt: Number(alt), lrm: Number(lrm)}}
geographyPolygon = geographyPrefix SQUOTE node:fullPolygonLiteral SQUOTE {return node}
fullPolygonLiteral = srid:sridLiteral node:polygonLiteral {return {...srid, ...node}}
polygonLiteral = "Polygon" node:polygonData {return node}
polygonData = OPEN head:ringLiteral tail:( COMMA @ringLiteral )* CLOSE {return {nodeType: "PolygonNode", rings: [head, ...tail]}}
ringLiteral = OPEN head:positionLiteral tail:( COMMA @positionLiteral )* CLOSE {return {positions:[head, ...tail]}}
geometryCollection = geometryPrefix SQUOTE node:fullCollectionLiteral SQUOTE {return node}
geometryLineString = geometryPrefix SQUOTE node:fullLineStringLiteral SQUOTE {return node}
geometryMultiLineString = geometryPrefix SQUOTE node:fullMultiLineStringLiteral SQUOTE {return node}
geometryMultiPoint = geometryPrefix SQUOTE node:fullMultiPointLiteral SQUOTE {return node}
geometryMultiPolygon = geometryPrefix SQUOTE node:fullMultiPolygonLiteral SQUOTE {return node}
geometryPoint = geometryPrefix SQUOTE node:fullPointLiteral SQUOTE {return node}
geometryPolygon = geometryPrefix SQUOTE node:fullPolygonLiteral SQUOTE {return node}
geographyPrefix = "geography"
geometryPrefix = "geometry"
/*
* 9. Punctuation
*/
//rws = required whitespace, bws = bad whitespace
RWS = ( SP / HTAB / "%20" / "%09" )+
BWS = ( SP / HTAB / "%20" / "%09" )*
AT = "@" / "%40"
COLON = ":" / "%3A"
COMMA = "," / "%2C"
EQ = "="
// the # character is not allowed in the query part
HASH = "%23"
SIGN = "+" / "%2B" / "-"
SEMI = ";" / "%3B"
STAR = "*" / "%2A"
SQUOTE = "'" / "%27"
OPEN = "(" / "%28"
CLOSE = ")" / "%29"
/*
* abnf core rules (vchar missing)
*/
ALPHA = [a-zA-Z] / "ä" / "ö" / "ü" / "Ä" / "Ö" / "Ü" / "ß"
DIGIT = [0-9]
HEXDIG = DIGIT / A_to_F
A_to_F = "A" / "B" / "C" / "D" / "E" / "F"
DQUOTE = '"'
SP = ' '
HTAB = ' '
`;
exports.rawParser = peggy_1.default.generate(exports.filterGrammar, { trace: false });
const myParser = peggy_1.default.generate(exports.filterGrammar, { trace: false });
exports.default = {
/**
* Parser for filter expressions
* @param expr filter expression as string
* @param options options for peggy.js parser
* @example filterParser.parse("Name eq 'Max'");
* @returns Abstract Syntax Tree (AST) of type FilterNode
*/
parse: (expr, options) => {
try {
expr = (0, filterExpressionPreProc_1.default)(expr);
let output = myParser.parse(expr, options);
output = (0, filterAstPostProc_1.default)(output);
return output;
}
catch (e) {
throw (0, utils_1.getOdatafyParserError)('malformed filter expression', errors_1.OdatafyQueryOptions.Filter);
}
}
};