travis-conditions 0.0.2 → 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.
- 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
|