plyushkin 0.0.5 → 0.0.6
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +1 -0
- data/.travis.yml +3 -0
- data/Gemfile.lock +1 -1
- data/README.md +208 -9
- data/Rakefile +5 -0
- data/lib/plyushkin/base_value.rb +2 -2
- data/lib/plyushkin/cache.rb +1 -0
- data/lib/plyushkin/cache/null.rb +10 -0
- data/lib/plyushkin/property.rb +5 -3
- data/lib/plyushkin/test/value_types.rb +3 -0
- data/lib/plyushkin/version.rb +1 -1
- data/spec/lib/base_value_spec.rb +18 -2
- data/spec/lib/persistence_spec.rb +7 -0
- data/spec/lib/property_spec.rb +40 -7
- metadata +4 -2
data/.gitignore
CHANGED
data/.travis.yml
ADDED
data/Gemfile.lock
CHANGED
data/README.md
CHANGED
@@ -1,6 +1,9 @@
|
|
1
1
|
# Plyushkin
|
2
2
|
|
3
|
-
|
3
|
+
[![Code Climate](https://codeclimate.com/github/OnlifeHealth/plyushkin.png)](https://codeclimate.com/github/OnlifeHealth/plyushkin) [![Gem Version](https://badge.fury.io/rb/plyushkin.svg)](http://badge.fury.io/rb/plyushkin)
|
4
|
+
[![Build Status](https://travis-ci.org/OnlifeHealth/plyushkin.png)](https://travis-ci.org/OnlifeHealth/plyushkin)
|
5
|
+
|
6
|
+
Plyushkin provides a way to capture historical data in an ActiveRecord class.
|
4
7
|
|
5
8
|
## Installation
|
6
9
|
|
@@ -18,7 +21,7 @@ Or install it yourself as:
|
|
18
21
|
|
19
22
|
## Usage
|
20
23
|
|
21
|
-
Configure the backend service that plyushkin will use in your environments
|
24
|
+
Configure the backend service that plyushkin will use in your environments/<environment>.rb file.
|
22
25
|
For example, to configure the stub service for running specs, the following code would go in your
|
23
26
|
config/environments/test.rb file of a Rails application.
|
24
27
|
|
@@ -27,20 +30,216 @@ config/environments/test.rb file of a Rails application.
|
|
27
30
|
end
|
28
31
|
|
29
32
|
To use plyushkin against a live web service,
|
30
|
-
|
33
|
+
|
34
|
+
config.before_initialize do |c|
|
35
|
+
Plyushkin::Service.service =
|
36
|
+
Plyushkin::Service::Web.new(:url => 'http://yourservice.com')
|
37
|
+
end
|
38
|
+
|
39
|
+
## Quick start
|
40
|
+
To add a property, use the `hoards` class macro on a class that inherits from ActiveRecord::Base.
|
41
|
+
|
42
|
+
class Vehicle < ActiveRecord::Base
|
43
|
+
hoards :mechanic
|
44
|
+
end
|
45
|
+
|
46
|
+
This will add a mechanic attribute to the Vehicle class. To set the mechanic for the vehicle
|
47
|
+
|
48
|
+
v = Vehicle.new
|
49
|
+
v.mechanic.create(:value => 'Mike')
|
50
|
+
|
51
|
+
This will update the in-memory version of the vehicle, but has not yet persisted the change. To
|
52
|
+
save the change to the plyushkin-service
|
53
|
+
|
54
|
+
v.save
|
55
|
+
|
56
|
+
To get the value of the vehicle's mechanic
|
57
|
+
|
58
|
+
v.mechanic.last.value # => 'Mike'
|
59
|
+
|
60
|
+
To get the date that the mechanic was last set
|
61
|
+
|
62
|
+
v.mechanic.last.date # => Wed, 09 Jul 2014 11:52:12 -0500
|
63
|
+
|
64
|
+
Every Plyushkin value has a `date` attribute.
|
65
|
+
|
66
|
+
Once we change the mechanic on the vehicle, we will now have historical data and can view all the
|
67
|
+
mechanics that worked on the vehicle.
|
68
|
+
|
69
|
+
v.mechanic.create(:value => 'Joe')
|
70
|
+
v.mechanic.all.map(&:value) # => ['Mike', 'Joe']
|
71
|
+
|
72
|
+
#### Setting past values
|
73
|
+
|
74
|
+
To store a property using a DateTime other than the current time
|
75
|
+
|
76
|
+
v.mechanic.create(:value => 'Sally', :date => 3.days.ago)
|
77
|
+
v.mechanic.last.date # => Sun, 06 Jul 2014 11:52:12 -0500
|
78
|
+
|
79
|
+
#### Value types
|
80
|
+
|
81
|
+
In the example above, we needed to use `last` and `all` methods to see the mechanic. This is
|
82
|
+
because the property getter returns a `Plyushkin::Property`. A property consists of
|
83
|
+
instances of classes that derive from `Plyushkin::BaseValue`.
|
84
|
+
|
85
|
+
`Plyushkin::BaseValue` provides basic value functionality. It provides a `date` attribute, that is
|
86
|
+
required for all Plyushkin values. And, it provides four formatters, `to_i`, `to_f`,
|
87
|
+
`to_date` and `to_bool`.
|
88
|
+
|
89
|
+
When not specifying a value type when calling `hoards`, Plyushkin will use a
|
90
|
+
`Plyushkin::StringValue` as the value type. In addition to the base implementation, this basic
|
91
|
+
implementation includes one attribute, `value`, which uses no formatter.
|
92
|
+
|
93
|
+
In most applications, you will not want to use `Plyushkin::StringValue`, and instead would want
|
94
|
+
to create your own value type implementation that derives from `Plyushkin::BaseValue` and use custom
|
95
|
+
value types.
|
96
|
+
|
97
|
+
##### Creating a custom value type
|
98
|
+
|
99
|
+
In our vehicle example, if we wanted to capture oil change history, we would start by creating a
|
100
|
+
custom value type.
|
101
|
+
|
102
|
+
class OilChangeValue < Plyushkin::BaseValue
|
103
|
+
persisted_attr :mileage, :formatter => :to_i
|
104
|
+
persisted_attr :oil_type
|
105
|
+
end
|
106
|
+
|
107
|
+
The `OilChangeValue` will have a mileage attribute that uses the `to_i` formatter. Formatters attempt to
|
108
|
+
convert the attribute assignment to a specified format. If no formatter is provided, the attribute will be
|
109
|
+
stored as a string.
|
110
|
+
|
111
|
+
We can now add this to our vehicle
|
112
|
+
|
113
|
+
class Vehicle < ActiveRecord::Base
|
114
|
+
hoards :mechanic
|
115
|
+
hoards :oil_change, :type => OilChangeValue
|
116
|
+
end
|
117
|
+
|
118
|
+
To set the oil_change
|
119
|
+
|
120
|
+
v.oil_change.create(:mileage => '1234', :oil_type => '10W30')
|
121
|
+
|
122
|
+
To get the latest oil change details
|
123
|
+
|
124
|
+
v.oil_change.last.mileage # => 1234
|
125
|
+
v.oil_change.last.oil_type # => '10W30'
|
126
|
+
|
127
|
+
##### Specifiying a callback after a value is stored
|
128
|
+
|
129
|
+
When defining a hoards property, you can set a callback for after the value is persisted. For example,
|
130
|
+
if the vehicle table contains a `next_oil_change_mileage` column, we might want to update it whenever
|
131
|
+
an oil_change is saved.
|
132
|
+
|
133
|
+
class Vehicle < ActiveRecord::Base
|
134
|
+
hoards :oil_change, :type => OilChangeValue,
|
135
|
+
:after_create => :calculate_next_oil_change_mileage
|
136
|
+
|
137
|
+
def calculate_next_oil_change_mileage
|
138
|
+
next_oil_change_mileage = oil_change.last.mileage + 3000
|
139
|
+
end
|
140
|
+
end
|
141
|
+
|
142
|
+
##### Ignoring unchanged values
|
143
|
+
|
144
|
+
There may be a case where we don't need to track history when a value is set, but is the same as the
|
145
|
+
previous value. For example, if the mechanic for your last maintenance is Mike and Mike again
|
146
|
+
performs the maintenance, we don't need two data points recorded.
|
147
|
+
|
148
|
+
In the current configuration, calling create twice would create two values
|
149
|
+
|
150
|
+
v.mechanic.create(:value => 'Joe')
|
151
|
+
v.mechanic.create(:value => 'Mike')
|
152
|
+
v.mechanic.create(:value => 'Mike')
|
153
|
+
v.mechanic.all.map(&:value) # => [ 'Joe', 'Mike', 'Mike' ]
|
154
|
+
|
155
|
+
By setting the `:ignore_unchanged_values` option, we can change this behavior.
|
156
|
+
|
157
|
+
class Vehicle < ActiveRecord::Base
|
158
|
+
hoards :mechanic, :ignore_unchanged_values => true
|
159
|
+
end
|
160
|
+
|
161
|
+
v.mechanic.create(:value => 'Joe')
|
162
|
+
v.mechanic.create(:value => 'Mike')
|
163
|
+
v.mechanic.create(:value => 'Mike')
|
164
|
+
v.mechanic.all.map(&:value) # => [ 'Joe', 'Mike' ]
|
165
|
+
|
166
|
+
In this example, the date of the last value will be the date that `mechanic` was first assigned 'Mike'.
|
167
|
+
|
168
|
+
###### Validation
|
169
|
+
|
170
|
+
Validation is done with `ActiveModel::Validations`. This is included in `Plyushkin::BaseValue`.
|
171
|
+
|
172
|
+
class OilChangeValue < Plyushkin::BaseValue
|
173
|
+
persisted_attr :oil_type
|
174
|
+
persisted_attr :mileage
|
175
|
+
|
176
|
+
validates :oil_type, :inclusion => { :in => ['10W30', '5W40'] }
|
177
|
+
validates :mileage, :numericality => { :only_integer => true,
|
178
|
+
:greater_than_or_equal_to => 0 }
|
179
|
+
end
|
180
|
+
|
181
|
+
###### Adding behavior
|
182
|
+
|
183
|
+
`Plyushkin::BaseValue` and it's subclasses are classes and additional behavior can be added to
|
184
|
+
encapsulate functionality.
|
185
|
+
|
186
|
+
class OilChange < Plyushkin::BaseValue
|
187
|
+
persisted_attr :mileage
|
188
|
+
|
189
|
+
def mileage_as_km
|
190
|
+
mileage / 0.6214
|
191
|
+
end
|
192
|
+
end
|
193
|
+
|
194
|
+
v.oil_change.create(:mileage => 10000)
|
195
|
+
v.oil_change.last.mileage # => 10000
|
196
|
+
v.oil_change.last.mileage_as_km # => 6214
|
197
|
+
|
198
|
+
##### Accessing a property that has not had any values assigned
|
199
|
+
|
200
|
+
`Plyushkin::NilValue` is a special case used when no value has been assigned to a property yet.
|
201
|
+
|
202
|
+
v = Vehicle.new
|
203
|
+
v.mechanic.last # => Plyushkin::NilValue
|
204
|
+
v.mechanic.last.value # => nil
|
205
|
+
v.oil_change.last # => Plyushkin::NilValue
|
206
|
+
v.oil_change.last.mileage # => nil
|
207
|
+
v.oil_change.last.oil_type # => nil
|
208
|
+
|
209
|
+
This is necessary so that consumers of the property do not need to check if a value is nil before trying
|
210
|
+
to access an attribute of the value.
|
211
|
+
|
212
|
+
In addition, `Plyushkin::Property` has a property `empty?` to indicate whether any values have been assigned
|
213
|
+
|
214
|
+
v.mechanic.empty? # => true
|
215
|
+
v.oil_change.empty? # => true
|
31
216
|
|
32
217
|
## Testing
|
33
218
|
|
34
219
|
Plyushkin provides RSpec matchers for testing class macros. To use these matchers,
|
35
|
-
add
|
220
|
+
add `config.include Plyushkin::Test::Matchers` to your RSpec.configure in spec_helper.
|
221
|
+
|
222
|
+
#### Testing custom value types
|
223
|
+
To test Plyushkin configuration in your custom value type:
|
224
|
+
|
225
|
+
describe OilChangeValue do
|
226
|
+
it { should persist_attribute(:mileage) }
|
227
|
+
it { should persist_attribute(:mileage).with_format(:to_i) }
|
228
|
+
it { should_not persist_attribute(:air_filter) }
|
229
|
+
it { should persist_attribute(:oil_type) }
|
230
|
+
it { should_not persist_attribute(:oil_type).with_format(:to_f) }
|
231
|
+
|
232
|
+
# RSpec Shoulda matchers also work to test validations here.
|
233
|
+
end
|
36
234
|
|
37
235
|
To test Plyushkin configuration in your model:
|
38
236
|
|
39
|
-
describe
|
40
|
-
it { should
|
41
|
-
it {
|
42
|
-
it { should
|
43
|
-
it {
|
237
|
+
describe Vehicle do
|
238
|
+
it { should hoard(:mechanic) }
|
239
|
+
it { should hoard(:mechanic).and_ignore_unchanged_values }
|
240
|
+
it { should hoard(:oil_change).of_type(OilChangeValue) }
|
241
|
+
it { should hoard(:oil_change).of_type(OilChangeValue).
|
242
|
+
and_after_create_call(:calculate_next_oil_change_mileage) }
|
44
243
|
end
|
45
244
|
|
46
245
|
## Contributing
|
data/Rakefile
CHANGED
data/lib/plyushkin/base_value.rb
CHANGED
@@ -93,8 +93,8 @@ class Plyushkin::BaseValue
|
|
93
93
|
end
|
94
94
|
|
95
95
|
def to_bool(value)
|
96
|
-
return true if
|
97
|
-
return false if
|
96
|
+
return true if [1, "1", "true"].include?(value)
|
97
|
+
return false if [0, "0", "false"].include?(value)
|
98
98
|
return value
|
99
99
|
end
|
100
100
|
|
data/lib/plyushkin/cache.rb
CHANGED
data/lib/plyushkin/property.rb
CHANGED
@@ -73,14 +73,16 @@ class Plyushkin::Property
|
|
73
73
|
end
|
74
74
|
|
75
75
|
def value_hashes
|
76
|
-
|
76
|
+
all_value_hashes = []
|
77
77
|
all.each do |value|
|
78
|
+
value_hash = {}
|
78
79
|
value_type.persisted_attributes.each do |attr|
|
79
|
-
|
80
|
+
value_hash[attr] = value.send(attr)
|
80
81
|
end
|
82
|
+
all_value_hashes << value_hash
|
81
83
|
end
|
82
84
|
|
83
|
-
|
85
|
+
all_value_hashes
|
84
86
|
end
|
85
87
|
|
86
88
|
def nil?
|
data/lib/plyushkin/version.rb
CHANGED
data/spec/lib/base_value_spec.rb
CHANGED
@@ -172,11 +172,27 @@ describe Plyushkin::BaseValue do
|
|
172
172
|
|
173
173
|
describe '#to_bool' do
|
174
174
|
it 'should return true if string true is passed in' do
|
175
|
-
Plyushkin::BaseValue.new.to_bool("true").should
|
175
|
+
Plyushkin::BaseValue.new.to_bool("true").should == true
|
176
|
+
end
|
177
|
+
|
178
|
+
it 'should return true if "1" is passed in' do
|
179
|
+
Plyushkin::BaseValue.new.to_bool("1").should == true
|
180
|
+
end
|
181
|
+
|
182
|
+
it 'should return false if "0" is passed in' do
|
183
|
+
Plyushkin::BaseValue.new.to_bool("0").should == false
|
184
|
+
end
|
185
|
+
|
186
|
+
it 'should return true if 1 is passed in' do
|
187
|
+
Plyushkin::BaseValue.new.to_bool(1).should == true
|
188
|
+
end
|
189
|
+
|
190
|
+
it 'should return false if 0 is passed in' do
|
191
|
+
Plyushkin::BaseValue.new.to_bool(0).should == false
|
176
192
|
end
|
177
193
|
|
178
194
|
it 'should return false if string false is passed in' do
|
179
|
-
Plyushkin::BaseValue.new.to_bool("false").should
|
195
|
+
Plyushkin::BaseValue.new.to_bool("false").should == false
|
180
196
|
end
|
181
197
|
|
182
198
|
it 'should return nil if nil is passed in' do
|
@@ -43,6 +43,13 @@ describe Plyushkin::Persistence do
|
|
43
43
|
service.get("widget", 1)["name"].last["value"].should == "Mike"
|
44
44
|
end
|
45
45
|
|
46
|
+
it 'should save multiple new values of the same property' do
|
47
|
+
persistence.properties[:name].create(:value => 'Bob')
|
48
|
+
persistence.save(1)
|
49
|
+
persistence.load(1)
|
50
|
+
persistence.properties[:name].all.length.should == 2
|
51
|
+
end
|
52
|
+
|
46
53
|
it 'should mark all properties as persisted' do
|
47
54
|
persistence.properties[:name].create(:value => "Mike")
|
48
55
|
persistence.properties[:weight].create(:value => "150")
|
data/spec/lib/property_spec.rb
CHANGED
@@ -227,13 +227,46 @@ describe Plyushkin::Property do
|
|
227
227
|
end
|
228
228
|
end
|
229
229
|
|
230
|
-
|
231
|
-
|
232
|
-
|
233
|
-
|
234
|
-
|
235
|
-
|
236
|
-
|
230
|
+
describe '#insert_position' do
|
231
|
+
it 'should get position in date order' do
|
232
|
+
property = Plyushkin::Property.new(:property_name)
|
233
|
+
value1 = property.create(:value => 7, :date => DateTime.now - 7.days)
|
234
|
+
value2 = property.create(:value => 5, :date => DateTime.now - 5.days)
|
235
|
+
property.insert_position(DateTime.now).should == 2
|
236
|
+
property.insert_position(DateTime.now - 6.days).should == 1
|
237
|
+
property.insert_position(DateTime.now - 8.days).should == 0
|
238
|
+
end
|
239
|
+
end
|
240
|
+
|
241
|
+
describe '#value_hashes' do
|
242
|
+
let(:property) { Plyushkin::Property.new(:property_name) }
|
243
|
+
|
244
|
+
it 'should include all values as hashes in an array' do
|
245
|
+
|
246
|
+
p1 = property.create(:value => 1)
|
247
|
+
p2 = property.create(:value => 2)
|
248
|
+
property.value_hashes.should == [
|
249
|
+
{ :date => p1.date, :value => 1 },
|
250
|
+
{ :date => p2.date, :value => 2 }
|
251
|
+
]
|
252
|
+
end
|
253
|
+
|
254
|
+
it 'should include all attributes in value hash' do
|
255
|
+
property = Plyushkin::Property.new(:property_name, :type => Plyushkin::Test::CoordinateValue)
|
256
|
+
|
257
|
+
c1 = property.create(:x => 5, :y => 10)
|
258
|
+
c2 = property.create(:x => 15, :y => 20)
|
259
|
+
property.value_hashes.should == [
|
260
|
+
{ :date => c1.date, :x => 5, :y => 10 },
|
261
|
+
{ :date => c2.date, :x => 15, :y => 20 }
|
262
|
+
]
|
263
|
+
|
264
|
+
end
|
265
|
+
|
266
|
+
it 'should be an empty array if no values are set' do
|
267
|
+
property.value_hashes.should == []
|
268
|
+
end
|
269
|
+
|
237
270
|
end
|
238
271
|
|
239
272
|
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: plyushkin
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.6
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -10,7 +10,7 @@ authors:
|
|
10
10
|
autorequire:
|
11
11
|
bindir: bin
|
12
12
|
cert_chain: []
|
13
|
-
date: 2014-
|
13
|
+
date: 2014-07-28 00:00:00.000000000 Z
|
14
14
|
dependencies:
|
15
15
|
- !ruby/object:Gem::Dependency
|
16
16
|
name: activerecord
|
@@ -70,6 +70,7 @@ extra_rdoc_files: []
|
|
70
70
|
files:
|
71
71
|
- .gitignore
|
72
72
|
- .rspec
|
73
|
+
- .travis.yml
|
73
74
|
- Gemfile
|
74
75
|
- Gemfile.lock
|
75
76
|
- LICENSE.txt
|
@@ -78,6 +79,7 @@ files:
|
|
78
79
|
- lib/plyushkin.rb
|
79
80
|
- lib/plyushkin/base_value.rb
|
80
81
|
- lib/plyushkin/cache.rb
|
82
|
+
- lib/plyushkin/cache/null.rb
|
81
83
|
- lib/plyushkin/cache/rails_cache.rb
|
82
84
|
- lib/plyushkin/cache/stub.rb
|
83
85
|
- lib/plyushkin/core_ext/active_record_base.rb
|