querylet 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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