value 1.1.1 → 1.1.2
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/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
|