Last active
October 12, 2018 06:15
-
-
Save bigfleet/4625050 to your computer and use it in GitHub Desktop.
My citrus effort
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
grammar Querying | |
rule statement | |
(compound_predicate | predicate) | |
end | |
rule compound_predicate | |
(predicate space* operator_and_predicate){ | |
Wither::CompoundPredicate.new(predicate.value, | |
operator_and_predicate.group_operator.value, | |
operator_and_predicate.predicate.value) | |
} | |
end | |
rule operator_and_predicate | |
(group_operator space* predicate) | |
end | |
rule predicate | |
(group | within_phrase | in_phrase | binary_phrase) | |
end | |
rule group | |
(lparen statement rparen){ | |
statement.value | |
} | |
end | |
rule within_phrase | |
('within' space* number space* 'of' space* keyword){ | |
Wither::WithinPhrase.new(number.value, keyword.value) | |
} | |
end | |
rule in_phrase | |
(keyword space* 'in' space* '[' value_list ']') { | |
Wither::InPhrase.new(keyword.value, value_list.value) | |
} | |
end | |
rule binary_phrase | |
(keyword space* binary_operator space* avalue){ | |
Wither::BinaryPhrase.new(keyword.value, binary_operator.value, avalue.value) | |
} | |
end | |
rule term | |
(keyword | value) | |
end | |
rule value_list | |
(avalue ',' value_list | avalue) | |
end | |
rule avalue | |
(literal | number) | |
end | |
rule keyword | |
(unreserved 1*) | |
end | |
rule literal | |
('"'ALPHA 1*'"') | |
end | |
rule number | |
(DIGIT 1*) | |
end | |
## Lexical syntax | |
# unreserved = ALPHA / DIGIT / "-" / "." / "_" | |
rule unreserved | |
(ALPHA | "_") | |
end | |
# reserved = gen-delims / sub-delims | |
rule reserved | |
group_operator | unary_operator | grouper | |
end | |
rule group_operator | |
"&" | "|" | "-" | |
end | |
rule binary_operator | |
"<=" | ">=" | "=" | "~" | ">" | "<" | |
end | |
rule DIGIT | |
[0-9] | |
end | |
rule ALPHA | |
[a-zA-Z] | |
end | |
rule lparen '(' space* end | |
rule rparen ')' space* end | |
rule space [ \t\n\r] end | |
end | |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
module Wither | |
class Engine | |
class << self | |
def activate | |
Citrus.require("#{::Rails.root}/config/wither.citrus") | |
end | |
def config(args={}) | |
Wither::Config.instance(args) | |
end | |
def parse(filter) | |
parse_text(filter.text) | |
end | |
def parse_text(text) | |
Querying.parse(text).value | |
end | |
end | |
end | |
class Walker | |
attr_accessor :ast | |
def initialize(ast) | |
@ast = ast | |
end | |
def walk | |
@ast.keywords | |
end | |
end | |
class Config | |
attr_accessor :args | |
def initialize(args) | |
@args = HashWithIndifferentAccess.new(args) | |
end | |
def load | |
self | |
end | |
def self.keywords(ast) | |
ast.keywords | |
end | |
def self.instance(args) | |
Wither::Config.new(args).load | |
end | |
end | |
class Statement | |
attr_accessor :captures | |
def initialize(captures) | |
@captures = captures | |
end | |
end | |
class CompoundPredicate | |
attr_accessor :lhs, :operator, :rhs | |
def initialize(p1, o, p2) | |
@lhs, @operator, @rhs = p1, o, p2 | |
end | |
def keywords | |
(@lhs.keywords + @rhs.keywords).sort.uniq | |
end | |
end | |
class BinaryPhrase | |
attr_accessor :keyword, :operator, :value | |
def initialize(k, o, v) | |
@keyword, @operator, @value = k,o,v | |
end | |
def keywords | |
[@keyword] | |
end | |
end | |
class InPhrase | |
attr_accessor :keyword, :value_list | |
def initialize(k, vl) | |
@keyword, @value_list = k,vl | |
end | |
def keywords | |
[@keyword] | |
end | |
end | |
class WithinPhrase | |
attr_accessor :magnitude, :origin | |
def initialize(m,o) | |
@magnitude, @origin = m,o | |
end | |
def keywords | |
[@origin] | |
end | |
end | |
class Filter | |
attr_accessor :text | |
def initialize(text) | |
@text = text | |
end | |
def parse | |
Wither::Engine.parse(self) | |
end | |
def to_sql | |
parse.to_sql | |
end | |
end | |
def self.filter(text) | |
parse(text).to_sql | |
end | |
def self.parse(text) | |
Wither::Filter.new(text).parse | |
end | |
end | |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
require 'spec_helper' | |
describe Wither do | |
describe Wither::Engine do | |
def attempt_to_parse(text) | |
lambda{ | |
Wither::Engine.parse_text(text) | |
}.should_not raise_error(Citrus::ParseError) | |
end | |
context "parsing" do | |
context "binary phrases" do | |
it "should parse > operators" do | |
attempt_to_parse("green_eggs > 1") | |
end | |
it "should parse < operators" do | |
attempt_to_parse("green_eggs < 1") | |
end | |
it "should parse >= operators" do | |
attempt_to_parse("green_eggs >= 1") | |
end | |
it "should parse <= operators" do | |
attempt_to_parse("green_eggs <= 1") | |
end | |
it "should parse = operators on numbers" do | |
attempt_to_parse("green_eggs = 2") | |
end | |
it "should parse = operators on alphas" do | |
attempt_to_parse("email_type = \"work\"") | |
end | |
it "should parse ~ operators" do | |
attempt_to_parse("note_text ~ \"taken\"") | |
end | |
end | |
context "specialty phrases" do | |
it "should parse proximity" do | |
attempt_to_parse("within 2 of user") | |
end | |
it "should parse single-term 'in' phrases" do | |
attempt_to_parse('state in ["KY"]') | |
end | |
it "should parse multi-term 'in' phrases" do | |
attempt_to_parse('state in ["KY","TN"]') | |
end | |
end | |
context "grouped phrases" do | |
it "should parse & operators" do | |
attempt_to_parse("corn > 500 & within 2 of origin") | |
end | |
it "should parse | operators" do | |
attempt_to_parse("corn > 500 | wheat > 500") | |
end | |
it "should parse - operators" do | |
attempt_to_parse('corn > 500 - status in ["purchased","pending"]') | |
end | |
end | |
context "precedent hints" do | |
it "should allow simple containing" do | |
attempt_to_parse("(corn > 500)") | |
end | |
it "should allow them first" do | |
attempt_to_parse("(corn > 500 & wheat > 500) | status = \"considering\"") | |
end | |
it "should allow them last" do | |
attempt_to_parse("status = \"considering\" | (corn > 500 & wheat > 500)") | |
end | |
it "should allow them grouped" do | |
attempt_to_parse("(corn > 500 -(phone_number_count > 2 - do_not_call = 1))") | |
end | |
end | |
pending "should consider starts|ends_with, exists, is_true, is_false" | |
end | |
context "keyword analysis" do | |
def keywords(text) | |
Wither::Engine.parse_text(text).keywords | |
end | |
context "for binary phrases" do | |
subject{ keywords("green_eggs > 1") } | |
it{ should have(1).thing } | |
it{ should include("green_eggs") } | |
end | |
context "for in phrases" do | |
subject{ keywords('state in ["KY"]') } | |
it{ should have(1).thing } | |
it{ should include("state") } | |
end | |
context "for proximity phrases" do | |
subject{ keywords("within 2 of origin") } | |
it{ should have(1).thing } | |
it{ should include("origin") } | |
end | |
context "for compound predicates" do | |
subject{ keywords("corn > 500 | wheat > 500") } | |
it{ should have(2).things } | |
it{ should include("corn") } | |
it{ should include("wheat") } | |
end | |
context "for groupings of compound predicates" do | |
subject{ keywords("status = \"considering\" | (corn > 500 & wheat > 500)") } | |
it{ should have(3).things } | |
it{ should include("status") } | |
it{ should include("corn") } | |
it{ should include("wheat") } | |
end | |
end | |
end | |
describe Wither::Config do | |
before(:each) do | |
@config = Wither::Config.new(WITHER_CONFIG) | |
end | |
end | |
describe Wither::Filter do | |
pending "should engage engine" do | |
target = "WITH corn as (select * from ag.entity_crops) select entity_id from corn where acres > 499" | |
Wither.filter("corn > 499").should == target | |
end | |
end | |
end | |
You can safely ignore the curly brace parts within the current grammar, I can eliminate them, or change them to raise "hell", and can never get them to invoke.
The revision you are currently looking at works!
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
I'd be open to https://gist.github.com/4625050#file-wither_spec-rb-L82 being anything, basically, but can't get a handle on anything lower than Statement no matter what's returned.
No matter what I put in the rule statement phrase, I never seem to get anything except statements, and can never end up invoking methods on anything more primitive in the language than that. All help appreciated.