plyushkin 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1,3 @@
1
+ .bundle
2
+ .DS_Store
3
+ *.swp
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --color
2
+ --format progress
data/Gemfile ADDED
@@ -0,0 +1,11 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in plyushkin.gemspec
4
+ gemspec
5
+
6
+ group :test do
7
+ gem 'rspec'
8
+ gem 'timecop'
9
+ gem 'pry-nav'
10
+ gem 'sqlite3'
11
+ end
data/Gemfile.lock ADDED
@@ -0,0 +1,58 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ plyushkin (0.0.1)
5
+ activerecord (~> 3.2.12)
6
+
7
+ GEM
8
+ remote: https://rubygems.org/
9
+ specs:
10
+ activemodel (3.2.16)
11
+ activesupport (= 3.2.16)
12
+ builder (~> 3.0.0)
13
+ activerecord (3.2.16)
14
+ activemodel (= 3.2.16)
15
+ activesupport (= 3.2.16)
16
+ arel (~> 3.0.2)
17
+ tzinfo (~> 0.3.29)
18
+ activesupport (3.2.16)
19
+ i18n (~> 0.6, >= 0.6.4)
20
+ multi_json (~> 1.0)
21
+ arel (3.0.3)
22
+ builder (3.0.4)
23
+ coderay (1.1.0)
24
+ diff-lcs (1.2.5)
25
+ i18n (0.6.9)
26
+ method_source (0.8.2)
27
+ multi_json (1.8.4)
28
+ pry (0.9.12.4)
29
+ coderay (~> 1.0)
30
+ method_source (~> 0.8)
31
+ slop (~> 3.4)
32
+ pry-nav (0.2.3)
33
+ pry (~> 0.9.10)
34
+ rake (10.1.1)
35
+ rspec (2.14.1)
36
+ rspec-core (~> 2.14.0)
37
+ rspec-expectations (~> 2.14.0)
38
+ rspec-mocks (~> 2.14.0)
39
+ rspec-core (2.14.7)
40
+ rspec-expectations (2.14.4)
41
+ diff-lcs (>= 1.1.3, < 2.0)
42
+ rspec-mocks (2.14.4)
43
+ slop (3.4.7)
44
+ sqlite3 (1.3.8)
45
+ timecop (0.7.1)
46
+ tzinfo (0.3.38)
47
+
48
+ PLATFORMS
49
+ ruby
50
+
51
+ DEPENDENCIES
52
+ bundler (~> 1.3)
53
+ plyushkin!
54
+ pry-nav
55
+ rake
56
+ rspec
57
+ sqlite3
58
+ timecop
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2014 Craig Israel
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,29 @@
1
+ # Plyushkin
2
+
3
+ TODO: Write a gem description
4
+
5
+ ## Installation
6
+
7
+ Add this line to your application's Gemfile:
8
+
9
+ gem 'plyushkin'
10
+
11
+ And then execute:
12
+
13
+ $ bundle
14
+
15
+ Or install it yourself as:
16
+
17
+ $ gem install plyushkin
18
+
19
+ ## Usage
20
+
21
+ TODO: Write usage instructions here
22
+
23
+ ## Contributing
24
+
25
+ 1. Fork it
26
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
27
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
28
+ 4. Push to the branch (`git push origin my-new-feature`)
29
+ 5. Create new Pull Request
data/Rakefile ADDED
@@ -0,0 +1 @@
1
+ require "bundler/gem_tasks"
data/lib/plyushkin.rb ADDED
@@ -0,0 +1,15 @@
1
+ require "plyushkin/version"
2
+ require "active_record"
3
+
4
+ module Plyushkin; end
5
+ path = File.dirname(File.expand_path(__FILE__))
6
+ require path + "/plyushkin/base_value"
7
+ require path + "/plyushkin/string_value"
8
+ require path + "/plyushkin/nil_value"
9
+ require path + "/plyushkin/property"
10
+ require path + "/plyushkin/model"
11
+ require path + "/plyushkin/service"
12
+ require path + "/plyushkin/persistence"
13
+ require path + "/plyushkin/validators/presence"
14
+ require path + "/plyushkin/core_ext/plyushkin_extensions"
15
+ require path + "/plyushkin/core_ext/active_record_base"
@@ -0,0 +1,59 @@
1
+ class Plyushkin::BaseValue
2
+ include ActiveModel::Validations
3
+
4
+ def initialize(attr={})
5
+ attr = attr.dup
6
+ @date = attr.delete(:date) || DateTime.current
7
+ attr.each do |k,v|
8
+ send("#{k}=", v) if respond_to?("#{k}=")
9
+ end
10
+ end
11
+
12
+ def self.persisted_attr(*attributes)
13
+ opts = attributes.last.is_a?(Hash) ? attributes.pop : {}
14
+ names = attributes
15
+ formatter = opts[:formatter]
16
+
17
+ persisted_attributes.concat(names)
18
+ attr_writer *names
19
+ names.each do |name|
20
+ define_method(name) do
21
+ value = instance_variable_get("@#{name}")
22
+ if formatter
23
+ send(formatter, value)
24
+ else
25
+ value
26
+ end
27
+ end
28
+ end
29
+ end
30
+
31
+ def self.persisted_attributes
32
+ if self.superclass == Object
33
+ @persisted_attributes ||= []
34
+ else
35
+ @persisted_attributes ||= superclass.persisted_attributes.dup
36
+ end
37
+ end
38
+
39
+ persisted_attr :date
40
+ validates_each :date do |record, attr_name, value|
41
+ record.errors.add(attr_name, "cannot be in the future") unless value < DateTime.now
42
+ end
43
+
44
+ def equal_value?(other)
45
+ self.class.persisted_attributes.each do |attribute|
46
+ return false if attribute != :date && send(attribute) != other.send(attribute)
47
+ end
48
+ true
49
+ end
50
+
51
+ def to_i(value)
52
+ value.to_i
53
+ end
54
+
55
+ def to_date(value)
56
+ value.is_a?(String) ? DateTime.parse(value) : value
57
+ end
58
+
59
+ end
@@ -0,0 +1,27 @@
1
+ ActiveRecord::Base.instance_eval do
2
+ def hoards(name, opts = {})
3
+ initialize_plyushkin
4
+
5
+ define_method(name) do
6
+ plyushkin.properties[name]
7
+ end
8
+ validates name, :plyushkin => true
9
+
10
+ plyushkin_model.register(name, opts[:type], opts)
11
+ plyushkin_model.register_callback(name, :after_create, opts[:after_create]) if opts[:after_create]
12
+ end
13
+
14
+ def initialize_plyushkin
15
+ class << self
16
+ def plyushkin_model
17
+ @plyushkin_model ||= Plyushkin::Model.new(Plyushkin::Service::Stub.new)
18
+ end
19
+ end
20
+
21
+ include PlyushkinExtensions
22
+
23
+ after_initialize :load_plyushkin
24
+ after_save :save_plyushkin
25
+ end
26
+ end
27
+
@@ -0,0 +1,26 @@
1
+ module PlyushkinExtensions
2
+ def reload(options=nil)
3
+ plyushkin.load(id)
4
+ super
5
+ end
6
+
7
+ def plyushkin
8
+ @plyushkin ||= Plyushkin::Persistence.new(self.class.plyushkin_model)
9
+ end
10
+
11
+ def load_plyushkin
12
+ self.class.plyushkin_model.callbacks.each do |name,callbacks|
13
+ callbacks.each do |callback, handler|
14
+ plyushkin.register_callback(name, callback) do
15
+ (handler && handler.is_a?(Symbol)) ? send(handler) : handler.call
16
+ end
17
+ end
18
+ end
19
+
20
+ plyushkin.load(id)
21
+ end
22
+
23
+ def save_plyushkin
24
+ plyushkin.save(id)
25
+ end
26
+ end
@@ -0,0 +1,35 @@
1
+ class Plyushkin::Model
2
+
3
+ def initialize(service)
4
+ @service = service
5
+ @types = {}
6
+ @ignore_unchanged_values = {}
7
+ @callbacks = {}
8
+ end
9
+
10
+ def register(name, type, opts={})
11
+ @types[name] = type
12
+ @ignore_unchanged_values[name] = opts[:ignore_unchanged_values]
13
+ end
14
+
15
+ def register_callback(name, callback, method_sym)
16
+ @callbacks[name] = { callback => method_sym }
17
+ end
18
+
19
+ def registered_types
20
+ @types.dup
21
+ end
22
+
23
+ def callbacks
24
+ @callbacks.dup
25
+ end
26
+
27
+ def ignore_unchanged_values
28
+ @ignore_unchanged_values.dup
29
+ end
30
+
31
+ def service
32
+ @service
33
+ end
34
+
35
+ end
@@ -0,0 +1,9 @@
1
+ class Plyushkin::NilValue
2
+ def method_missing(sym, *args)
3
+ return nil
4
+ end
5
+
6
+ def valid?
7
+ true
8
+ end
9
+ end
@@ -0,0 +1,50 @@
1
+ class Plyushkin::Persistence
2
+
3
+ def initialize(model)
4
+ @model = model
5
+ @callbacks = {}
6
+ end
7
+
8
+ def properties
9
+ @properties
10
+ end
11
+
12
+ def save(id)
13
+ hash = {}
14
+ (@properties || {}).each do |name, property|
15
+ hash[name] = [property.value_hashes]
16
+ end
17
+ model.service.put(id, hash.to_json)
18
+ end
19
+
20
+ def load(id)
21
+ @properties = {}
22
+ model.service.get(id).each do |name, values|
23
+ property = Plyushkin::Property.build(name, model.registered_types[name.to_sym], values,
24
+ :callbacks => @callbacks[name.to_sym],
25
+ :ignore_unchanged_values => @model.ignore_unchanged_values[name.to_sym] )
26
+ @properties[name.to_sym] = property
27
+ end
28
+ add_missing_properties
29
+ end
30
+
31
+ def register_callback(name, callback, &block)
32
+ @callbacks[name] = { callback => block }
33
+ end
34
+
35
+ private
36
+ def model
37
+ @model
38
+ end
39
+
40
+ def add_missing_properties
41
+ (model.registered_types.keys - @properties.keys).each do |name|
42
+ property = Plyushkin::Property.new(name,
43
+ :type => model.registered_types[name],
44
+ :callbacks => @callbacks[name],
45
+ :ignore_unchanged_values => @model.ignore_unchanged_values[name])
46
+ @properties[name] = property
47
+ end
48
+ end
49
+
50
+ end
@@ -0,0 +1,76 @@
1
+ class Plyushkin::Property
2
+ include ActiveModel::Validations
3
+
4
+ DEFAULT_CALLBACK = lambda{}
5
+
6
+ attr_reader :name
7
+
8
+ validate do |property|
9
+ last.errors.full_messages.each do |message|
10
+ errors[:base] << "#{name.camelize}: #{message}"
11
+ end unless last.valid?
12
+ end
13
+
14
+ def initialize(name, opts={})
15
+ @values = []
16
+ @value_type = opts[:type]
17
+ @callbacks = opts[:callbacks]
18
+ @ignore_unchanged_values = opts[:ignore_unchanged_values]
19
+ @name = name.to_s
20
+ end
21
+
22
+ def create(attr={})
23
+ value = value_type.new(attr)
24
+ if @ignore_unchanged_values
25
+ last = @values.last
26
+ return last if last && last.equal_value?(value)
27
+ end
28
+
29
+ @values.insert(insert_position(value.date), value)
30
+ trigger_callback(:after_create)
31
+ value
32
+ end
33
+
34
+ def trigger_callback(sym)
35
+ @callbacks.fetch(sym, DEFAULT_CALLBACK).call if @callbacks
36
+ end
37
+
38
+ def all
39
+ @values
40
+ end
41
+
42
+ def last
43
+ @values.last || Plyushkin::NilValue.new
44
+ end
45
+
46
+ def empty?
47
+ @values.empty?
48
+ end
49
+
50
+ def insert_position(datetime)
51
+ index = @values.rindex{|v| v.date < datetime}
52
+ index.nil? ? 0 : index + 1
53
+ end
54
+
55
+ def value_type
56
+ @value_type ||= Plyushkin::StringValue
57
+ end
58
+
59
+ def self.build(name, type, values, opts={})
60
+ opts[:type] = type
61
+ Plyushkin::Property.new(name, opts).tap do |p|
62
+ values.each { |value| p.create(value) }
63
+ end
64
+ end
65
+
66
+ def value_hashes
67
+ value_hashes = {}
68
+ all.each do |value|
69
+ value_type.persisted_attributes.each do |attr|
70
+ value_hashes[attr] = value.send(attr)
71
+ end
72
+ end
73
+
74
+ value_hashes
75
+ end
76
+ end
@@ -0,0 +1,3 @@
1
+ module Plyushkin::Service; end
2
+
3
+ require File.dirname(File.expand_path(__FILE__)) + "/service/stub"
@@ -0,0 +1,15 @@
1
+ class Plyushkin::Service::Stub
2
+
3
+ def initialize
4
+ @store = {}
5
+ end
6
+
7
+ def get(id)
8
+ JSON.parse(@store[id] || "{}")
9
+ end
10
+
11
+ def put(id, json)
12
+ @store[id] = json
13
+ end
14
+
15
+ end
@@ -0,0 +1,3 @@
1
+ class Plyushkin::StringValue < Plyushkin::BaseValue
2
+ persisted_attr :value
3
+ end
@@ -0,0 +1 @@
1
+ require File.dirname(File.expand_path(__FILE__)) + "/test/value_types.rb"
@@ -0,0 +1,17 @@
1
+ module Plyushkin::Test
2
+ class Plyushkin::Test::PresenceTestValue < Plyushkin::BaseValue
3
+ persisted_attr :value
4
+ validates :value, :presence => true
5
+ end
6
+
7
+ class Plyushkin::Test::CoordinateValue < Plyushkin::BaseValue
8
+ persisted_attr :x, :y
9
+ validates :x, :y, :presence => true
10
+ end
11
+
12
+ class Plyushkin::Test::Member < ActiveRecord::Base; end
13
+
14
+ class Plyushkin::Test::DateValue < Plyushkin::BaseValue
15
+ persisted_attr :value, :formatter => :to_date
16
+ end
17
+ end
@@ -0,0 +1,5 @@
1
+ class PlyushkinValidator < ActiveModel::EachValidator
2
+ def validate_each(record, attr_name, property)
3
+ record.errors.add(attr_name, :valid, options) unless property.valid?
4
+ end
5
+ end
@@ -0,0 +1,3 @@
1
+ module Plyushkin
2
+ VERSION = "0.0.1"
3
+ end
data/plyushkin.gemspec ADDED
@@ -0,0 +1,25 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'plyushkin/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "plyushkin"
8
+ spec.version = Plyushkin::VERSION
9
+ spec.authors = ["Craig Israel", "Jeremy Hinkle"]
10
+ spec.email = ["craig@theisraels.net", "jchinkle@gmail.com"]
11
+ spec.description = %q{Provides active record extension to capture historical property data}
12
+ spec.summary = %q{Plyushkin - the attribute hoarder}
13
+ spec.homepage = "http://github.com/OnlifeHealth/plyushkin"
14
+ spec.license = "MIT"
15
+
16
+ spec.files = `git ls-files`.split($/)
17
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
+ spec.require_paths = ["lib"]
20
+
21
+ spec.add_dependency "activerecord", "~> 3.2.12"
22
+
23
+ spec.add_development_dependency "bundler", "~> 1.3"
24
+ spec.add_development_dependency "rake"
25
+ end
@@ -0,0 +1,128 @@
1
+ require 'spec_helper'
2
+
3
+ describe Plyushkin::BaseValue do
4
+ describe '#valid?' do
5
+ it 'should not be valid user-defined value if validations fail' do
6
+ value = Plyushkin::Test::CoordinateValue.new
7
+ value.valid?
8
+ value.errors.count.should == 2
9
+ end
10
+
11
+ it 'should be valid user-defined value if validations pass' do
12
+ value = Plyushkin::Test::CoordinateValue.new(:x => 5, :y => 10)
13
+ value.valid?
14
+ value.errors.count.should == 0
15
+ end
16
+
17
+ it 'should not be valid if date is in the future' do
18
+ value = Plyushkin::StringValue.new(:date => 2.days.from_now)
19
+ value.should_not be_valid
20
+ value.errors.full_messages.should == ["Date cannot be in the future"]
21
+ end
22
+ end
23
+
24
+ describe '##persisted_attr' do
25
+ it 'should add attribute to persisted_attributes' do
26
+ clazz = Class.new(Plyushkin::BaseValue) do
27
+ persisted_attr :my_attr
28
+ end
29
+
30
+ clazz.persisted_attributes.should == [ :date, :my_attr ]
31
+ end
32
+
33
+ it 'should add attributes to persisted_attributes when called multiple times' do
34
+ clazz = Class.new(Plyushkin::BaseValue) do
35
+ persisted_attr :my_attr
36
+ persisted_attr :my_other_attr
37
+ end
38
+
39
+ clazz.persisted_attributes.should == [ :date, :my_attr, :my_other_attr ]
40
+ end
41
+
42
+ it 'should add attributes to persisted_attributes when called with multiple attributes' do
43
+ clazz = Class.new(Plyushkin::BaseValue) do
44
+ persisted_attr :my_attr, :my_other_attr
45
+ end
46
+
47
+ clazz.persisted_attributes.should == [ :date, :my_attr, :my_other_attr ]
48
+ end
49
+
50
+ it 'should add reader and writer for attribute' do
51
+ clazz = Class.new(Plyushkin::BaseValue) do
52
+ persisted_attr :my_attr
53
+ end
54
+
55
+ value = clazz.new
56
+ value.my_attr = 5
57
+ value.my_attr.should == 5
58
+ end
59
+
60
+ it "should format value readers using format methods when specified" do
61
+ clazz = Class.new(Plyushkin::BaseValue) do
62
+ persisted_attr :my_attr, :formatter => :to_i
63
+ end
64
+
65
+ value = clazz.new
66
+ value.my_attr = "5"
67
+ value.my_attr.should == 5
68
+ end
69
+
70
+ it "should format multiple value readers using format methods when specified" do
71
+ clazz = Class.new(Plyushkin::BaseValue) do
72
+ persisted_attr :my_attr, :my_attr2, :formatter => :to_i
73
+ end
74
+
75
+ value = clazz.new
76
+ value.my_attr = "5"
77
+ value.my_attr2 = "9999"
78
+ value.my_attr.should == 5
79
+ value.my_attr2.should == 9999
80
+ end
81
+
82
+ describe '#to_date' do
83
+ it 'format a json string to a date' do
84
+ value = Plyushkin::BaseValue.new
85
+ now = DateTime.now
86
+ value.to_date(now.to_json).should === now
87
+ end
88
+ end
89
+
90
+ end
91
+
92
+ describe '#equal_value' do
93
+ it 'should test equality of persisted_attributes' do
94
+ clazz = Class.new(Plyushkin::BaseValue) do
95
+ persisted_attr :my_attr, :my_attr2
96
+ end
97
+
98
+ value1 = clazz.new
99
+ value1.my_attr = 5
100
+ value1.my_attr2 = "test"
101
+
102
+ value2 = clazz.new
103
+ value2.my_attr = 5
104
+ value2.my_attr2 = "test"
105
+
106
+ value1.equal_value?(value2).should be_true
107
+ end
108
+
109
+ it 'should be true if dates are different' do
110
+ clazz = Class.new(Plyushkin::BaseValue) do
111
+ persisted_attr :my_attr, :my_attr2
112
+ end
113
+
114
+ value1 = clazz.new
115
+ value1.my_attr = 5
116
+ value1.my_attr2 = "test"
117
+ value1.date = DateTime.now - 3.days
118
+
119
+ value2 = clazz.new
120
+ value2.my_attr = 5
121
+ value2.my_attr2 = "test"
122
+ value2.date = DateTime.now - 5.days
123
+
124
+ value1.equal_value?(value2).should be_true
125
+
126
+ end
127
+ end
128
+ end
@@ -0,0 +1,160 @@
1
+ require 'spec_helper'
2
+
3
+ describe ActiveRecord::Base do
4
+ describe '##hoards' do
5
+ describe 'read' do
6
+ it 'should return the same property instance when called multiple times' do
7
+ clazz = Class.new(Plyushkin::Test::Member) do
8
+ hoards :login_date
9
+ end
10
+
11
+ member = clazz.new
12
+ property = member.login_date
13
+ property.class.should == Plyushkin::Property
14
+ property.should == member.login_date
15
+ end
16
+
17
+ it 'should register a type for a hoarding property with persistence class' do
18
+ clazz = Class.new(Plyushkin::Test::Member) do
19
+ hoards :login_date
20
+ hoards :geolocation, :type => Plyushkin::Test::CoordinateValue
21
+ end
22
+
23
+ clazz.plyushkin_model.registered_types[:login_date].should be_nil
24
+ clazz.plyushkin_model.registered_types[:geolocation].should == Plyushkin::Test::CoordinateValue
25
+ end
26
+
27
+ it 'should allow specifying value type' do
28
+ clazz = Class.new(Plyushkin::Test::Member) do
29
+ hoards :geolocation, :type => Plyushkin::Test::CoordinateValue
30
+ end
31
+
32
+ member = clazz.new
33
+ property = member.geolocation
34
+ property.value_type.should == Plyushkin::Test::CoordinateValue
35
+ end
36
+ end
37
+
38
+ describe 'after_create option' do
39
+ it 'should call method specified by option after property create is called' do
40
+ clazz = Class.new(Plyushkin::Test::Member) do
41
+ attr_accessor :login_date_created_called
42
+ hoards :login_date, :after_create => :login_date_created
43
+
44
+ def login_date_created
45
+ self.login_date_created_called = true
46
+ end
47
+ end
48
+
49
+ member = clazz.new
50
+ member.login_date.create :value => DateTime.now
51
+ member.login_date_created_called.should be_true
52
+ end
53
+
54
+ it 'should call method specified by option after property create is called for the property named' do
55
+ clazz = Class.new(Plyushkin::Test::Member) do
56
+ attr_accessor :login_date_created_called, :other_prop_created_called
57
+
58
+ hoards :login_date, :after_create => :login_date_created
59
+ hoards :other_prop, :after_create => :other_prop_created
60
+
61
+ def login_date_created
62
+ self.login_date_created_called = true
63
+ end
64
+
65
+ def other_prop_created
66
+ self.other_prop_created_called = true
67
+ end
68
+ end
69
+
70
+ member = clazz.new
71
+ member.other_prop.create :value => DateTime.now
72
+ member.other_prop_created_called.should be_true
73
+ member.login_date_created_called.should be_false
74
+ end
75
+ end
76
+
77
+ describe 'ignore_unchanged_value option' do
78
+ it 'should not add a value when ignore_unchanged_value is true and value is the same as the last value' do
79
+ clazz = Class.new(Plyushkin::Test::Member) do
80
+ hoards :login_date, :ignore_unchanged_values => true
81
+ end
82
+
83
+ member = clazz.new
84
+ now = DateTime.now
85
+ member.login_date.create(:value => now)
86
+ member.login_date.create(:value => now)
87
+
88
+ member.login_date.all.length.should == 1
89
+ end
90
+
91
+ end
92
+
93
+ describe '#validates' do
94
+ it 'should not be valid if hoarding property is not valid' do
95
+ clazz = Class.new(Plyushkin::Test::Member) do
96
+ hoards :geolocation, :type => Plyushkin::Test::CoordinateValue
97
+ end
98
+ clazz.stub(:model_name).and_return(ActiveModel::Name.new(clazz, nil, "test"))
99
+
100
+ member = clazz.new
101
+ member.geolocation.create(:x => 5)
102
+ member.should_not be_valid
103
+ member.errors.messages[:geolocation].count.should == 1
104
+ end
105
+ end
106
+
107
+ describe '#save' do
108
+ let(:clazz) do
109
+ clazz = Class.new(Plyushkin::Test::Member) do
110
+ hoards :login_date, :type => Plyushkin::Test::DateValue
111
+ end
112
+ end
113
+
114
+ it 'should load plyushkin persistence after find is called' do
115
+ member1 = clazz.new
116
+ now = DateTime.now
117
+ member1.login_date.create(:value => now)
118
+ member1.save!
119
+
120
+ member2 = clazz.find(member1.id)
121
+ member2.login_date.last.value.should === now
122
+ end
123
+
124
+ it 'should save hoarding property by id' do
125
+ member1 = clazz.new
126
+ now = DateTime.now
127
+ later = DateTime.now + 5
128
+ member1.login_date.create(:value => now)
129
+ member1.save!
130
+
131
+ member2 = clazz.new
132
+ member2.login_date.create(:value => later)
133
+ member2.save!
134
+
135
+ member1.reload.login_date.last.value.should === now
136
+ member2.reload.login_date.last.value.should === later
137
+ end
138
+ end
139
+
140
+ describe '#reload' do
141
+ let(:clazz) do
142
+ clazz = Class.new(Plyushkin::Test::Member) do
143
+ hoards :login_date, :type => Plyushkin::Test::DateValue
144
+ end
145
+ end
146
+
147
+ it 'should reload new data from persisted store' do
148
+ member = clazz.new
149
+ now = DateTime.now
150
+ member.login_date.create(:value => now)
151
+ member.save!
152
+ member.login_date.create(:value => now + 5)
153
+
154
+ member.reload
155
+ member.login_date.last.value.should === now
156
+ end
157
+ end
158
+
159
+ end
160
+ end
@@ -0,0 +1,11 @@
1
+ require 'spec_helper'
2
+
3
+ describe Plyushkin::NilValue do
4
+ it "should return nil for everything" do
5
+ n = Plyushkin::NilValue.new
6
+ n.steve_sucks.should be_nil
7
+ n.apple.should be_nil
8
+ n.value.should be_nil
9
+ n.date.should be_nil
10
+ end
11
+ end
@@ -0,0 +1,79 @@
1
+ require 'spec_helper'
2
+
3
+ describe Plyushkin::Persistence do
4
+ let(:service) do
5
+ service = Plyushkin::Service::Stub.new
6
+ service.put(1,
7
+ { :name => [{ :value => "Steve" }] ,
8
+ :weight => [{ :value => 150 }],
9
+ :udt => [{ :x => 10, :y => 20 }]
10
+ }.to_json)
11
+ service
12
+ end
13
+
14
+ let(:model) do
15
+ m = Plyushkin::Model.new(service)
16
+ m.register(:name, Plyushkin::StringValue)
17
+ m.register(:weight, Plyushkin::StringValue)
18
+ m.register(:udt, Plyushkin::Test::CoordinateValue)
19
+ m
20
+ end
21
+
22
+ let(:persistence) do
23
+ p = Plyushkin::Persistence.new(model)
24
+ p.load(1)
25
+ p
26
+ end
27
+
28
+ describe '#properties' do
29
+
30
+ it 'should allow access to properties' do
31
+ persistence.properties.length.should == 3
32
+ end
33
+ end
34
+
35
+ describe '#save' do
36
+ it 'should save to source that load uses' do
37
+ persistence.properties[:name].create(:value => "Mike")
38
+ persistence.save(1)
39
+ persistence.load(1)
40
+ persistence.properties[:name].last.value.should == "Mike"
41
+ end
42
+ end
43
+
44
+ describe '#load' do
45
+ it 'should parse json into an hash of properties' do
46
+ persistence.load(1)
47
+ property = persistence.properties[:name]
48
+ property.class.should == Plyushkin::Property
49
+ property.last.value.should == "Steve"
50
+ end
51
+
52
+ it 'should parse json for a user-defined type' do
53
+ persistence.load(1)
54
+ property = persistence.properties[:udt]
55
+ property.last.x.should == 10
56
+ property.last.y.should == 20
57
+ end
58
+
59
+ it 'should parse all history for a property' do
60
+ service.put(1,
61
+ {
62
+ :name => [
63
+ { :value => "Ms. Julie Jones", :date => DateTime.now - 2.days },
64
+ { :value => "Mrs. Julie Smith", :date => DateTime.now - 3.days }
65
+ ]
66
+ }.to_json)
67
+
68
+ property = persistence.properties[:name]
69
+ property.all.length.should == 2
70
+ property.all[0].value.should == "Mrs. Julie Smith"
71
+ property.all[1].value.should == "Ms. Julie Jones"
72
+ end
73
+
74
+ it "should add an empty array for a property that isn't returned from service" do
75
+ model.register(:missing_property, Plyushkin::StringValue)
76
+ persistence.properties[:missing_property].all.should == []
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,136 @@
1
+ require 'spec_helper'
2
+
3
+ describe Plyushkin::Property do
4
+ describe '#create' do
5
+ it 'should add a StringValue' do
6
+ property = Plyushkin::Property.new(:property_name)
7
+ value = property.create(:value => 5)
8
+ value.class.should == Plyushkin::StringValue
9
+ end
10
+
11
+ it 'should pass the attributes to the simple value' do
12
+ property = Plyushkin::Property.new(:property_name)
13
+ date = DateTime.now - 3.days
14
+ value = property.create(:value => 5, :date => date)
15
+ value.value.should == 5
16
+ value.date.should == date
17
+ end
18
+
19
+ it 'should add a user-defined value' do
20
+ property = Plyushkin::Property.new(:property_name, :type => Plyushkin::Test::CoordinateValue)
21
+ value = property.create(:x => 5, :y => 10)
22
+ value.class.should == Plyushkin::Test::CoordinateValue
23
+ value.x.should == 5
24
+ value.y.should == 10
25
+ end
26
+
27
+ describe 'callbacks' do
28
+ it 'should call after_create callback after create' do
29
+ called = 0
30
+ callbacks = { :after_create => lambda{ called += 1 } }
31
+ property = Plyushkin::Property.new(:property_name, :callbacks => callbacks)
32
+ property.create(:value => 5)
33
+ called.should == 1
34
+ property.last.value.should == 5
35
+ end
36
+
37
+ it 'should not require an after_create callback' do
38
+ called = 0
39
+ callbacks = { :before_create => lambda{ called += 1 } }
40
+ property = Plyushkin::Property.new(:property_name, :callbacks => callbacks)
41
+ property.create(:value => 5)
42
+ called.should == 0
43
+ end
44
+ end
45
+
46
+ describe 'ignore_unchanged_value' do
47
+ it 'should not add a value when ignore_unchanged_value is true and value does not change' do
48
+ property = Plyushkin::Property.new(:property_name, :ignore_unchanged_values => true)
49
+ property.create(:value => 5)
50
+ property.create(:value => 5)
51
+ property.all.length.should == 1
52
+ end
53
+ end
54
+ end
55
+
56
+ describe '#all' do
57
+ it 'should return an array of all created values' do
58
+ property = Plyushkin::Property.new(:property_name)
59
+ value1 = property.create(:value => 5)
60
+ value2 = property.create(:value => 7)
61
+ property.all.should == [ value1, value2 ]
62
+ end
63
+
64
+ it 'should be sorted by value class date' do
65
+ property = Plyushkin::Property.new(:property_name)
66
+ value1 = property.create(:value => 5, :date => DateTime.now - 5.days)
67
+ value2 = property.create(:value => 7, :date => DateTime.now - 7.days)
68
+ property.all.should == [ value2, value1 ]
69
+ end
70
+ end
71
+
72
+ describe '#last' do
73
+ it 'should return the most-recently added value' do
74
+ property = Plyushkin::Property.new(:property_name)
75
+ value1 = property.create(:value => 5)
76
+ value2 = property.create(:value => 7)
77
+ property.last.should == value2
78
+ end
79
+
80
+ it 'should return a new value if there are no values' do
81
+ property = Plyushkin::Property.new(:property_name)
82
+ property.last.should be_a(Plyushkin::NilValue)
83
+ end
84
+ end
85
+
86
+ describe '#valid?' do
87
+ it 'should be valid if no values exist' do
88
+ property = Plyushkin::Property.new(:property_name)
89
+ property.should be_valid
90
+ end
91
+
92
+ it 'should be valid if last is valid and other values in array are not valid' do
93
+ property = Plyushkin::Property.new(:property_name, :type => Plyushkin::Test::PresenceTestValue)
94
+ value1 = property.create
95
+ value2 = property.create(:value => 5)
96
+ property.should be_valid
97
+ end
98
+
99
+ it 'should not be valid if last is not valid' do
100
+ property = Plyushkin::Property.new(:property_name, :type => Plyushkin::Test::PresenceTestValue)
101
+ value1 = property.create(:value => 5)
102
+ value2 = property.create
103
+ property.should_not be_valid
104
+ end
105
+
106
+ it 'should populate errors from values when not valid' do
107
+ property = Plyushkin::Property.new(:property_name, :type => Plyushkin::Test::PresenceTestValue)
108
+ property.create
109
+ property.valid?
110
+ property.errors.full_messages.should == [ "PropertyName: Value can't be blank" ]
111
+ end
112
+ end
113
+
114
+ describe '#empty?' do
115
+ it 'should return true if no values were created' do
116
+ property = Plyushkin::Property.new(:property_name, :type => Plyushkin::Test::PresenceTestValue)
117
+ property.should be_empty
118
+ end
119
+
120
+ it 'should return false if values were not created' do
121
+ property = Plyushkin::Property.new(:property_name, :type => Plyushkin::Test::PresenceTestValue)
122
+ property.create
123
+ property.should_not be_empty
124
+ end
125
+ end
126
+
127
+ it '#insert_position' do
128
+ property = Plyushkin::Property.new(:property_name)
129
+ value1 = property.create(:value => 7, :date => DateTime.now - 7.days)
130
+ value2 = property.create(:value => 5, :date => DateTime.now - 5.days)
131
+ property.insert_position(DateTime.now).should == 2
132
+ property.insert_position(DateTime.now - 6.days).should == 1
133
+ property.insert_position(DateTime.now - 8.days).should == 0
134
+ end
135
+
136
+ end
@@ -0,0 +1,30 @@
1
+ require 'spec_helper'
2
+
3
+ describe Plyushkin::StringValue do
4
+ describe '#new' do
5
+ it 'should allow storing an integer' do
6
+ val = Plyushkin::StringValue.new(:value => 5)
7
+ val.value.should == 5
8
+ end
9
+
10
+ it 'should allow storing a string' do
11
+ val = Plyushkin::StringValue.new(:value => 'test')
12
+ val.value.should == 'test'
13
+ end
14
+
15
+ it 'should use current if not specified' do
16
+ Timecop.freeze do
17
+ val = Plyushkin::StringValue.new(:value => 5)
18
+ val.date.should == DateTime.current
19
+ end
20
+ end
21
+
22
+ it 'should use provided date if specified' do
23
+ date = DateTime.new(2013, 12, 20)
24
+ val = Plyushkin::StringValue.new(:value => 5, :date => date)
25
+ val.date.should == date
26
+ end
27
+ end
28
+
29
+ end
30
+
@@ -0,0 +1,27 @@
1
+ # This file was generated by the `rspec --init` command. Conventionally, all
2
+ # specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`.
3
+ # Require this file using `require "spec_helper"` to ensure that it is only
4
+ # loaded once.
5
+ #
6
+ # See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration
7
+
8
+ path = File.dirname(File.expand_path(__FILE__))
9
+ require path + '/../lib/plyushkin'
10
+ require path + '/../lib/plyushkin/test'
11
+ Dir[path + "/support/**/*.rb"].each{ |f| require f }
12
+
13
+ require 'timecop'
14
+ require 'pry'
15
+
16
+ RSpec.configure do |config|
17
+ config.treat_symbols_as_metadata_keys_with_true_values = true
18
+ config.run_all_when_everything_filtered = true
19
+ config.filter_run :focus
20
+
21
+ # Run specs in random order to surface order dependencies. If you find an
22
+ # order dependency and want to debug it, you can fix the order by providing
23
+ # the seed, which is printed after each run.
24
+ # --seed 1234
25
+ config.order = 'random'
26
+ end
27
+
@@ -0,0 +1,12 @@
1
+ # Add sqlite
2
+ I18n.enforce_available_locales = false
3
+ ActiveRecord::Migration.verbose = false
4
+ ActiveRecord::Base.establish_connection adapter: "sqlite3", database: ":memory:"
5
+ class CreateMembers < ActiveRecord::Migration
6
+ def self.up
7
+ create_table :members do |t|
8
+ t.timestamps
9
+ end
10
+ end
11
+ end
12
+ CreateMembers.up
metadata ADDED
@@ -0,0 +1,135 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: plyushkin
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Craig Israel
9
+ - Jeremy Hinkle
10
+ autorequire:
11
+ bindir: bin
12
+ cert_chain: []
13
+ date: 2014-01-22 00:00:00.000000000 Z
14
+ dependencies:
15
+ - !ruby/object:Gem::Dependency
16
+ name: activerecord
17
+ requirement: !ruby/object:Gem::Requirement
18
+ none: false
19
+ requirements:
20
+ - - ~>
21
+ - !ruby/object:Gem::Version
22
+ version: 3.2.12
23
+ type: :runtime
24
+ prerelease: false
25
+ version_requirements: !ruby/object:Gem::Requirement
26
+ none: false
27
+ requirements:
28
+ - - ~>
29
+ - !ruby/object:Gem::Version
30
+ version: 3.2.12
31
+ - !ruby/object:Gem::Dependency
32
+ name: bundler
33
+ requirement: !ruby/object:Gem::Requirement
34
+ none: false
35
+ requirements:
36
+ - - ~>
37
+ - !ruby/object:Gem::Version
38
+ version: '1.3'
39
+ type: :development
40
+ prerelease: false
41
+ version_requirements: !ruby/object:Gem::Requirement
42
+ none: false
43
+ requirements:
44
+ - - ~>
45
+ - !ruby/object:Gem::Version
46
+ version: '1.3'
47
+ - !ruby/object:Gem::Dependency
48
+ name: rake
49
+ requirement: !ruby/object:Gem::Requirement
50
+ none: false
51
+ requirements:
52
+ - - ! '>='
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ type: :development
56
+ prerelease: false
57
+ version_requirements: !ruby/object:Gem::Requirement
58
+ none: false
59
+ requirements:
60
+ - - ! '>='
61
+ - !ruby/object:Gem::Version
62
+ version: '0'
63
+ description: Provides active record extension to capture historical property data
64
+ email:
65
+ - craig@theisraels.net
66
+ - jchinkle@gmail.com
67
+ executables: []
68
+ extensions: []
69
+ extra_rdoc_files: []
70
+ files:
71
+ - .gitignore
72
+ - .rspec
73
+ - Gemfile
74
+ - Gemfile.lock
75
+ - LICENSE.txt
76
+ - README.md
77
+ - Rakefile
78
+ - lib/plyushkin.rb
79
+ - lib/plyushkin/base_value.rb
80
+ - lib/plyushkin/core_ext/active_record_base.rb
81
+ - lib/plyushkin/core_ext/plyushkin_extensions.rb
82
+ - lib/plyushkin/model.rb
83
+ - lib/plyushkin/nil_value.rb
84
+ - lib/plyushkin/persistence.rb
85
+ - lib/plyushkin/property.rb
86
+ - lib/plyushkin/service.rb
87
+ - lib/plyushkin/service/stub.rb
88
+ - lib/plyushkin/string_value.rb
89
+ - lib/plyushkin/test.rb
90
+ - lib/plyushkin/test/value_types.rb
91
+ - lib/plyushkin/validators/presence.rb
92
+ - lib/plyushkin/version.rb
93
+ - plyushkin.gemspec
94
+ - spec/lib/base_value_spec.rb
95
+ - spec/lib/core_ext/active_record_base_spec.rb
96
+ - spec/lib/nil_value_spec.rb
97
+ - spec/lib/persistence_spec.rb
98
+ - spec/lib/property_spec.rb
99
+ - spec/lib/value_spec.rb
100
+ - spec/spec_helper.rb
101
+ - spec/support/sqlite.rb
102
+ homepage: http://github.com/OnlifeHealth/plyushkin
103
+ licenses:
104
+ - MIT
105
+ post_install_message:
106
+ rdoc_options: []
107
+ require_paths:
108
+ - lib
109
+ required_ruby_version: !ruby/object:Gem::Requirement
110
+ none: false
111
+ requirements:
112
+ - - ! '>='
113
+ - !ruby/object:Gem::Version
114
+ version: '0'
115
+ required_rubygems_version: !ruby/object:Gem::Requirement
116
+ none: false
117
+ requirements:
118
+ - - ! '>='
119
+ - !ruby/object:Gem::Version
120
+ version: '0'
121
+ requirements: []
122
+ rubyforge_project:
123
+ rubygems_version: 1.8.23
124
+ signing_key:
125
+ specification_version: 3
126
+ summary: Plyushkin - the attribute hoarder
127
+ test_files:
128
+ - spec/lib/base_value_spec.rb
129
+ - spec/lib/core_ext/active_record_base_spec.rb
130
+ - spec/lib/nil_value_spec.rb
131
+ - spec/lib/persistence_spec.rb
132
+ - spec/lib/property_spec.rb
133
+ - spec/lib/value_spec.rb
134
+ - spec/spec_helper.rb
135
+ - spec/support/sqlite.rb