querylet 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 6eae3b4c9cb1beeea8edfbb41bab195ad133ac2c91d6393942f942d075860324
4
+ data.tar.gz: 6632e474232ac27c6e64cd3d991b65e5331e61e97116280b9b8a9b00c1702a5f
5
+ SHA512:
6
+ metadata.gz: c33031f332be7c159ce11f3fb4ec86cf8b606125ad3cf51cfd28c4aa7bb3600d2d5188b7bcb91664bc7b00d307cd258784b09fed852e5886cc8cd4e243ea22d6
7
+ data.tar.gz: 1d5b1e8f732f7dd43dd110c442528a5af5b6d9939593d57bb5a9f5724e2e9d8fab9083265a885f2e57977f6612724babdc3a451aa0c786b0c4f7ab0edca19c30
@@ -0,0 +1,76 @@
1
+ ## Querylet
2
+
3
+ #Variables
4
+
5
+ {{my_variable}}
6
+
7
+ #Partials
8
+
9
+ {{>include 'link.to.path' param='{{testing}}'}}
10
+
11
+ #Partial blocks
12
+ {{#array}}
13
+ {{/array}}
14
+
15
+ # If Else
16
+ {{#if variable}}
17
+ {{else}}
18
+ {{/end}}
19
+
20
+ ## Parslet
21
+
22
+ https://www.youtube.com/watch?time_continue=873&v=ET_POMJNWNs&feature=emb_title
23
+ https://kschiess.github.io/parslet/parser.html
24
+ https://www.rubydoc.info/github/kschiess/parslet/Parslet/Atoms/Scope
25
+
26
+ https://www.rubydoc.info/github/kschiess/parslet/Parslet/Atoms/Capture
27
+
28
+ # Extra input after last repetition
29
+ https://stackoverflow.com/questions/27095141/how-do-i-get-my-parser-atom-to-terminate-inside-a-rule-including-optional-spaces
30
+
31
+
32
+ # Parset Slice
33
+
34
+ str('Boston').parse('Boston') #=> "Boston"@0
35
+
36
+ The @0 is the offset
37
+
38
+
39
+ https://www.sitepoint.com/parsing-parslet-gem/
40
+
41
+ # Parset Debugging
42
+
43
+ If the parser.rb runs into errors you can add the following to help
44
+ debug what is happening
45
+
46
+ ```rb
47
+ require 'parslet/convenience'
48
+ parser.parse_with_debug(input)
49
+ ```
50
+
51
+ ### How it works:
52
+
53
+ * parser.rb — Create a grammar: What should be legal syntax?
54
+ * transform.rb — Annotate the grammar: What is important data?
55
+ * tree.rb - Create a transformation: How do I want to work with that data?
56
+
57
+ ## Parser
58
+
59
+ Parslet parsers output deep nested hashes.
60
+
61
+ To see all the define rules check out `parser_helper.rb`
62
+
63
+ ```
64
+ rspec spec/parser_helper.rb
65
+ ```
66
+
67
+ ## Transform
68
+
69
+ You capture patterns in your deep nested hash and the pass to a tree
70
+
71
+ ## Tree
72
+
73
+ The tree does something with your captured patterns
74
+
75
+
76
+
@@ -0,0 +1,35 @@
1
+ require_relative 'querylet/parser'
2
+ require_relative 'querylet/transform'
3
+ require_relative 'querylet/tree'
4
+ require_relative 'querylet/template'
5
+ require_relative 'querylet/context'
6
+
7
+ module Querylet
8
+ class Querylet
9
+ include Context
10
+ def initialize(path:)
11
+ @sql_path = path
12
+ end
13
+
14
+ def compile(content)
15
+ parser = Parser.new
16
+ transform = Transform.new
17
+ #puts "parser.parse"
18
+ deep_nested_hash = parser.parse_with_debug(content)
19
+ #puts deep_nested_hash
20
+ abstract_syntax_tree = transform.apply deep_nested_hash
21
+ #puts abstract_syntax_tree
22
+ Template.new self, abstract_syntax_tree
23
+ end
24
+
25
+ def set_context(ctx)
26
+ @data = ctx
27
+ end
28
+
29
+ def get_partial name, dot_path
30
+ path = @sql_path + '/' + dot_path.to_s.split('.').join('/') + '.sql'
31
+ template = File.read(path).to_s.chomp
32
+ self.compile(template).call(@data)
33
+ end
34
+ end # class Querylet
35
+ end # module Querylet
@@ -0,0 +1,31 @@
1
+ module Querylet
2
+ module Context
3
+ def get(value)
4
+ @data.merge(locals)[value.to_sym]
5
+ end
6
+
7
+ def add_item(key, value)
8
+ locals[key.to_sym] = value
9
+ end
10
+
11
+ def add_items(hash)
12
+ hash.map { |k, v| add_item(k, v) }
13
+ end
14
+
15
+ def with_temporary_context(args = {})
16
+ saved = args.keys.collect { |key| [key, get(key.to_s)] }.to_h
17
+
18
+ add_items(args)
19
+ block_result = yield
20
+ locals.merge!(saved)
21
+
22
+ block_result
23
+ end
24
+
25
+ private
26
+
27
+ def locals
28
+ @locals ||= {}
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,153 @@
1
+ require 'parslet'
2
+ require 'parslet/convenience'
3
+
4
+ module Querylet
5
+ class Parser < Parslet::Parser
6
+ # Every parser has a root. This designates where parsing should start.
7
+ # It is like an entry point to your parser.
8
+ root :document
9
+
10
+ # "|" is an Alternation which is the equivlent of an OR operator
11
+ # .repeat means to match repeatly the following atoms
12
+ rule(:document) { item.repeat.as(:items) }
13
+
14
+ rule(:item) { ifelse | block | partial | filter | variable | content }
15
+
16
+ # {{ indentifer parameter }}
17
+ rule(:filter) {
18
+ docurly >>
19
+ space? >>
20
+ id.as(:filter) >>
21
+ (space? >> parameter.as(:parameter)) >>
22
+ space? >>
23
+ dccurly
24
+ }
25
+
26
+ rule(:partial) {
27
+ docurly >>
28
+ space? >>
29
+ gt >>
30
+ space? >>
31
+ id.as(:partial) >>
32
+ space? >>
33
+ path >>
34
+ space? >>
35
+ (
36
+ parameter_kv >>
37
+ space?
38
+ ).repeat.as(:parameters) >>
39
+ space? >>
40
+ dccurly
41
+ }
42
+
43
+ rule(:block){
44
+ docurly >>
45
+ space? >>
46
+ hash >>
47
+ space? >>
48
+ id.capture(:block).as(:block) >>
49
+ space? >>
50
+ dccurly >>
51
+ space? >>
52
+ scope {
53
+ document
54
+ } >>
55
+ dynamic { |src, scope|
56
+ docurly >> space? >> slash >> space? >> str(scope.captures[:block]) >> space? >> dccurly
57
+ }
58
+ }
59
+
60
+ rule(:ifelse){
61
+ # {{#if variable}}
62
+ docurly >>
63
+ space? >>
64
+ hash >>
65
+ if_kw.as(:if_kind) >>
66
+ space? >>
67
+ id.as(:if_variable) >>
68
+ space? >>
69
+ dccurly >>
70
+
71
+ scope {
72
+ document
73
+ } >>
74
+
75
+ scope {
76
+ docurly >>
77
+ space? >>
78
+ slash >>
79
+ space? >>
80
+ else_kw >>
81
+ space? >>
82
+ dccurly >>
83
+ scope { document.as(:else_item) }
84
+ }.maybe >>
85
+
86
+ # {{/if}}
87
+ docurly >>
88
+ space? >>
89
+ slash >>
90
+ if_kw >>
91
+ space? >>
92
+ dccurly
93
+ }
94
+
95
+ # {{ indentifer }}
96
+ rule(:variable) { docurly >> space? >> id.as(:variable) >> space? >> dccurly}
97
+
98
+ # Can either be a variable or a string:
99
+ rule(:parameter) { id.as(:variable) | string }
100
+
101
+ # used in includes eg. hello='world'
102
+ rule(:parameter_kv) {
103
+ id.as(:key) >>
104
+ space? >>
105
+ eq >>
106
+ space? >>
107
+ string.as(:value) >>
108
+ space?
109
+ }
110
+
111
+ rule(:content) {
112
+ (
113
+ nocurly.repeat(1) | # A sequence of non-curlies
114
+ ocurly >> ccurly >> nocurly | # Open and closing curely that doesn't complete a {{}}
115
+ ocurly >> nocurly | # Opening curly that doesn't start a {{}}
116
+ ccurly | # Closing curly that is not inside a {{}}
117
+ ocurly >> eof # Opening curly that doesn't start a {{}} because it's the end
118
+ ).repeat(1).as(:content)
119
+ }
120
+
121
+ rule(:space) { match('\s').repeat(1) }
122
+ rule(:space?) { space.maybe }
123
+ rule(:dot) { str('.') }
124
+ rule(:gt) { str('>')}
125
+ rule(:eq) { str('=')}
126
+ rule(:hash) { str('#')}
127
+ rule(:slash) { str('/')}
128
+
129
+ rule(:if_kw) { str('if') | str('unless') }
130
+ rule(:else_kw) { str('else') }
131
+
132
+ # Open Curled Bracket eg. "{"
133
+ rule(:ocurly) { str('{')}
134
+
135
+ # Closed Cured Bracket eg. "}"
136
+ rule(:ccurly) { str('}')}
137
+
138
+ # Double Open Curled Bracked eg. "{{"
139
+ rule(:docurly) { ocurly >> ocurly }
140
+
141
+ # Double Closed Curled Bracked eg. "}}"
142
+ rule(:dccurly) { ccurly >> ccurly }
143
+
144
+ rule(:id) { match['a-zA-Z0-9_'].repeat(1) }
145
+ rule(:path) { match("'") >> (id >> (dot >> id).repeat).as(:path) >> match("'") }
146
+
147
+ rule(:nocurly) { match('[^{}]') }
148
+ rule(:eof) { any.absent? }
149
+
150
+ # String contained in Single Qoutes 'content'
151
+ rule(:string) { match("'") >> match("[^']").repeat(1).as(:string) >> match("'") }
152
+ end
153
+ end
@@ -0,0 +1,19 @@
1
+ #require_relative 'context'
2
+
3
+ module Querylet
4
+ class Template
5
+ def initialize(querylet, ast)
6
+ @querylet = querylet
7
+ @ast = ast
8
+ end
9
+
10
+ def call(args = nil)
11
+ if args
12
+ @querylet.set_context(args)
13
+ end
14
+
15
+ # AST should return a Querylet::Tree and call its eval method
16
+ @ast.eval(@querylet)
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,59 @@
1
+ require_relative 'tree'
2
+
3
+ module Querylet
4
+ class Transform < Parslet::Transform
5
+ rule(content: simple(:content)) {
6
+ Tree::TemplateContent.new(content)
7
+ }
8
+
9
+ rule(variable: simple(:item)) {
10
+ Tree::Variable.new(item)
11
+ }
12
+
13
+ rule(string: simple(:content)) {
14
+ Tree::String.new(content)
15
+ }
16
+
17
+ rule(
18
+ if_kind: simple(:if_kind),
19
+ if_variable: simple(:variable),
20
+ items: subtree(:items),
21
+ else_item: subtree(:else_items)
22
+ ) {
23
+ Tree::IfElseBlock.new(if_kind, variable, items, else_items)
24
+ }
25
+
26
+ rule(
27
+ if_kind: simple(:if_kind),
28
+ if_variable: simple(:variable),
29
+ items: subtree(:items)
30
+ ) {
31
+ Tree::IfBlock.new(if_kind, variable, items)
32
+ }
33
+
34
+ rule(
35
+ partial: simple(:partial),
36
+ path: simple(:path),
37
+ parameters: subtree(:parameters),
38
+ ) {
39
+ Tree::Partial.new(partial, path, parameters)
40
+ }
41
+
42
+ rule(
43
+ filter: simple(:filter),
44
+ parameter: subtree(:parameter)
45
+ ) {
46
+ Tree::Filter.new(filter,parameter)
47
+ }
48
+
49
+ rule(
50
+ block: simple(:block),
51
+ items: subtree(:items)
52
+ ) {
53
+ Tree::Block.new(block,items)
54
+ }
55
+
56
+ rule(items: subtree(:items)) {Tree::Items.new(items)}
57
+ end
58
+ end
59
+
@@ -0,0 +1,171 @@
1
+ module Querylet
2
+ class Tree < Parslet::Transform
3
+ class TreeItem < Struct
4
+ def eval(context)
5
+ _eval(context)
6
+ end
7
+ end
8
+
9
+ class TemplateContent < TreeItem.new(:content)
10
+ def _eval(context)
11
+ return content
12
+ end
13
+ end
14
+
15
+ class Variable < TreeItem.new(:item)
16
+ def _eval(context)
17
+ context.get(item)
18
+ end
19
+ end
20
+
21
+ class String < TreeItem.new(:content)
22
+ def _eval(context)
23
+ return content
24
+ end
25
+ end
26
+
27
+ class Parameter < TreeItem.new(:name)
28
+ def _eval(context)
29
+ if name.is_a?(Parslet::Slice)
30
+ context.get(name.to_s)
31
+ else
32
+ name._eval(context)
33
+ end
34
+ end
35
+ end
36
+
37
+ class Partial < TreeItem.new(:partial, :path, :parameters)
38
+ def _eval(context)
39
+ [parameters].flatten.map(&:values).map do |vals|
40
+ context.add_item vals.first.to_s, vals.last._eval(context)
41
+ end
42
+ content = context.get_partial(partial.to_s, path)
43
+ if partial == 'array'
44
+ <<-HEREDOC.chomp
45
+ (SELECT COALESCE(array_to_json(array_agg(row_to_json(array_row))),'[]'::json) FROM (
46
+ #{content}
47
+ ) array_row)
48
+ HEREDOC
49
+ elsif partial == 'object'
50
+ <<-HEREDOC.chomp
51
+ (SELECT COALESCE(row_to_json(object_row),'{}'::json) FROM (
52
+ #{content}
53
+ ) object_row)
54
+ HEREDOC
55
+ elsif partial == 'include'
56
+ content
57
+ end
58
+ end
59
+ end
60
+
61
+ class IfBlock < TreeItem.new(:if_kind, :variable, :items)
62
+ def _eval(context)
63
+ if if_kind == 'if'
64
+ if context.get(variable)
65
+ items.map {|item| item._eval(context)}.join()
66
+ end
67
+ elsif if_kind == 'unless'
68
+ unless context.get(variable)
69
+ items.map {|item| item._eval(context)}.join()
70
+ end
71
+ end
72
+ end
73
+ end
74
+
75
+ class IfElseBlock < TreeItem.new(:if_kind, :variable, :items, :else_items)
76
+ def _eval(context)
77
+ if if_kind == 'if'
78
+ if context.get(variable)
79
+ items.map {|item| item._eval(context)}.join()
80
+ else
81
+ else_items.items.map {|item| item._eval(context)}.join()
82
+ end
83
+ elsif if_kind == 'unless'
84
+ unless context.get(variable)
85
+ items.map {|item| item._eval(context)}.join()
86
+ else
87
+ else_items.items.map {|item| item._eval(context)}.join()
88
+ end
89
+ end
90
+ end
91
+ end
92
+
93
+ class Filter < TreeItem.new(:filter,:parameter)
94
+ def _eval(context)
95
+ val = context.get(parameter.item)
96
+ if filter == 'int'
97
+ if val.is_a?(Integer)
98
+ val.to_s
99
+ else
100
+ raise "expected input for: #{parameter.item} to be an Integer"
101
+ end
102
+ elsif filter == 'float'
103
+ if val.is_a?(Float)
104
+ val.to_s
105
+ else
106
+ raise "expected input for: #{parameter.item} to be a Float"
107
+ end
108
+ elsif filter == 'arr'
109
+ if val.is_a?(Array)
110
+ if val.all?{|a| a.class.to_s == 'String' } # to_a? was not working
111
+ "'#{val.join("','")}'"
112
+ elsif val.all?{|a| a.is_a?(Integer) }
113
+ val.join(',')
114
+ elsif val.all?{|a| a.is_a?(Float) }
115
+ val.join(',')
116
+ else
117
+ raise "expected input for: #{parameter.item} to be an Array with all of the same datatype eg String, Integer, Float"
118
+ end
119
+ else
120
+ raise "expected input for: #{parameter.item} to be an Array"
121
+ end
122
+ elsif filter == 'str'
123
+ if val.class.to_s == 'String' # to_a? was not working
124
+ "'#{val}'"
125
+ else
126
+ raise "expected input for: #{parameter.item} to be a String"
127
+ end
128
+ elsif filter == 'wild'
129
+ if val.class.to_s == 'String' || val.is_a?(Integer) || val.is_a?(Float)
130
+ "'%#{val}%'"
131
+ else
132
+ raise "expected input for: #{parameter.item} to be String, Integer or Array"
133
+ end
134
+ end
135
+ end
136
+ end
137
+
138
+ class Block < TreeItem.new(:block,:items)
139
+ def _eval(context)
140
+ content = items.map {|item| item._eval(context)}.join()
141
+ if block == 'array'
142
+ <<-HEREDOC.chomp
143
+ (SELECT COALESCE(array_to_json(array_agg(row_to_json(array_row))),'[]'::json) FROM (
144
+ #{content}
145
+ ) array_row)
146
+ HEREDOC
147
+ elsif block == 'object'
148
+ <<-HEREDOC.chomp
149
+ (SELECT COALESCE(row_to_json(object_row),'{}'::json) FROM (
150
+ #{content}
151
+ ) object_row)
152
+ HEREDOC
153
+ else
154
+ content
155
+ end
156
+ end
157
+ end
158
+
159
+ class Items < TreeItem.new(:items)
160
+ def _eval(context)
161
+ items.map {|item| item._eval(context)}.join()
162
+ end
163
+ alias :fn :_eval
164
+
165
+ def add_item(i)
166
+ items << i
167
+ end
168
+ end
169
+
170
+ end
171
+ end
@@ -0,0 +1,4 @@
1
+ module Querylet
2
+ VERSION = "1.0.0"
3
+ end
4
+
@@ -0,0 +1,82 @@
1
+ require_relative '../lib/querylet/parser'
2
+ require 'pry'
3
+
4
+ RSpec.describe Querylet::Parser do
5
+ let(:parser) {Querylet::Parser.new() }
6
+
7
+ context 'recognizes' do
8
+ it 'content' do
9
+ expect(parser.parse('Deep Space Nine')).to eq({items:[{content: 'Deep Space Nine'}]})
10
+ end
11
+
12
+ it 'string' do
13
+ (SELECT COALESCE(row_to_json(object_row),'{}'::json) FROM (
14
+
15
+ expect(parser.parse_with_debug("(SELECT COALESCE(row_to_json(object_row),'{}'::json) FROM (")).to eq({items:[{content: "(SELECT COALESCE(row_to_json(object_row),'{}'::json) FROM ("}]})
16
+ end
17
+ it 'variable' do
18
+ expect(parser.parse('{{worf}}')).to eq({items:[{variable: 'worf'}]})
19
+ expect(parser.parse('{{ worf}}')).to eq({items:[{variable: 'worf'}]})
20
+ expect(parser.parse('{{worf }}')).to eq({items:[{variable: 'worf'}]})
21
+ expect(parser.parse('{{ worf }}')).to eq({items:[{variable: 'worf'}]})
22
+ end
23
+
24
+ it 'filter' do
25
+ expect(parser.parse('{{ quote worf }}')).to eq({items:[{
26
+ filter: 'quote',
27
+ parameter: {variable: 'worf'}
28
+ }]})
29
+
30
+ expect(parser.parse("{{ quote 'worf' }}")).to eq({items:[{
31
+ filter: 'quote',
32
+ parameter: {string: 'worf'}
33
+ }]})
34
+ end
35
+
36
+ it 'partial' do
37
+ expect(parser.parse("{{> include 'path.to.template' }}")).to eq({items:[{
38
+ partial: 'include',
39
+ path: 'path.to.template',
40
+ parameters: []
41
+ }]})
42
+
43
+ expect(parser.parse("{{> include 'path.to.template' hello='world'}}")).to eq({items:[{
44
+ partial: 'include',
45
+ path: 'path.to.template',
46
+ parameters: [
47
+ {key: 'hello', value: {string: 'world'}}
48
+ ]
49
+ }]})
50
+
51
+ expect(parser.parse("{{> include 'path.to.template' hello='world' star='trek' }}")).to eq({items:[{
52
+ partial: 'include',
53
+ path: 'path.to.template',
54
+ parameters: [
55
+ {key: 'hello', value: {string: 'world'}},
56
+ {key: 'star', value: {string: 'trek'}}
57
+ ]
58
+ }]})
59
+ end
60
+
61
+ it 'block' do
62
+ results = parser.parse("{{#object}}this is a test{{/object}}")
63
+ expect(results).to eq({items:[
64
+ { block: 'object', items: [{ content: 'this is a test' }] }
65
+ ]})
66
+ end
67
+
68
+ it 'if' do
69
+ results = parser.parse("{{#if hello}} this is true {{/if}}")
70
+ expect(results).to eq(
71
+ {:items=>[{:if_kind=>"if", :if_variable=>"hello", :items=>[{:content=>" this is true "}]}]}
72
+ )
73
+ end
74
+
75
+ it 'ifelse' do
76
+ results = parser.parse_with_debug("{{#unless hello}}true{{/else}}false{{/if}}")
77
+ expect(results).to eq(
78
+ {:items=>[{:else_item=>{:items=>[{:content=>"false"}]}, :if_kind=>"unless", :if_variable=>"hello", :items=>[{:content=>"true"}]}]}
79
+ )
80
+ end
81
+ end # context 'recognizes' do
82
+ end
@@ -0,0 +1,156 @@
1
+ require_relative '../lib/querylet'
2
+ require 'pry'
3
+
4
+ describe Querylet::Querylet do
5
+ let(:querylet) {Querylet::Querylet.new path: File.expand_path('./') }
6
+
7
+ def evaluate(template, data = {})
8
+ querylet.compile(template).call(data)
9
+ end
10
+
11
+ context 'evaluating' do
12
+ it 'a dummy template' do
13
+ expect(evaluate('My simple template')).to eq('My simple template')
14
+ end
15
+
16
+ it 'a simple replacement' do
17
+ expect(evaluate('Hello {{name}}', {name: 'world'})).to eq('Hello world')
18
+ end
19
+
20
+ it 'a double braces replacement with nil' do
21
+ expect(evaluate('Hello {{name}}', {name: nil})).to eq('Hello ')
22
+ end
23
+
24
+ context 'helpers' do
25
+ it 'include' do
26
+ query = <<-SQL.chomp
27
+ (SELECT
28
+ users.email
29
+ FROM users
30
+ WHERE
31
+ users.id = 1) as email
32
+ SQL
33
+ expect(evaluate("({{> include 'examples.include' }}) as email")).to eq(query)
34
+ end
35
+
36
+ it 'include with variables' do
37
+ query = <<-SQL.chomp
38
+ (SELECT
39
+ users.email
40
+ FROM users
41
+ WHERE
42
+ users.id = 100) as email
43
+ SQL
44
+ template = "({{> include 'examples.include_with_vars' }}) as email"
45
+ expect(evaluate(template, {id: 100})).to eq(query)
46
+ end
47
+
48
+ it 'include with parameters' do
49
+ query = <<-SQL.chomp
50
+ (SELECT
51
+ users.email,
52
+ 'andrew' as name
53
+ FROM users
54
+ WHERE
55
+ users.id = 200) as email
56
+ SQL
57
+ template = "({{> include 'examples.include_with_params' id='200' name='andrew' }}) as email"
58
+ expect(evaluate(template, {id: 100})).to eq(query)
59
+ end
60
+
61
+ it 'object' do
62
+ query = <<-SQL.chomp
63
+ (SELECT COALESCE(row_to_json(object_row),'{}'::json) FROM (
64
+ SELECT
65
+ users.id,
66
+ users.email
67
+ FROM users
68
+ WHERE
69
+ users.id = 1
70
+ ) object_row) as user
71
+ SQL
72
+ output = evaluate("{{> object 'examples.object' }} as user")
73
+ expect(output).to eq(query)
74
+ end
75
+
76
+ it 'array' do
77
+ query = <<-SQL.chomp
78
+ (SELECT COALESCE(array_to_json(array_agg(row_to_json(array_row))),'[]'::json) FROM (
79
+ SELECT
80
+ users.id,
81
+ users.email
82
+ FROM users
83
+ ) array_row) as users
84
+ SQL
85
+ output = evaluate("{{> array 'examples.array' }} as users")
86
+ expect(output).to eq(query)
87
+ end
88
+
89
+ it 'if block' do
90
+ template = "{{#if user_id }}yes{{/if}}"
91
+ expect(evaluate(template, {user_id: true})).to eq('yes')
92
+ end
93
+
94
+ it 'unless block' do
95
+ template = "{{#unless user_id }}yes{{/unless}}"
96
+ expect(evaluate(template, {user_id: nil})).to eq('yes')
97
+ end
98
+
99
+ it 'if else block' do
100
+ template = "{{#if user_id }}yes{{/else}}no{{/if}}"
101
+ expect(evaluate(template, {user_id: false})).to eq('no')
102
+ end
103
+
104
+ it 'array block' do
105
+ query = <<-SQL.chomp
106
+ (SELECT COALESCE(array_to_json(array_agg(row_to_json(array_row))),'[]'::json) FROM (
107
+ yes
108
+ ) array_row)
109
+ SQL
110
+ template = "{{#array}}yes{{/array}}"
111
+ expect(evaluate(template)).to eq(query)
112
+ end
113
+
114
+ it 'object block' do
115
+ query = <<-SQL.chomp
116
+ (SELECT COALESCE(row_to_json(object_row),'{}'::json) FROM (
117
+ yes
118
+ ) object_row)
119
+ SQL
120
+ template = "{{#object}}yes{{/object}}"
121
+ expect(evaluate(template)).to eq(query)
122
+ end
123
+
124
+ it 'int filter' do
125
+ template = "{{int user_id}}"
126
+ expect(evaluate(template,{user_id: 2})).to eq('2')
127
+ end
128
+
129
+ it 'float filter' do
130
+ template = "{{float user_id}}"
131
+ expect(evaluate(template,{user_id: 2.2})).to eq('2.2')
132
+ end
133
+
134
+ it 'arr filter' do
135
+ template = "{{arr ids}}"
136
+ expect(evaluate(template,{ids: [1,2,3,4,5]})).to eq("1,2,3,4,5")
137
+ end
138
+
139
+ it 'arr filter' do
140
+ template = "{{arr values}}"
141
+ expect(evaluate(template,{values: ['hello','world']})).to eq("'hello','world'")
142
+ end
143
+
144
+ it 'str filter' do
145
+ template = "{{str world}}"
146
+ expect(evaluate(template,world: 'banana')).to eq("'banana'")
147
+ end
148
+
149
+ it 'wildcard filter' do
150
+ template = "{{wild email}}"
151
+ expect(evaluate(template,{email: 'andrew@exampro.co'})).to eq("'%andrew@exampro.co%'")
152
+ end
153
+
154
+ end # context 'helpers'
155
+ end # context 'evaluating'
156
+ end
@@ -0,0 +1,100 @@
1
+ # This file was generated by the `rspec --init` command. Conventionally, all
2
+ # specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`.
3
+ # The generated `.rspec` file contains `--require spec_helper` which will cause
4
+ # this file to always be loaded, without a need to explicitly require it in any
5
+ # files.
6
+ #
7
+ # Given that it is always loaded, you are encouraged to keep this file as
8
+ # light-weight as possible. Requiring heavyweight dependencies from this file
9
+ # will add to the boot time of your test suite on EVERY test run, even for an
10
+ # individual file that may not need all of that loaded. Instead, consider making
11
+ # a separate helper file that requires the additional dependencies and performs
12
+ # the additional setup, and require it from the spec files that actually need
13
+ # it.
14
+ #
15
+ # See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration
16
+ RSpec.configure do |config|
17
+ # rspec-expectations config goes here. You can use an alternate
18
+ # assertion/expectation library such as wrong or the stdlib/minitest
19
+ # assertions if you prefer.
20
+ config.expect_with :rspec do |expectations|
21
+ # This option will default to `true` in RSpec 4. It makes the `description`
22
+ # and `failure_message` of custom matchers include text for helper methods
23
+ # defined using `chain`, e.g.:
24
+ # be_bigger_than(2).and_smaller_than(4).description
25
+ # # => "be bigger than 2 and smaller than 4"
26
+ # ...rather than:
27
+ # # => "be bigger than 2"
28
+ expectations.include_chain_clauses_in_custom_matcher_descriptions = true
29
+ end
30
+
31
+ # rspec-mocks config goes here. You can use an alternate test double
32
+ # library (such as bogus or mocha) by changing the `mock_with` option here.
33
+ config.mock_with :rspec do |mocks|
34
+ # Prevents you from mocking or stubbing a method that does not exist on
35
+ # a real object. This is generally recommended, and will default to
36
+ # `true` in RSpec 4.
37
+ mocks.verify_partial_doubles = true
38
+ end
39
+
40
+ # This option will default to `:apply_to_host_groups` in RSpec 4 (and will
41
+ # have no way to turn it off -- the option exists only for backwards
42
+ # compatibility in RSpec 3). It causes shared context metadata to be
43
+ # inherited by the metadata hash of host groups and examples, rather than
44
+ # triggering implicit auto-inclusion in groups with matching metadata.
45
+ config.shared_context_metadata_behavior = :apply_to_host_groups
46
+
47
+ # The settings below are suggested to provide a good initial experience
48
+ # with RSpec, but feel free to customize to your heart's content.
49
+ =begin
50
+ # This allows you to limit a spec run to individual examples or groups
51
+ # you care about by tagging them with `:focus` metadata. When nothing
52
+ # is tagged with `:focus`, all examples get run. RSpec also provides
53
+ # aliases for `it`, `describe`, and `context` that include `:focus`
54
+ # metadata: `fit`, `fdescribe` and `fcontext`, respectively.
55
+ config.filter_run_when_matching :focus
56
+
57
+ # Allows RSpec to persist some state between runs in order to support
58
+ # the `--only-failures` and `--next-failure` CLI options. We recommend
59
+ # you configure your source control system to ignore this file.
60
+ config.example_status_persistence_file_path = "spec/examples.txt"
61
+
62
+ # Limits the available syntax to the non-monkey patched syntax that is
63
+ # recommended. For more details, see:
64
+ # - http://rspec.info/blog/2012/06/rspecs-new-expectation-syntax/
65
+ # - http://www.teaisaweso.me/blog/2013/05/27/rspecs-new-message-expectation-syntax/
66
+ # - http://rspec.info/blog/2014/05/notable-changes-in-rspec-3/#zero-monkey-patching-mode
67
+ config.disable_monkey_patching!
68
+
69
+ # This setting enables warnings. It's recommended, but in some cases may
70
+ # be too noisy due to issues in dependencies.
71
+ config.warnings = true
72
+
73
+ # Many RSpec users commonly either run the entire suite or an individual
74
+ # file, and it's useful to allow more verbose output when running an
75
+ # individual spec file.
76
+ if config.files_to_run.one?
77
+ # Use the documentation formatter for detailed output,
78
+ # unless a formatter has already been configured
79
+ # (e.g. via a command-line flag).
80
+ config.default_formatter = "doc"
81
+ end
82
+
83
+ # Print the 10 slowest examples and example groups at the
84
+ # end of the spec run, to help surface which specs are running
85
+ # particularly slow.
86
+ config.profile_examples = 10
87
+
88
+ # Run specs in random order to surface order dependencies. If you find an
89
+ # order dependency and want to debug it, you can fix the order by providing
90
+ # the seed, which is printed after each run.
91
+ # --seed 1234
92
+ config.order = :random
93
+
94
+ # Seed global randomization in this process using the `--seed` CLI option.
95
+ # Setting this allows you to use `--seed` to deterministically reproduce
96
+ # test failures related to randomization by passing the same `--seed` value
97
+ # as the one that triggered the failure.
98
+ Kernel.srand config.seed
99
+ =end
100
+ end
metadata ADDED
@@ -0,0 +1,99 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: querylet
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ platform: ruby
6
+ authors:
7
+ - ExamPro
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2020-12-05 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: parslet
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: pry
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rspec
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ description: Querylet
56
+ email:
57
+ - andrew@exampro.co
58
+ executables: []
59
+ extensions: []
60
+ extra_rdoc_files: []
61
+ files:
62
+ - README.md
63
+ - lib/querylet.rb
64
+ - lib/querylet/context.rb
65
+ - lib/querylet/parser.rb
66
+ - lib/querylet/template.rb
67
+ - lib/querylet/transform.rb
68
+ - lib/querylet/tree.rb
69
+ - lib/querylet/version.rb
70
+ - spec/parser_spec.rb
71
+ - spec/querylet_spec.rb
72
+ - spec/spec_helper.rb
73
+ homepage: http://exampro.co
74
+ licenses:
75
+ - MIT
76
+ metadata: {}
77
+ post_install_message:
78
+ rdoc_options: []
79
+ require_paths:
80
+ - lib
81
+ required_ruby_version: !ruby/object:Gem::Requirement
82
+ requirements:
83
+ - - ">="
84
+ - !ruby/object:Gem::Version
85
+ version: '0'
86
+ required_rubygems_version: !ruby/object:Gem::Requirement
87
+ requirements:
88
+ - - ">="
89
+ - !ruby/object:Gem::Version
90
+ version: '0'
91
+ requirements: []
92
+ rubygems_version: 3.1.2
93
+ signing_key:
94
+ specification_version: 4
95
+ summary: Querylet
96
+ test_files:
97
+ - spec/spec_helper.rb
98
+ - spec/parser_spec.rb
99
+ - spec/querylet_spec.rb