txus-aversion 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 +17 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +72 -0
- data/Rakefile +10 -0
- data/aversion.gemspec +19 -0
- data/lib/aversion/version.rb +3 -0
- data/lib/aversion.rb +131 -0
- data/test/aversion_test.rb +97 -0
- data/test/test_helper.rb +4 -0
- metadata +57 -0
data/.gitignore
ADDED
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
Copyright (c) 2012 Josep M. Bach
|
|
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,72 @@
|
|
|
1
|
+
# aversion
|
|
2
|
+
|
|
3
|
+
Aversion makes your Ruby objects versionable. It also makes them immutable, so
|
|
4
|
+
the only way to obtain transformed copies is to explicitly mutate state in
|
|
5
|
+
`#transform` calls, which will return the modified copy, leaving the original
|
|
6
|
+
intact.
|
|
7
|
+
|
|
8
|
+
You can also compute the difference between two versions, expressed as an array
|
|
9
|
+
of transformations, and apply it onto an arbitrary object.
|
|
10
|
+
|
|
11
|
+
## Installation
|
|
12
|
+
|
|
13
|
+
Add this line to your application's Gemfile:
|
|
14
|
+
|
|
15
|
+
gem 'txus-aversion'
|
|
16
|
+
|
|
17
|
+
And then execute:
|
|
18
|
+
|
|
19
|
+
$ bundle
|
|
20
|
+
|
|
21
|
+
Or install it yourself as:
|
|
22
|
+
|
|
23
|
+
$ gem install txus-aversion
|
|
24
|
+
|
|
25
|
+
Yeah, I know the name sucks. There is another gem called aversion so I have to
|
|
26
|
+
prefix mine with my name :(
|
|
27
|
+
|
|
28
|
+
## Usage
|
|
29
|
+
|
|
30
|
+
```ruby
|
|
31
|
+
class Person
|
|
32
|
+
include Aversion
|
|
33
|
+
|
|
34
|
+
def initialize(hunger)
|
|
35
|
+
@hunger = hunger
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def eat
|
|
39
|
+
transform do
|
|
40
|
+
@hunger -= 5
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Objects are immutable. Calls to mutate state will return new modified
|
|
46
|
+
# copies (thanks to #transform):
|
|
47
|
+
john = Person.new
|
|
48
|
+
new_john = john.eat
|
|
49
|
+
newer_john = new_john.eat
|
|
50
|
+
|
|
51
|
+
# You can roll back to a previous state:
|
|
52
|
+
new_john_again = newer_john.rollback
|
|
53
|
+
|
|
54
|
+
# Calculate deltas between objects, and replay the differences to get to the
|
|
55
|
+
# desired state:
|
|
56
|
+
difference = newer_john - john
|
|
57
|
+
newer_john_again = john.replay(difference)
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## Contributing
|
|
61
|
+
|
|
62
|
+
1. Fork it
|
|
63
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
|
64
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
|
65
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
|
66
|
+
5. Create new Pull Request
|
|
67
|
+
|
|
68
|
+
## Who's this
|
|
69
|
+
|
|
70
|
+
This was made by [Josep M. Bach (Txus)](http://txustice.me) under the MIT
|
|
71
|
+
license. I'm [@txustice](http://twitter.com/txustice) on twitter (where you
|
|
72
|
+
should probably follow me!).
|
data/Rakefile
ADDED
data/aversion.gemspec
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
|
4
|
+
require 'aversion/version'
|
|
5
|
+
|
|
6
|
+
Gem::Specification.new do |gem|
|
|
7
|
+
gem.name = "txus-aversion"
|
|
8
|
+
gem.version = Aversion::VERSION
|
|
9
|
+
gem.authors = ["Josep M. Bach"]
|
|
10
|
+
gem.email = ["josep.m.bach@gmail.com"]
|
|
11
|
+
gem.description = %q{Make your Ruby objects versionable}
|
|
12
|
+
gem.summary = %q{Make your Ruby objects versionable}
|
|
13
|
+
gem.homepage = "https://github.com/txus/aversion"
|
|
14
|
+
|
|
15
|
+
gem.files = `git ls-files`.split($/)
|
|
16
|
+
gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
|
|
17
|
+
gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
|
|
18
|
+
gem.require_paths = ["lib"]
|
|
19
|
+
end
|
data/lib/aversion.rb
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
require "aversion/version"
|
|
2
|
+
|
|
3
|
+
# Public: Aversion makes your Ruby objects versionable. It also makes them
|
|
4
|
+
# immutable, so the only way to obtain transformed copies is to explicitly
|
|
5
|
+
# mutate state in #transform calls, which will return the modified copy, leaving
|
|
6
|
+
# the original intact.
|
|
7
|
+
#
|
|
8
|
+
# Examples
|
|
9
|
+
#
|
|
10
|
+
# class Person
|
|
11
|
+
# include Aversion
|
|
12
|
+
#
|
|
13
|
+
# def initialize(hunger)
|
|
14
|
+
# @hunger = hunger
|
|
15
|
+
# end
|
|
16
|
+
#
|
|
17
|
+
# def eat
|
|
18
|
+
# transform do
|
|
19
|
+
# @hunger -= 5
|
|
20
|
+
# end
|
|
21
|
+
# end
|
|
22
|
+
# end
|
|
23
|
+
#
|
|
24
|
+
# # Objects are immutable. Calls to mutate state will return new modified
|
|
25
|
+
# # copies (thanks to #transform):
|
|
26
|
+
# john = Person.new
|
|
27
|
+
# new_john = john.eat
|
|
28
|
+
# newer_john = new_john.eat
|
|
29
|
+
#
|
|
30
|
+
# # You can roll back to a previous state:
|
|
31
|
+
# new_john_again = newer_john.rollback
|
|
32
|
+
#
|
|
33
|
+
# # Calculate deltas between objects, and replay the differences to get to the
|
|
34
|
+
# # desired state:
|
|
35
|
+
# difference = newer_john - john
|
|
36
|
+
# newer_john_again = john.replay(difference)
|
|
37
|
+
#
|
|
38
|
+
module Aversion
|
|
39
|
+
# Public: When we include Aversion, we override .new with an immutable
|
|
40
|
+
# constructor and provide a .new_mutable version.
|
|
41
|
+
def self.included(base)
|
|
42
|
+
base.class_eval do
|
|
43
|
+
# Public: Initializes an immutable instance.
|
|
44
|
+
def self.new(*args)
|
|
45
|
+
new_mutable(*args).freeze
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Public: Initializes a mutable instance.
|
|
49
|
+
def self.new_mutable(*args)
|
|
50
|
+
allocate.tap do |instance|
|
|
51
|
+
instance.send :initialize, *args
|
|
52
|
+
instance.instance_eval do
|
|
53
|
+
@transformations = []
|
|
54
|
+
@initial_args = args
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Public: Returns a mutable version of the object, in case anyone needs it. We
|
|
62
|
+
# do need it internally to perform transformations.
|
|
63
|
+
def mutable
|
|
64
|
+
self.class.new_mutable(*@initial_args).tap do |mutable|
|
|
65
|
+
instance_variables.each do |ivar|
|
|
66
|
+
mutable.instance_variable_set(ivar, instance_variable_get(ivar))
|
|
67
|
+
mutable.instance_variable_set(:@transformations, @transformations.dup)
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Public: The only way to transform state.
|
|
73
|
+
#
|
|
74
|
+
# Returns a new, immutable copy with the transformation applied.
|
|
75
|
+
def transform(&block)
|
|
76
|
+
mutable.tap do |new_instance|
|
|
77
|
+
new_instance.replay([block.dup])
|
|
78
|
+
end.freeze
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Public: Rolls back to a previous version of the state.
|
|
82
|
+
#
|
|
83
|
+
# Returns a new, immutable copy with the previous state.
|
|
84
|
+
def rollback
|
|
85
|
+
self.class.new_mutable(*@initial_args).tap do |instance|
|
|
86
|
+
instance.replay(history[0..-2])
|
|
87
|
+
end.freeze
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# Public: Replays an array of transformations (procs).
|
|
91
|
+
#
|
|
92
|
+
# transformations - the Array of Procs to apply.
|
|
93
|
+
#
|
|
94
|
+
# Returns a new, immutable copy with those transformations applied.
|
|
95
|
+
def replay(transformations)
|
|
96
|
+
(frozen? ? mutable : self).tap do |object|
|
|
97
|
+
transformations.each do |transformation|
|
|
98
|
+
object.history << transformation
|
|
99
|
+
object.instance_eval(&transformation)
|
|
100
|
+
end
|
|
101
|
+
end.freeze
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# Internal: Returns the history of this object.
|
|
105
|
+
def history
|
|
106
|
+
@transformations
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# Internal: Sets the history of this object to a specific array fo
|
|
110
|
+
# transformations.
|
|
111
|
+
def history=(transformations)
|
|
112
|
+
@transformations = transformations
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# Public: Returns the difference between two versioned objects, which is an
|
|
116
|
+
# array of the transformations one lacks from the other.
|
|
117
|
+
def -(other)
|
|
118
|
+
younger, older = [history, other.history].sort { |a,b| a.length <=> b.length }
|
|
119
|
+
difference = (older.length - younger.length) - 1
|
|
120
|
+
older[difference..-1]
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
# Public: Returns whether two versionable objects are equal.
|
|
124
|
+
#
|
|
125
|
+
# They will be equal as long as they have the same initial args with they were
|
|
126
|
+
# constructed with and their history is the same.
|
|
127
|
+
def ==(other)
|
|
128
|
+
@initial_args == other.instance_variable_get(:@initial_args) &&
|
|
129
|
+
history == other.history
|
|
130
|
+
end
|
|
131
|
+
end
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
require 'test_helper'
|
|
2
|
+
|
|
3
|
+
class Person
|
|
4
|
+
include Aversion
|
|
5
|
+
attr_reader :age, :hunger
|
|
6
|
+
|
|
7
|
+
def initialize(age)
|
|
8
|
+
@age = age
|
|
9
|
+
@hunger = 100
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def eat
|
|
13
|
+
transform do
|
|
14
|
+
@hunger -= 5
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
describe Aversion do
|
|
20
|
+
let(:person) { Person.new(20) }
|
|
21
|
+
|
|
22
|
+
describe 'immutability' do
|
|
23
|
+
it 'makes objects immutable' do
|
|
24
|
+
person.frozen?.must_equal true
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
it 'allows for constructing mutable copies' do
|
|
28
|
+
Person.new_mutable(20).frozen?.must_equal false
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
it 'exposes a mutable version of the object' do
|
|
32
|
+
person.mutable.frozen?.must_equal false
|
|
33
|
+
person.age.must_equal 20
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
describe '#transform' do
|
|
38
|
+
it 'returns a new, modified instance' do
|
|
39
|
+
new_person = person.eat
|
|
40
|
+
new_person.hunger.must_equal 95
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
it 'preserves the original object' do
|
|
44
|
+
person.eat
|
|
45
|
+
person.hunger.must_equal 100
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
describe '#rollback' do
|
|
50
|
+
it 'rolls back to a previous state' do
|
|
51
|
+
new_person = person.eat
|
|
52
|
+
new_person.rollback.must_equal person
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
describe 'calculating and applying deltas' do
|
|
57
|
+
let(:new_person) { person.eat }
|
|
58
|
+
let(:newer_person) { new_person.eat }
|
|
59
|
+
let(:even_newer_person) { newer_person.eat }
|
|
60
|
+
|
|
61
|
+
let(:difference) { even_newer_person - new_person }
|
|
62
|
+
|
|
63
|
+
describe '#difference' do
|
|
64
|
+
it 'returns an array of deltas (transformations)' do
|
|
65
|
+
difference.length.must_equal 2
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
describe '#replay' do
|
|
70
|
+
it 'returns an array of deltas (transformations)' do
|
|
71
|
+
new_person.replay(difference).must_equal even_newer_person
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
describe '#==' do
|
|
77
|
+
describe 'when two objects have the same history' do
|
|
78
|
+
describe 'and the same initial args' do
|
|
79
|
+
it 'returns true' do
|
|
80
|
+
(person.eat.rollback == person).must_equal true
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
describe 'but different initial args' do
|
|
85
|
+
it 'returns false' do
|
|
86
|
+
(Person.new(10) == person).must_equal false
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
describe 'when two objects differ in history' do
|
|
92
|
+
it 'returns false' do
|
|
93
|
+
(person.eat == person).must_equal false
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
data/test/test_helper.rb
ADDED
metadata
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: txus-aversion
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.0.1
|
|
5
|
+
prerelease:
|
|
6
|
+
platform: ruby
|
|
7
|
+
authors:
|
|
8
|
+
- Josep M. Bach
|
|
9
|
+
autorequire:
|
|
10
|
+
bindir: bin
|
|
11
|
+
cert_chain: []
|
|
12
|
+
date: 2012-10-20 00:00:00.000000000 Z
|
|
13
|
+
dependencies: []
|
|
14
|
+
description: Make your Ruby objects versionable
|
|
15
|
+
email:
|
|
16
|
+
- josep.m.bach@gmail.com
|
|
17
|
+
executables: []
|
|
18
|
+
extensions: []
|
|
19
|
+
extra_rdoc_files: []
|
|
20
|
+
files:
|
|
21
|
+
- .gitignore
|
|
22
|
+
- Gemfile
|
|
23
|
+
- LICENSE.txt
|
|
24
|
+
- README.md
|
|
25
|
+
- Rakefile
|
|
26
|
+
- aversion.gemspec
|
|
27
|
+
- lib/aversion.rb
|
|
28
|
+
- lib/aversion/version.rb
|
|
29
|
+
- test/aversion_test.rb
|
|
30
|
+
- test/test_helper.rb
|
|
31
|
+
homepage: https://github.com/txus/aversion
|
|
32
|
+
licenses: []
|
|
33
|
+
post_install_message:
|
|
34
|
+
rdoc_options: []
|
|
35
|
+
require_paths:
|
|
36
|
+
- lib
|
|
37
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
38
|
+
none: false
|
|
39
|
+
requirements:
|
|
40
|
+
- - ! '>='
|
|
41
|
+
- !ruby/object:Gem::Version
|
|
42
|
+
version: '0'
|
|
43
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
44
|
+
none: false
|
|
45
|
+
requirements:
|
|
46
|
+
- - ! '>='
|
|
47
|
+
- !ruby/object:Gem::Version
|
|
48
|
+
version: '0'
|
|
49
|
+
requirements: []
|
|
50
|
+
rubyforge_project:
|
|
51
|
+
rubygems_version: 1.8.24
|
|
52
|
+
signing_key:
|
|
53
|
+
specification_version: 3
|
|
54
|
+
summary: Make your Ruby objects versionable
|
|
55
|
+
test_files:
|
|
56
|
+
- test/aversion_test.rb
|
|
57
|
+
- test/test_helper.rb
|