travis-conditions 0.0.2 → 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +65 -0
- data/Gemfile.lock +1 -1
- data/NOTES.md +107 -0
- data/README.md +261 -13
- data/bin/travis-conditions +34 -0
- data/lib/travis/conditions.rb +10 -16
- data/lib/travis/conditions/v0.rb +30 -0
- data/lib/travis/conditions/v0/data.rb +57 -0
- data/lib/travis/conditions/v0/eval.rb +70 -0
- data/lib/travis/conditions/v0/parser.rb +204 -0
- data/lib/travis/conditions/v1.rb +19 -0
- data/lib/travis/conditions/v1/boolean.rb +71 -0
- data/lib/travis/conditions/v1/data.rb +75 -0
- data/lib/travis/conditions/v1/eval.rb +114 -0
- data/lib/travis/conditions/v1/helper.rb +30 -0
- data/lib/travis/conditions/v1/parser.rb +214 -0
- data/lib/travis/conditions/version.rb +1 -1
- data/spec/conditions_spec.rb +15 -0
- data/spec/v0/conditions_spec.rb +15 -0
- data/spec/{data_spec.rb → v0/data_spec.rb} +6 -1
- data/spec/{eval_spec.rb → v0/eval_spec.rb} +1 -1
- data/spec/v0/fixtures/failures.txt +342 -0
- data/spec/v0/fixtures/passes.txt +1685 -0
- data/spec/{parser_spec.rb → v0/parser_spec.rb} +1 -1
- data/spec/v1/conditions_spec.rb +44 -0
- data/spec/v1/data_spec.rb +30 -0
- data/spec/v1/eval_spec.rb +349 -0
- data/spec/v1/fixtures/failures.txt +336 -0
- data/spec/v1/fixtures/passes.txt +1634 -0
- data/spec/v1/parser/boolean_spec.rb +215 -0
- data/spec/v1/parser/call_spec.rb +68 -0
- data/spec/v1/parser/comma_spec.rb +28 -0
- data/spec/v1/parser/cont_spec.rb +41 -0
- data/spec/v1/parser/eq_spec.rb +16 -0
- data/spec/v1/parser/in_list_spec.rb +60 -0
- data/spec/v1/parser/is_pred_spec.rb +24 -0
- data/spec/v1/parser/list_spec.rb +36 -0
- data/spec/v1/parser/operand_spec.rb +16 -0
- data/spec/v1/parser/parens_spec.rb +28 -0
- data/spec/v1/parser/quoted_spec.rb +24 -0
- data/spec/v1/parser/re_spec.rb +16 -0
- data/spec/v1/parser/regex_spec.rb +12 -0
- data/spec/v1/parser/space_spec.rb +40 -0
- data/spec/v1/parser/term_spec.rb +84 -0
- data/spec/v1/parser/val_spec.rb +24 -0
- data/spec/v1/parser/var_spec.rb +16 -0
- data/spec/v1/parser_spec.rb +486 -0
- data/spec/v1/user_spec.rb +223 -0
- metadata +48 -9
- data/lib/travis/conditions/data.rb +0 -45
- data/lib/travis/conditions/eval.rb +0 -68
- data/lib/travis/conditions/parser.rb +0 -202
@@ -0,0 +1,34 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
$: << 'lib'
|
4
|
+
|
5
|
+
require 'json'
|
6
|
+
require 'optparse'
|
7
|
+
require 'travis/conditions'
|
8
|
+
|
9
|
+
def help
|
10
|
+
abort <<~help
|
11
|
+
Usage:
|
12
|
+
travis-conditions "branch = master" --data '{"branch": "master"}'
|
13
|
+
travis-conditions echo '{"branch": "master"}' | travis-conditions "branch = master"
|
14
|
+
|
15
|
+
The given data JSON hash can include known attributes (such as branch, tag,
|
16
|
+
repo) and an "env" key that can either hold a hash, or an array of strings:
|
17
|
+
|
18
|
+
{"env": {"foo": "bar"}}
|
19
|
+
{"env": ["foo=bar"]}
|
20
|
+
help
|
21
|
+
end
|
22
|
+
|
23
|
+
data = $stdin.read unless $stdin.tty?
|
24
|
+
|
25
|
+
ARGV.options do |opts|
|
26
|
+
opts.on("-d", "--data DATA") { |val| data = val }
|
27
|
+
opts.on_tail("-h", "--help") { help }
|
28
|
+
opts.parse!
|
29
|
+
end
|
30
|
+
|
31
|
+
cond = ARGV.shift
|
32
|
+
abort help unless cond && data
|
33
|
+
|
34
|
+
p Travis::Conditions.eval(cond, JSON.parse(data), version: :v1)
|
data/lib/travis/conditions.rb
CHANGED
@@ -1,29 +1,23 @@
|
|
1
|
-
require 'travis/conditions/
|
2
|
-
require 'travis/conditions/
|
3
|
-
require 'travis/conditions/parser'
|
1
|
+
require 'travis/conditions/v0'
|
2
|
+
require 'travis/conditions/v1'
|
4
3
|
|
5
4
|
module Travis
|
6
5
|
module Conditions
|
7
|
-
|
6
|
+
Error = Class.new(::ArgumentError)
|
7
|
+
ArgumentError = Class.new(Error)
|
8
|
+
ParseError = Class.new(Error)
|
8
9
|
|
9
10
|
class << self
|
10
|
-
def eval(str, data)
|
11
|
-
|
11
|
+
def eval(str, data, opts = {})
|
12
|
+
const(opts).eval(str, data)
|
12
13
|
end
|
13
14
|
|
14
15
|
def parse(str, opts = {})
|
15
|
-
|
16
|
-
Transform.new.apply(tree)
|
17
|
-
rescue Parslet::ParseFailed
|
18
|
-
raise ParseError
|
16
|
+
const(opts).parse(str, opts)
|
19
17
|
end
|
20
18
|
|
21
|
-
def
|
22
|
-
|
23
|
-
end
|
24
|
-
|
25
|
-
def parsers
|
26
|
-
@parsers ||= {}
|
19
|
+
def const(opts)
|
20
|
+
opts[:version] == :v1 ? V1 : V0
|
27
21
|
end
|
28
22
|
end
|
29
23
|
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
require 'travis/conditions/v0/data'
|
2
|
+
require 'travis/conditions/v0/eval'
|
3
|
+
require 'travis/conditions/v0/parser'
|
4
|
+
|
5
|
+
module Travis
|
6
|
+
module Conditions
|
7
|
+
module V0
|
8
|
+
class << self
|
9
|
+
def eval(str, data, opts = {})
|
10
|
+
Eval.new(parse(str, keys: data.keys), Data.new(data)).apply
|
11
|
+
end
|
12
|
+
|
13
|
+
def parse(str, opts = {})
|
14
|
+
tree = parser(opts).parse(str)
|
15
|
+
Transform.new.apply(tree)
|
16
|
+
rescue Parslet::ParseFailed
|
17
|
+
raise ParseError
|
18
|
+
end
|
19
|
+
|
20
|
+
def parser(opts)
|
21
|
+
parsers[opts] ||= Parser.new(opts)
|
22
|
+
end
|
23
|
+
|
24
|
+
def parsers
|
25
|
+
@parsers ||= {}
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
module Travis
|
2
|
+
module Conditions
|
3
|
+
module V0
|
4
|
+
class Data < Struct.new(:data)
|
5
|
+
def initialize(data)
|
6
|
+
super(normalize(data))
|
7
|
+
end
|
8
|
+
|
9
|
+
def [](key)
|
10
|
+
data[key.to_sym]
|
11
|
+
end
|
12
|
+
|
13
|
+
def env(key)
|
14
|
+
data.fetch(:env, {})[key.to_sym]
|
15
|
+
end
|
16
|
+
|
17
|
+
private
|
18
|
+
|
19
|
+
def split(str)
|
20
|
+
str.split('=', 2)
|
21
|
+
end
|
22
|
+
|
23
|
+
def symbolize(value)
|
24
|
+
case value
|
25
|
+
when Hash
|
26
|
+
value.map { |key, value| [key.to_sym, symbolize(value)] }.to_h
|
27
|
+
when Array
|
28
|
+
value.map { |value| symbolize(value) }
|
29
|
+
else
|
30
|
+
value
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def normalize(data)
|
35
|
+
data = symbolize(data)
|
36
|
+
data[:env] = normalize_env(data[:env])
|
37
|
+
data
|
38
|
+
end
|
39
|
+
|
40
|
+
def normalize_env(env)
|
41
|
+
symbolize(to_h(env || {}))
|
42
|
+
rescue ::ArgumentError
|
43
|
+
raise ArgumentError.new("Cannot normalize data[:env] (#{env.inspect} given)")
|
44
|
+
end
|
45
|
+
|
46
|
+
def to_h(obj)
|
47
|
+
case obj
|
48
|
+
when Hash
|
49
|
+
obj
|
50
|
+
else
|
51
|
+
Array(obj).map { |obj| split(obj.to_s) }.to_h
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
@@ -0,0 +1,70 @@
|
|
1
|
+
module Travis
|
2
|
+
module Conditions
|
3
|
+
module V0
|
4
|
+
class Eval < Struct.new(:sexp, :data)
|
5
|
+
def apply
|
6
|
+
!!evl(sexp)
|
7
|
+
end
|
8
|
+
|
9
|
+
private
|
10
|
+
|
11
|
+
def evl(value)
|
12
|
+
case value
|
13
|
+
when Array
|
14
|
+
send(*value)
|
15
|
+
else
|
16
|
+
data[value]
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def not(lft)
|
21
|
+
!evl(lft)
|
22
|
+
end
|
23
|
+
|
24
|
+
def or(lft, rgt)
|
25
|
+
evl(lft) || evl(rgt)
|
26
|
+
end
|
27
|
+
|
28
|
+
def and(lft, rgt)
|
29
|
+
evl(lft) && evl(rgt)
|
30
|
+
end
|
31
|
+
|
32
|
+
def eq(lft, rgt)
|
33
|
+
evl(lft) == rgt
|
34
|
+
end
|
35
|
+
|
36
|
+
def not_eq(lft, rgt)
|
37
|
+
not eq(lft, rgt)
|
38
|
+
end
|
39
|
+
|
40
|
+
def match(lft, rgt)
|
41
|
+
evl(lft) =~ Regexp.new(rgt)
|
42
|
+
end
|
43
|
+
|
44
|
+
def not_match(lft, rgt)
|
45
|
+
not match(lft, rgt)
|
46
|
+
end
|
47
|
+
|
48
|
+
def in(lft, rgt)
|
49
|
+
rgt.include?(evl(lft))
|
50
|
+
end
|
51
|
+
|
52
|
+
def is(lft, rgt)
|
53
|
+
send(rgt, evl(lft))
|
54
|
+
end
|
55
|
+
|
56
|
+
def env(key)
|
57
|
+
data.env(key)
|
58
|
+
end
|
59
|
+
|
60
|
+
def present(value)
|
61
|
+
value.respond_to?(:empty?) && !value.empty?
|
62
|
+
end
|
63
|
+
|
64
|
+
def blank(value)
|
65
|
+
!present(value)
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
@@ -0,0 +1,204 @@
|
|
1
|
+
require 'parslet'
|
2
|
+
|
3
|
+
module Travis
|
4
|
+
module Conditions
|
5
|
+
module V0
|
6
|
+
class Parser < Struct.new(:opts)
|
7
|
+
FUNCS = %w(env)
|
8
|
+
PRESENCE = %w(present blank)
|
9
|
+
|
10
|
+
def parse(str)
|
11
|
+
parser.parse(str)
|
12
|
+
end
|
13
|
+
|
14
|
+
private
|
15
|
+
|
16
|
+
def parser
|
17
|
+
@parser ||= define_parser(opts[:keys]).new
|
18
|
+
end
|
19
|
+
|
20
|
+
def define_parser(keywords)
|
21
|
+
Class.new(Parslet::Parser) do
|
22
|
+
root :expr_or
|
23
|
+
|
24
|
+
rule :expr_or do
|
25
|
+
spaced(expr_and.as(:lft), op_or, expr_or.as(:rgt)).as(:or) | expr_and
|
26
|
+
end
|
27
|
+
|
28
|
+
rule :expr_and do
|
29
|
+
spaced(expr_inner.as(:lft), op_and, expr_and.as(:rgt)).as(:and) | expr_inner
|
30
|
+
end
|
31
|
+
|
32
|
+
rule :expr_inner do
|
33
|
+
lnot(parens(expr_or) | expr_incl | expr_is | expr_regex | expr_cmp)
|
34
|
+
end
|
35
|
+
|
36
|
+
rule :expr_cmp do
|
37
|
+
spaced(lhs.as(:lft), op_cmp.as(:op), value.as(:rgt)).as(:cmp)
|
38
|
+
end
|
39
|
+
|
40
|
+
rule :expr_regex do
|
41
|
+
spaced(lhs.as(:lft), op_regex.as(:op), regex.as(:rgt)).as(:cmp)
|
42
|
+
end
|
43
|
+
|
44
|
+
rule :expr_incl do
|
45
|
+
spaced(lhs.as(:lft), op_incl, parens(list).as(:rgt)).as(:incl)
|
46
|
+
end
|
47
|
+
|
48
|
+
rule :expr_is do
|
49
|
+
spaced(lhs.as(:lft), op_is, presence.as(:rgt)).as(:is)
|
50
|
+
end
|
51
|
+
|
52
|
+
def lnot(node)
|
53
|
+
(stri('not').maybe >> space >> node.as(:rgt)).as(:not) | node
|
54
|
+
end
|
55
|
+
|
56
|
+
rule :list do
|
57
|
+
(value >> (ts(str(',')) >> value).repeat)
|
58
|
+
end
|
59
|
+
|
60
|
+
rule :lhs do
|
61
|
+
func | keyword
|
62
|
+
end
|
63
|
+
|
64
|
+
rule :keyword do
|
65
|
+
stris(*keywords)
|
66
|
+
end
|
67
|
+
|
68
|
+
rule :func do
|
69
|
+
(stris(*FUNCS).as(:name) >> parens(word)).as(:func)
|
70
|
+
end
|
71
|
+
|
72
|
+
rule :word do
|
73
|
+
match('[\w_\-]').repeat(1).as(:str)
|
74
|
+
end
|
75
|
+
|
76
|
+
rule :regex do
|
77
|
+
quoted('/') | match('[\S]').repeat(1).as(:str)
|
78
|
+
end
|
79
|
+
|
80
|
+
rule :value do
|
81
|
+
unquoted | quoted('"') | quoted("'")
|
82
|
+
end
|
83
|
+
|
84
|
+
rule :unquoted do
|
85
|
+
match('[^\s\"\'\(\),]').repeat(1).as(:str)
|
86
|
+
end
|
87
|
+
|
88
|
+
def quoted(chr)
|
89
|
+
str(chr) >> match("[^#{chr}]").repeat.as(:str) >> str(chr)
|
90
|
+
end
|
91
|
+
|
92
|
+
rule :presence do
|
93
|
+
(stris(*PRESENCE)).as(:str)
|
94
|
+
end
|
95
|
+
|
96
|
+
rule :op_is do
|
97
|
+
stri('is')
|
98
|
+
end
|
99
|
+
|
100
|
+
rule :op_cmp do
|
101
|
+
str('=') | str('!=')
|
102
|
+
end
|
103
|
+
|
104
|
+
rule :op_regex do
|
105
|
+
str('=~') | str('!~')
|
106
|
+
end
|
107
|
+
|
108
|
+
rule :op_incl do
|
109
|
+
stri('in')
|
110
|
+
end
|
111
|
+
|
112
|
+
rule :op_or do
|
113
|
+
stri('or')
|
114
|
+
end
|
115
|
+
|
116
|
+
rule :op_and do
|
117
|
+
stri('and')
|
118
|
+
end
|
119
|
+
|
120
|
+
rule :space do
|
121
|
+
match('\s').repeat(1)
|
122
|
+
end
|
123
|
+
|
124
|
+
rule :space? do
|
125
|
+
space.maybe
|
126
|
+
end
|
127
|
+
|
128
|
+
def stris(*strs)
|
129
|
+
strs.inject(stri(strs.shift)) { |node, str| node | stri(str) }
|
130
|
+
end
|
131
|
+
|
132
|
+
def stri(str)
|
133
|
+
str(str) | str(str.upcase)
|
134
|
+
end
|
135
|
+
|
136
|
+
def parens?(node)
|
137
|
+
spaced?(str('(').maybe, node, str(')').maybe)
|
138
|
+
end
|
139
|
+
|
140
|
+
def parens(node)
|
141
|
+
spaced?(str('('), node, str(')'))
|
142
|
+
end
|
143
|
+
|
144
|
+
def spaced?(*nodes)
|
145
|
+
nodes.zip([space?] * nodes.size).flatten[0..-2].inject(&:>>)
|
146
|
+
end
|
147
|
+
|
148
|
+
def spaced(*nodes)
|
149
|
+
# nodes.zip([space] * nodes.size).flatten[0..-2].inject(&:>>)
|
150
|
+
nodes.zip([space?] * nodes.size).flatten[0..-2].inject(&:>>)
|
151
|
+
end
|
152
|
+
|
153
|
+
def ls(node)
|
154
|
+
space? >> node
|
155
|
+
end
|
156
|
+
|
157
|
+
def ts(node)
|
158
|
+
node >> space?
|
159
|
+
end
|
160
|
+
end
|
161
|
+
end
|
162
|
+
end
|
163
|
+
|
164
|
+
class Transform < Parslet::Transform
|
165
|
+
OP = {
|
166
|
+
'=' => :eq,
|
167
|
+
'!=' => :not_eq,
|
168
|
+
'=~' => :match,
|
169
|
+
'!~' => :not_match,
|
170
|
+
}
|
171
|
+
|
172
|
+
str = ->(node) { node.is_a?(Hash) ? node[:str].to_s : node.to_s }
|
173
|
+
sym = ->(node) { str.(node).downcase.to_sym }
|
174
|
+
func = ->(node) { [sym.(node[:func][:name]), str.(node[:func])] }
|
175
|
+
list = ->(node) { node.is_a?(Array) ? node.map { |v| str.(v) } : [str.(node)] }
|
176
|
+
lhs = ->(node) { node.is_a?(Hash) && node.key?(:func) ? func.(node) : str.(node) }
|
177
|
+
|
178
|
+
rule not: { rgt: subtree(:rgt) } do
|
179
|
+
[:not, rgt]
|
180
|
+
end
|
181
|
+
|
182
|
+
rule or: { lft: subtree(:lft), rgt: subtree(:rgt) } do
|
183
|
+
[:or, lft, rgt]
|
184
|
+
end
|
185
|
+
|
186
|
+
rule and: { lft: subtree(:lft), rgt: subtree(:rgt) } do
|
187
|
+
[:and, lft, rgt]
|
188
|
+
end
|
189
|
+
|
190
|
+
rule cmp: { op: simple(:op), lft: subtree(:lft), rgt: subtree(:rgt) } do
|
191
|
+
[OP[op.to_s], lhs.(lft), str.(rgt)]
|
192
|
+
end
|
193
|
+
|
194
|
+
rule incl: { lft: subtree(:lft), rgt: subtree(:rgt) } do
|
195
|
+
[:in, lhs.(lft), list.(rgt)]
|
196
|
+
end
|
197
|
+
|
198
|
+
rule is: { lft: subtree(:lft), rgt: subtree(:rgt) } do
|
199
|
+
[:is, lhs.(lft), sym.(rgt)]
|
200
|
+
end
|
201
|
+
end
|
202
|
+
end
|
203
|
+
end
|
204
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
require 'travis/conditions/v1/data'
|
2
|
+
require 'travis/conditions/v1/eval'
|
3
|
+
require 'travis/conditions/v1/parser'
|
4
|
+
|
5
|
+
module Travis
|
6
|
+
module Conditions
|
7
|
+
module V1
|
8
|
+
class << self
|
9
|
+
def eval(str, data)
|
10
|
+
Eval.new(parse(str), Data.new(data)).apply
|
11
|
+
end
|
12
|
+
|
13
|
+
def parse(str, _ = nil)
|
14
|
+
Parser.new(str).parse
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|