rulix 0.1.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 +7 -0
- data/.gitignore +9 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +21 -0
- data/README.md +254 -0
- data/Rakefile +10 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/lib/rulix/core_ext/hash.rb +96 -0
- data/lib/rulix/mutator.rb +17 -0
- data/lib/rulix/registry.rb +65 -0
- data/lib/rulix/validator.rb +72 -0
- data/lib/rulix/validators/alpha_validator.rb +19 -0
- data/lib/rulix/validators/format_validator.rb +24 -0
- data/lib/rulix/validators/length_validator.rb +34 -0
- data/lib/rulix/version.rb +3 -0
- data/lib/rulix.rb +8 -0
- data/rulix.gemspec +22 -0
- metadata +104 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA1:
|
|
3
|
+
metadata.gz: 4c4b442c751d4de70d8ae4dc25d2433da4820fa3
|
|
4
|
+
data.tar.gz: d4d3c2d5928c563975905a70534e89117cb758b4
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 503fe431b92c2906eb9cf1dc4c0cdd4718458609c9bb7937fb9cfa7ea9919da5c07c0dae54fe1e8d5def127373fc1aba2deed4357039c41fcc6dc826a7f25064
|
|
7
|
+
data.tar.gz: 0c0d83b8f3831d08f7d7ae78f6368f7f0d044a20b428d36805bc7fa9f1b3a67e97deed61c8b709cacbd375625629f3bce911f45c5ec4d66c2f25b69f22d16e89
|
data/.gitignore
ADDED
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2015 Mitch Monsen
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
|
13
|
+
all copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
# Rulix
|
|
2
|
+
|
|
3
|
+
Rulix is a gem for defining and using rules to manipulate, validate, and transform datasets.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
Add this line to your application's Gemfile:
|
|
8
|
+
|
|
9
|
+
```ruby
|
|
10
|
+
gem 'rulix'
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
And then execute:
|
|
14
|
+
|
|
15
|
+
$ bundle
|
|
16
|
+
|
|
17
|
+
Or install it yourself as:
|
|
18
|
+
|
|
19
|
+
$ gem install rulix
|
|
20
|
+
|
|
21
|
+
## Usage
|
|
22
|
+
|
|
23
|
+
Rulix is an engine for defining rulesets that you can apply to your datasets. It has two primary functions; Mutation and Validation.
|
|
24
|
+
|
|
25
|
+
## Rules
|
|
26
|
+
|
|
27
|
+
Rules in Rulix are, at their core, procs. Anything that can be coerced into a proc can be used as a rule.
|
|
28
|
+
|
|
29
|
+
```ruby
|
|
30
|
+
# Procs and lambdas work fine
|
|
31
|
+
-> (value) { value.gsub /\d+/, '' }
|
|
32
|
+
|
|
33
|
+
# Symbols respond to :to_proc, so they work too
|
|
34
|
+
:strip
|
|
35
|
+
|
|
36
|
+
# Any object that can be coerced into a proc will work!
|
|
37
|
+
class DashStripper
|
|
38
|
+
def strip_dashes string
|
|
39
|
+
string.gsub /-/, ''
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def to_proc
|
|
43
|
+
method(:strip_dashes)
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
DashStripper.new
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
#### Using Rules
|
|
51
|
+
|
|
52
|
+
Rules can be supplied directly in a ruleset as long as they can be coerced into procs with `to_proc`.
|
|
53
|
+
|
|
54
|
+
```ruby
|
|
55
|
+
ruleset = {
|
|
56
|
+
first_name: -> (name) { name.reverse },
|
|
57
|
+
last_name: :strip
|
|
58
|
+
}
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
For a more permanent rule definition, you can register a procable object or block under a symbol through the embedded registry for Rulix to use.
|
|
62
|
+
|
|
63
|
+
```ruby
|
|
64
|
+
my_super_neat_transformation = -> (string) { string.upcase.chars.shuffle.join }
|
|
65
|
+
|
|
66
|
+
Rulix::Mutator.register :make_super_neat, my_super_neat_transformation
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Then, you can supply the symbol in a Rulix ruleset.
|
|
70
|
+
|
|
71
|
+
```ruby
|
|
72
|
+
dataset = {
|
|
73
|
+
foo: 'oh man this is going to be so cool'
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
rules = {
|
|
77
|
+
foo: :make_super_neat
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
Rulix::Mutator.run dataset, rules
|
|
81
|
+
#=> {:foo=>" T OI INHOHMSNSTS B OIAOEOGGOLC "}
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
#### Configuring Rules
|
|
85
|
+
|
|
86
|
+
Some rules may need additional definition or configuration based on context. If you are writing a rule that needs additional options passed in, make the options a hash that is either taken as initialization arguments, or as the first argument of your proc.
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
```ruby
|
|
90
|
+
# Configurable proc
|
|
91
|
+
-> (options, value) { value[0..options[:trim_to] - 1] }
|
|
92
|
+
|
|
93
|
+
# Configured procable object
|
|
94
|
+
class CharacterRemover
|
|
95
|
+
attr_accessor :character
|
|
96
|
+
|
|
97
|
+
def initialize options = nil
|
|
98
|
+
options ||= {}
|
|
99
|
+
|
|
100
|
+
self.character = options[:character]
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def remove_from string
|
|
104
|
+
string.gsub character, ''
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def to_proc
|
|
108
|
+
method(:remove_from)
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
## Rulesets
|
|
114
|
+
|
|
115
|
+
Rulix rulesets are just Ruby hashes.
|
|
116
|
+
|
|
117
|
+
```ruby
|
|
118
|
+
ruleset = { first_name: :strip }
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
In order to properly map to a dataset, they need to follow the structure of the dataset that they're validating.
|
|
122
|
+
|
|
123
|
+
```ruby
|
|
124
|
+
ruleset = { first_name: :strip }
|
|
125
|
+
|
|
126
|
+
# This will work
|
|
127
|
+
proper_dataset = { first_name: 'Bob ' }
|
|
128
|
+
|
|
129
|
+
# This one won't :(
|
|
130
|
+
bad_dataset = { person: { first_name: 'Bob ' } }
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
Rulix does support nested hashes in a dataset, as long as the ruleset matches the format.
|
|
134
|
+
|
|
135
|
+
```ruby
|
|
136
|
+
dataset = {
|
|
137
|
+
person: {
|
|
138
|
+
first_name: 'Bob '
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
ruleset = {
|
|
143
|
+
person: {
|
|
144
|
+
first_name: :strip
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
Multiple rules can be applied to the same data point in a dataset.
|
|
150
|
+
|
|
151
|
+
```ruby
|
|
152
|
+
dataset = { name: 'Bob Johnson ' }
|
|
153
|
+
|
|
154
|
+
ruleset = { name: [:squeeze_spaces, :strip] }
|
|
155
|
+
|
|
156
|
+
Rulix::Mutator.run dataset, ruleset
|
|
157
|
+
#=> { name: 'Bob Johnson' }
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
## Mutation
|
|
161
|
+
|
|
162
|
+
You can mutate a dataset with `Rulix::Mutator`. This is most useful for sanitizing user inputs (like params hashes in Rails), but it can be used for any operation that needs to perform consistent mutations on a set of data.
|
|
163
|
+
|
|
164
|
+
```ruby
|
|
165
|
+
dataset = {
|
|
166
|
+
first_name: 'Bob ',
|
|
167
|
+
last_name: 'Johnson '
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
ruleset = {
|
|
171
|
+
first_name: :strip,
|
|
172
|
+
last_name: :strip
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
Rulix::Mutator.run dataset, ruleset
|
|
176
|
+
#=> { first_name: 'Bob', last_name: 'Johnson' }
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
## Validation
|
|
180
|
+
|
|
181
|
+
Use `Rulix::Validator` to validate a dataset. You can test if a dataset is valid with `Rulix::Validator.valid?(dataset, ruleset)`. If your dataset fails validation, you can extract errors with `Rulix::Validator.errors(dataset, ruleset)`
|
|
182
|
+
|
|
183
|
+
```ruby
|
|
184
|
+
dataset = {
|
|
185
|
+
phone: {
|
|
186
|
+
number: '800-555-5555'
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
ruleset = {
|
|
191
|
+
phone: {
|
|
192
|
+
number: [{format: /\d{10}/, message: 'does not match format'}]
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
Rulix::Validator.valid? dataset, ruleset
|
|
197
|
+
#=> false
|
|
198
|
+
Rulix::Validator.errors dataset, ruleset
|
|
199
|
+
#=> { phone: { number: ['does not match format'] } }
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
## Custom Validators and Mutators
|
|
203
|
+
|
|
204
|
+
You can write your own mutators and validators and make them available to Rulix for building rulesets as long as they implement their respective interfaces.
|
|
205
|
+
|
|
206
|
+
#### Custom Mutators
|
|
207
|
+
|
|
208
|
+
A mutator changes data, so its only interface requirement is that it returns a modified version of the supplied argument.
|
|
209
|
+
|
|
210
|
+
```ruby
|
|
211
|
+
string = 'A String of Sorts'
|
|
212
|
+
data = { foo: string }
|
|
213
|
+
bad_rule = -> (str) { [str] }
|
|
214
|
+
|
|
215
|
+
rules = { foo: [bad_rule, :strip] }
|
|
216
|
+
|
|
217
|
+
Rulix::Mutator.run data, rules
|
|
218
|
+
#=> NoMethodError: undefined method `strip' for ["A String of Sorts"]:Array
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
#### Custom Validators
|
|
222
|
+
|
|
223
|
+
Validators should return `true` if the rule is satisfied, and `[false, error_message]` if the validation fails. Rulix will run all supplied validations against the left-hand argument and compile the errors into a single array in place.
|
|
224
|
+
|
|
225
|
+
`Rulix::Validator` will always return an array under the given validated key, even if there is only one error.
|
|
226
|
+
|
|
227
|
+
```ruby
|
|
228
|
+
Rulix::Validator.register :doesnt_end_in_oo do |val|
|
|
229
|
+
val.end_with?('oo') ? [false, 'ends in oo'] : true
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
Rulix::Validator.register :digits_only do |val|
|
|
233
|
+
/^[0-9]+$/ === val ? true : [false, 'contains non-digits']
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
data = {
|
|
237
|
+
my_field: 'foo'
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
rules = {
|
|
241
|
+
my_field: [:digits_only, :doesnt_end_in_oo]
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
Rulix::Validator.run data, rules
|
|
245
|
+
#=> { my_field: ['contains non-digits', 'ends in oo'] }
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
## Contributing
|
|
249
|
+
|
|
250
|
+
1. Fork it ( https://github.com/blarshk/rulix/fork )
|
|
251
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
|
252
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
|
253
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
|
254
|
+
5. Create a new Pull Request
|
data/Rakefile
ADDED
data/bin/console
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
|
|
3
|
+
require "bundler/setup"
|
|
4
|
+
require "rulix"
|
|
5
|
+
|
|
6
|
+
# You can add fixtures and/or initialization code here to make experimenting
|
|
7
|
+
# with your gem easier. You can also use a different console, if you like.
|
|
8
|
+
|
|
9
|
+
# (If you use this, don't forget to add pry to your Gemfile!)
|
|
10
|
+
# require "pry"
|
|
11
|
+
# Pry.start
|
|
12
|
+
|
|
13
|
+
require "irb"
|
|
14
|
+
IRB.start
|
data/bin/setup
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
# Stole this from Rails; rather copy the source than introduce
|
|
2
|
+
# ActiveSupport as a dependency
|
|
3
|
+
class Hash
|
|
4
|
+
# Returns a new hash with +self+ and +other_hash+ merged recursively.
|
|
5
|
+
#
|
|
6
|
+
# h1 = { a: true, b: { c: [1, 2, 3] } }
|
|
7
|
+
# h2 = { a: false, b: { x: [3, 4, 5] } }
|
|
8
|
+
#
|
|
9
|
+
# h1.deep_merge(h2) # => { a: false, b: { c: [1, 2, 3], x: [3, 4, 5] } }
|
|
10
|
+
#
|
|
11
|
+
# Like with Hash#merge in the standard library, a block can be provided
|
|
12
|
+
# to merge values:
|
|
13
|
+
#
|
|
14
|
+
# h1 = { a: 100, b: 200, c: { c1: 100 } }
|
|
15
|
+
# h2 = { b: 250, c: { c1: 200 } }
|
|
16
|
+
# h1.deep_merge(h2) { |key, this_val, other_val| this_val + other_val }
|
|
17
|
+
# # => { a: 100, b: 450, c: { c1: 300 } }
|
|
18
|
+
def deep_merge(other_hash, &block)
|
|
19
|
+
dup.deep_merge!(other_hash, &block)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# Same as +deep_merge+, but modifies +self+.
|
|
23
|
+
def deep_merge!(other_hash, &block)
|
|
24
|
+
other_hash.each_pair do |current_key, other_value|
|
|
25
|
+
this_value = self[current_key]
|
|
26
|
+
|
|
27
|
+
self[current_key] = if this_value.is_a?(Hash) && other_value.is_a?(Hash)
|
|
28
|
+
this_value.deep_merge(other_value, &block)
|
|
29
|
+
else
|
|
30
|
+
if block_given? && key?(current_key)
|
|
31
|
+
block.call(current_key, this_value, other_value)
|
|
32
|
+
else
|
|
33
|
+
other_value
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
self
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Returns a hash, removing all values that cause the block to evaluate to true
|
|
42
|
+
# Iterates recursively over nested hashes
|
|
43
|
+
def deep_reject &block
|
|
44
|
+
dup.deep_reject! &block
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Same as +deep_reject+, but modifies +self+.
|
|
48
|
+
def deep_reject! &block
|
|
49
|
+
each_pair do |current_key, value|
|
|
50
|
+
this_value = self[current_key]
|
|
51
|
+
|
|
52
|
+
if this_value.is_a?(Hash)
|
|
53
|
+
self[current_key] = this_value.deep_reject &block
|
|
54
|
+
else
|
|
55
|
+
if block_given? && key?(current_key)
|
|
56
|
+
self.delete current_key if block.call current_key, this_value
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Returns a hash, removing all elements that
|
|
63
|
+
# respond to and return true from :empty?
|
|
64
|
+
# Iterates recursively over nested hashes
|
|
65
|
+
# Will continue to call itself until the second run
|
|
66
|
+
# does not differ from the first (kind of gross)
|
|
67
|
+
# TODO: Try to make this less gross
|
|
68
|
+
def deep_compact
|
|
69
|
+
result = dup.deep_compact!
|
|
70
|
+
result2 = result.dup.deep_compact!
|
|
71
|
+
|
|
72
|
+
if result != result2
|
|
73
|
+
result = result2
|
|
74
|
+
|
|
75
|
+
result.deep_compact
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
result
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def deep_compact!
|
|
82
|
+
each_pair do |current_key, value|
|
|
83
|
+
this_value = self[current_key]
|
|
84
|
+
|
|
85
|
+
if this_value.respond_to?(:empty?)
|
|
86
|
+
if this_value.empty?
|
|
87
|
+
self.delete current_key
|
|
88
|
+
elsif this_value.is_a?(Hash)
|
|
89
|
+
self[current_key] = this_value.deep_compact
|
|
90
|
+
end
|
|
91
|
+
else
|
|
92
|
+
self.delete current_key if this_value.nil?
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
require_relative './registry'
|
|
2
|
+
|
|
3
|
+
module Rulix
|
|
4
|
+
class Mutator
|
|
5
|
+
include Rulix::Registry
|
|
6
|
+
|
|
7
|
+
def self.run dataset, ruleset
|
|
8
|
+
dataset.deep_merge ruleset do |key, val1, val2|
|
|
9
|
+
val2 = [val2] unless val2.is_a? Array
|
|
10
|
+
|
|
11
|
+
ops = get_operations val2
|
|
12
|
+
|
|
13
|
+
ops.reduce(val1) { |val, op| op.call(val) }
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
module Rulix
|
|
2
|
+
module Registry
|
|
3
|
+
def self.included other
|
|
4
|
+
other.class_eval do
|
|
5
|
+
@registry ||= {}
|
|
6
|
+
|
|
7
|
+
def self.register symbol, procable = nil, &block
|
|
8
|
+
return register_block symbol, &block if block_given?
|
|
9
|
+
|
|
10
|
+
if !procable.respond_to?(:to_proc)
|
|
11
|
+
unless (procable.respond_to?(:new) && procable.new.respond_to?(:to_proc))
|
|
12
|
+
raise ArgumentError, "You attempted to register :#{symbol}, but the argument you passed can't be coerced into a proc!"
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
@registry[symbol] = procable
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
private
|
|
20
|
+
|
|
21
|
+
def self.register_block symbol, &block
|
|
22
|
+
@registry[symbol] = block.to_proc
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def self.get_operations operations
|
|
26
|
+
operations.map do |op|
|
|
27
|
+
get_operation op
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def self.get_operation operation
|
|
32
|
+
case operation
|
|
33
|
+
when Symbol
|
|
34
|
+
return @registry[operation].to_proc if @registry[operation] && @registry[operation].respond_to?(:to_proc)
|
|
35
|
+
|
|
36
|
+
operation.to_proc
|
|
37
|
+
when Hash
|
|
38
|
+
# If you're passing a hash as a rule argument, we assume that it's been registered
|
|
39
|
+
# The registered rule must be instantiatable, and we assume the args passed
|
|
40
|
+
# should be passed to the object as config options
|
|
41
|
+
key = operation.keys.first
|
|
42
|
+
arguments = operation[key]
|
|
43
|
+
|
|
44
|
+
registered_procable = @registry[key]
|
|
45
|
+
|
|
46
|
+
raise ArgumentError, "You've supplied a hash argument for a rule, but there's no rule registered for #{key}!" unless registered_procable
|
|
47
|
+
|
|
48
|
+
case registered_procable
|
|
49
|
+
when Proc
|
|
50
|
+
registered_procable.curry[arguments ]
|
|
51
|
+
else
|
|
52
|
+
registered_procable.new(arguments).to_proc
|
|
53
|
+
end
|
|
54
|
+
when Proc
|
|
55
|
+
operation
|
|
56
|
+
else
|
|
57
|
+
raise ArgumentError, "Can't coerce #{operation} into useable proc!" unless operation.respond_to? :to_proc
|
|
58
|
+
|
|
59
|
+
operation.to_proc
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
require_relative './registry'
|
|
2
|
+
|
|
3
|
+
module Rulix
|
|
4
|
+
class Validator
|
|
5
|
+
include Rulix::Registry
|
|
6
|
+
|
|
7
|
+
def self.run dataset, ruleset
|
|
8
|
+
dataset = data_for_ruleset dataset, ruleset
|
|
9
|
+
|
|
10
|
+
dataset.deep_merge ruleset do |key, val1, val2|
|
|
11
|
+
val2 = [val2] unless val2.is_a? Array
|
|
12
|
+
|
|
13
|
+
ops = get_operations val2
|
|
14
|
+
|
|
15
|
+
success, errors = ops.reduce([true, []]) do |result, op|
|
|
16
|
+
success, errors = result
|
|
17
|
+
|
|
18
|
+
new_success, *new_errors = op.call(val1)
|
|
19
|
+
|
|
20
|
+
[success && new_success, errors.concat(new_errors)]
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
errors
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def self.valid? dataset, ruleset
|
|
28
|
+
run = run dataset, ruleset
|
|
29
|
+
|
|
30
|
+
run.deep_compact.empty?
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def self.errors dataset, ruleset
|
|
34
|
+
run = run dataset, ruleset
|
|
35
|
+
|
|
36
|
+
run.deep_compact
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
private
|
|
40
|
+
|
|
41
|
+
def self.data_for_ruleset dataset, ruleset
|
|
42
|
+
seed = {}
|
|
43
|
+
|
|
44
|
+
reduce_into_hash seed, dataset, ruleset
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def self.reduce_into_hash hash, dataset, ruleset
|
|
48
|
+
ruleset.reduce(hash) do |data_hash, values|
|
|
49
|
+
key, val = values
|
|
50
|
+
|
|
51
|
+
if val.is_a? Hash
|
|
52
|
+
seed = {}
|
|
53
|
+
nested_value = value_from_dataset dataset, key
|
|
54
|
+
|
|
55
|
+
data_hash[key] = reduce_into_hash seed, nested_value, val
|
|
56
|
+
else
|
|
57
|
+
data_hash[key] = value_from_dataset dataset, key
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
data_hash
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def self.value_from_dataset dataset, key
|
|
65
|
+
if dataset.respond_to? :[]
|
|
66
|
+
dataset[key]
|
|
67
|
+
else
|
|
68
|
+
dataset.public_send(key)
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
module Rulix
|
|
2
|
+
module Validators
|
|
3
|
+
class AlphaValidator
|
|
4
|
+
def self.to_proc
|
|
5
|
+
new.method(:call)
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
def call string
|
|
9
|
+
/^[a-zA-Z\s?]*$/ === string || [false, error_message(string)]
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def error_message string
|
|
13
|
+
"contains non-alpha characters"
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
Rulix::Validator.register :alpha_only, Rulix::Validators::AlphaValidator
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
module Rulix
|
|
2
|
+
module Validators
|
|
3
|
+
class FormatValidator
|
|
4
|
+
attr_accessor :format, :message
|
|
5
|
+
|
|
6
|
+
def initialize options = nil
|
|
7
|
+
options ||= {}
|
|
8
|
+
|
|
9
|
+
self.format = options[:format]
|
|
10
|
+
self.message = options.fetch :message, 'does not match format'
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def call string
|
|
14
|
+
format === string || [false, message]
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def to_proc
|
|
18
|
+
method(:call)
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
Rulix::Validator.register :format, Rulix::Validators::FormatValidator
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
module Rulix
|
|
2
|
+
module Validators
|
|
3
|
+
class LengthValidator
|
|
4
|
+
attr_accessor :min, :max
|
|
5
|
+
|
|
6
|
+
def initialize options = nil
|
|
7
|
+
options ||= {}
|
|
8
|
+
min = options[:min] || options[:exactly] || 0
|
|
9
|
+
max = options[:max] || options[:exactly] || min + 1
|
|
10
|
+
|
|
11
|
+
self.min = min
|
|
12
|
+
self.max = max
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def call string
|
|
16
|
+
(min..max).cover?(string.length) || [false, error_message(string)]
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def error_message string
|
|
20
|
+
if string.length < min
|
|
21
|
+
"is too short"
|
|
22
|
+
elsif string.length > max
|
|
23
|
+
"is too long"
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def to_proc
|
|
28
|
+
method(:call)
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
Rulix::Validator.register :length, Rulix::Validators::LengthValidator
|
data/lib/rulix.rb
ADDED
data/rulix.gemspec
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# coding: utf-8
|
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
|
4
|
+
require 'rulix/version'
|
|
5
|
+
|
|
6
|
+
Gem::Specification.new do |spec|
|
|
7
|
+
spec.name = "rulix"
|
|
8
|
+
spec.version = Rulix::VERSION
|
|
9
|
+
spec.authors = ["Mitch Monsen"]
|
|
10
|
+
spec.email = ["mmonsen7@gmail.com"]
|
|
11
|
+
|
|
12
|
+
spec.summary = "Simple data manipulation and validation through rulesets"
|
|
13
|
+
spec.description = "Rulix lets you fold complex rulesets onto complex data structures; useful for validation, data sanitization, and mutation."
|
|
14
|
+
spec.homepage = "https://github.com/blarshk/rulix"
|
|
15
|
+
|
|
16
|
+
spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
|
|
17
|
+
spec.require_paths = ["lib"]
|
|
18
|
+
|
|
19
|
+
spec.add_development_dependency "bundler", "~> 1.12"
|
|
20
|
+
spec.add_development_dependency "rake", "~> 10.0"
|
|
21
|
+
spec.add_development_dependency "pry"
|
|
22
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: rulix
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Mitch Monsen
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: bin
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2016-06-17 00:00:00.000000000 Z
|
|
12
|
+
dependencies:
|
|
13
|
+
- !ruby/object:Gem::Dependency
|
|
14
|
+
name: bundler
|
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
|
16
|
+
requirements:
|
|
17
|
+
- - "~>"
|
|
18
|
+
- !ruby/object:Gem::Version
|
|
19
|
+
version: '1.12'
|
|
20
|
+
type: :development
|
|
21
|
+
prerelease: false
|
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
23
|
+
requirements:
|
|
24
|
+
- - "~>"
|
|
25
|
+
- !ruby/object:Gem::Version
|
|
26
|
+
version: '1.12'
|
|
27
|
+
- !ruby/object:Gem::Dependency
|
|
28
|
+
name: rake
|
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
|
30
|
+
requirements:
|
|
31
|
+
- - "~>"
|
|
32
|
+
- !ruby/object:Gem::Version
|
|
33
|
+
version: '10.0'
|
|
34
|
+
type: :development
|
|
35
|
+
prerelease: false
|
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
37
|
+
requirements:
|
|
38
|
+
- - "~>"
|
|
39
|
+
- !ruby/object:Gem::Version
|
|
40
|
+
version: '10.0'
|
|
41
|
+
- !ruby/object:Gem::Dependency
|
|
42
|
+
name: pry
|
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
|
44
|
+
requirements:
|
|
45
|
+
- - ">="
|
|
46
|
+
- !ruby/object:Gem::Version
|
|
47
|
+
version: '0'
|
|
48
|
+
type: :development
|
|
49
|
+
prerelease: false
|
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
51
|
+
requirements:
|
|
52
|
+
- - ">="
|
|
53
|
+
- !ruby/object:Gem::Version
|
|
54
|
+
version: '0'
|
|
55
|
+
description: Rulix lets you fold complex rulesets onto complex data structures; useful
|
|
56
|
+
for validation, data sanitization, and mutation.
|
|
57
|
+
email:
|
|
58
|
+
- mmonsen7@gmail.com
|
|
59
|
+
executables: []
|
|
60
|
+
extensions: []
|
|
61
|
+
extra_rdoc_files: []
|
|
62
|
+
files:
|
|
63
|
+
- ".gitignore"
|
|
64
|
+
- Gemfile
|
|
65
|
+
- LICENSE.txt
|
|
66
|
+
- README.md
|
|
67
|
+
- Rakefile
|
|
68
|
+
- bin/console
|
|
69
|
+
- bin/setup
|
|
70
|
+
- lib/rulix.rb
|
|
71
|
+
- lib/rulix/core_ext/hash.rb
|
|
72
|
+
- lib/rulix/mutator.rb
|
|
73
|
+
- lib/rulix/registry.rb
|
|
74
|
+
- lib/rulix/validator.rb
|
|
75
|
+
- lib/rulix/validators/alpha_validator.rb
|
|
76
|
+
- lib/rulix/validators/format_validator.rb
|
|
77
|
+
- lib/rulix/validators/length_validator.rb
|
|
78
|
+
- lib/rulix/version.rb
|
|
79
|
+
- rulix.gemspec
|
|
80
|
+
homepage: https://github.com/blarshk/rulix
|
|
81
|
+
licenses: []
|
|
82
|
+
metadata: {}
|
|
83
|
+
post_install_message:
|
|
84
|
+
rdoc_options: []
|
|
85
|
+
require_paths:
|
|
86
|
+
- lib
|
|
87
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
88
|
+
requirements:
|
|
89
|
+
- - ">="
|
|
90
|
+
- !ruby/object:Gem::Version
|
|
91
|
+
version: '0'
|
|
92
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
93
|
+
requirements:
|
|
94
|
+
- - ">="
|
|
95
|
+
- !ruby/object:Gem::Version
|
|
96
|
+
version: '0'
|
|
97
|
+
requirements: []
|
|
98
|
+
rubyforge_project:
|
|
99
|
+
rubygems_version: 2.4.6
|
|
100
|
+
signing_key:
|
|
101
|
+
specification_version: 4
|
|
102
|
+
summary: Simple data manipulation and validation through rulesets
|
|
103
|
+
test_files: []
|
|
104
|
+
has_rdoc:
|