value 1.1.1 → 1.1.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/README +163 -1
- data/Rakefile +13 -4
- data/lib/value-1.0.rb +91 -28
- data/lib/value-1.0/attributes.rb +102 -0
- data/lib/value-1.0/comparable.rb +19 -0
- data/lib/{value → value-1.0}/version.rb +7 -8
- data/test/unit/{value/values.rb → value-1.0/attributes.rb} +0 -0
- data/test/unit/{value → value-1.0}/comparable.rb +0 -0
- data/test/unit/{value → value-1.0}/version.rb +0 -0
- metadata +225 -35
- data/lib/value/comparable.rb +0 -13
- data/lib/value/values.rb +0 -44
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 668d28c3bd798b898afcc5b782521df80f2a9e5b
|
4
|
+
data.tar.gz: 745278ed7c2d47b1629f7d50cc0fbade7ab424ea
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 5784be59177957109238bdc88aa732d1917e8d633e20cebddba6fe873466b49a92a405a0d5f052d1e3b170722a4c2773d590f9a755f71fde2b3caec142346939
|
7
|
+
data.tar.gz: 8331cbb10dff97c705408169cae3dcf938a6bb52642ca1baa02fb4f7477765a8b5f4b60bc1dffe5033921681138c3672aa0d4c4b964e97a2662794ff1964c825
|
data/README
CHANGED
@@ -1,3 +1,165 @@
|
|
1
1
|
Value
|
2
2
|
|
3
|
-
Value is a library for defining value objects in Ruby.
|
3
|
+
Value is a library for defining immutable value objects in Ruby. A value
|
4
|
+
object is an object whose equality to other objects is determined by its
|
5
|
+
value, not its identity, think dates and amounts of money. A value object
|
6
|
+
should also be immutable, as you don’t want the date “2013-04-22” itself to
|
7
|
+
change but the current date to change from “2013-04-22” to “2013-04-23”.
|
8
|
+
That is, you don’t want entries in a calendar for 2013-04-22 to move to
|
9
|
+
2013-04-23 simply because the current date changes from 2013-04-22 to
|
10
|
+
2013-04-23.
|
11
|
+
|
12
|
+
A value object consists of one or more attributes stored in instance
|
13
|
+
variables. Value sets up an #initialize method for you that let’s you set
|
14
|
+
these attributes, as, value objects being immutable, this’ll be your only
|
15
|
+
chance to do so. Value also adds equality checks ‹#==› and ‹#eql?› (which
|
16
|
+
are themselves equivalent), a ‹#hash› method, a nice ‹#inspect› method, and a
|
17
|
+
protected attribute reader for each attribute. You may of course add any
|
18
|
+
additional methods that your value object will benefit from.
|
19
|
+
|
20
|
+
That’s basically all there’s too it. Let’s now look at using the Value
|
21
|
+
library.
|
22
|
+
|
23
|
+
§ Usage
|
24
|
+
|
25
|
+
You create value object class by invoking ‹#Value› inside the class
|
26
|
+
(module) you wish to make into a value object class. Let’s create a class
|
27
|
+
that represent points on a plane:
|
28
|
+
|
29
|
+
class Point
|
30
|
+
Value :x, :y
|
31
|
+
end
|
32
|
+
|
33
|
+
A ‹Point› is thus a value object consisting of two sub-values ‹x› and ‹y›
|
34
|
+
(the coordinates). Just from invoking ‹#Value›, a ‹Point› object will have
|
35
|
+
a constructor that takes two arguments to set instance variables ‹@x› and
|
36
|
+
‹@y›, equality checks ‹#==› and ‹#eql?› (which are the same), a ‹#hash›
|
37
|
+
method, a nice ‹#inspect› method, and two protected attribute readers ‹#x›
|
38
|
+
and ‹#y›. We can thus already creat ‹Point›s:
|
39
|
+
|
40
|
+
origo = Point.new(0, 0)
|
41
|
+
|
42
|
+
The default of making the attribute readers protected is often good
|
43
|
+
practice, but for a ‹Point› it probably makes sense to be able to access
|
44
|
+
its coordinates:
|
45
|
+
|
46
|
+
class Point
|
47
|
+
public(*attributes)
|
48
|
+
end
|
49
|
+
|
50
|
+
This’ll make all attributes of ‹Point› public. You can of course choose to
|
51
|
+
only make certain attributes public:
|
52
|
+
|
53
|
+
class Point
|
54
|
+
public :x
|
55
|
+
end
|
56
|
+
|
57
|
+
Note that this public is standard Ruby functionality. Adding a method to
|
58
|
+
‹Point› is of course also possible and very much Rubyish:
|
59
|
+
|
60
|
+
class Point
|
61
|
+
def distance(other)
|
62
|
+
Math.sqrt((other.x - x)**2 + (other.y - y)**2)
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
For some value object classes you might want to support optional
|
67
|
+
attributes. This is done by providing a default value for the attribute,
|
68
|
+
like so:
|
69
|
+
|
70
|
+
class Money
|
71
|
+
Value :amount, [:currency, :USD]
|
72
|
+
end
|
73
|
+
|
74
|
+
Here, the ‹currency› attribute will default to ‹:USD›. You can create
|
75
|
+
‹Money› via
|
76
|
+
|
77
|
+
dollars = Money.new(2)
|
78
|
+
|
79
|
+
but also
|
80
|
+
|
81
|
+
kronor = Money.new(2, :SEK)
|
82
|
+
|
83
|
+
All required attributes must come before any optional attributes.
|
84
|
+
|
85
|
+
Splat attributes are also supported:
|
86
|
+
|
87
|
+
class List
|
88
|
+
Value :'*elements'
|
89
|
+
end
|
90
|
+
|
91
|
+
empty = List.new
|
92
|
+
suits = List.new(:spades, :hearts, :diamonds, :clubs)
|
93
|
+
|
94
|
+
Splat attributes are optional.
|
95
|
+
|
96
|
+
Finally, block attributes are also available:
|
97
|
+
|
98
|
+
class Block
|
99
|
+
Value :'&block'
|
100
|
+
end
|
101
|
+
|
102
|
+
block = Block.new{ |e| e * 2 }
|
103
|
+
|
104
|
+
Block attributes are optional.
|
105
|
+
|
106
|
+
Comparison beyond ‹#==› is possible by specifingy the ‹:comparable› option
|
107
|
+
to ‹#Value›, listing one or more attributes that should be included in the
|
108
|
+
comparison:
|
109
|
+
|
110
|
+
class Vector
|
111
|
+
Value :a, :b, :comparable => :a
|
112
|
+
end
|
113
|
+
|
114
|
+
Note that equality (‹#==› and ‹#eql?›) is always defined based on all
|
115
|
+
attributes, regardless of arguments to ‹:comparable›.
|
116
|
+
|
117
|
+
Here we say that comparisons between ‹Vector›s should be made between the
|
118
|
+
values of the ‹a› attribute only. We can also make comparisons between all
|
119
|
+
attributes of a value object:
|
120
|
+
|
121
|
+
class Vector
|
122
|
+
Value :a, :b, :comparable => true
|
123
|
+
end
|
124
|
+
|
125
|
+
To sum things up, let’s use all possible arguments to ‹#Value› at once:
|
126
|
+
|
127
|
+
class Method
|
128
|
+
Value :file, :line, [:name, 'unnamed'], :'*args', :'&block',
|
129
|
+
:comparable => [:file, :line]
|
130
|
+
end
|
131
|
+
|
132
|
+
A ‹Method› consists of file and line information, a possible name, some
|
133
|
+
arguments, possibly a block, and is comparable on the file and line on
|
134
|
+
which they appear.
|
135
|
+
|
136
|
+
Check out the {full API documentation}¹ for a more explicit description,
|
137
|
+
should you need it or should you want to extend it.
|
138
|
+
|
139
|
+
¹ http://disu.se/software/value/api/
|
140
|
+
|
141
|
+
§ Financing
|
142
|
+
|
143
|
+
Currently, most of my time is spent at my day job and in my rather busy
|
144
|
+
private life. Please motivate me to spend time on this piece of software
|
145
|
+
by donating some of your money to this project. Yeah, I realize that
|
146
|
+
requesting money to develop software is a bit, well, capitalistic of me.
|
147
|
+
But please realize that I live in a capitalistic society and I need money
|
148
|
+
to have other people give me the things that I need to continue living
|
149
|
+
under the rules of said society. So, if you feel that this piece of
|
150
|
+
software has helped you out enough to warrant a reward, please PayPal a
|
151
|
+
donation to now@disu.se¹. Thanks! Your support won’t go unnoticed!
|
152
|
+
|
153
|
+
¹ Send a donation:
|
154
|
+
https://www.paypal.com/cgi-bin/webscr?cmd=_donations&business=now%40disu%2ese&item_name=Nikolai%20Weibull%20Software%20Services
|
155
|
+
|
156
|
+
§ Reporting Bugs
|
157
|
+
|
158
|
+
Please report any bugs that you encounter to the {issue tracker}¹.
|
159
|
+
|
160
|
+
¹ See https://github.com/now/value/issues
|
161
|
+
|
162
|
+
§ Authors
|
163
|
+
|
164
|
+
Nikolai Weibull wrote the code, the tests, the manual pages, and this
|
165
|
+
README.
|
data/Rakefile
CHANGED
@@ -1,13 +1,22 @@
|
|
1
1
|
# -*- coding: utf-8 -*-
|
2
2
|
|
3
|
-
require 'inventory
|
4
|
-
require 'lookout/rake-3.0'
|
3
|
+
require 'inventory-rake-1.0'
|
5
4
|
|
6
|
-
load File.expand_path('../lib/value/version.rb', __FILE__)
|
5
|
+
load File.expand_path('../lib/value-1.0/version.rb', __FILE__)
|
7
6
|
|
8
7
|
Inventory::Rake::Tasks.define Value::Version, :gem => proc{ |_, s|
|
9
8
|
s.author = 'Nikolai Weibull'
|
10
9
|
s.email = 'now@bitwi.se'
|
11
10
|
s.homepage = 'https://github.com/now/value'
|
12
11
|
}
|
13
|
-
|
12
|
+
|
13
|
+
Inventory::Rake::Tasks.unless_installing_dependencies do
|
14
|
+
require 'lookout-rake-3.0'
|
15
|
+
Lookout::Rake::Tasks::Test.new
|
16
|
+
|
17
|
+
require 'inventory-rake-tasks-yard-1.0'
|
18
|
+
Inventory::Rake::Tasks::YARD.new do |t|
|
19
|
+
t.options += %w'--plugin yard-heuristics-1.0'
|
20
|
+
t.globals[:source_code_url] = 'https://github.com/now/%s/blob/v%s/%%s#L%%d' % [t.inventory.package, t.inventory]
|
21
|
+
end
|
22
|
+
end
|
data/lib/value-1.0.rb
CHANGED
@@ -1,71 +1,134 @@
|
|
1
1
|
# -*- coding: utf-8 -*-
|
2
2
|
|
3
|
+
# Represents an immutable [value
|
4
|
+
# object](http://en.wikipedia.org/wiki/Value_object) with an {#initialize}
|
5
|
+
# method, equality checks {#==} and {#eql?}, a {#hash} function, and an
|
6
|
+
# {#inspect} method.
|
7
|
+
#
|
8
|
+
# @example A Point Value Object
|
9
|
+
# class Point
|
10
|
+
# Value :x, :y
|
11
|
+
# end
|
12
|
+
#
|
13
|
+
# @example A Point Value Object with Public Accessors
|
14
|
+
# class Point
|
15
|
+
# Value :x, :y
|
16
|
+
# public(*attributes)
|
17
|
+
# end
|
18
|
+
#
|
19
|
+
# @example A Value Object with Optional Attributes
|
20
|
+
# class Money
|
21
|
+
# Value :amount, [:currency, :USD]
|
22
|
+
# end
|
23
|
+
#
|
24
|
+
# @example A Value Object with a Comparable Attribute
|
25
|
+
# class Vector
|
26
|
+
# Value :a, :b, :comparable => :a
|
27
|
+
# end
|
3
28
|
module Value
|
29
|
+
# Creates a new value object, using ARGUMENTS to set up the value’s
|
30
|
+
# {Attributes#required required}, {Attributes#optional optional}, and
|
31
|
+
# {Attributes#splat splat} attributes, and the given block, if given,
|
32
|
+
# if a {Attributes#block block} is desired.
|
33
|
+
#
|
34
|
+
# @param [Object, …] arguments
|
35
|
+
# @raise [ArgumentError] If ARGUMENTS#length is less than the number of
|
36
|
+
# {Attributes#required required} attributes
|
37
|
+
# @raise [ArgumentError] If ARGUMENTS#length is greater than the number of
|
38
|
+
# {Attributes#required required} and {Attributes#optional optional}
|
39
|
+
# attributes
|
40
|
+
# @yield [?]
|
4
41
|
def initialize(*arguments)
|
5
42
|
raise ArgumentError,
|
6
43
|
'wrong number of arguments (%d for %d)' %
|
7
|
-
[arguments.length,
|
8
|
-
arguments.length <
|
44
|
+
[arguments.length, attributes.required.length] if
|
45
|
+
arguments.length < attributes.required.length
|
9
46
|
raise ArgumentError,
|
10
47
|
'wrong number of arguments (%d for %d)' %
|
11
48
|
[arguments.length,
|
12
|
-
|
13
|
-
arguments.length >
|
14
|
-
|
15
|
-
instance_variable_set :"@#{
|
16
|
-
block_given? ? Proc.new : nil if
|
17
|
-
instance_variable_set :"@#{
|
18
|
-
arguments[
|
19
|
-
|
20
|
-
|
49
|
+
attributes.required.length + attributes.optional.length] if
|
50
|
+
arguments.length > attributes.required.length +
|
51
|
+
attributes.optional.length and not attributes.splat
|
52
|
+
instance_variable_set :"@#{attributes.block}",
|
53
|
+
block_given? ? Proc.new : nil if attributes.block
|
54
|
+
instance_variable_set :"@#{attributes.splat}",
|
55
|
+
arguments[attributes.required.length +
|
56
|
+
attributes.optional.length..-1] if attributes.splat
|
57
|
+
attributes.required.each_with_index do |value, index|
|
21
58
|
instance_variable_set :"@#{value}", arguments[index]
|
22
59
|
end
|
23
|
-
|
24
|
-
offset =
|
60
|
+
attributes.optional.each_with_index do |(value, default), index|
|
61
|
+
offset = attributes.required.length + index
|
25
62
|
instance_variable_set :"@#{value}",
|
26
63
|
offset >= arguments.length ? default : arguments[offset]
|
27
64
|
end
|
28
65
|
end
|
29
66
|
|
67
|
+
# @return [Boolean] True if the receiver’s class and all of its {Attributes}
|
68
|
+
# `#==` those of OTHER
|
30
69
|
def ==(other)
|
31
|
-
self.class == other.class and
|
32
|
-
values.all?{ |value| send(value) == other.send(value) }
|
70
|
+
self.class == other.class and attributes.all?{ |e| send(e) == other.send(e) }
|
33
71
|
end
|
34
72
|
|
73
|
+
# (see #==)
|
35
74
|
alias eql? ==
|
36
75
|
|
76
|
+
# @return [Fixnum] The hash value of the receiver’s class XORed with the hash
|
77
|
+
# value of the Array of the values of the {Attributes} of the receiver
|
37
78
|
def hash
|
38
|
-
self.class.hash ^
|
79
|
+
self.class.hash ^ attributes.map{ |e| send(e) }.hash
|
39
80
|
end
|
40
81
|
|
82
|
+
# @return [String] The inspection of the receiver
|
41
83
|
def inspect
|
42
84
|
'%s.new(%s)' %
|
43
85
|
[self.class,
|
44
|
-
[
|
45
|
-
|
86
|
+
[attributes.required.map{ |name| send(name).inspect },
|
87
|
+
attributes.optional.
|
46
88
|
map{ |name, default| [send(name), default] }.
|
47
|
-
select{ |value, default| value != default or
|
89
|
+
select{ |value, default| value != default or attributes.splat }.
|
48
90
|
map{ |value, _| value },
|
49
|
-
|
50
|
-
|
91
|
+
attributes.splat ? send(attributes.splat).map{ |value| value.inspect } : [],
|
92
|
+
attributes.block ? ['&%s' % [send(attributes.block).inspect]] : []].
|
51
93
|
flatten.join(', ')]
|
52
94
|
end
|
53
95
|
|
54
|
-
|
96
|
+
private
|
97
|
+
|
98
|
+
def values
|
99
|
+
warn "%s#%s: deprecated, use #attributes" % [self.class, __method__]
|
100
|
+
attributes
|
101
|
+
end
|
102
|
+
|
103
|
+
load File.expand_path('../value-1.0/version.rb', __FILE__)
|
55
104
|
Version.load
|
56
105
|
end
|
57
106
|
|
107
|
+
# Ruby’s Module class, extended to add {Module#Value #Value}, a way of creating
|
108
|
+
# value object classes.
|
58
109
|
class Module
|
110
|
+
# @overload Value(first, *rest, options = {})
|
111
|
+
#
|
112
|
+
# Marks the module as a {::Value} object with attributes FIRST and REST.
|
113
|
+
# This’ll add protected attribute readers for each of these attributes and
|
114
|
+
# a private method #attributes that returns the {Value::Attributes}
|
115
|
+
# themselves, include {Value::Comparable} if COMPARABLE isn’t falsy, and
|
116
|
+
# finally include {::Value}.
|
117
|
+
#
|
118
|
+
# @param (see ::Value::Attributes#initialize)
|
119
|
+
# @option (see ::Value::Attributes#initialize)
|
120
|
+
# @raise (see ::Value::Attributes#initialize)
|
59
121
|
def Value(first, *rest)
|
60
122
|
options = Hash === rest.last ? rest.pop : {}
|
61
|
-
|
62
|
-
attr_reader(*
|
63
|
-
protected(*
|
64
|
-
define_method :
|
65
|
-
|
123
|
+
attributes = Value::Attributes.new(first, *rest.dup.push({:comparable => options[:comparable]}))
|
124
|
+
attr_reader(*attributes)
|
125
|
+
protected(*attributes)
|
126
|
+
define_method :attributes do
|
127
|
+
attributes
|
66
128
|
end
|
67
|
-
private :
|
129
|
+
private :attributes
|
68
130
|
include Value::Comparable if options[:comparable]
|
69
131
|
include Value
|
132
|
+
self
|
70
133
|
end
|
71
134
|
end
|
@@ -0,0 +1,102 @@
|
|
1
|
+
# -*- coding: utf-8 -*-
|
2
|
+
|
3
|
+
# Keeps track of the structure of the attributes associated with a {Value}
|
4
|
+
# object. This is an ordered set of {#required}, {#optional}, {#splat}, and
|
5
|
+
# {#block} arguments to the value object’s {Value#initialize initialize}
|
6
|
+
# method, some or all of which may be {#comparable}.
|
7
|
+
class Value::Attributes
|
8
|
+
include Enumerable
|
9
|
+
|
10
|
+
# @overload initialize(first, *rest, options = {})
|
11
|
+
#
|
12
|
+
# Sets up a {Value} objects attributes FIRST and REST. If the last
|
13
|
+
# attribute starts with ‘&’, then the value object’s {Value#initialize
|
14
|
+
# initialize} method may take an optional block. If the last or second to
|
15
|
+
# last, depending on the previous sentence, attribute starts with ‘*’, then
|
16
|
+
# the value object’s {Value#initialize initialize} method may take a splat
|
17
|
+
# argument. The rest of the attributes are split at the first element
|
18
|
+
# that’s an Array. All attributes before this element are required
|
19
|
+
# arguments to the value object’s {Value#initialize initialize} method.
|
20
|
+
# The first element that’s an Array and all following elements, which
|
21
|
+
# should also be Arrays, are optional arguments to the value object’s
|
22
|
+
# {Value#initialize initialize} method, where the first element of these
|
23
|
+
# Arrays is the attribute and the last element is its default value.
|
24
|
+
#
|
25
|
+
# @param [Symbol, Array<Symbol, Object>] first
|
26
|
+
# @param [Array<Symbol, Array<Symbol, Object>>] rest
|
27
|
+
# @param [Hash] options
|
28
|
+
# @option options [Array<Symbol>, Boolean, nil] :comparable (nil) The
|
29
|
+
# subset of first and rest that should be used for comparing instances of
|
30
|
+
# this value object class, or a truthy value to use the whole set, or a
|
31
|
+
# falsy value to use an empty set
|
32
|
+
# @raise [ArgumentError] If any element of COMPARABLE isn’t an element of
|
33
|
+
# first and rest
|
34
|
+
def initialize(first, *rest)
|
35
|
+
options = Hash === rest.last ? rest.pop : {}
|
36
|
+
names = [first] + rest
|
37
|
+
names.pop if @block = names.last.to_s.start_with?('&') ?
|
38
|
+
names.last.to_s[1..-1].to_sym : nil
|
39
|
+
names.pop if @splat = (names.last and names.last.to_s.start_with?('*')) ?
|
40
|
+
names.last.to_s[1..-1].to_sym : nil
|
41
|
+
@required = names.take_while{ |e| not(Array === e) }.map(&:to_sym)
|
42
|
+
@optional = names[required.length..-1].map{ |e| [e.first.to_sym, e.last] }
|
43
|
+
all = to_a
|
44
|
+
@comparable =
|
45
|
+
case options[:comparable]
|
46
|
+
when Array
|
47
|
+
options[:comparable].each{ |e|
|
48
|
+
raise ArgumentError, '%p is not among comparable members %s' %
|
49
|
+
[e, all.map(&:inspect).join(', ')] unless all.include?(e)
|
50
|
+
}
|
51
|
+
when false, nil
|
52
|
+
[]
|
53
|
+
else
|
54
|
+
all
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
# @overload
|
59
|
+
# Enumerates the attributes.
|
60
|
+
#
|
61
|
+
# @yieldparam [Symbol] attribute
|
62
|
+
# @overload
|
63
|
+
# @return [Enumerator<Symbol>] An Enumerator over the attributes
|
64
|
+
def each
|
65
|
+
return enum_for(__method__) unless block_given?
|
66
|
+
required.each do |name|
|
67
|
+
yield name
|
68
|
+
end
|
69
|
+
optional.each do |name, _|
|
70
|
+
yield name
|
71
|
+
end
|
72
|
+
yield splat if splat
|
73
|
+
yield block if block
|
74
|
+
self
|
75
|
+
end
|
76
|
+
|
77
|
+
# @return [Boolean] True if the receiver’s class and its required, optional,
|
78
|
+
# splat, and block attributes `#==` those of OTHER
|
79
|
+
def ==(other)
|
80
|
+
self.class == other.class and
|
81
|
+
required == other.required and
|
82
|
+
optional == other.optional and
|
83
|
+
splat == other.splat and
|
84
|
+
block == other.block
|
85
|
+
end
|
86
|
+
|
87
|
+
# @return [Array<Symbol>] The required attributes
|
88
|
+
attr_reader :required
|
89
|
+
|
90
|
+
# @return [Array<Array<Symbol, Object>>] The optional attributes and their
|
91
|
+
# defaults
|
92
|
+
attr_reader :optional
|
93
|
+
|
94
|
+
# @return [Symbol, nil] The splat attribute
|
95
|
+
attr_reader :splat
|
96
|
+
|
97
|
+
# @return [Symbol, nil] The block attribute
|
98
|
+
attr_reader :block
|
99
|
+
|
100
|
+
# @return [Array<Symbol>] The comparable attributes
|
101
|
+
attr_reader :comparable
|
102
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
# -*- coding: utf-8 -*-
|
2
|
+
|
3
|
+
# Module included by {Module#Value} when passed the :comparable option.
|
4
|
+
module Value::Comparable
|
5
|
+
include ::Comparable
|
6
|
+
|
7
|
+
# @return [Integer, nil] The first non-zero comparison of the
|
8
|
+
# {Attributes#comparable} values of the receiver with those of OTHER (zero
|
9
|
+
# if they’re all equal), or nil if the #class of the receiver doesn’t `#==`
|
10
|
+
# that of OTHER
|
11
|
+
def <=>(other)
|
12
|
+
return nil unless self.class == other.class
|
13
|
+
v = nil
|
14
|
+
(attributes.comparable.any?{ |e| v = (send(e) <=> other.send(e)).nonzero? } and v) or 0
|
15
|
+
end
|
16
|
+
|
17
|
+
# (see #==)
|
18
|
+
alias eql? ==
|
19
|
+
end
|
@@ -3,21 +3,20 @@
|
|
3
3
|
require 'inventory-1.0'
|
4
4
|
|
5
5
|
module Value
|
6
|
-
Version = Inventory.new(1, 1,
|
6
|
+
Version = Inventory.new(1, 1, 2){
|
7
7
|
def dependencies
|
8
8
|
super + Inventory::Dependencies.new{
|
9
|
-
development 'inventory-rake', 1,
|
9
|
+
development 'inventory-rake', 1, 4, 0
|
10
10
|
development 'lookout', 3, 0, 0
|
11
11
|
development 'lookout-rake', 3, 0, 0
|
12
|
-
development 'yard', 0,
|
12
|
+
development 'yard', 0, 8, 0
|
13
|
+
development 'yard-heuristics', 1, 1, 0
|
13
14
|
}
|
14
15
|
end
|
15
16
|
|
16
|
-
def
|
17
|
-
%w
|
18
|
-
|
19
|
-
value/values.rb
|
20
|
-
'
|
17
|
+
def package_libs
|
18
|
+
%w[attributes.rb
|
19
|
+
comparable.rb]
|
21
20
|
end
|
22
21
|
}
|
23
22
|
end
|
File without changes
|
File without changes
|
File without changes
|
metadata
CHANGED
@@ -1,86 +1,278 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: value
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.1.
|
5
|
-
prerelease:
|
4
|
+
version: 1.1.2
|
6
5
|
platform: ruby
|
7
6
|
authors:
|
8
7
|
- Nikolai Weibull
|
9
8
|
autorequire:
|
10
9
|
bindir: bin
|
11
10
|
cert_chain: []
|
12
|
-
date:
|
11
|
+
date: 2013-04-30 00:00:00.000000000 Z
|
13
12
|
dependencies:
|
14
13
|
- !ruby/object:Gem::Dependency
|
15
14
|
name: inventory
|
16
|
-
requirement:
|
17
|
-
none: false
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
18
16
|
requirements:
|
19
17
|
- - ~>
|
20
18
|
- !ruby/object:Gem::Version
|
21
|
-
version: '1.
|
19
|
+
version: '1.4'
|
22
20
|
type: :runtime
|
23
21
|
prerelease: false
|
24
|
-
version_requirements:
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ~>
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '1.4'
|
25
27
|
- !ruby/object:Gem::Dependency
|
26
28
|
name: inventory-rake
|
27
|
-
requirement:
|
28
|
-
none: false
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
29
30
|
requirements:
|
30
31
|
- - ~>
|
31
32
|
- !ruby/object:Gem::Version
|
32
|
-
version: '1.
|
33
|
+
version: '1.4'
|
33
34
|
type: :development
|
34
35
|
prerelease: false
|
35
|
-
version_requirements:
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ~>
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '1.4'
|
36
41
|
- !ruby/object:Gem::Dependency
|
37
42
|
name: lookout
|
38
|
-
requirement:
|
39
|
-
none: false
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
40
44
|
requirements:
|
41
45
|
- - ~>
|
42
46
|
- !ruby/object:Gem::Version
|
43
47
|
version: '3.0'
|
44
48
|
type: :development
|
45
49
|
prerelease: false
|
46
|
-
version_requirements:
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ~>
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '3.0'
|
47
55
|
- !ruby/object:Gem::Dependency
|
48
56
|
name: lookout-rake
|
49
|
-
requirement:
|
50
|
-
none: false
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
51
58
|
requirements:
|
52
59
|
- - ~>
|
53
60
|
- !ruby/object:Gem::Version
|
54
61
|
version: '3.0'
|
55
62
|
type: :development
|
56
63
|
prerelease: false
|
57
|
-
version_requirements:
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - ~>
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '3.0'
|
58
69
|
- !ruby/object:Gem::Dependency
|
59
70
|
name: yard
|
60
|
-
requirement:
|
61
|
-
none: false
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
62
72
|
requirements:
|
63
73
|
- - ~>
|
64
74
|
- !ruby/object:Gem::Version
|
65
|
-
version: 0.
|
75
|
+
version: 0.8.0
|
66
76
|
type: :development
|
67
77
|
prerelease: false
|
68
|
-
version_requirements:
|
69
|
-
|
70
|
-
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - ~>
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: 0.8.0
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: yard-heuristics
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - ~>
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '1.1'
|
90
|
+
type: :development
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - ~>
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '1.1'
|
97
|
+
description: |2
|
98
|
+
Value
|
99
|
+
|
100
|
+
Value is a library for defining immutable value objects in Ruby. A value
|
101
|
+
object is an object whose equality to other objects is determined by its
|
102
|
+
value, not its identity, think dates and amounts of money. A value object
|
103
|
+
should also be immutable, as you don’t want the date “2013-04-22” itself to
|
104
|
+
change but the current date to change from “2013-04-22” to “2013-04-23”.
|
105
|
+
That is, you don’t want entries in a calendar for 2013-04-22 to move to
|
106
|
+
2013-04-23 simply because the current date changes from 2013-04-22 to
|
107
|
+
2013-04-23.
|
108
|
+
|
109
|
+
A value object consists of one or more attributes stored in instance
|
110
|
+
variables. Value sets up an #initialize method for you that let’s you set
|
111
|
+
these attributes, as, value objects being immutable, this’ll be your only
|
112
|
+
chance to do so. Value also adds equality checks ‹#==› and ‹#eql?› (which
|
113
|
+
are themselves equivalent), a ‹#hash› method, a nice ‹#inspect› method, and a
|
114
|
+
protected attribute reader for each attribute. You may of course add any
|
115
|
+
additional methods that your value object will benefit from.
|
116
|
+
|
117
|
+
That’s basically all there’s too it. Let’s now look at using the Value
|
118
|
+
library.
|
119
|
+
|
120
|
+
§ Usage
|
121
|
+
|
122
|
+
You create value object class by invoking ‹#Value› inside the class
|
123
|
+
(module) you wish to make into a value object class. Let’s create a class
|
124
|
+
that represent points on a plane:
|
125
|
+
|
126
|
+
class Point
|
127
|
+
Value :x, :y
|
128
|
+
end
|
129
|
+
|
130
|
+
A ‹Point› is thus a value object consisting of two sub-values ‹x› and ‹y›
|
131
|
+
(the coordinates). Just from invoking ‹#Value›, a ‹Point› object will have
|
132
|
+
a constructor that takes two arguments to set instance variables ‹@x› and
|
133
|
+
‹@y›, equality checks ‹#==› and ‹#eql?› (which are the same), a ‹#hash›
|
134
|
+
method, a nice ‹#inspect› method, and two protected attribute readers ‹#x›
|
135
|
+
and ‹#y›. We can thus already creat ‹Point›s:
|
136
|
+
|
137
|
+
origo = Point.new(0, 0)
|
138
|
+
|
139
|
+
The default of making the attribute readers protected is often good
|
140
|
+
practice, but for a ‹Point› it probably makes sense to be able to access
|
141
|
+
its coordinates:
|
142
|
+
|
143
|
+
class Point
|
144
|
+
public(*attributes)
|
145
|
+
end
|
146
|
+
|
147
|
+
This’ll make all attributes of ‹Point› public. You can of course choose to
|
148
|
+
only make certain attributes public:
|
149
|
+
|
150
|
+
class Point
|
151
|
+
public :x
|
152
|
+
end
|
153
|
+
|
154
|
+
Note that this public is standard Ruby functionality. Adding a method to
|
155
|
+
‹Point› is of course also possible and very much Rubyish:
|
156
|
+
|
157
|
+
class Point
|
158
|
+
def distance(other)
|
159
|
+
Math.sqrt((other.x - x)**2 + (other.y - y)**2)
|
160
|
+
end
|
161
|
+
end
|
162
|
+
|
163
|
+
For some value object classes you might want to support optional
|
164
|
+
attributes. This is done by providing a default value for the attribute,
|
165
|
+
like so:
|
166
|
+
|
167
|
+
class Money
|
168
|
+
Value :amount, [:currency, :USD]
|
169
|
+
end
|
170
|
+
|
171
|
+
Here, the ‹currency› attribute will default to ‹:USD›. You can create
|
172
|
+
‹Money› via
|
173
|
+
|
174
|
+
dollars = Money.new(2)
|
175
|
+
|
176
|
+
but also
|
177
|
+
|
178
|
+
kronor = Money.new(2, :SEK)
|
179
|
+
|
180
|
+
All required attributes must come before any optional attributes.
|
181
|
+
|
182
|
+
Splat attributes are also supported:
|
183
|
+
|
184
|
+
class List
|
185
|
+
Value :'*elements'
|
186
|
+
end
|
187
|
+
|
188
|
+
empty = List.new
|
189
|
+
suits = List.new(:spades, :hearts, :diamonds, :clubs)
|
190
|
+
|
191
|
+
Splat attributes are optional.
|
192
|
+
|
193
|
+
Finally, block attributes are also available:
|
194
|
+
|
195
|
+
class Block
|
196
|
+
Value :'&block'
|
197
|
+
end
|
198
|
+
|
199
|
+
block = Block.new{ |e| e * 2 }
|
200
|
+
|
201
|
+
Block attributes are optional.
|
202
|
+
|
203
|
+
Comparison beyond ‹#==› is possible by specifingy the ‹:comparable› option
|
204
|
+
to ‹#Value›, listing one or more attributes that should be included in the
|
205
|
+
comparison:
|
206
|
+
|
207
|
+
class Vector
|
208
|
+
Value :a, :b, :comparable => :a
|
209
|
+
end
|
210
|
+
|
211
|
+
Note that equality (‹#==› and ‹#eql?›) is always defined based on all
|
212
|
+
attributes, regardless of arguments to ‹:comparable›.
|
213
|
+
|
214
|
+
Here we say that comparisons between ‹Vector›s should be made between the
|
215
|
+
values of the ‹a› attribute only. We can also make comparisons between all
|
216
|
+
attributes of a value object:
|
217
|
+
|
218
|
+
class Vector
|
219
|
+
Value :a, :b, :comparable => true
|
220
|
+
end
|
221
|
+
|
222
|
+
To sum things up, let’s use all possible arguments to ‹#Value› at once:
|
223
|
+
|
224
|
+
class Method
|
225
|
+
Value :file, :line, [:name, 'unnamed'], :'*args', :'&block',
|
226
|
+
:comparable => [:file, :line]
|
227
|
+
end
|
228
|
+
|
229
|
+
A ‹Method› consists of file and line information, a possible name, some
|
230
|
+
arguments, possibly a block, and is comparable on the file and line on
|
231
|
+
which they appear.
|
232
|
+
|
233
|
+
Check out the {full API documentation}¹ for a more explicit description,
|
234
|
+
should you need it or should you want to extend it.
|
235
|
+
|
236
|
+
¹ http://disu.se/software/value/api/
|
237
|
+
|
238
|
+
§ Financing
|
239
|
+
|
240
|
+
Currently, most of my time is spent at my day job and in my rather busy
|
241
|
+
private life. Please motivate me to spend time on this piece of software
|
242
|
+
by donating some of your money to this project. Yeah, I realize that
|
243
|
+
requesting money to develop software is a bit, well, capitalistic of me.
|
244
|
+
But please realize that I live in a capitalistic society and I need money
|
245
|
+
to have other people give me the things that I need to continue living
|
246
|
+
under the rules of said society. So, if you feel that this piece of
|
247
|
+
software has helped you out enough to warrant a reward, please PayPal a
|
248
|
+
donation to now@disu.se¹. Thanks! Your support won’t go unnoticed!
|
249
|
+
|
250
|
+
¹ Send a donation:
|
251
|
+
https://www.paypal.com/cgi-bin/webscr?cmd=_donations&business=now%40disu%2ese&item_name=Nikolai%20Weibull%20Software%20Services
|
252
|
+
|
253
|
+
§ Reporting Bugs
|
254
|
+
|
255
|
+
Please report any bugs that you encounter to the {issue tracker}¹.
|
256
|
+
|
257
|
+
¹ See https://github.com/now/value/issues
|
258
|
+
|
259
|
+
§ Authors
|
260
|
+
|
261
|
+
Nikolai Weibull wrote the code, the tests, the manual pages, and this
|
262
|
+
README.
|
71
263
|
email: now@bitwi.se
|
72
264
|
executables: []
|
73
265
|
extensions: []
|
74
266
|
extra_rdoc_files: []
|
75
267
|
files:
|
76
|
-
- lib/value/
|
77
|
-
- lib/value/
|
268
|
+
- lib/value-1.0/attributes.rb
|
269
|
+
- lib/value-1.0/comparable.rb
|
78
270
|
- lib/value-1.0.rb
|
79
|
-
- lib/value/version.rb
|
80
|
-
- test/unit/value/
|
81
|
-
- test/unit/value/
|
271
|
+
- lib/value-1.0/version.rb
|
272
|
+
- test/unit/value-1.0/attributes.rb
|
273
|
+
- test/unit/value-1.0/comparable.rb
|
82
274
|
- test/unit/value-1.0.rb
|
83
|
-
- test/unit/value/version.rb
|
275
|
+
- test/unit/value-1.0/version.rb
|
84
276
|
- README
|
85
277
|
- Rakefile
|
86
278
|
homepage: https://github.com/now/value
|
@@ -91,21 +283,19 @@ rdoc_options: []
|
|
91
283
|
require_paths:
|
92
284
|
- lib
|
93
285
|
required_ruby_version: !ruby/object:Gem::Requirement
|
94
|
-
none: false
|
95
286
|
requirements:
|
96
|
-
- -
|
287
|
+
- - '>='
|
97
288
|
- !ruby/object:Gem::Version
|
98
289
|
version: '0'
|
99
290
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
100
|
-
none: false
|
101
291
|
requirements:
|
102
|
-
- -
|
292
|
+
- - '>='
|
103
293
|
- !ruby/object:Gem::Version
|
104
294
|
version: '0'
|
105
295
|
requirements: []
|
106
296
|
rubyforge_project:
|
107
|
-
rubygems_version:
|
297
|
+
rubygems_version: 2.0.0
|
108
298
|
signing_key:
|
109
299
|
specification_version: 4
|
110
|
-
summary: Value is a library for defining value objects in Ruby.
|
300
|
+
summary: Value is a library for defining immutable value objects in Ruby.
|
111
301
|
test_files: []
|
data/lib/value/comparable.rb
DELETED
@@ -1,13 +0,0 @@
|
|
1
|
-
# -*- coding: utf-8 -*-
|
2
|
-
|
3
|
-
module Value::Comparable
|
4
|
-
include Comparable
|
5
|
-
|
6
|
-
def <=>(other)
|
7
|
-
return nil unless self.class == other.class
|
8
|
-
v = nil
|
9
|
-
(values.comparable.any?{ |e| v = (send(e) <=> other.send(e)).nonzero? } and v) or 0
|
10
|
-
end
|
11
|
-
|
12
|
-
alias eql? ==
|
13
|
-
end
|
data/lib/value/values.rb
DELETED
@@ -1,44 +0,0 @@
|
|
1
|
-
# -*- coding: utf-8 -*-
|
2
|
-
|
3
|
-
class Value::Values
|
4
|
-
include Enumerable
|
5
|
-
|
6
|
-
def initialize(*values)
|
7
|
-
options = Hash === values.last ? values.pop : {}
|
8
|
-
values.pop if @block = values.last.to_s.start_with?('&') ? values.last.to_s[1..-1].to_sym : nil
|
9
|
-
values.pop if @splat = (values.last and values.last.to_s.start_with?('*')) ?
|
10
|
-
values.last.to_s[1..-1].to_sym : nil
|
11
|
-
required, optional = values.partition{ |e| not(Array === e) }
|
12
|
-
@required = required.map(&:to_sym)
|
13
|
-
@optional = optional.map{ |e| [e.first.to_sym, e.last] }
|
14
|
-
all = to_a
|
15
|
-
@comparable = Array === options[:comparable] ?
|
16
|
-
options[:comparable].each{ |e|
|
17
|
-
raise ArgumentError, '%p is not among comparable members %s' % [e, all.map(&:inspect).join(', ')] unless all.include?(e)
|
18
|
-
} :
|
19
|
-
all
|
20
|
-
end
|
21
|
-
|
22
|
-
def each
|
23
|
-
return enum_for(__method__) unless block_given?
|
24
|
-
required.each do |value|
|
25
|
-
yield value
|
26
|
-
end
|
27
|
-
optional.each do |value, _|
|
28
|
-
yield value
|
29
|
-
end
|
30
|
-
yield splat if splat
|
31
|
-
yield block if block
|
32
|
-
self
|
33
|
-
end
|
34
|
-
|
35
|
-
def ==(other)
|
36
|
-
self.class == other.class and
|
37
|
-
required == other.required and
|
38
|
-
optional == other.optional and
|
39
|
-
splat == other.splat and
|
40
|
-
block == other.block
|
41
|
-
end
|
42
|
-
|
43
|
-
attr_reader :required, :optional, :splat, :block, :comparable
|
44
|
-
end
|