querylet 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/README.md +76 -0
- data/lib/querylet.rb +35 -0
- data/lib/querylet/context.rb +31 -0
- data/lib/querylet/parser.rb +153 -0
- data/lib/querylet/template.rb +19 -0
- data/lib/querylet/transform.rb +59 -0
- data/lib/querylet/tree.rb +171 -0
- data/lib/querylet/version.rb +4 -0
- data/spec/parser_spec.rb +82 -0
- data/spec/querylet_spec.rb +156 -0
- data/spec/spec_helper.rb +100 -0
- metadata +99 -0
checksums.yaml
ADDED
@@ -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
|
data/README.md
ADDED
@@ -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
|
+
|
data/lib/querylet.rb
ADDED
@@ -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
|
data/spec/parser_spec.rb
ADDED
@@ -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
|
data/spec/spec_helper.rb
ADDED
@@ -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
|