fact_checker 0.0.3 → 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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 49efc644177e73e8127c70768faeb8d3c4f29b54
4
+ data.tar.gz: 7c91d43ed59898af6d528880e15cd69375eb4940
5
+ SHA512:
6
+ metadata.gz: 69ae38383592c810071ecef53093fbfc13844eeb30aac26b5679a8757243ffb2cc476cb9a6dd4e2e6297f12ff46a30cc8db554ca8cbc14dd0b342afd060d960d
7
+ data.tar.gz: 8103359a9eeff6f0d438dffbbaccd8fa33bef1d2b5a3e614a2bea835f10f90250d0eaea9c1c15f50d17b2015c849bcbbe4926714cabec5c709743fc57baf4de0
data/.ruby-gemset ADDED
@@ -0,0 +1 @@
1
+ fact_checker
data/.ruby-version ADDED
@@ -0,0 +1 @@
1
+ ruby-2.0.0
data/.travis.yml ADDED
@@ -0,0 +1,4 @@
1
+ language: ruby
2
+ rvm:
3
+ - 1.9.2
4
+ - 1.9.3
data/README.md CHANGED
@@ -1,10 +1,7 @@
1
- # Fact Checker
1
+ # Fact Checker [![Build Status](https://secure.travis-ci.org/alexis/fact_checker.png?branch=master)](http://travis-ci.org/alexis/fact_checker)
2
+ [![Code Climate](https://codeclimate.com/github/alexis/fact_checker.png)](https://codeclimate.com/github/alexis/fact_checker)
2
3
 
3
- Simple ruby gem to check hierarchically dependent "facts" about objects.
4
-
5
- ## Installation
6
-
7
- gem install fact_checker
4
+ Simple ruby gem to define and check hierarchically dependent "facts" about objects.
8
5
 
9
6
  ## Synopsys
10
7
 
@@ -12,20 +9,108 @@
12
9
  class Person
13
10
  include FactChecker
14
11
 
15
- def_fact :good_job, if: ->(p) { p.job.good? }
16
- def_fact :good_family, if: ->(p) { p.family.good? }
17
- def_fact :is_healthy, if: ->(p) { p.health.good? }
18
- def_fact :is_happy => [:is_healthy, :good_family, :good_job], if: ->(p) { ! p.too_clever? }
12
+ define_fact(:good_job) { p.job.good? }
13
+ define_fact(:good_family) { p.family.good? }
14
+ define_fact(:is_healthy) { p.health.good? }
15
+ define_fact(:is_happy => [:is_healthy, :good_family, :good_job]) { ! p.too_clever? }
19
16
 
20
17
  ...
21
18
  end
22
19
 
23
- p = Person.new(:job => good_job, :family => good_family, :health => :good, :intellect => :too_clever)
24
- p.good_job? # => true
25
- p.good_family? # => true
26
- p.is_healthy? # => true
27
- p.fact_possible?(:is_happy) # => true (dependency satisfied)
28
- p.is_happy? # => false
20
+ p = Person.new(job: good_job, family: good_family, health: :good, intellect: :too_clever)
21
+ p.good_job? # => true
22
+ p.good_family? # => true
23
+ p.is_healthy? # => true
24
+ p.is_happy.available? # => true (dependency satisfied)
25
+ p.is_happy? # => false
26
+ ```
27
+
28
+ ## Description
29
+
30
+ The gem is most usefull when you have something
31
+ like a checklist, a list of tasks that your users should complete to achieve some goal.
32
+
33
+ For example, let's say that in order to publish an article, user have to:
34
+
35
+ 1. write the article
36
+ 2. name the article (user may complete steps 1 & 2 in any order)
37
+ 3. choose its category
38
+ 4. assign tags to the article (user may complete steps 3 & 4 in any order, but only after steps 1 & 2)
39
+ 5. mark article as ready for review (only after steps 1-3 are completed, but step 4 is not required)
40
+ 6. recieve approvement from one of moderators (all steps 1-5 are required)
41
+
42
+ <!--- The imporant thing here - which makes fact_checker worth its use - is that you want to display this
43
+ checklist for users in a way that they could instantly understand which steps are completed, which
44
+ is not available yet, and which are ready for action.
45
+ This means that each step could be in 3 different states: "completed", "ready for action" and "not available".
46
+ -->
47
+
48
+ Using fact_checker that logic could be implemented like this:
49
+
50
+ ```ruby
51
+ include FactChecker
52
+
53
+ define_fact(:step1) { content.present? }
54
+ define_fact(:step2) { name.present? }
55
+ define_fact(:step3 => [:step1, :step2]) { category.present? }
56
+ define_fact(:step4 => [:step1, :step2]) { tag_list.present? }
57
+ define_fact(:step5 => :step3) { ready_for_approvement? }
58
+ define_fact(:step6 => [:step4, :step5]) { approved? }
59
+
60
+ def state_of(step)
61
+ return 'completed' if valid?(step)
62
+ return 'ready_for_action' if available?(step)
63
+ return 'not_available'
64
+ end
65
+ ```
66
+
67
+ Just to compare, a possible alternative implimentation without fact_checker:
68
+
69
+ ``` ruby
70
+ def step1_available?
71
+ true
72
+ end
73
+ def step1_valid?
74
+ content.present?
75
+ end
76
+ def step2_available?
77
+ true
78
+ end
79
+ def step2_valid?
80
+ name.present?
81
+ end
82
+ def step3_available?
83
+ step1? && step2?
84
+ end
85
+ def step3_valid?
86
+ step3_available? && a.category.present?
87
+ end
88
+ def step4_available?
89
+ step1? && step2?
90
+ end
91
+ def step4_valid?
92
+ step4_available? && tag_list.present?
93
+ end
94
+ def step5_available?
95
+ step3
96
+ end
97
+ def step5_valid?
98
+ step5_available? && a.ready_for_approvement?
99
+ end
100
+ def step6_available?
101
+ step4? && step5
102
+ end
103
+ def step6_valid?
104
+ step6_available? && a.approved?
105
+ end
29
106
 
30
- p.possible_facts - p.accomplished_facts # => [:happy]
107
+ def state_of(step)
108
+ return 'completed' if self.send(step + '_valid?')
109
+ return 'ready_for_action' if self.send(step + '_available?')
110
+ return 'not_available'
111
+ end
31
112
  ```
113
+
114
+ ## Installation
115
+
116
+ gem install fact_checker
data/fact_checker.gemspec CHANGED
@@ -20,11 +20,12 @@ Gem::Specification.new do |s|
20
20
  s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
21
21
  s.require_paths = ["lib"]
22
22
 
23
- s.required_ruby_version = ">= 1.8.7"
24
- s.required_rubygems_version = ">= 1.3.5"
23
+ s.required_ruby_version = '>= 1.9.2'
24
+ s.required_rubygems_version = '>= 1.3.5'
25
25
 
26
26
  s.add_development_dependency 'rake'
27
27
  s.add_development_dependency 'bundler'
28
- s.add_development_dependency 'rspec', '~> 2.9.0'
29
- s.add_development_dependency 'simplecov', '~> 0.6.1'
28
+ s.add_development_dependency 'rspec', '~> 2.14'
29
+ s.add_development_dependency 'simplecov'
30
+ s.add_development_dependency 'debugger'
30
31
  end
@@ -0,0 +1,32 @@
1
+ # encoding: utf-8
2
+
3
+ module FactChecker
4
+ def self.included(klass)
5
+ klass.extend ClassMethods
6
+ end
7
+
8
+ module ClassMethods
9
+ def define_fact(arg, &block)
10
+ fail ArgumentError, 'block not supplied' unless block
11
+
12
+ name, dependencies = arg.is_a?(Hash) ? arg.to_a.flatten(1) : [arg, []]
13
+
14
+ (@fact_checker_facts ||= []) << name.to_sym
15
+
16
+ define_method(name) do
17
+ dependencies_satisfied = [*dependencies].all?{ |dep_name| send(dep_name).valid? }
18
+ Result.new(dependencies_satisfied, instance_eval(&block))
19
+ end
20
+
21
+ define_method("#{name}?") do
22
+ send(name).valid?
23
+ end
24
+
25
+ private name, "#{name}?" if name[0] == '_'
26
+ end
27
+
28
+ def facts
29
+ ancestors.reverse.map{ |klass| klass.instance_eval{ @fact_checker_facts } }.compact.flatten(1)
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,18 @@
1
+ # encoding: utf-8
2
+
3
+ module FactChecker
4
+ class Result
5
+ def initialize(dependency_state, requirement_state)
6
+ @dependency_state = dependency_state
7
+ @requirement_state = requirement_state
8
+ end
9
+
10
+ def valid?
11
+ @dependency_state && @requirement_state
12
+ end
13
+
14
+ def available?
15
+ @dependency_state
16
+ end
17
+ end
18
+ end
@@ -1,5 +1,5 @@
1
1
  # encoding: utf-8
2
2
 
3
3
  module FactChecker
4
- VERSION = '0.0.3'
4
+ VERSION = '0.1.0'
5
5
  end
data/lib/fact_checker.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # encoding: utf-8
2
2
 
3
- require 'fact_checker/base'
4
3
  require 'fact_checker/version'
5
- require 'fact_checker/boot'
4
+ require 'fact_checker/result'
5
+ require 'fact_checker/mixin'
@@ -0,0 +1,238 @@
1
+ # encoding: utf-8
2
+
3
+ require 'spec_helper'
4
+
5
+ describe FactChecker do
6
+ context "when included into a class" do
7
+
8
+ let(:klass) { Class.new { include FactChecker } }
9
+
10
+ describe ".facts" do
11
+ it "exists" do
12
+ expect(klass.superclass.methods).to_not include(:facts)
13
+ expect(klass.methods).to include(:facts)
14
+ end
15
+
16
+ it "returns [] when no facts were defined" do
17
+ expect(klass.facts).to eq([])
18
+ end
19
+
20
+ it "returns list of all defined facts names" do
21
+ klass.class_eval do
22
+ define_fact(:x) {}
23
+ define_fact(:y => [:x, :x1]) {}
24
+ end
25
+
26
+ expect(klass.facts).to eq([:x, :y])
27
+ end
28
+
29
+ it "returns independent results for independent classes" do
30
+ klass1 = Class.new { include FactChecker; define_fact(:x) {} }
31
+ klass2 = Class.new { include FactChecker; define_fact(:y) {} }
32
+ expect(klass1.facts).to eq([:x])
33
+ expect(klass2.facts).to eq([:y])
34
+ end
35
+
36
+ it "returns one-way dependent results for superclass and subclasses" do
37
+ klass1 = Class.new { include FactChecker; define_fact(:x) {} }
38
+ klass2 = Class.new(klass1) { define_fact(:y) {} }
39
+ klass3 = Class.new(klass2) { define_fact(:z) {} }
40
+ klass1.class_eval { define_fact(:q) {} }
41
+
42
+ expect(klass1.facts).to eq([:x, :q])
43
+ expect(klass2.facts).to eq([:x, :q, :y])
44
+ expect(klass3.facts).to eq([:x, :q, :y, :z])
45
+ end
46
+
47
+ it "returns symbols even when facts were defined with string names" do
48
+ klass.define_fact('x') {}
49
+ expect(klass.facts).to eq([:x])
50
+ end
51
+ end
52
+
53
+
54
+ describe ".define_fact" do
55
+ it "exists" do
56
+ expect(klass.superclass.methods).to_not include(:define_fact)
57
+ expect(klass.methods).to include(:define_fact)
58
+ end
59
+
60
+ context "when called with no arguments" do
61
+ it "raises an ArgumentError" do
62
+ expect{klass.define_fact}.to raise_error(ArgumentError)
63
+ end
64
+ end
65
+
66
+ context "when called with (:name)" do
67
+ it "raises an ArgumentError" do
68
+ expect{klass.define_fact(:x)}.to raise_error(ArgumentError)
69
+ end
70
+ end
71
+
72
+ context "when called with (:name, &block)" do
73
+ it "behaves as if it was called with (:name => [], &block)" do
74
+ klass.define_fact(:x) { :result }
75
+ expect(klass.instance_methods).to include(:x)
76
+ expect(klass.instance_methods).to include(:x?)
77
+ expect(klass.new.x.valid?).to be(:result)
78
+ expect(klass.new.x.available?).to be(true)
79
+ expect(klass.new.x?).to be(:result)
80
+ end
81
+ end
82
+
83
+ context "when called with (:name => :dependency_name, &block)" do
84
+ it "behaves as if it was called with (:name => [:dependency_name], &block)" do
85
+ klass.define_fact(:y) { false }
86
+ klass.define_fact(:x => :y) { :result }
87
+ expect(klass.instance_methods).to include(:x)
88
+ expect(klass.instance_methods).to include(:x?)
89
+ expect(klass.new.x.valid?).to be(false)
90
+ expect(klass.new.x.available?).to be(false)
91
+ expect(klass.new.x?).to be(false)
92
+
93
+ klass.define_fact(:y) { true }
94
+ klass.define_fact(:x => :y) { :result }
95
+ expect(klass.new.x.valid?).to be(:result)
96
+ expect(klass.new.x.available?).to be(true)
97
+ expect(klass.new.x?).to be(:result)
98
+ end
99
+ end
100
+
101
+ context "when called with (:name => dependency_names, &block)" do
102
+ it "defines an instance method #name" do
103
+ expect(klass.instance_methods).to_not include(:x)
104
+ klass.define_fact(:x => []) {}
105
+ expect(klass.instance_methods).to include(:x)
106
+ end
107
+
108
+ it "defines an instance method #name?" do
109
+ expect(klass.instance_methods).to_not include(:x?)
110
+ klass.define_fact(:x => []) {}
111
+ expect(klass.instance_methods).to include(:x?)
112
+ end
113
+
114
+ context "and the newly defined instance methods:" do
115
+ let(:instance) { klass.new }
116
+
117
+ describe "#<name>" do
118
+ it "returns an object of FactChecker::Result" do
119
+ klass.define_fact(:x => []) {}
120
+ expect(instance.x).to be_a_kind_of(FactChecker::Result)
121
+ end
122
+
123
+ it "evaluates block in the context of the current instance" do
124
+ klass.define_fact(:x => []) { check? }
125
+ klass.class_eval { def check?; end }
126
+ expect(instance).to receive(:check?).and_call_original
127
+ instance.x
128
+ end
129
+
130
+ context "when the :name fact has blank dependencies" do
131
+ before(:each) { klass.define_fact(:x => []) {:result} }
132
+
133
+ it "returns result with .valid? == block.call" do
134
+ expect(instance.x.valid?).to be(:result)
135
+ end
136
+
137
+ it "returns result with .available? == true" do
138
+ expect(instance.x.available?).to be_true
139
+ end
140
+ end
141
+
142
+ context "when the :name fact has valid dependencies" do
143
+ before(:each) do
144
+ klass.class_eval do
145
+ define_fact(:x) { true }
146
+ define_fact(:y) { :truthy }
147
+ define_fact(:z => [:x, :y]) { :result }
148
+ end
149
+ end
150
+
151
+ it "returns result with .valid? == block.call" do
152
+ expect(instance.z.valid?).to be(:result)
153
+ end
154
+
155
+ it "returns result with .available? == true" do
156
+ expect(instance.z.available?).to be_true
157
+ end
158
+ end
159
+
160
+ context "when the :name fact has at least 1 invalid dependency" do
161
+ before(:each) do
162
+ klass.class_eval do
163
+ define_fact(:x) { true }
164
+ define_fact(:y) { false }
165
+ define_fact(:z => [:x, :y]) { :something }
166
+ end
167
+ end
168
+
169
+ it "returns result with .valid? == false" do
170
+ expect(instance.z.valid?).to be(false)
171
+ end
172
+
173
+ it "returns result with .available? == false" do
174
+ expect(instance.z.available?).to be(false)
175
+ end
176
+ end
177
+
178
+ context "when the :name fact has dependencies inherited from a superclass" do
179
+ it "return correct results" do
180
+ klass1 = Class.new(klass)
181
+ klass.define_fact(:x) { true }
182
+ klass.define_fact(:y) { false }
183
+ klass1.define_fact(:z1 => [:x, :y]) { true }
184
+ klass1.define_fact(:z2 => [:x]) { true }
185
+ klass1.define_fact(:z3 => [:x]) { false }
186
+ instance = klass1.new
187
+
188
+ expect(instance.z1.valid?).to be(false)
189
+ expect(instance.z1.available?).to be(false)
190
+ expect(instance.z2.valid?).to be(true)
191
+ expect(instance.z3.available?).to be(true)
192
+ expect(instance.z3.valid?).to be(false)
193
+ expect(instance.z2.available?).to be(true)
194
+ end
195
+ end
196
+ end
197
+
198
+ describe "#<name>?" do
199
+ it "delegates to name.valid?" do
200
+ klass.define_fact(:x) {}
201
+ result = double(:valid? => :yeah!)
202
+ expect(instance).to receive(:x).and_return(result)
203
+ expect(instance.x?).to be(:yeah!)
204
+ end
205
+ end
206
+ end
207
+ end
208
+
209
+ context "when called with ('name' => string_deppendency_names, &block)" do
210
+ it "defines an instance method #name" do
211
+ expect(klass.instance_methods).to_not include(:x)
212
+ klass.define_fact('x' => []) {}
213
+ expect(klass.instance_methods).to include(:x)
214
+ end
215
+
216
+ it "defines an instance method #name?" do
217
+ expect(klass.instance_methods).to_not include(:x?)
218
+ klass.define_fact('x' => []) {}
219
+ expect(klass.instance_methods).to include(:x?)
220
+ end
221
+ end
222
+
223
+ context "when called with (:_underscored_name => dependency_names, &block)" do
224
+ it "marks new #_underscored_name method as private" do
225
+ klass.define_fact(:_x) {}
226
+ expect{ klass.new._x }.to raise_error
227
+ end
228
+
229
+ it "marks new #_undersrored_name? method as private" do
230
+ klass.define_fact(:_x) {}
231
+ expect{ klass.new._x? }.to raise_error
232
+ end
233
+ end
234
+ end
235
+
236
+ end
237
+ end
238
+