versioned_record 0.2.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 +7 -0
- data/.coveralls.yml +2 -0
- data/.gitignore +17 -0
- data/.rspec +2 -0
- data/.travis.yml +13 -0
- data/Gemfile +6 -0
- data/History.rdoc +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +94 -0
- data/Rakefile +1 -0
- data/lib/versioned_record/attribute_builder.rb +25 -0
- data/lib/versioned_record/class_methods.rb +58 -0
- data/lib/versioned_record/connection_adapters/postgresql.rb +23 -0
- data/lib/versioned_record/version.rb +3 -0
- data/lib/versioned_record.rb +108 -0
- data/spec/spec_helper.rb +23 -0
- data/spec/support/database.rb +11 -0
- data/spec/support/database.yml +15 -0
- data/spec/support/versioned_product.rb +5 -0
- data/spec/version_class_methods_spec.rb +70 -0
- data/spec/versions_spec.rb +144 -0
- data/versioned_record.gemspec +33 -0
- metadata +198 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 949068fec057f339188cdb54a297c3237d806a3c
|
4
|
+
data.tar.gz: 4408334975f62ef080055cc2f1351a4ef7730595
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: d8b8c37d7f54b9a978b8166a66908426ea6060f8ce5f799dd1948f318226cc78c349cb4aadce06c7708341a41acc08532eba13114405ebff36b85fe9ff7f060f
|
7
|
+
data.tar.gz: ce25740a5627cffd3dcdec8cad7c76518819eaf7b279ba2b6009e1258a8227a5aa1fdf06ec7a6ce58ae4b5dec4e5609680d46f14bec23449f2be2dc02838bb2b
|
data/.coveralls.yml
ADDED
data/.gitignore
ADDED
data/.rspec
ADDED
data/.travis.yml
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
language: ruby
|
2
|
+
rvm:
|
3
|
+
- "2.1.1"
|
4
|
+
script: bundle exec rspec spec
|
5
|
+
addons:
|
6
|
+
postgresql: "9.3"
|
7
|
+
before_script:
|
8
|
+
- export VERSIONED_RECORD_ENV=travis
|
9
|
+
- psql -c 'create database versioned_record_test;' -U postgres
|
10
|
+
notifications:
|
11
|
+
hipchat: 83906daf46b0ce558ce21f13353861@246078
|
12
|
+
on_success: always
|
13
|
+
on_failure: always
|
data/Gemfile
ADDED
data/History.rdoc
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2014 Dan Draper
|
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,94 @@
|
|
1
|
+
# VersionedRecord
|
2
|
+
|
3
|
+
[](https://codeclimate.com/github/jobready/versioned_record)
|
4
|
+
[](https://travis-ci.org/jobready/versioned_record)
|
5
|
+
[](https://coveralls.io/r/jobready/versioned_record)
|
6
|
+
|
7
|
+
[RDocs](http://rdoc.info/github/jobready/versioned_record/master/frames)
|
8
|
+
|
9
|
+
Versioned Record allows the creation of multiple versions of an active record that share an ID.
|
10
|
+
The version and ID columns of a record form a composite primary key and as such this gem relies on the
|
11
|
+
composite_primary_keys gem.
|
12
|
+
|
13
|
+
Why another versioning tool for ActiveRecord? Mainly because this gem does not rely on serializing data or storing of
|
14
|
+
multiple attributes in a single column. It uses databases as they were intended which means it is fast, reliable and flexible.
|
15
|
+
|
16
|
+
## Installation
|
17
|
+
|
18
|
+
Add this line to your application's Gemfile:
|
19
|
+
|
20
|
+
gem 'versioned_record'
|
21
|
+
|
22
|
+
And then execute:
|
23
|
+
|
24
|
+
$ bundle
|
25
|
+
|
26
|
+
Or install it yourself as:
|
27
|
+
|
28
|
+
$ gem install versioned_record
|
29
|
+
|
30
|
+
## Usage
|
31
|
+
|
32
|
+
### Setting up your table
|
33
|
+
|
34
|
+
To create a versioned record, start by adding the `versioned: true` option to your migration and then (re)running.
|
35
|
+
|
36
|
+
create_table :products, versioned: true do |t|
|
37
|
+
t.string :name
|
38
|
+
t.decimal :price
|
39
|
+
end
|
40
|
+
|
41
|
+
The versioned option will create the `id`, `version` and `is_current_version` columns and create a composite primary key on the id and version columns.
|
42
|
+
|
43
|
+
Next, include the VersionedRecord module in your ActiveRecord model
|
44
|
+
|
45
|
+
class Product < ActiveRecord::Base
|
46
|
+
include VersionedRecord
|
47
|
+
end
|
48
|
+
|
49
|
+
### Creating records and versions
|
50
|
+
|
51
|
+
Creating a record happens as normal.
|
52
|
+
|
53
|
+
product = Product.create(name: 'Coffee Cup', price: 4.99)
|
54
|
+
|
55
|
+
The product will have `version` set to 0 and `is_current_version` set to true
|
56
|
+
|
57
|
+
product.version => 0
|
58
|
+
product.is_current_version? => true
|
59
|
+
|
60
|
+
To create a version simply call `create_version!` on the record.
|
61
|
+
|
62
|
+
product_v2 = product.create_version!(price: 5.99)
|
63
|
+
|
64
|
+
Note that any attributes not specified in the `create_version!` call will be copied from the previous version.
|
65
|
+
|
66
|
+
product_v2.name => 'Coffee Cup'
|
67
|
+
|
68
|
+
Also, the `is_current_version` flag is unset for the old version and set for the new one
|
69
|
+
|
70
|
+
product.is_current_version? => false
|
71
|
+
product_v2.is_current_version? => true
|
72
|
+
|
73
|
+
## TODO: Associations
|
74
|
+
|
75
|
+
|
76
|
+
## Database Support
|
77
|
+
|
78
|
+
Right now, only PostgreSQL has been tested. MySQL may or may not work but if you'd like to test/add support please fork and contribute!
|
79
|
+
|
80
|
+
## Limitations
|
81
|
+
|
82
|
+
Does not currently work with ActiveRecord 4.1+
|
83
|
+
|
84
|
+
## Author
|
85
|
+
|
86
|
+
Dan Draper, dan at codehire dot com, [Codehire](http://www.codehire.com/), [@danieldraper](http://www.twitter.com/danieldraper)
|
87
|
+
|
88
|
+
## Contributing
|
89
|
+
|
90
|
+
1. Fork it
|
91
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
92
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
93
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
94
|
+
5. Create new Pull Request
|
data/Rakefile
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require "bundler/gem_tasks"
|
@@ -0,0 +1,25 @@
|
|
1
|
+
module VersionedRecord
|
2
|
+
# Builds a set of attributes for a new version of a record
|
3
|
+
class AttributeBuilder
|
4
|
+
|
5
|
+
# New Builder
|
6
|
+
# @param [ActiveRecord::Base] record the record we are versioning
|
7
|
+
def initialize(record)
|
8
|
+
@record = record
|
9
|
+
end
|
10
|
+
|
11
|
+
# Get new attributes hash for the new version
|
12
|
+
# Attributes missing in new_attrs will be filled in from
|
13
|
+
# the previous version (specified in the constructor)
|
14
|
+
#
|
15
|
+
# @param [Hash] new_attrs are the attributes we are changing in this version
|
16
|
+
def attributes(new_attrs)
|
17
|
+
@new_attrs = new_attrs.stringify_keys
|
18
|
+
@record.attributes.merge(@new_attrs.merge({
|
19
|
+
is_current_version: true,
|
20
|
+
id: @record.id,
|
21
|
+
version: @record.version + 1
|
22
|
+
}))
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
module VersionedRecord
|
2
|
+
module ClassMethods
|
3
|
+
# Scope to limit records to only the current versions
|
4
|
+
def current_versions
|
5
|
+
where(is_current_version: true)
|
6
|
+
end
|
7
|
+
|
8
|
+
# Scope to exclude current version from a query
|
9
|
+
def exclude_current
|
10
|
+
where(is_current_version: false)
|
11
|
+
end
|
12
|
+
|
13
|
+
# Finds a record as per ActiveRecord::Base
|
14
|
+
# If only an ID is provided then it returns the current version for that ID
|
15
|
+
# Otherwise, both an ID and a version can be provided
|
16
|
+
#
|
17
|
+
# @see ActiveRecord::Base#find
|
18
|
+
# @see #find_current
|
19
|
+
#
|
20
|
+
# @example Single Argument
|
21
|
+
#
|
22
|
+
# Model.find(1) => # The latest record
|
23
|
+
#
|
24
|
+
# @example An ID and a version
|
25
|
+
#
|
26
|
+
# Model.find(1, 0) => # The first version
|
27
|
+
#
|
28
|
+
def find(*args)
|
29
|
+
if args.length == 1 && !args.first.kind_of?(Array)
|
30
|
+
find_current(args.first)
|
31
|
+
else
|
32
|
+
super
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
# Find the current version of the record with the given ID
|
37
|
+
# @param [Integer] id the record's ID
|
38
|
+
# @raise [ActiveRecord::RecordNotFound] if no record with the given ID is found
|
39
|
+
def find_current(id)
|
40
|
+
current_versions.where(id: id).first.tap do |record|
|
41
|
+
raise ActiveRecord::RecordNotFound unless record.present?
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
# Scope to exclude the given record from results.
|
46
|
+
# This is handy when retrieving all versions of a record except for one
|
47
|
+
# Say when we are viewing all other versions to a given record
|
48
|
+
#
|
49
|
+
# @example
|
50
|
+
#
|
51
|
+
# person = Person.find(1, 3)
|
52
|
+
# other_versions = person.versions.exclude(person)
|
53
|
+
#
|
54
|
+
def exclude(record)
|
55
|
+
where(id: record.id).where('version != ?', record.version)
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
module ActiveRecord
|
2
|
+
module ConnectionAdapters
|
3
|
+
class PostgreSQLAdapter
|
4
|
+
|
5
|
+
# Provide versioned: true to create a versioned table
|
6
|
+
# Note that schema.rb cannot be used with versioned tables
|
7
|
+
def create_table(table_name, options = {})
|
8
|
+
if options[:versioned]
|
9
|
+
super(table_name, options.merge(id: false)) do |table|
|
10
|
+
table.column :id, :serial
|
11
|
+
table.integer :version, default: 0
|
12
|
+
table.boolean :is_current_version, default: true
|
13
|
+
yield(table)
|
14
|
+
end
|
15
|
+
add_index(table_name, [:id, :version], unique: true, name: "#{table_name}_versioned_pkey")
|
16
|
+
execute("ALTER TABLE #{table_name} ADD PRIMARY KEY USING INDEX #{table_name}_versioned_pkey")
|
17
|
+
else
|
18
|
+
super
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,108 @@
|
|
1
|
+
|
2
|
+
require 'active_record'
|
3
|
+
require 'active_record/connection_adapters/postgresql_adapter'
|
4
|
+
|
5
|
+
require 'composite_primary_keys'
|
6
|
+
require 'versioned_record/attribute_builder'
|
7
|
+
require 'versioned_record/class_methods'
|
8
|
+
require 'versioned_record/connection_adapters/postgresql'
|
9
|
+
require 'versioned_record/version'
|
10
|
+
|
11
|
+
module VersionedRecord
|
12
|
+
def self.included(model_class)
|
13
|
+
model_class.extend ClassMethods
|
14
|
+
model_class.primary_keys = :id, :version
|
15
|
+
model_class.after_save :ensure_version_deprecation!, on: :create
|
16
|
+
end
|
17
|
+
|
18
|
+
# Create a new version of the existing record
|
19
|
+
# A new version can only be created once for a given record and subsequent
|
20
|
+
# versions must be created by calling create_version! on the latest version
|
21
|
+
#
|
22
|
+
# Attributes that are not specified here will be copied to the new version from
|
23
|
+
# the previous version
|
24
|
+
#
|
25
|
+
# This method will still fire ActiveRecord callbacks for save/create etc as per
|
26
|
+
# normal record creation
|
27
|
+
#
|
28
|
+
# @example
|
29
|
+
#
|
30
|
+
# person_v1 = Person.create(name: 'Dan')
|
31
|
+
# person_v2 = person_v1.create_version!(name: 'Daniel')
|
32
|
+
#
|
33
|
+
def create_version!(new_attrs = {})
|
34
|
+
create_operation do
|
35
|
+
self.class.create!(new_version_attrs(new_attrs))
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
# Same as #create_version! but will not raise if the record is invalid
|
40
|
+
# @see VersionedRecord#create_version!
|
41
|
+
#
|
42
|
+
def create_version(new_attrs = {})
|
43
|
+
create_operation do
|
44
|
+
self.class.create(new_version_attrs(new_attrs))
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
# Build (but do not save) a new version of the record
|
49
|
+
# This allows you to use the object in forms etc
|
50
|
+
# After the record is saved, all previous versions will be deprecated
|
51
|
+
# and this record will be marked as current
|
52
|
+
#
|
53
|
+
# @example
|
54
|
+
#
|
55
|
+
# new_version = first_version.build_version
|
56
|
+
# new_version.save
|
57
|
+
#
|
58
|
+
def build_version(new_attrs = {})
|
59
|
+
new_version = self.class.new(new_version_attrs(new_attrs)).tap do |built|
|
60
|
+
built.deprecate_old_versions_after_create!
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
# Retrieve all versions of this record
|
65
|
+
# Can be chained with other scopes
|
66
|
+
#
|
67
|
+
# @example Versions ordered by version number
|
68
|
+
#
|
69
|
+
# person.versions.order(:version)
|
70
|
+
#
|
71
|
+
def versions
|
72
|
+
self.class.where(id: self.id)
|
73
|
+
end
|
74
|
+
|
75
|
+
# Ensure that old versions are deprecated when we save
|
76
|
+
# (only applies on create)
|
77
|
+
def deprecate_old_versions_after_create!
|
78
|
+
@deprecate_old_versions_after_create = true
|
79
|
+
end
|
80
|
+
|
81
|
+
private
|
82
|
+
def new_version_attrs(new_attrs)
|
83
|
+
attr_builder = AttributeBuilder.new(self)
|
84
|
+
attr_builder.attributes(new_attrs)
|
85
|
+
end
|
86
|
+
|
87
|
+
def deprecate_old_versions(current_version)
|
88
|
+
versions.exclude(current_version).update_all(is_current_version: false)
|
89
|
+
end
|
90
|
+
|
91
|
+
def create_operation
|
92
|
+
self.class.transaction do
|
93
|
+
yield.tap do |created|
|
94
|
+
deprecate_old_versions(created) if created.persisted?
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
def deprecate_old_versions_after_create?
|
100
|
+
@deprecate_old_versions_after_create
|
101
|
+
end
|
102
|
+
|
103
|
+
def ensure_version_deprecation!
|
104
|
+
if deprecate_old_versions_after_create?
|
105
|
+
deprecate_old_versions(self)
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
require 'coveralls'
|
2
|
+
Coveralls.wear!
|
3
|
+
|
4
|
+
#require 'active_record'
|
5
|
+
require 'versioned_record'
|
6
|
+
require 'database_cleaner'
|
7
|
+
|
8
|
+
Dir[("./spec/support/**/*.rb")].each {|f| require f}
|
9
|
+
|
10
|
+
# See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration
|
11
|
+
RSpec.configure do |config|
|
12
|
+
config.treat_symbols_as_metadata_keys_with_true_values = true
|
13
|
+
config.run_all_when_everything_filtered = true
|
14
|
+
config.mock_with :rspec
|
15
|
+
config.filter_run :focus
|
16
|
+
config.order = 'random'
|
17
|
+
config.before :each do
|
18
|
+
DatabaseCleaner.start
|
19
|
+
end
|
20
|
+
config.after :each do
|
21
|
+
DatabaseCleaner.clean
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,11 @@
|
|
1
|
+
env = ENV['VERSIONED_RECORD_ENV'] || 'development'
|
2
|
+
dbconfig = YAML::load_file(File.join(__dir__, 'database.yml'))
|
3
|
+
ActiveRecord::Base.establish_connection(dbconfig[env]['postgresql'])
|
4
|
+
|
5
|
+
ActiveRecord::Schema.define :version => 0 do
|
6
|
+
|
7
|
+
create_table :versioned_products, versioned: true, force: true do |t|
|
8
|
+
t.string :name
|
9
|
+
t.decimal :price
|
10
|
+
end
|
11
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
development:
|
2
|
+
postgresql:
|
3
|
+
adapter: postgresql
|
4
|
+
host: localhost
|
5
|
+
username: daniel
|
6
|
+
password:
|
7
|
+
database: versioned_record_test
|
8
|
+
travis:
|
9
|
+
postgresql:
|
10
|
+
adapter: postgresql
|
11
|
+
host: localhost
|
12
|
+
username: postgres
|
13
|
+
password:
|
14
|
+
database: versioned_record_test
|
15
|
+
|
@@ -0,0 +1,70 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe VersionedProduct do
|
4
|
+
describe '::create' do
|
5
|
+
let(:record) { VersionedProduct.create(name: 'Macbook Pro') }
|
6
|
+
|
7
|
+
specify do
|
8
|
+
expect(record).to be_is_current_version
|
9
|
+
expect(record.version).to eq(0)
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
describe '::find' do
|
14
|
+
context 'a single non-array argument' do
|
15
|
+
it 'retrieves the latest version with the given ID' do
|
16
|
+
expect(VersionedProduct).to receive(:find_current).with(1)
|
17
|
+
VersionedProduct.find(1)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
context 'a single array argument' do
|
22
|
+
it 'retrieves the latest version with the given ID' do
|
23
|
+
expect(ActiveRecord::Base).to receive(:find).with([1, 0])
|
24
|
+
VersionedProduct.find([1, 0])
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
context 'multiple arguments' do
|
29
|
+
it 'retrieves the latest version with the given ID' do
|
30
|
+
expect(ActiveRecord::Base).to receive(:find).with(1, 0)
|
31
|
+
VersionedProduct.find(1, 0)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
describe 'scopes' do
|
37
|
+
let!(:first_version) { VersionedProduct.create(name: 'Macbook Pro') }
|
38
|
+
let!(:second_version) { first_version.create_version!(name: 'Macbook Retina') }
|
39
|
+
|
40
|
+
describe '::find_current' do
|
41
|
+
it 'finds the latest version' do
|
42
|
+
expect(VersionedProduct.find_current(first_version.id)).to eq(second_version)
|
43
|
+
end
|
44
|
+
|
45
|
+
context 'where no matching record exists' do
|
46
|
+
it 'finds the latest version' do
|
47
|
+
expect { VersionedProduct.find_current(0) }.to raise_error(ActiveRecord::RecordNotFound)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
describe '::current_versions' do
|
53
|
+
it 'returns only the current version' do
|
54
|
+
expect(VersionedProduct.current_versions).to eq([second_version])
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
describe '::exclude_current' do
|
59
|
+
it 'returns only the current version' do
|
60
|
+
expect(VersionedProduct.exclude_current).to eq([first_version])
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
describe '::exclude' do
|
65
|
+
it 'returns everything but the excluded record' do
|
66
|
+
expect(VersionedProduct.exclude(first_version)).to eq([second_version])
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
@@ -0,0 +1,144 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe VersionedProduct do
|
4
|
+
let!(:first_version) { VersionedProduct.create(name: 'iPad', price: 100) }
|
5
|
+
|
6
|
+
shared_examples 'successful version creation' do
|
7
|
+
it 'creates a new version' do
|
8
|
+
expect { perform }.to change { VersionedProduct.count }.by(1)
|
9
|
+
end
|
10
|
+
|
11
|
+
it 'unsets current version on first version' do
|
12
|
+
perform
|
13
|
+
expect(first_version.reload).to_not be_is_current_version
|
14
|
+
end
|
15
|
+
|
16
|
+
describe 'the new version' do
|
17
|
+
subject { perform.reload }
|
18
|
+
specify { expect(subject).to be_is_current_version }
|
19
|
+
specify { expect(subject.version).to eq(1) }
|
20
|
+
specify { expect(subject.name).to eq('iPad 2') }
|
21
|
+
specify { expect(subject.price).to eq(100) }
|
22
|
+
specify { expect(subject).to be_valid }
|
23
|
+
specify { expect(subject).to be_persisted }
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
describe '#create_version!' do
|
28
|
+
def perform
|
29
|
+
first_version.create_version!(name: 'iPad 2')
|
30
|
+
end
|
31
|
+
|
32
|
+
include_examples 'successful version creation'
|
33
|
+
|
34
|
+
context 'when created record is invalid' do
|
35
|
+
it 'raises an error' do
|
36
|
+
expect { first_version.create_version!(name: nil) }.to raise_error(ActiveRecord::RecordInvalid)
|
37
|
+
end
|
38
|
+
|
39
|
+
it 'does not change the current version flag on first version' do
|
40
|
+
begin
|
41
|
+
first_version.create_version!(name: nil)
|
42
|
+
rescue
|
43
|
+
expect(first_version.reload).to be_is_current_version
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
describe '#create_version' do
|
50
|
+
def perform
|
51
|
+
first_version.create_version(name: 'iPad 2')
|
52
|
+
end
|
53
|
+
|
54
|
+
include_examples 'successful version creation'
|
55
|
+
|
56
|
+
context 'when created record is invalid' do
|
57
|
+
it 'does not create a new version' do
|
58
|
+
expect { first_version.create_version(name: 'f') }.to_not change { VersionedProduct.count }
|
59
|
+
end
|
60
|
+
|
61
|
+
describe 'create result' do
|
62
|
+
subject { first_version.create_version(name: 'f') }
|
63
|
+
|
64
|
+
specify { expect(subject).to_not be_valid }
|
65
|
+
specify { expect(subject).to_not be_persisted }
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
describe '#build_version' do
|
71
|
+
context 'with attributes provided directly' do
|
72
|
+
def perform
|
73
|
+
first_version.build_version(name: 'iPad 2').tap do |new_version|
|
74
|
+
new_version.save!
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
include_examples 'successful version creation'
|
79
|
+
|
80
|
+
context 'when created record is invalid' do
|
81
|
+
it 'does not create a new version' do
|
82
|
+
expect {
|
83
|
+
new_version = first_version.build_version(name: 'f')
|
84
|
+
new_version.save
|
85
|
+
}.to_not change { VersionedProduct.count }
|
86
|
+
end
|
87
|
+
|
88
|
+
describe 'create result' do
|
89
|
+
subject do
|
90
|
+
first_version.build_version(name: 'f').tap do |new_version|
|
91
|
+
new_version.save
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
specify { expect(subject).to_not be_valid }
|
96
|
+
specify { expect(subject).to_not be_persisted }
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
context 'with attributes provided after initialization' do
|
102
|
+
def perform
|
103
|
+
first_version.build_version.tap do |new_version|
|
104
|
+
new_version.name = 'iPad 2'
|
105
|
+
new_version.save!
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
include_examples 'successful version creation'
|
110
|
+
|
111
|
+
context 'when created record is invalid' do
|
112
|
+
it 'does not create a new version' do
|
113
|
+
expect {
|
114
|
+
new_version = first_version.build_version
|
115
|
+
new_version.name = 'f'
|
116
|
+
new_version.save
|
117
|
+
}.to_not change { VersionedProduct.count }
|
118
|
+
end
|
119
|
+
|
120
|
+
describe 'create result' do
|
121
|
+
subject do
|
122
|
+
first_version.build_version.tap do |new_version|
|
123
|
+
new_version.name = 'f'
|
124
|
+
new_version.save
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
specify { expect(subject).to_not be_valid }
|
129
|
+
specify { expect(subject).to_not be_persisted }
|
130
|
+
end
|
131
|
+
end
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
describe '#versions' do
|
136
|
+
subject(:first_version) { VersionedProduct.create(name: 'iPad') }
|
137
|
+
specify { expect(subject).to have(1).versions }
|
138
|
+
|
139
|
+
context 'after creating a version' do
|
140
|
+
before { first_version.create_version!(name: 'iPad 2') }
|
141
|
+
specify { expect(subject).to have(2).versions }
|
142
|
+
end
|
143
|
+
end
|
144
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'versioned_record/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "versioned_record"
|
8
|
+
spec.version = VersionedRecord::VERSION
|
9
|
+
spec.authors = ["Dan Draper"]
|
10
|
+
spec.email = ["daniel@codehire.com"]
|
11
|
+
spec.description = %q{Version ActiveRecord models using composite primary keys}
|
12
|
+
spec.summary = %q{Version ActiveRecord models using composite primary keys}
|
13
|
+
spec.homepage = ""
|
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.required_ruby_version = '>= 2.0.0'
|
22
|
+
|
23
|
+
spec.add_runtime_dependency "activerecord", "~> 4.0.0"
|
24
|
+
spec.add_runtime_dependency 'composite_primary_keys', '>= 6.0.3'
|
25
|
+
|
26
|
+
spec.add_development_dependency "bundler", "~> 1.3"
|
27
|
+
spec.add_development_dependency "rake"
|
28
|
+
spec.add_development_dependency "rspec", "~> 2.14"
|
29
|
+
spec.add_development_dependency "pg"
|
30
|
+
spec.add_development_dependency "database_cleaner"
|
31
|
+
spec.add_development_dependency "yard"
|
32
|
+
spec.add_development_dependency "coveralls"
|
33
|
+
end
|
metadata
ADDED
@@ -0,0 +1,198 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: versioned_record
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.2.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Dan Draper
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2014-04-28 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: activerecord
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: 4.0.0
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: 4.0.0
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: composite_primary_keys
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: 6.0.3
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ">="
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: 6.0.3
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: bundler
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '1.3'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '1.3'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: rake
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - ">="
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - ">="
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '0'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: rspec
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - "~>"
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '2.14'
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - "~>"
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '2.14'
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: pg
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - ">="
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '0'
|
90
|
+
type: :development
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - ">="
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '0'
|
97
|
+
- !ruby/object:Gem::Dependency
|
98
|
+
name: database_cleaner
|
99
|
+
requirement: !ruby/object:Gem::Requirement
|
100
|
+
requirements:
|
101
|
+
- - ">="
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: '0'
|
104
|
+
type: :development
|
105
|
+
prerelease: false
|
106
|
+
version_requirements: !ruby/object:Gem::Requirement
|
107
|
+
requirements:
|
108
|
+
- - ">="
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: '0'
|
111
|
+
- !ruby/object:Gem::Dependency
|
112
|
+
name: yard
|
113
|
+
requirement: !ruby/object:Gem::Requirement
|
114
|
+
requirements:
|
115
|
+
- - ">="
|
116
|
+
- !ruby/object:Gem::Version
|
117
|
+
version: '0'
|
118
|
+
type: :development
|
119
|
+
prerelease: false
|
120
|
+
version_requirements: !ruby/object:Gem::Requirement
|
121
|
+
requirements:
|
122
|
+
- - ">="
|
123
|
+
- !ruby/object:Gem::Version
|
124
|
+
version: '0'
|
125
|
+
- !ruby/object:Gem::Dependency
|
126
|
+
name: coveralls
|
127
|
+
requirement: !ruby/object:Gem::Requirement
|
128
|
+
requirements:
|
129
|
+
- - ">="
|
130
|
+
- !ruby/object:Gem::Version
|
131
|
+
version: '0'
|
132
|
+
type: :development
|
133
|
+
prerelease: false
|
134
|
+
version_requirements: !ruby/object:Gem::Requirement
|
135
|
+
requirements:
|
136
|
+
- - ">="
|
137
|
+
- !ruby/object:Gem::Version
|
138
|
+
version: '0'
|
139
|
+
description: Version ActiveRecord models using composite primary keys
|
140
|
+
email:
|
141
|
+
- daniel@codehire.com
|
142
|
+
executables: []
|
143
|
+
extensions: []
|
144
|
+
extra_rdoc_files: []
|
145
|
+
files:
|
146
|
+
- ".coveralls.yml"
|
147
|
+
- ".gitignore"
|
148
|
+
- ".rspec"
|
149
|
+
- ".travis.yml"
|
150
|
+
- Gemfile
|
151
|
+
- History.rdoc
|
152
|
+
- LICENSE.txt
|
153
|
+
- README.md
|
154
|
+
- Rakefile
|
155
|
+
- lib/versioned_record.rb
|
156
|
+
- lib/versioned_record/attribute_builder.rb
|
157
|
+
- lib/versioned_record/class_methods.rb
|
158
|
+
- lib/versioned_record/connection_adapters/postgresql.rb
|
159
|
+
- lib/versioned_record/version.rb
|
160
|
+
- spec/spec_helper.rb
|
161
|
+
- spec/support/database.rb
|
162
|
+
- spec/support/database.yml
|
163
|
+
- spec/support/versioned_product.rb
|
164
|
+
- spec/version_class_methods_spec.rb
|
165
|
+
- spec/versions_spec.rb
|
166
|
+
- versioned_record.gemspec
|
167
|
+
homepage: ''
|
168
|
+
licenses:
|
169
|
+
- MIT
|
170
|
+
metadata: {}
|
171
|
+
post_install_message:
|
172
|
+
rdoc_options: []
|
173
|
+
require_paths:
|
174
|
+
- lib
|
175
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
176
|
+
requirements:
|
177
|
+
- - ">="
|
178
|
+
- !ruby/object:Gem::Version
|
179
|
+
version: 2.0.0
|
180
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
181
|
+
requirements:
|
182
|
+
- - ">="
|
183
|
+
- !ruby/object:Gem::Version
|
184
|
+
version: '0'
|
185
|
+
requirements: []
|
186
|
+
rubyforge_project:
|
187
|
+
rubygems_version: 2.2.2
|
188
|
+
signing_key:
|
189
|
+
specification_version: 4
|
190
|
+
summary: Version ActiveRecord models using composite primary keys
|
191
|
+
test_files:
|
192
|
+
- spec/spec_helper.rb
|
193
|
+
- spec/support/database.rb
|
194
|
+
- spec/support/database.yml
|
195
|
+
- spec/support/versioned_product.rb
|
196
|
+
- spec/version_class_methods_spec.rb
|
197
|
+
- spec/versions_spec.rb
|
198
|
+
has_rdoc:
|