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,71 @@
1
+ # https://www.engr.mun.ca/~theo/Misc/exp_parsing.htm
2
+ #
3
+ # term = 'A' | 'B' | 'C' | 'D' | 'E' | 'F'
4
+ #
5
+ # not = 'not' | 'NOT';
6
+ # and = 'and' | 'AND' | '&&';
7
+ # or = 'or' | 'OR' | '||';
8
+ #
9
+ # expr = expr or expr
10
+ # | expr and expr
11
+ # | not expr
12
+ # | '(' expr ')'
13
+ # | var
14
+
15
+ # E --> T {( "or" ) T}
16
+ # T --> P {( "and" ) P}
17
+ # P --> v | "(" E ")" | "NOT" E
18
+
19
+ # expr = exp2 { or exp2 }
20
+ # exp2 = oprd { and oprd }
21
+ # oprd = term | '(' expr ')' | not expr
22
+
23
+ module Travis
24
+ module Conditions
25
+ module V1
26
+ module Boolean
27
+ AND = /(and|&&)/i
28
+ OR = /(or|\|\|)/i
29
+ NOT = /(not|!)/i
30
+
31
+ BOP = {
32
+ 'and' => :and,
33
+ '&&' => :and,
34
+ 'or' => :or,
35
+ '||' => :and,
36
+ 'not' => :not
37
+ }
38
+
39
+ def expr
40
+ lft = expr_
41
+ lft = [:or, lft, expr_] while op(OR)
42
+ lft
43
+ end
44
+
45
+ def expr_
46
+ lft = oprd
47
+ lft = [:and, lft, oprd] while op(AND)
48
+ lft
49
+ end
50
+
51
+ def oprd
52
+ t = parens { expr } and return t
53
+ t = not_ { oprd } and return t
54
+ term
55
+ end
56
+
57
+ def not_
58
+ pos = self.pos
59
+ space { scan(NOT) } or return
60
+ t = yield and return [:not, t]
61
+ str.pos = pos
62
+ nil
63
+ end
64
+
65
+ def op(op)
66
+ op = space { scan(op) } and BOP[op.downcase]
67
+ end
68
+ end
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,75 @@
1
+ module Travis
2
+ module Conditions
3
+ module V1
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 symbolize(obj)
20
+ case obj
21
+ when Hash
22
+ obj.map { |key, value| [key&.to_sym, symbolize(value)] }.to_h
23
+ when Array
24
+ obj.map { |obj| symbolize(obj) }
25
+ else
26
+ obj
27
+ end
28
+ end
29
+
30
+ def cast(obj)
31
+ case obj.to_s.downcase
32
+ when 'false'
33
+ false
34
+ when 'true'
35
+ true
36
+ else
37
+ obj
38
+ end
39
+ end
40
+
41
+ def normalize(data)
42
+ data = symbolize(data)
43
+ data[:env] = normalize_env(data[:env])
44
+ data
45
+ end
46
+
47
+ def normalize_env(env)
48
+ symbolize(to_h(env || {}))
49
+ rescue TypeError
50
+ raise arg_error(env)
51
+ end
52
+
53
+ def to_h(obj)
54
+ case obj
55
+ when Hash
56
+ obj
57
+ else
58
+ Array(obj).map { |obj| split(obj.to_s) }.to_h
59
+ end
60
+ end
61
+
62
+ def split(str)
63
+ str = str.strip
64
+ raise arg_error(str) if str.empty? || !str.include?("=")
65
+ lft, rgt = str.split('=', 2)
66
+ [lft, cast(rgt)]
67
+ end
68
+
69
+ def arg_error(arg)
70
+ ArgumentError.new("Invalid env data (#{arg.inspect} given)")
71
+ end
72
+ end
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,114 @@
1
+ module Travis
2
+ module Conditions
3
+ module V1
4
+ class Eval < Struct.new(:sexp, :data)
5
+ def apply
6
+ !!evl(sexp)
7
+ end
8
+
9
+ private
10
+
11
+ def evl(expr)
12
+ expr = send(*expr) if expr.is_a?(Array)
13
+ cast(expr)
14
+ end
15
+
16
+ def not(lft)
17
+ !evl(lft)
18
+ end
19
+
20
+ def or(lft, rgt)
21
+ evl(lft) || evl(rgt)
22
+ end
23
+
24
+ def and(lft, rgt)
25
+ evl(lft) && evl(rgt)
26
+ end
27
+
28
+ def eq(lft, rgt)
29
+ evl(lft) == evl(rgt)
30
+ end
31
+
32
+ def not_eq(lft, rgt)
33
+ not eq(lft, rgt)
34
+ end
35
+
36
+ def match(lft, rgt)
37
+ evl(lft) =~ evl(rgt)
38
+ end
39
+
40
+ def not_match(lft, rgt)
41
+ not match(lft, rgt)
42
+ end
43
+
44
+ def in(lft, rgt)
45
+ rgt = rgt.map { |rgt| evl(rgt) }
46
+ rgt.include?(evl(lft))
47
+ end
48
+
49
+ def not_in(lft, rgt)
50
+ not evl([:in, lft, rgt])
51
+ end
52
+
53
+ def is(lft, rgt)
54
+ send(rgt, evl(lft))
55
+ end
56
+
57
+ def is_not(lft, rgt)
58
+ not evl([:is, lft, rgt])
59
+ end
60
+
61
+ def val(str)
62
+ str
63
+ end
64
+
65
+ def reg(expr)
66
+ Regexp.new(evl(expr))
67
+ end
68
+
69
+ def var(name)
70
+ data[name]
71
+ end
72
+
73
+ def call(name, args)
74
+ send(name, *args.map { |arg| evl(arg) })
75
+ end
76
+
77
+ def env(key)
78
+ data.env(key)
79
+ end
80
+
81
+ def concat(*args)
82
+ args.inject('') { |str, arg| str + arg.to_s }
83
+ end
84
+
85
+ def present(value)
86
+ value.respond_to?(:empty?) && !value.empty?
87
+ end
88
+
89
+ def blank(value)
90
+ !present(value)
91
+ end
92
+
93
+ def true(value)
94
+ !!value
95
+ end
96
+
97
+ def false(value)
98
+ !value
99
+ end
100
+
101
+ def cast(obj)
102
+ case obj.to_s.downcase
103
+ when 'false'
104
+ false
105
+ when 'true'
106
+ true
107
+ else
108
+ obj
109
+ end
110
+ end
111
+ end
112
+ end
113
+ end
114
+ end
@@ -0,0 +1,30 @@
1
+ module Travis
2
+ module Conditions
3
+ module V1
4
+ module Helper
5
+ QUOTE = /["']{1}/
6
+ OPEN = /\(/
7
+ CLOSE = /\)/
8
+ SPACE = /\s*/
9
+
10
+ def quoted
11
+ return unless quote = scan(QUOTE)
12
+ scan(/[^#{quote}]*/).tap { scan(/#{quote}/) || err(quote) }
13
+ end
14
+
15
+ def parens
16
+ space { skip(OPEN) } and space { yield }.tap { skip(CLOSE) || err(')') }
17
+ end
18
+
19
+ def space
20
+ skip(SPACE)
21
+ yield.tap { skip(SPACE) }
22
+ end
23
+
24
+ def err(char)
25
+ raise ParseError, "expected #{char} at position #{pos} in: #{string.inspect}"
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,214 @@
1
+ require 'strscan'
2
+ require 'forwardable'
3
+ require 'travis/conditions/v1/boolean'
4
+ require 'travis/conditions/v1/helper'
5
+
6
+ # var = 'type'
7
+ # | 'repo'
8
+ # | 'head_repo'
9
+ # | 'os'
10
+ # | 'dist'
11
+ # | 'group'
12
+ # | 'sudo'
13
+ # | 'language'
14
+ # | 'sender'
15
+ # | 'fork'
16
+ # | 'branch'
17
+ # | 'head_branch'
18
+ # | 'tag'
19
+ # | 'commit_message';
20
+ #
21
+ # func = 'env' || 'concat';
22
+ # pred = 'present' | 'blank';
23
+ #
24
+ # eq = '=' | '==' | '!=';
25
+ # re = '=~' | '~=` | '!~';
26
+ # in = 'in' | 'not in' | 'IN' | 'NOT IN';
27
+ # is = 'is' | 'is not' | 'IS' | 'IS NOT';
28
+ # or = 'or' | 'OR' | '||';
29
+ # and = 'and' | 'AND' | '&&';
30
+ #
31
+ # list = oprd | oprd ',' list;
32
+ # call = func '(' list ')';
33
+ # val = word | quoted;
34
+ #
35
+ # oprd = var | val | call;
36
+ #
37
+ # term = oprd is pred
38
+ # | oprd in '(' list ')'
39
+ # | oprd re regx
40
+ # | oprd eq oprd
41
+ # | oprd;
42
+ #
43
+ # expr = expr or expr
44
+ # | expr and expr
45
+ # | not expr
46
+ # | '(' expr ')'
47
+ # | term
48
+
49
+ module Travis
50
+ module Conditions
51
+ module V1
52
+ class Parser
53
+ extend Forwardable
54
+ include Boolean, Helper
55
+
56
+ VAR = /type|repo|head_repo|os|dist|group|sudo|language|sender|fork|
57
+ branch|head_branch|tag|commit_message/ix
58
+
59
+ PRED = /present|blank|true|false/i
60
+ FUNC = /env|concat/i
61
+ IN = /in|not in/i
62
+ IS = /is not|is/i
63
+ EQ = /==|=/
64
+ NEQ = /!=/
65
+ RE = /=~|~=/
66
+ NRE = /!~/
67
+ COMMA = /,/
68
+ WORD = /[^\s\(\)"',=!]+/
69
+ REGEX = %r(/.+/|\S*[^\s\)]+)
70
+ CONT = /\\\s*[\n\r]/
71
+
72
+ OP = {
73
+ '=' => :eq,
74
+ '==' => :eq,
75
+ '!=' => :not_eq,
76
+ '=~' => :match,
77
+ '~=' => :match,
78
+ '!~' => :not_match
79
+ }
80
+
81
+ MSGS = {
82
+ parse_error: 'Could not parse %s',
83
+ shell_var: 'Variable names cannot start with a dollar (shell code does not work). If you are trying to compare to an env var, please use env("name")',
84
+ shell_str: 'Strings cannot start with a dollar (shell code does not work). This can be bypassed by quoting the string.'
85
+ }
86
+
87
+ def_delegators :str, :scan, :skip, :string, :pos, :peek
88
+ attr_reader :str
89
+
90
+ def initialize(str)
91
+ @str = StringScanner.new(filter(str))
92
+ end
93
+
94
+ def filter(str)
95
+ str.gsub(CONT, ' ')
96
+ end
97
+
98
+ def parse
99
+ res = expr
100
+ error(:parse_error, string.inspect) unless res && !str.rest?
101
+ res
102
+ end
103
+
104
+ def term
105
+ lft = operand
106
+ lst = in_list(lft) and return lst
107
+ prd = is_pred(lft) and return prd
108
+ op = re and return [op, lft, regex]
109
+ op = eq and return [op, lft, operand]
110
+ lft
111
+ end
112
+
113
+ def operand
114
+ op = space { var || call || val }
115
+ op or err('an operand')
116
+ end
117
+
118
+ def regex
119
+ val = call
120
+ return [:reg, val] if val
121
+ reg = space { scan(REGEX) }
122
+ [:reg, reg.gsub(/^\/|\/$/, '')] or err('an operand')
123
+ end
124
+
125
+ def eq
126
+ op = space { scan(EQ) || scan(NEQ) } and OP[op]
127
+ end
128
+
129
+ def re
130
+ op = space { scan(RE) || scan(NRE) } and OP[op]
131
+ end
132
+
133
+ def var
134
+ pos = str.pos
135
+ var = scan(VAR)
136
+ error(:shell_var) if var && var[0] == '$'
137
+ return [:var, var.downcase.to_sym] if var && boundary?
138
+ str.pos = pos
139
+ nil
140
+ end
141
+
142
+ BOUND = /[\s,=)|]/
143
+
144
+ def boundary?
145
+ peek(1) =~ BOUND || str.eos?
146
+ end
147
+
148
+ def call
149
+ return unless name = func
150
+ args = parens { list }
151
+ args or return
152
+ return [:call, name.to_sym, args]
153
+ end
154
+
155
+ def val
156
+ val = quoted || word and [:val, val]
157
+ end
158
+
159
+ def is_pred(term)
160
+ op = is or return
161
+ pr = pred or return
162
+ [op.downcase.sub(' ', '_').to_sym, term, pr.downcase.to_sym]
163
+ end
164
+
165
+ def in_list(term)
166
+ op = self.in or return
167
+ list = parens { list! }
168
+ [op.downcase.sub(' ', '_').to_sym, term, list]
169
+ end
170
+
171
+ def list!
172
+ list.tap { |list| err 'a list of values' if list.empty? }
173
+ end
174
+
175
+ def list
176
+ return [] unless item = var || call || val
177
+ list = comma ? [item] + self.list : [item]
178
+ skip(COMMA)
179
+ list.compact
180
+ end
181
+
182
+ def func
183
+ space { scan(FUNC) }
184
+ end
185
+
186
+ def pred
187
+ space { scan(PRED)&.to_sym }
188
+ end
189
+
190
+ def word
191
+ str = space { scan(WORD) }
192
+ error(:shell_str) if str && str[0] == '$'
193
+ str
194
+ end
195
+
196
+ def in
197
+ space { scan(IN) }
198
+ end
199
+
200
+ def is
201
+ space { scan(IS) }
202
+ end
203
+
204
+ def comma
205
+ space { scan(COMMA) }
206
+ end
207
+
208
+ def error(key, *vals)
209
+ raise ParseError, MSGS[key] % vals
210
+ end
211
+ end
212
+ end
213
+ end
214
+ end