csv_decision 0.0.7 → 0.0.8

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 076055f3dd0219da9a4d8aa3d0032fd26e320741
4
- data.tar.gz: 7ed7716a9f1285b7ed86e3134321e08c829ae7e5
3
+ metadata.gz: 63aeefecf31a4b0c640e6b345fa1fdcd8af686ab
4
+ data.tar.gz: 4c1f45f5272e4c565ccd08482beb72e1f8566835
5
5
  SHA512:
6
- metadata.gz: b87561c15e01daca109a562f68628b15b6f5d33a725ee527a10ad74d79733e5ffe7a8495735d8a41f18a930a4d906312f566a3c5d22fad182b9d94621a7c141d
7
- data.tar.gz: be6922a755c26fea0b3d416ee75729bf76eec40a66d386d664aa4e3944be8f311f95d0bdfb004a314da049e2ebe5f0b39987752ccfbe446f01529f7fd62281ba
6
+ metadata.gz: 0bf8ca1cacd4a5c31198a544c32e4ff0c8138bb9c69771062a91df22ccb4d4e164db7b4134bd21e4be7f857e2909c730a22b6382d21e422c82a030c1641b454c
7
+ data.tar.gz: 3a54e85c4c8af0859dafcd871a86f143a8cf7f3ae608dda674642c34f5321cee3955fc26910abe496ea7c7912cadf5b26ea378e1bc3b8d884441e42a23139e17
data/CHANGELOG.md CHANGED
@@ -1,3 +1,17 @@
1
+ ## v0.0.8, 31 December 2017.
2
+ *Additions*
3
+ - Guard conditions can use `=~` and `!~` for regular expressions.
4
+
5
+ *Fixes*
6
+ - Bug with column symbol expression not recognising >= and <=.
7
+
8
+ ## v0.0.7, 30 December 2017.
9
+ *Additions*
10
+ - Guard conditions using column symbols and expressions.
11
+ - Guard columns.
12
+ - Symbol functions (0-arity) in output columns.
13
+ - Update YARD documentation.
14
+
1
15
  ## v0.0.6, 26 December 2017.
2
16
  *Additions*
3
17
  - Update YARD documentation.
data/README.md CHANGED
@@ -20,16 +20,16 @@ producing a decision as an output hash.
20
20
 
21
21
  Typical "business logic" is notoriously illogical -- full of corner cases and one-off
22
22
  exceptions.
23
- A decision table can capture data-based decisions in a way that comes more naturally
23
+ A decision table can express data-based decisions in a way that comes more naturally
24
24
  to subject matter experts, who typically prefer spreadsheet models.
25
25
  Business logic may then be encapsulated, avoiding the need to write tortuous
26
26
  conditional expressions in Ruby that draw the ire of `rubocop` and its ilk.
27
27
 
28
28
  This gem and the examples below take inspiration from
29
29
  [rufus/decision](https://github.com/jmettraux/rufus-decision).
30
- (However, that gem is no longer maintained and CSV Decision has better
31
- decision-time performance for the trade-off of slower table parse times and more memory --
32
- see `benchmarks/rufus_decision.rb`)
30
+ (That gem is no longer maintained and CSV Decision has better
31
+ decision-time performance, at the expense of slower table parse times and more memory --
32
+ see `benchmarks/rufus_decision.rb`.)
33
33
 
34
34
  ### Installation
35
35
 
@@ -68,14 +68,14 @@ When the topic is `finance` and the region is `Europe` the team member `Donald`
68
68
  is selected.
69
69
 
70
70
  This is a "first match" decision table in that as soon as a match is made execution
71
- stops and a single output value (hash) is returned.
71
+ stops and a single output row (hash) is returned.
72
72
 
73
73
  The ordering of rows matters. `Ernest`, who is in charge of `finance` for the rest of
74
74
  the world, except for `America` and `Europe`, *must* come after his colleagues
75
75
  `Charlie` and `Donald`. `Zach` has been placed last, catching all the input combos
76
76
  not matching any other row.
77
77
 
78
- Here it is as code:
78
+ Here is the example as code:
79
79
 
80
80
  ```ruby
81
81
  # Valid CSV string
@@ -94,9 +94,9 @@ Here it is as code:
94
94
 
95
95
  table = CSVDecision.parse(data)
96
96
 
97
- table.decide(topic: 'finance', region: 'Europe') # returns team_member: 'Donald'
98
- table.decide(topic: 'sports', region: nil) # returns team_member: 'Bob'
99
- table.decide(topic: 'culture', region: 'America') # team_member: 'Zach'
97
+ table.decide(topic: 'finance', region: 'Europe') #=> { team_member: 'Donald' }
98
+ table.decide(topic: 'sports', region: nil) #=> { team_member: 'Bob' }
99
+ table.decide(topic: 'culture', region: 'America') #=> { team_member: 'Zach' }
100
100
  ```
101
101
 
102
102
  An empty `in` cell means "matches any value", even nils.
@@ -108,12 +108,12 @@ the table from a CSV file:
108
108
  table = CSVDecision.parse(Pathname('spec/data/valid/simple_example.csv'))
109
109
  ```
110
110
 
111
- We can also load this same table using the option: `first_match: false`, which means that
112
- all matching rows will be accumulated into an array of hashes.
111
+ We can also load this same table using the option: `first_match: false`, which means that
112
+ *all* matching rows will be accumulated into an array of hashes.
113
113
 
114
114
  ```ruby
115
115
  table = CSVDecision.parse(data, first_match: false)
116
- table.decide(topic: 'finance', region: 'Europe') # returns team_member: %w[Donald Ernest Zach]
116
+ table.decide(topic: 'finance', region: 'Europe') #=> { team_member: %w[Donald Ernest Zach] }
117
117
  ```
118
118
 
119
119
  For more examples see `spec/csv_decision/table_spec.rb`.
@@ -121,18 +121,24 @@ Complete documentation of all table parameters is in the code - see
121
121
  `lib/csv_decision/parse.rb` and `lib/csv_decision/table.rb`.
122
122
 
123
123
  ### CSV Decision features
124
+ * Either returns the first matching row as a hash (default), or accumulates all matches as an
125
+ array of hashes (i.e., `parse` option `first_match: false` or CSV file option `accumulate`).
124
126
  * Fast decision-time performance (see `benchmarks` folder).
125
- * In addition to simple string matching, can match common Ruby constants,
126
- regular expressions, numeric comparisons and Ruby-style ranges.
127
- * Can use column symbols in comparisons for guard conditions -- e.g., > :column.
127
+ * In addition to simple strings, `csv_decision` can match basic Ruby constants (e.g., `=nil`),
128
+ regular expressions (e.g., `=~ on|off`), comparisons (e.g., `> 100.0` ) and
129
+ Ruby-style ranges (e.g., `1..10`)
130
+ * Can compare an input column versus another input hash key - e.g., `> :column`.
131
+ * Any cell starting with `#` is treated as a comment, and comments may appear anywhere in the
132
+ table. (Comment cells are always interpreted as the empty string.)
133
+ * Can use column symbol expressions or Ruby methods (0-arity) in input columns for
134
+ matching - e.g., `:column.zero?` or `:column == 0`.
135
+ * May also use Ruby methods in output columns - e.g., `:column.length`.
128
136
  * Accepts data as a file, CSV string or an array of arrays. (For safety all input data is
129
137
  force encoded to UTF-8, and non-ascii strings are converted to empty strings.)
130
138
  * All CSV cells are parsed for correctness, and helpful error messages generated for bad
131
- inputs.
132
- * Either returns the first matching row as a hash, or accumulates all matches as an
133
- array of hashes.
139
+ input.
134
140
 
135
- ### Constants other than strings
141
+ #### Constants other than strings
136
142
  Although `csv_decision` is string oriented, it does recognise other types of constant
137
143
  present in the input hash. Specifically, the following classes are recognized:
138
144
  `Integer`, `BigDecimal`, `NilClass`, `TrueClass` and `FalseClass`.
@@ -157,7 +163,7 @@ For example:
157
163
  table.decide(constant: BigDecimal('100.0')) # returns value: BigDecimal('100.0')
158
164
  ```
159
165
 
160
- ### Column header symbols
166
+ #### Column header symbols
161
167
  All input and output column names are symbolized, and can be used to form simple
162
168
  expressions that refer to values in the input hash.
163
169
 
@@ -175,8 +181,9 @@ For example:
175
181
  ```
176
182
 
177
183
  Note that there is no need to include an input column for `:node` in the decision
178
- table - it just needs to be present in the input hash. Also, `== :node` can be
179
- shortened to just `:node`, so the above decision table may be simplified to:
184
+ table - it just needs to be present in the input hash. The expression, `== :node` should be
185
+ read as `:parent == :node`. It can also be shortened to just `:node`, so the above decision table
186
+ may be simplified to:
180
187
 
181
188
  ```ruby
182
189
  data = <<~DATA
@@ -188,7 +195,7 @@ shortened to just `:node`, so the above decision table may be simplified to:
188
195
  These comparison operators are also supported: `!=`, `>`, `>=`, `<`, `<=`.
189
196
  For more simple examples see `spec/csv_decision/examples_spec.rb`.
190
197
 
191
- ### Column guard conditions
198
+ #### Column guard conditions
192
199
  Sometimes it's more convenient to write guard conditions in a single column specialized for that purpose.
193
200
  For example:
194
201
 
@@ -208,7 +215,9 @@ table.decide(country: 'US', CUSIP: '123456789') #=> { ID: '123456789', ID_type:
208
215
  table.decide(country: 'EU', CUSIP: '123456789', ISIN:'123456789012')
209
216
  #=> { ID: '123456789012', ID_type: 'ISIN', len: 12 }
210
217
  ```
211
- Guard columns may be anonymous, and must contain non-constant expressions.
218
+ Guard columns may be anonymous, and must contain non-constant expressions. In addition to
219
+ 0-arity Ruby methods, the following comparison operators are also supported: `==`, `!=`,
220
+ `>`, `>=`, `<` and `<=`. Also, regular expressions are supported - i.e., `=~` and `!~`.
212
221
 
213
222
  ### Testing
214
223
 
@@ -223,12 +232,19 @@ Guard columns may be anonymous, and must contain non-constant expressions.
223
232
  ### Planned features
224
233
  `csv_decision` is still a work in progress, and will be enhanced to support
225
234
  the following features:
226
- * Use of column symbol expressions or built-in guard functions in the input
227
- columns for matching.
228
- * Input columns may be indexed for faster lookup performance.
229
- * May use functions in the output columns to formulate the final decision.
230
- * Input hash values may be conditionally defaulted using a constant or a function call
231
- * Output columns may use interpolated strings referencing column symbols.
232
- * May be extended with a user-supplied library of Ruby functions for tailored logic.
235
+ * Text-only input columns may be indexed for faster lookup performance.
236
+ * Supply a pre-defined library of functions that can be called within input columns to
237
+ implement matching logic or from the output columns to formulate the final decision.
238
+ * Available functions may be extended with a user-supplied library of Ruby methods
239
+ for tailored logic.
240
+ * Input hash values may be (conditionally) defaulted with a constant or a function call.
241
+ * Output columns may construct interpolated strings referencing column symbols.
233
242
  * Can use post-match guard conditions to filter the results of multi-row
234
- decision output.
243
+ decision output.
244
+
245
+ ### Reasons for the limitations of column expressions
246
+ The simple column expressions allowed by `csv_decision` are purposely limited for reasons of
247
+ understandability and maintainability. The whole point of this gem is to make decision rules
248
+ easier to express and comprehend as declarative, tabular logic.
249
+ While Ruby makes it easy to execute arbitrary code embedded within a CSV file,
250
+ this could easily result in hard to debug logic that also poses safety risks.
data/csv_decision.gemspec CHANGED
@@ -5,7 +5,7 @@ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
5
5
 
6
6
  Gem::Specification.new do |spec|
7
7
  spec.name = 'csv_decision'
8
- spec.version = '0.0.7'
8
+ spec.version = '0.0.8'
9
9
  spec.authors = ['Brett Vickers']
10
10
  spec.email = ['brett@phillips-vickers.com']
11
11
  spec.description = 'CSV based Ruby decision tables.'
@@ -30,6 +30,10 @@ module CSVDecision
30
30
  # @return [Hash{Integer=>Entry}] All output column dictionary entries.
31
31
  attr_accessor :outs
32
32
 
33
+ # if: columns.
34
+ # @return [Hash{Integer=>Entry}] All if: column dictionary entries.
35
+ attr_accessor :ifs
36
+
33
37
  # TODO: Input hash path - optional (planned feature)
34
38
  # attr_accessor :path
35
39
 
@@ -39,6 +43,7 @@ module CSVDecision
39
43
  def initialize
40
44
  @ins = {}
41
45
  @outs = {}
46
+ @ifs = {}
42
47
  # TODO: @path = {}
43
48
  # TODO: @defaults = {}
44
49
  end
@@ -60,6 +65,12 @@ module CSVDecision
60
65
  @dictionary.outs
61
66
  end
62
67
 
68
+ # if: columns hash keyed by column index.
69
+ # @return [Hash{Index=>Entry}]
70
+ def ifs
71
+ @dictionary.ifs
72
+ end
73
+
63
74
  # Input columns with defaults specified (planned feature)
64
75
  # def defaults
65
76
  # @dictionary.defaults
@@ -16,10 +16,20 @@ module CSVDecision
16
16
 
17
17
  # Column types recognised in the header row.
18
18
  COLUMN_TYPE = %r{
19
- \A(?<type>in|out|in/text|out/text|guard)
19
+ \A(?<type>in|out|in/text|out/text|guard|if)
20
20
  \s*:\s*(?<name>\S?.*)\z
21
21
  }xi
22
22
 
23
+ COLUMN_ENTRY = {
24
+ in: { type: :in, eval: nil },
25
+ 'in/text': { type: :in, eval: false },
26
+ out: { type: :out, eval: nil },
27
+ 'out/text': { type: :out, eval: false },
28
+ guard: { type: :guard, eval: true },
29
+ if: { type: :if, eval: true }
30
+ }.freeze
31
+ private_constant :COLUMN_ENTRY
32
+
23
33
  # TODO: implement all anonymous column types
24
34
  # COLUMN_TYPE_ANONYMOUS = Set.new(%i[path if guard]).freeze
25
35
  # These column types do not need a name
@@ -88,7 +98,7 @@ module CSVDecision
88
98
  end
89
99
  private_class_method :input_column?
90
100
 
91
- def self.validate_header_column(cell:)
101
+ def self.validate_column(cell:)
92
102
  match = COLUMN_TYPE.match(cell)
93
103
  raise CellValidationError, 'column name is not well formed' unless match
94
104
 
@@ -99,7 +109,7 @@ module CSVDecision
99
109
  rescue CellValidationError => exp
100
110
  raise CellValidationError, "header column '#{cell}' is not valid as the #{exp.message}"
101
111
  end
102
- private_class_method :validate_header_column
112
+ private_class_method :validate_column
103
113
 
104
114
  # Array of all empty column indices.
105
115
  def self.empty_columns?(row:)
@@ -131,25 +141,13 @@ module CSVDecision
131
141
  # Returns the normalized column type, along with an indication if
132
142
  # the column requires evaluation
133
143
  def self.column_type(column_name, type)
134
- case type
135
- when :'in/text'
136
- Columns::Entry.new(column_name, false, :in)
137
-
138
- when :guard
139
- Columns::Entry.new(column_name, true, :guard)
140
-
141
- when :'out/text'
142
- Columns::Entry.new(column_name, false, :out)
143
-
144
- # Column may turn out to be constants only, or not
145
- else
146
- Columns::Entry.new(column_name, nil, type.to_sym)
147
- end
144
+ entry = COLUMN_ENTRY[type]
145
+ Columns::Entry.new(column_name, entry[:eval], entry[:type])
148
146
  end
149
147
  private_class_method :column_type
150
148
 
151
149
  def self.parse_cell(cell:, index:, dictionary:)
152
- column_type, column_name = validate_header_column(cell: cell)
150
+ column_type, column_name = validate_column(cell: cell)
153
151
 
154
152
  entry = column_type(column_name, column_type)
155
153
 
@@ -22,7 +22,7 @@ module CSVDecision
22
22
  GUARD =
23
23
  "(?<negate>#{Matchers::NEGATE}?)\\s*" \
24
24
  ":(?<name>#{Header::COLUMN_NAME})\\s*" \
25
- "(?<method>#{Matchers::EQUALS}|!=|<=|>=|>|<|\\.)\\s*" \
25
+ "(?<method>!=|=~|!~|<=|>=|>|<|#{Matchers::EQUALS}|\\.)\\s*" \
26
26
  "(?<param>\\S.*)"
27
27
  private_constant :GUARD
28
28
 
@@ -30,9 +30,10 @@ module CSVDecision
30
30
  private_constant :GUARD_RE
31
31
 
32
32
  # Negated methods
33
- NEGATION = { '=' => '!=', '==' => '!=', ':=' => '!=', '!=' => '=',
34
- '>' => '<=', '>=' => '<', '<' => '>=', '<=' => '>',
35
- '.' => '!.' }.freeze
33
+ NEGATION = { '=' => '!=', '==' => '!=', ':=' => '!=', '!=' => '=',
34
+ '>' => '<=', '>=' => '<', '<' => '>=', '<=' => '>',
35
+ '.' => '!.',
36
+ '=~' => '!~', '!~' => '=~' }.freeze
36
37
  private_constant :NEGATION
37
38
 
38
39
  # Note: value has already been converted to an Integer or BigDecimal.
@@ -46,13 +47,20 @@ module CSVDecision
46
47
  }.freeze
47
48
  private_constant :NUMERIC_COMPARE
48
49
 
49
- def self.symbol_function(symbol, value, hash)
50
- hash[symbol].respond_to?(value) && hash[symbol].send(value)
50
+ def self.symbol_function(symbol, method, hash)
51
+ hash[symbol].respond_to?(method) && hash[symbol].send(method)
52
+ end
53
+
54
+ def self.regexp_match(symbol, value, hash)
55
+ value.is_a?(String) && hash[symbol].is_a?(String) &&
56
+ Matchers.regexp(value).match(hash[symbol])
51
57
  end
52
58
 
53
59
  FUNCTION = {
54
- '.' => proc { |symbol, value, hash| symbol_function(symbol, value, hash) },
55
- '!.' => proc { |symbol, value, hash| !symbol_function(symbol, value, hash) }
60
+ '.' => proc { |symbol, method, hash| symbol_function(symbol, method, hash) },
61
+ '!.' => proc { |symbol, method, hash| !symbol_function(symbol, method, hash) },
62
+ '=~' => proc { |symbol, value, hash| regexp_match(symbol, value, hash) },
63
+ '!~' => proc { |symbol, value, hash| !regexp_match(symbol, value, hash) }
56
64
  }.freeze
57
65
  private_constant :FUNCTION
58
66
 
@@ -120,7 +128,7 @@ module CSVDecision
120
128
  end
121
129
  private_class_method :symbol_guard
122
130
 
123
- # (see Matchers::Matcher#matches?)
131
+ # (see Matcher#matches?)
124
132
  def self.matches?(cell)
125
133
  proc = symbol_proc(cell)
126
134
  return proc if proc
@@ -131,7 +139,7 @@ module CSVDecision
131
139
  # @param (see Matcher#matches?)
132
140
  # @return (see Matcher#matches?)
133
141
  def matches?(cell)
134
- Matchers::Guard.matches?(cell)
142
+ Guard.matches?(cell)
135
143
  end
136
144
 
137
145
  # @return (see Matcher#outs?)
@@ -10,9 +10,9 @@ module CSVDecision
10
10
  class Matchers
11
11
  # Match cell against a symbolic expression - e.g., :column, > :column
12
12
  class Symbol < Matcher
13
- # Symbol comparison - e.g., > :column or != :column
13
+ # Column symbol comparison - e.g., > :column or != :column
14
14
  SYMBOL_COMPARE =
15
- "(?<comparator>#{Matchers::EQUALS}|!=|<|>|>=|<=)?\\s*:(?<name>#{Header::COLUMN_NAME})"
15
+ "(?<comparator>!=|>=|<=|<|>|#{Matchers::EQUALS})?\\s*:(?<name>#{Header::COLUMN_NAME})"
16
16
  private_constant :SYMBOL_COMPARE
17
17
 
18
18
  # Symbol comparision regular expression.
@@ -24,7 +24,7 @@ module CSVDecision
24
24
  def self.scan_matchers(column:, matchers:, cell:)
25
25
  matchers.each do |matcher|
26
26
  # Guard function only accepts the same matchers as an output column.
27
- next if column.type == :guard && !matcher.outs?
27
+ next if guard_ins_matcher?(column, matcher)
28
28
 
29
29
  proc = scan_proc(column: column, cell: cell, matcher: matcher)
30
30
  return proc if proc
@@ -35,6 +35,12 @@ module CSVDecision
35
35
  end
36
36
  private_class_method :scan_matchers
37
37
 
38
+ # A guard column can only use output matchers
39
+ def self.guard_ins_matcher?(column, matcher)
40
+ column.type == :guard && !matcher.outs?
41
+ end
42
+ private_class_method :guard_ins_matcher?
43
+
38
44
  def self.scan_proc(column:, cell:, matcher:)
39
45
  proc = matcher.matches?(cell)
40
46
  guard_constant?(type: proc.type, column: column) if proc
@@ -37,12 +37,14 @@ describe CSVDecision::Matchers::Symbol do
37
37
  { cell: ':=:col', value: 0, hash: { col: 0 }, result: true },
38
38
  { cell: '= :col', value: '0', hash: { col: 0 }, result: false },
39
39
  { cell: '>=:col', value: 1, hash: { col: 0 }, result: true },
40
+ { cell: '>=:col', value: 1, hash: { col: 1 }, result: true },
40
41
  { cell: '>=:col', value: 0, hash: { col: 1 }, result: false },
41
42
  { cell: '<=:col', value: 0, hash: { col: 1 }, result: true },
42
43
  { cell: '<=:col', value: 1, hash: { col: 0 }, result: false },
44
+ { cell: '<=:col', value: 1, hash: { col: 1 }, result: true },
43
45
  { cell: '<=:col', value: '1', hash: { col: 1 }, result: false },
46
+ { cell: '<=:col', value: '1', hash: { col: '1' }, result: true },
44
47
  ]
45
-
46
48
  examples.each do |ex|
47
49
  it "cell #{ex[:cell]} matches value: #{ex[:value]} to hash: #{ex[:hash]}" do
48
50
  proc = matcher.matches?(ex[:cell])
@@ -131,6 +131,22 @@ describe CSVDecision::Table do
131
131
  , maniac, Korolev
132
132
  DATA
133
133
  },
134
+ {
135
+ example: 'guard condition',
136
+ options: { regexp_implicit: false },
137
+ data: <<~DATA
138
+ in :age, guard:, out :salesperson
139
+ 18..35, :trait == maniac, Adelsky
140
+ 23..40, :trait =~ bad|maniac, Bronco
141
+ 36..50, :trait =~ bad.*, Espadas
142
+ ==100, , Thorsten
143
+ 44..100, :trait !~ maniac, Ojiisan
144
+ > 100, :trait =~ maniac.*, Chester
145
+ 23..35, :trait =~ .*rich, Kerfelden
146
+ , :trait == cheerful, Swanson
147
+ , :trait == maniac, Korolev
148
+ DATA
149
+ },
134
150
  {
135
151
  example: 'multiple in column references',
136
152
  options: { regexp_implicit: false },
@@ -239,6 +255,14 @@ describe CSVDecision::Table do
239
255
  , no
240
256
  DATA
241
257
  },
258
+ { example: 'uses ==:parent & != :parent',
259
+ options: { first_match: false },
260
+ data: <<~DATA
261
+ in :node, out :top?
262
+ == :parent, yes
263
+ != :parent, no
264
+ DATA
265
+ },
242
266
  { example: 'uses != :parent, drops :parent input column',
243
267
  options: {},
244
268
  data: <<~DATA
@@ -246,6 +270,14 @@ describe CSVDecision::Table do
246
270
  != :parent, no
247
271
  , yes
248
272
  DATA
273
+ },
274
+ { example: 'uses != :parent and == :parent',
275
+ options: { first_match: false },
276
+ data: <<~DATA
277
+ in :node, out :top?
278
+ != :parent, no
279
+ == :parent, yes
280
+ DATA
249
281
  }
250
282
  ]
251
283
 
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: csv_decision
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.7
4
+ version: 0.0.8
5
5
  platform: ruby
6
6
  authors:
7
7
  - Brett Vickers
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2017-12-30 00:00:00.000000000 Z
11
+ date: 2017-12-31 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport