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.
Files changed (53) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +65 -0
  3. data/Gemfile.lock +1 -1
  4. data/NOTES.md +107 -0
  5. data/README.md +261 -13
  6. data/bin/travis-conditions +34 -0
  7. data/lib/travis/conditions.rb +10 -16
  8. data/lib/travis/conditions/v0.rb +30 -0
  9. data/lib/travis/conditions/v0/data.rb +57 -0
  10. data/lib/travis/conditions/v0/eval.rb +70 -0
  11. data/lib/travis/conditions/v0/parser.rb +204 -0
  12. data/lib/travis/conditions/v1.rb +19 -0
  13. data/lib/travis/conditions/v1/boolean.rb +71 -0
  14. data/lib/travis/conditions/v1/data.rb +75 -0
  15. data/lib/travis/conditions/v1/eval.rb +114 -0
  16. data/lib/travis/conditions/v1/helper.rb +30 -0
  17. data/lib/travis/conditions/v1/parser.rb +214 -0
  18. data/lib/travis/conditions/version.rb +1 -1
  19. data/spec/conditions_spec.rb +15 -0
  20. data/spec/v0/conditions_spec.rb +15 -0
  21. data/spec/{data_spec.rb → v0/data_spec.rb} +6 -1
  22. data/spec/{eval_spec.rb → v0/eval_spec.rb} +1 -1
  23. data/spec/v0/fixtures/failures.txt +342 -0
  24. data/spec/v0/fixtures/passes.txt +1685 -0
  25. data/spec/{parser_spec.rb → v0/parser_spec.rb} +1 -1
  26. data/spec/v1/conditions_spec.rb +44 -0
  27. data/spec/v1/data_spec.rb +30 -0
  28. data/spec/v1/eval_spec.rb +349 -0
  29. data/spec/v1/fixtures/failures.txt +336 -0
  30. data/spec/v1/fixtures/passes.txt +1634 -0
  31. data/spec/v1/parser/boolean_spec.rb +215 -0
  32. data/spec/v1/parser/call_spec.rb +68 -0
  33. data/spec/v1/parser/comma_spec.rb +28 -0
  34. data/spec/v1/parser/cont_spec.rb +41 -0
  35. data/spec/v1/parser/eq_spec.rb +16 -0
  36. data/spec/v1/parser/in_list_spec.rb +60 -0
  37. data/spec/v1/parser/is_pred_spec.rb +24 -0
  38. data/spec/v1/parser/list_spec.rb +36 -0
  39. data/spec/v1/parser/operand_spec.rb +16 -0
  40. data/spec/v1/parser/parens_spec.rb +28 -0
  41. data/spec/v1/parser/quoted_spec.rb +24 -0
  42. data/spec/v1/parser/re_spec.rb +16 -0
  43. data/spec/v1/parser/regex_spec.rb +12 -0
  44. data/spec/v1/parser/space_spec.rb +40 -0
  45. data/spec/v1/parser/term_spec.rb +84 -0
  46. data/spec/v1/parser/val_spec.rb +24 -0
  47. data/spec/v1/parser/var_spec.rb +16 -0
  48. data/spec/v1/parser_spec.rb +486 -0
  49. data/spec/v1/user_spec.rb +223 -0
  50. metadata +48 -9
  51. data/lib/travis/conditions/data.rb +0 -45
  52. data/lib/travis/conditions/eval.rb +0 -68
  53. 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)
@@ -1,29 +1,23 @@
1
- require 'travis/conditions/data'
2
- require 'travis/conditions/eval'
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
- ParseError = Class.new(StandardError)
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
- Eval.new(parse(str, keys: data.keys), Data.new(data)).apply
11
+ def eval(str, data, opts = {})
12
+ const(opts).eval(str, data)
12
13
  end
13
14
 
14
15
  def parse(str, opts = {})
15
- tree = parser(opts).parse(str)
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 parser(opts)
22
- parsers[opts] ||= Parser.new(opts)
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