A quick re-cap of where we are so far:
Now I need to tackle $filterby, which is bit of a mammoth, as can be seen from the description from the OData Uri conventions
Eq Equal /Suppliers?$filter=Address/City eq 'Redmond'
Ne Not equal /Suppliers?$filter=Address/City ne 'London'
Gt Greater than /Products?$filter=Price gt 20
Ge Greater than or equal /Products?$filter=Price ge 10
Lt Less than /Products?$filter=Price lt 20
Le Less than or equal /Products?$filter=Price le 100
And Logical and /Products?$filter=Price le 200 and Price gt 3.5
Or Logical or /Products?$filter=Price le 3.5 or Price gt 200
Not Logical negation /Products?$filter=not endswith(Description,'milk')
Add Addition /Products?$filter=Price add 5 gt 10
Sub Subtraction /Products?$filter=Price sub 5 gt 10
Mul Multiplication /Products?$filter=Price mul 2 gt 2000
Div Division /Products?$filter=Price div 2 gt 4
Mod Modulo /Products?$filter=Price mod 2 eq 0
( ) Precedence grouping /Products?$filter=(Price sub 5) gt 10
And this is before we even have a look at the supported *"functions"* (we'll leave these until the next entry I think!)
Thankfully this is all pretty much the same deal and boils down to simple recursive expression parsing.
Implementing Eq
I'll not do this for all of them, but you can assume I've just implemented them the same way only with "Ge, etc" substituted for whatever...
test("/some/resource?$filterby=Foo eq 2", "OData", function(result) {
it("A filter should be present", function() {
assert.notEqual(result.options.$filterby, null)
})
it("Filter should be an instance of 'eq'", function() {
assert.equal(result.options.$filterby[0], "eq")
})
it("lhr should be Foo", function() {
assert.equal(result.options.$filterby[1].name, "Foo")
})
it("rhr should be 2", function() {
assert.equal(result.options.$filterby[2], 2)
})
})
The idea for this stuff is that I want to generate an AST for further processing by say, a SQL generator. The easiest way to do this is to generate arrays for consumption. This can be ran through a further OMeta processing step to generate SQL later on.
I'm not so comfortable with the bit where I address the filterby[1].name, it feels as addressing down a path should be dealt with in the same way as the rest of the AST (perhaps everywhere else I should be generating an array instead of those nested objects).
I actually have some other ideas about how I'd do this so I'll park that as well (as I'm having a conversation and review of this code tomorrow in the office)
The implementation
FilterByOption =
seq("$filterby=")
FilterByExpression:expr -> { name: "$filterby", value: expr }
,
FilterByExpression =
PropertyPath:lhs
seq(" eq ")
Number:rhs -> [ "eq", lhs, rhs ]
,
So I'm keeping it simple by making some assumptions that'll get proved wrong in a sec
Adding not equals
test("/some/resource?$filterby=Foo ne 2", "OData", function(result) {
it("A filter should be present", function() {
assert.notEqual(result.options.$filterby, null)
})
it("Filter should be an instance of 'ne'", function() {
assert.equal(result.options.$filterby[0], "ne")
})
it("lhr should be Foo", function() {
assert.equal(result.options.$filterby[1].name, "Foo")
})
it("rhr should be 2", function() {
assert.equal(result.options.$filterby[2], 2)
})
})
Can be dealt with by saying that our Operand is a choice
FilterByOption =
seq("$filterby=")
FilterByExpression:expr -> { name: "$filterby", value: expr }
,
FilterByExpression =
PropertyPath:lhs
FilterByOperand:op
Number:rhs -> [ op, lhs, rhs ]
,
FilterByOperand =
seq(" eq ") -> "eq"
| seq(" ne ") -> "ne"
Can now do the same for
Like so
FilterByOperand =
spaces
(
seq("eq")
| seq("ne")
| seq("gt")
| seq("ge")
| seq("lt")
| seq("le")
):op
spaces -> op
,
Note that I tidied it up, and allowed any white space either side and got rid of my own strings (the last return result is automatically assigned to 'op'
And I'll parameterise the test to get this covered easily and document my progress
function operandTest(op) {
test("/some/resource?$filterby=Foo " + op + " 2", "OData", function(result) {
it("A filter should be present", function() {
assert.notEqual(result.options.$filterby, null)
})
it("Filter should be an instance of '" + op + "'", function() {
assert.equal(result.options.$filterby[0], op)
})
it("lhr should be Foo", function() {
assert.equal(result.options.$filterby[1].name, "Foo")
})
it("rhr should be 2", function() {
assert.equal(result.options.$filterby[2], 2)
})
})
}
operandTest("eq")
operandTest("ne")
operandTest("gt")
operandTest("ge")
operandTest("lt")
operandTest("le")
Not everything is a number
Now for the next thing, what can we have as that Rhs? Well, let's go with
As that's what I can think of from the docs
Here is a test for the quoted string:
test("/some/resource?$filterby=Foo eq 'bar'", "OData", function(result) {
it("A filter should be present", function() {
assert.notEqual(result.options.$filterby, null)
})
it("Filter should be an instance of 'eq'", function() {
assert.equal(result.options.$filterby[0], op)
})
it("lhr should be Foo", function() {
assert.equal(result.options.$filterby[1].name, "Foo")
})
it("rhr should be 2", function() {
assert.equal(result.options.$filterby[2], 'bar')
})
})
Same deal again, let's make this extendable
FilterByExpression =
PropertyPath:lhs
FilterByOperand:op
FilterByValue:rhs -> [ op, lhs, rhs ]
,
With
FilterByValue =
Number
| QuotedText
Where QuotedText looks something like this:
QuotedText =
'\''
Text:t
'\'' -> t
,
Imaginative.
Next up we'll have to think about what else we can expect to see in our expressions - thinking about the Arithmetic operators and grouping operators. shudder
2020 © Rob Ashton. ALL Rights Reserved.