ruy 0.2.1 → 0.3.0

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: 1892173ce87ec0352f8eb0058be130276ac3992d
4
- data.tar.gz: 56e8ecb959603b387278c3c7b771eace0cd797dd
3
+ metadata.gz: a3f59c87798c8d6d5aa92ca7639e4605f60c36c6
4
+ data.tar.gz: 4d51c3dd0c5e0a9d7c4a8175caf38eccb19e7763
5
5
  SHA512:
6
- metadata.gz: bdc7bc60c215ca1514262f9b9ff0cce49208590e9fc5597440698a68678c40be21bd740b3539504409f137d433affe2e88ba09df9166f72f3b84e1d4b7cb88bc
7
- data.tar.gz: 889a05b473558941b51c3eb4c3ffdd07eb786aef86b2dc8043bb4d51813b9608079f5795285beba19362bec7dd9f830ef8b907e9d69774bfbb4364d8587a6bf2
6
+ metadata.gz: 71e65bfa1ace338acdcefafbe9a234e5c415bdc1b8075c330fe4831e8f6cebfbd3f70a435656266c7604338c0180692f39af888d1bf7df6f64088233446b550b
7
+ data.tar.gz: 8c8bb8c7db01b3254d83f53fd44a41dd9257fd2544929dc513360a27ad5805d38676e4568a9d868b6946efe525c7db4f0d101128463eab98a40cfe22f624f0f2
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2014 Moove-iT
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 all
13
+ 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 THE
21
+ SOFTWARE.
@@ -0,0 +1,197 @@
1
+ # ruy
2
+
3
+ Ruy is a library for defining a set of conditions and evaluating them against a context.
4
+
5
+ ``` ruby
6
+ # discount_day.rb
7
+ gifter = Ruy::RuleSet.new
8
+
9
+ gifter.eq :friday, :day_of_week
10
+
11
+ gifter.outcome 8 do
12
+ greater_than_or_equal 300, :amount
13
+ end
14
+
15
+ gifter.outcome 7 do
16
+ greater_than_or_equal 100, :amount
17
+ end
18
+
19
+ gifter.outcome 3
20
+
21
+ gifter.fallback 0
22
+ ```
23
+
24
+ RuleSets are evaluated against a context (the `Hash` being passed to `#call`) and return the first outcome that matches.
25
+
26
+ ``` ruby
27
+ gifter.call(day_of_week: :friday, amount: 314)
28
+
29
+ # => 8
30
+ ```
31
+
32
+ ``` ruby
33
+ gifter.call(day_of_week: :friday, amount: 256)
34
+
35
+ # => 7
36
+ ```
37
+
38
+ If no outcome matches, the default one is returned.
39
+ ``` ruby
40
+ gifter.call(day_of_week: :friday, amount: 99)
41
+
42
+ # => 3
43
+ ```
44
+
45
+ If conditions are not met, the fallback value is returned.
46
+ ``` ruby
47
+ gifter.call(day_of_week: :monday, amount: 124)
48
+
49
+ # => 0
50
+ ```
51
+
52
+ ## Key concepts
53
+
54
+ Ruy at its core is about evaluating a set of conditions against a context in order to return a result.
55
+
56
+ ### Conditions
57
+
58
+ A condition evaluates the state of the context.
59
+
60
+ Available conditions:
61
+
62
+ - all *All of the nested conditions must suffice*
63
+ - any *At least one of its nested conditions must suffice*
64
+ - assert *A context value must be truish*
65
+ - between *Evaluates that a context value must belong to a specified range*
66
+ - cond *At least one slice of two nested conditions must suffice*
67
+ - day_of_week *Evaluates that a Date/DateTime/Time weekday is matched*
68
+ - eq *Tests a context value for equality*
69
+ - except *Evaluates that a context value is not equal to a specified value*
70
+ - greater_than *Tests that context value is greater than something*
71
+ - greater_than_or_equal *Tests that context value is greater than or equal to something*
72
+ - in *A context value must belong to a specified list of values*
73
+ - in_cyclic_order *TBD*
74
+ - include *The context attribute must include a specified value*
75
+ - less_than_or_equal *Tests that context value is less than or equal to something*
76
+ - less_than *Tests that context value is less than something*
77
+
78
+ Conditions can be nested. In such case, for the nesting condition to be met, the nested conditions must
79
+ also be met.
80
+
81
+ ``` ruby
82
+ between 0, 1_000, :amount do
83
+ eq :friday, :day_of_week
84
+ end
85
+ ```
86
+
87
+ is equivalent to:
88
+
89
+ ``` ruby
90
+ all do
91
+ between 0, 1_000, :amount
92
+ eq :friday, :day_of_week
93
+ end
94
+ ```
95
+
96
+ ### Rulsets
97
+
98
+ A ruleset is a set of conditions that must suffice and returns a value resulting from either an
99
+ outcome or a fallback.
100
+
101
+ ### Contexts
102
+
103
+ A context is a `Hash` from which values are fetched in order to evaluate a ruleset.
104
+
105
+ ### Lazy values
106
+
107
+ Rulesets can define lazy values. The context must provide a proc which is evaluted only once the first time the value is needed. The result returned by the proc is memoized and used to evaluate subsequent conditions.
108
+
109
+
110
+ ``` ruby
111
+ # premium_discount_day.rb
112
+ gifter = Ruy::RuleSet.new
113
+
114
+ gifter.let :amounts_average # an expensive calculation
115
+
116
+ gifter.eq :friday, :week_of_day
117
+
118
+ gifter.greater_than_or_equal 10_000, :amounts_average
119
+
120
+ gifter.outcome true
121
+ ```
122
+
123
+ ``` ruby
124
+ gifter.call(day_of_week: :friday, amounts_average: -> { Stats::Amounts.compute_average })
125
+ ```
126
+ ### Outcomes
127
+
128
+ An outcome is the result of a successful ruleset evaluation. An outcome can also have nested
129
+ conditions, in such case, if the conditions meet, the outcome value is returned.
130
+
131
+ A RuleSet can have multiple outcomes, the first matching one is returned.
132
+
133
+ ### Time Zone awareness
134
+
135
+ When it comes to matching times in different time zones, Ruy is bundled with a built in `tz` block that will enable specific matchers to make time zone-aware comparisons.
136
+
137
+ ```ruby
138
+ ruleset = Ruy::RuleSet.new
139
+
140
+ ruleset.tz 'America/New_York' do
141
+ eq '2015-01-01T00:00:00', :timestamp
142
+ end
143
+
144
+ ruleset.outcome 'Happy New Year, NYC!'
145
+ ```
146
+
147
+ For example, if the timestamp provided in the context is a Ruby Time object in UTC (zero offset from UTC), `eq` as child of a `tz` block will take the time zone passed as argument to the block (`America/New_York`) to calculate the current offset and make the comparison.
148
+
149
+ String time patterns follow the Ruy's well-formed time pattern structure as follows:
150
+
151
+ `YYYY-MM-DDTHH:MM:SS[z<IANA Time Zone Database identifier>]`
152
+
153
+ Where the time zone identifier is optional, but if you specify it, will take precedence over the block's identifier. In case you don't specify it, Ruy will get the time zone from the `tz` block's argument. If neither the block nor the pettern specify it, UTC will be used.
154
+
155
+ #### Days of week matcher
156
+
157
+ Inside any `tz` block, there's a matcher to look for a specific day of the week in the time zone of the block.
158
+
159
+ ```ruby
160
+ ruleset = Ruy::RuleSet.new
161
+
162
+ ruleset.any do
163
+ tz 'America/New_York' do
164
+ day_of_week :saturday, :timestamp
165
+ end
166
+
167
+ tz 'America/New_York' do
168
+ day_of_week 0, :timestamp # Sunday
169
+ end
170
+ end
171
+
172
+ ruleset.outcome 'Have a nice weekend, NYC!'
173
+ ```
174
+
175
+ This matcher supports both the `Symbol` and number syntax in the range `(0..6)` starting on Sunday.
176
+
177
+ The day of week matcher will try to parse timestamps using the ISO8601 format unless the context passes a Time object.
178
+
179
+ #### Nested blocks support
180
+
181
+ You cannot use matchers inside nested blocks in a `tz` block expecting them to work as if they were immediate children of `tz`.
182
+
183
+ A possible workaround for this is to use `tz` blocks inside the nested block in question:
184
+
185
+ ```ruby
186
+ ruleset = Ruy::RuleSet.new
187
+ any do
188
+ tz 'America/New_York' { eq '2015-01-01T00:00:00', :timestamp }
189
+ tz 'America/New_York' { eq '2015-01-01T02:00:00zUTC', :timestamp }
190
+ end
191
+
192
+ ruleset.outcome 'Happy New Year, NYC!'
193
+ ```
194
+
195
+ Support for time zone awareness in nested blocks inside `tz` blocks is planned. This workaround could stop working in future versions; use it at your own risk.
196
+
197
+ Ruy depends on [TZInfo](http://tzinfo.github.io/ "TZ Info website") to calculate offsets using IANA's Time Zone Database. Check their website for information about time zone identifiers.
data/lib/ruy.rb CHANGED
@@ -1,7 +1,8 @@
1
+ require_relative 'ruy/utils'
2
+ require_relative 'ruy/dsl'
1
3
  require_relative 'ruy/rule'
2
4
  require_relative 'ruy/rule_set'
3
5
  require_relative 'ruy/conditions'
4
6
  require_relative 'ruy/context'
5
- require_relative 'ruy/variable_context'
6
7
  require_relative 'ruy/outcome'
7
8
  require_relative 'ruy/time_pattern'
@@ -2,13 +2,15 @@ require_relative 'conditions/all'
2
2
  require_relative 'conditions/any'
3
3
  require_relative 'conditions/assert'
4
4
  require_relative 'conditions/between'
5
+ require_relative 'conditions/in_cyclic_order'
5
6
  require_relative 'conditions/cond'
6
7
  require_relative 'conditions/day_of_week'
7
8
  require_relative 'conditions/eq'
8
9
  require_relative 'conditions/except'
10
+ require_relative 'conditions/greater_than'
9
11
  require_relative 'conditions/greater_than_or_equal'
12
+ require_relative 'conditions/in'
10
13
  require_relative 'conditions/include'
11
- require_relative 'conditions/included'
12
14
  require_relative 'conditions/less_than'
13
15
  require_relative 'conditions/less_than_or_equal'
14
16
  require_relative 'conditions/tz'
@@ -3,9 +3,9 @@ module Ruy
3
3
 
4
4
  # Expects that all rules will succeed.
5
5
  class All < Ruy::Rule
6
- def call(var_ctx)
6
+ def call(ctx)
7
7
  @conditions.all? do |condition|
8
- condition.call(var_ctx)
8
+ condition.call(ctx)
9
9
  end
10
10
  end
11
11
  end
@@ -3,9 +3,9 @@ module Ruy
3
3
 
4
4
  # Expects that at least one of the rules will succeed.
5
5
  class Any < Ruy::Rule
6
- def call(var_ctx)
6
+ def call(ctx)
7
7
  @conditions.any? do |condition|
8
- condition.call(var_ctx)
8
+ condition.call(ctx)
9
9
  end
10
10
  end
11
11
 
@@ -11,8 +11,8 @@ module Ruy
11
11
  @attr = attr
12
12
  end
13
13
 
14
- def call(var_ctx)
15
- var_ctx.resolve(@attr)
14
+ def call(ctx)
15
+ ctx.resolve(@attr)
16
16
  end
17
17
 
18
18
  def ==(o)
@@ -7,21 +7,22 @@ module Ruy
7
7
  class Between < Ruy::Rule
8
8
  attr_reader :attr, :from, :to
9
9
 
10
- # @param attr Name of the attribute that will be evaluated
11
10
  # @param from Range lower bound
12
11
  # @param to Range upper bound
12
+ # @param attr Name of the attribute that will be evaluated
13
+ #
13
14
  # @yield a block in the context of the current rule
14
- def initialize(attr, from, to, &block)
15
+ def initialize(from, to, attr, &block)
15
16
  super
16
- @attr = attr
17
17
  @from = from
18
18
  @to = to
19
+ @attr = attr
19
20
  instance_exec(&block) if block_given?
20
21
  end
21
22
 
22
- def call(var_ctx)
23
- value = var_ctx.resolve(@attr)
24
- @from <= value && value <= @to && super
23
+ def call(ctx)
24
+ value = ctx.resolve(@attr)
25
+ @from <= value && @to >= value && super
25
26
  end
26
27
 
27
28
  def ==(o)
@@ -8,14 +8,14 @@ module Ruy
8
8
  #
9
9
  # Cond is handy for mocking if/else if/else constructs.
10
10
  class Cond < Ruy::Rule
11
- def call(var_ctx)
11
+ def call(ctx)
12
12
  clauses = @conditions.each_slice(2)
13
13
 
14
14
  clauses.any? do |rule_1, rule_2|
15
- result = rule_1.call(var_ctx)
15
+ result = rule_1.call(ctx)
16
16
 
17
17
  if rule_2
18
- result && rule_2.call(var_ctx)
18
+ result && rule_2.call(ctx)
19
19
  else
20
20
  result
21
21
  end
@@ -4,25 +4,24 @@ require 'tzinfo'
4
4
  module Ruy
5
5
  module Conditions
6
6
 
7
- # Expects that a Time object's date corresponds to a specified day of the week
7
+ # Expects that a Time object's date corresponds to a specified day of the week.
8
8
  class DayOfWeek < Ruy::Rule
9
9
  DAYS_INTO_WEEK = %w(sunday monday tuesday wednesday thursday friday saturday)
10
10
  attr_reader :attr, :value, :tz_identifier
11
11
 
12
- # @param attr
13
12
  # @param value
13
+ # @param attr
14
14
  # @param tz_identifier
15
- def initialize(attr, value, tz_identifier = 'UTC')
15
+ def initialize(value, attr, tz_identifier = 'UTC')
16
16
  super
17
- @attr = attr
18
17
  @value = value
18
+ @attr = attr
19
19
  @tz_identifier = tz_identifier
20
20
  @tz = TZInfo::Timezone.get(tz_identifier)
21
21
  end
22
22
 
23
- # @param [Ruy::VariableContext] var_ctx
24
- def call(var_ctx)
25
- resolved = var_ctx.resolve(@attr)
23
+ def call(ctx)
24
+ resolved = ctx.resolve(@attr)
26
25
  cmp = @tz.utc_to_local(resolved.to_time.utc)
27
26
 
28
27
  if @value.is_a?(Fixnum)
@@ -32,7 +31,6 @@ module Ruy
32
31
  end
33
32
  end
34
33
 
35
- # @param o
36
34
  def ==(o)
37
35
  o.kind_of?(DayOfWeek) &&
38
36
  attr == o.attr &&
@@ -2,20 +2,19 @@ module Ruy
2
2
  module Conditions
3
3
 
4
4
  # Expects that a context attribute will be equal to a given value.
5
- #
6
5
  class Eq < Ruy::Rule
7
6
  attr_reader :attr, :value
8
7
 
9
- # @param attr Context attribute's name
10
8
  # @param value Expected value
11
- def initialize(attr, value)
9
+ # @param attr Context attribute's name
10
+ def initialize(value, attr)
12
11
  super
13
- @attr = attr
14
12
  @value = value
13
+ @attr = attr
15
14
  end
16
15
 
17
- def call(var_ctx)
18
- var_ctx.resolve(@attr) == @value
16
+ def call(ctx)
17
+ @value == ctx.resolve(@attr)
19
18
  end
20
19
 
21
20
  def ==(o)
@@ -9,25 +9,25 @@ module Ruy
9
9
  class Except < Ruy::Rule
10
10
  attr_reader :attr, :value
11
11
 
12
- # @param attr Context attribute's name
13
12
  # @param value Non-expected value
13
+ # @param attr Context attribute's name
14
14
  # @yield a block in the context of the current rule
15
- def initialize(attr = nil, value = nil, &block)
15
+ def initialize(value = nil, attr = nil, &block)
16
16
  super
17
- @attr = attr
18
17
  @value = value
18
+ @attr = attr
19
19
  instance_exec(&block) if block_given?
20
20
  end
21
21
 
22
- def call(var_ctx)
22
+ def call(ctx)
23
23
  result = true
24
24
 
25
25
  if @attr
26
- result &&= var_ctx.resolve(@attr) != @value
26
+ result &&= ctx.resolve(@attr) != @value
27
27
  end
28
28
 
29
29
  if self.conditions.any?
30
- result &&= !super(var_ctx)
30
+ result &&= !super(ctx)
31
31
  end
32
32
 
33
33
  result
@@ -0,0 +1,30 @@
1
+ module Ruy
2
+ module Conditions
3
+
4
+ # Expects that a context attribute will be greater than the given value.
5
+ class GreaterThan < Ruy::Rule
6
+ attr_reader :attr, :value
7
+
8
+ # @param value
9
+ # @param attr Context attribute's name
10
+ # @yield a block in the context of the current rule
11
+ def initialize(value, attr, &block)
12
+ super
13
+ @value = value
14
+ @attr = attr
15
+ instance_exec(&block) if block_given?
16
+ end
17
+
18
+ def call(var_ctx)
19
+ @value < var_ctx.resolve(@attr) && super
20
+ end
21
+
22
+ def ==(o)
23
+ o.kind_of?(GreaterThan) &&
24
+ attr == o.attr &&
25
+ value == o.value &&
26
+ self.conditions == o.conditions
27
+ end
28
+ end
29
+ end
30
+ end