plyushkin 0.0.1

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.
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