anima 0.1.0 → 0.3.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: 8829390b1d9de51fddff518775263b3d349298bc
4
- data.tar.gz: b7cfafb8c2c03195da23c1cfe3a26be3d20c93fb
2
+ SHA256:
3
+ metadata.gz: 56e64bf9fa6d98eacb6fbee0c76efd33a60e7493bff45d1672ae5c2f16dcb69d
4
+ data.tar.gz: 560ecd27777601ad003518606d67031875c8cf58f337054d39d89ae1eb5815ee
5
5
  SHA512:
6
- metadata.gz: 9ef1d565f8abe13a9914af0439b2bae68a3350819b240a1dbedf565d50a5c1639696282b676db17ede25d8d82a82aa2355fe7ad2584fbcdd502ee0cddc602769
7
- data.tar.gz: cef667e63a0f638bd8ed31474fce8dde15df03922b1f3d069a4829b328a795e5d906267e09ca1c999cc675084beb77deb556412d784c485d865820cb141e435a
6
+ metadata.gz: 69d90b65d31f7214b28a9b2b70dd6f9f9248adca8c95438d41b172971548fca719bc8421e227a4c21dcb7472c51edecf79103c2bbb9c0f05195bfd8bf44c4d6f
7
+ data.tar.gz: 7cfdcef4ca9bbc3f82e43530885643af1a9c5ea87cc77014c883714aead23ff47091c8265ea3b626914d439e3b5b37417eb01d5051c8c44f436324c154f4d469
data/README.md CHANGED
@@ -1,11 +1,9 @@
1
1
  anima
2
2
  =====
3
3
 
4
- [![Build Status](https://secure.travis-ci.org/mbj/anima.png?branch=master)](http://travis-ci.org/mbj/anima)
5
- [![Dependency Status](https://gemnasium.com/mbj/anima.png)](https://gemnasium.com/mbj/anima)
6
- [![Code Climate](https://codeclimate.com/github/mbj/anima.png)](https://codeclimate.com/github/mbj/anima)
4
+ ![CI](https://github.com/mbj/anima/workflows/CI/badge.svg)
7
5
 
8
- Simple library to declare read only attributes on value-objects that are initialized via attributes hash.
6
+ Simple library to declare read only attributes on value-objects that are initialized via attributes hash.
9
7
 
10
8
  Installation
11
9
  ------------
@@ -15,7 +13,9 @@ Install the gem `anima` via your preferred method.
15
13
  Examples
16
14
  --------
17
15
 
18
- ```
16
+ ```ruby
17
+ require 'anima'
18
+
19
19
  # Definition
20
20
  class Person
21
21
  include Anima.new(:salutation, :firstname, :lastname)
@@ -23,28 +23,30 @@ end
23
23
 
24
24
  # Every day operation
25
25
  a = Person.new(
26
- :salutation => 'Mr',
27
- :firstname => 'Markus',
28
- :lastname => 'Schirp'
26
+ salutation: 'Mr',
27
+ firstname: 'Markus',
28
+ lastname: 'Schirp'
29
29
  )
30
30
 
31
+ # Returns expected values
31
32
  a.salutation # => "Mr"
32
33
  a.firstname # => "Markus"
33
34
  a.lastname # => "Schirp"
34
- a.frozen? # => true
35
+ a.frozen? # => false
35
36
 
36
37
  b = Person.new(
37
- :salutation => 'Mr',
38
- :firstname => 'John',
39
- :lastname => 'Doe'
38
+ salutation: 'Mr',
39
+ firstname: 'John',
40
+ lastname: 'Doe'
40
41
  )
41
42
 
42
- a = Person.new(
43
- :salutation => 'Mr',
44
- :firstname => 'Markus',
45
- :lastname => 'Schirp'
43
+ c = Person.new(
44
+ salutation: 'Mr',
45
+ firstname: 'Markus',
46
+ lastname: 'Schirp'
46
47
  )
47
48
 
49
+ # Equality based on attributes
48
50
  a == b # => false
49
51
  a.eql?(b) # => false
50
52
  a.equal?(b) # => false
@@ -53,19 +55,25 @@ a == c # => true
53
55
  a.eql?(c) # => true
54
56
  a.equal?(c) # => false
55
57
 
56
- # Functional updates
57
- class Person
58
- include Anima::Update
59
- end
60
-
61
- d = b.update(
62
- :salutation => 'Mrs',
63
- :firstname => 'Sue',
58
+ # Functional-style updates
59
+ d = b.with(
60
+ salutation: 'Mrs',
61
+ firstname: 'Sue',
64
62
  )
65
63
 
66
64
  # It returns copies, no inplace modification
67
65
  d.equal?(b) # => false
68
66
 
67
+ # Hash representation
68
+ d.to_h # => { salutation: 'Mrs', firstname: 'Sue', lastname: 'Doe' }
69
+
70
+ # Disallows initialization with incompatible attributes
71
+
72
+ Person.new(
73
+ # :saluatation key missing
74
+ "firstname" => "Markus", # does NOT coerce this by intention
75
+ :lastname => "Schirp"
76
+ ) # raises Anima::Error with message "Person attributes missing: [:salutation, :firstname], unknown: ["firstname"]
69
77
  ```
70
78
 
71
79
  Credits
@@ -1,45 +1,37 @@
1
- require 'backports'
2
1
  require 'adamantium'
3
2
  require 'equalizer'
4
3
  require 'abstract_type'
5
4
 
6
5
  # Main library namespace and mixin
6
+ # @api private
7
7
  class Anima < Module
8
8
  include Adamantium::Flat, Equalizer.new(:attributes)
9
9
 
10
10
  # Return names
11
11
  #
12
- # @return [AttriuteSet]
13
- #
14
- # @api private
15
- #
12
+ # @return [AttributeSet]
16
13
  attr_reader :attributes
17
14
 
18
15
  # Initialize object
19
16
  #
20
17
  # @return [undefined]
21
- #
22
- # @api private
23
- #
24
18
  def initialize(*names)
25
- @attributes = names.uniq.map { |name| Attribute.new(name) }.freeze
19
+ @attributes = names.uniq.map(&Attribute.method(:new)).freeze
26
20
  end
27
21
 
28
- # Return new anima with attributes removed
22
+ # Return new anima with attributes added
29
23
  #
30
24
  # @return [Anima]
31
25
  #
32
26
  # @example
33
27
  # anima = Anima.new(:foo)
34
- # anima.add(:foo) # equals Anima.new(:foo, :bar)
28
+ # anima.add(:bar) # equals Anima.new(:foo, :bar)
35
29
  #
36
- # @api public
37
- #
38
- def remove(*names)
39
- new(attribute_names - names)
30
+ def add(*names)
31
+ new(attribute_names + names)
40
32
  end
41
33
 
42
- # Return new anima with attributes added
34
+ # Return new anima with attributes removed
43
35
  #
44
36
  # @return [Anima]
45
37
  #
@@ -47,10 +39,8 @@ class Anima < Module
47
39
  # anima = Anima.new(:foo, :bar)
48
40
  # anima.remove(:bar) # equals Anima.new(:foo)
49
41
  #
50
- # @api private
51
- #
52
- def add(*names)
53
- new(attribute_names + names)
42
+ def remove(*names)
43
+ new(attribute_names - names)
54
44
  end
55
45
 
56
46
  # Return attributes hash for instance
@@ -58,9 +48,6 @@ class Anima < Module
58
48
  # @param [Object] object
59
49
  #
60
50
  # @return [Hash]
61
- #
62
- # @api private
63
- #
64
51
  def attributes_hash(object)
65
52
  attributes.each_with_object({}) do |attribute, attributes_hash|
66
53
  attributes_hash[attribute.name] = attribute.get(object)
@@ -70,9 +57,6 @@ class Anima < Module
70
57
  # Return attribute names
71
58
  #
72
59
  # @return [Enumerable<Symbol>]
73
- #
74
- # @api private
75
- #
76
60
  def attribute_names
77
61
  attributes.map(&:name)
78
62
  end
@@ -85,124 +69,114 @@ class Anima < Module
85
69
  # @param [Hash] attribute_hash
86
70
  #
87
71
  # @return [self]
88
- #
89
- # @api private
90
- #
91
72
  def initialize_instance(object, attribute_hash)
73
+ assert_known_attributes(object.class, attribute_hash)
92
74
  attributes.each do |attribute|
93
75
  attribute.load(object, attribute_hash)
94
76
  end
77
+ self
78
+ end
95
79
 
96
- overflow = attribute_hash.keys - attribute_names
80
+ # Static instance methods for anima infected classes
81
+ module InstanceMethods
82
+ # Initialize an anima infected object
83
+ #
84
+ # @param [#to_h] attributes
85
+ # a hash that matches anima defined attributes
86
+ #
87
+ # @return [undefined]
88
+ def initialize(attributes)
89
+ self.class.anima.initialize_instance(self, attributes)
90
+ end
97
91
 
98
- unless overflow.empty?
99
- raise Error::Unknown.new(object.class, overflow)
92
+ # Return a hash representation of an anima infected object
93
+ #
94
+ # @example
95
+ # anima.to_h # => { :foo => : bar }
96
+ #
97
+ # @return [Hash]
98
+ #
99
+ # @api public
100
+ def to_h
101
+ self.class.anima.attributes_hash(self)
100
102
  end
101
103
 
102
- self
103
- end
104
+ # Return updated instance
105
+ #
106
+ # @example
107
+ # klass = Class.new do
108
+ # include Anima.new(:foo, :bar)
109
+ # end
110
+ #
111
+ # foo = klass.new(:foo => 1, :bar => 2)
112
+ # updated = foo.with(:foo => 3)
113
+ # updated.foo # => 3
114
+ # updated.bar # => 2
115
+ #
116
+ # @param [Hash] attributes
117
+ #
118
+ # @return [Anima]
119
+ #
120
+ # @api public
121
+ def with(attributes)
122
+ self.class.new(to_h.update(attributes))
123
+ end
124
+ end # InstanceMethods
104
125
 
105
- private
126
+ private
106
127
 
107
- # Hook called when module is included
128
+ # Infect the instance with anima
108
129
  #
109
130
  # @param [Class, Module] scope
110
131
  #
111
132
  # @return [undefined]
112
- #
113
- # @api private
114
- #
115
- def included(scope)
116
- define_anima_method(scope)
117
- define_initializer(scope)
118
- define_attribute_readers(scope)
119
- define_attribute_hash_reader(scope)
120
- define_equalizer(scope)
121
- end
133
+ def included(descendant)
134
+ descendant.instance_exec(self, attribute_names) do |anima, names|
135
+ # Define anima method
136
+ define_singleton_method(:anima) { anima }
122
137
 
123
- # Return new instance
124
- #
125
- # @param [Enumerable<Symbol>] attributes
126
- #
127
- # @return [Anima]
128
- #
129
- # @api private
130
- #
131
- def new(attributes)
132
- self.class.new(*attributes)
133
- end
138
+ # Define instance methods
139
+ include InstanceMethods
134
140
 
135
- # Define anima method on scope
136
- #
137
- # @param [Class, Module] scope
138
- #
139
- # @return [undefined]
140
- #
141
- # @api private
142
- #
143
- def define_anima_method(scope)
144
- anima = self
141
+ # Define attribute readers
142
+ attr_reader(*names)
145
143
 
146
- scope.define_singleton_method(:anima) do
147
- anima
144
+ # Define equalizer
145
+ include Equalizer.new(*names)
148
146
  end
149
147
  end
150
148
 
151
- # Define equalizer on scope
152
- #
153
- # @param [Class, Module] scope
154
- #
155
- # @return [undefined]
149
+ # Fail unless keys in +attribute_hash+ matches #attribute_names
156
150
  #
157
- # @api private
158
- #
159
- def define_equalizer(scope)
160
- scope.send(:include, Equalizer.new(*attribute_names))
161
- end
162
-
163
- # Define attribute readers
151
+ # @param [Class] klass
152
+ # the class being initialized
164
153
  #
165
- # @param [Class, Module] scope
154
+ # @param [Hash] attribute_hash
155
+ # the attributes to initialize +object+ with
166
156
  #
167
157
  # @return [undefined]
168
158
  #
169
- # @api private
170
- #
171
- def define_attribute_readers(scope)
172
- attributes.each do |attribute|
173
- attribute.define_reader(scope)
174
- end
175
- end
159
+ # @raise [Error]
160
+ def assert_known_attributes(klass, attribute_hash)
161
+ keys = attribute_hash.keys
176
162
 
177
- # Define initializer
178
- #
179
- # @param [Class, Module] scope
180
- #
181
- # @return [undefined]
182
- #
183
- # @api private
184
- #
185
- def define_initializer(scope)
186
- scope.send(:define_method, :initialize) do |attributes|
187
- self.class.anima.initialize_instance(self, attributes)
163
+ unknown = keys - attribute_names
164
+ missing = attribute_names - keys
165
+
166
+ unless unknown.empty? && missing.empty?
167
+ fail Error.new(klass, missing, unknown)
188
168
  end
189
169
  end
190
170
 
191
- # Define attribute hash reader
192
- #
193
- # @param [Class, Module] scope
194
- #
195
- # @return [undefined]
171
+ # Return new instance
196
172
  #
197
- # @api private
173
+ # @param [Enumerable<Symbol>] attributes
198
174
  #
199
- def define_attribute_hash_reader(scope)
200
- scope.define_singleton_method(:attributes_hash) do |object|
201
- anima.attributes_hash(object)
202
- end
175
+ # @return [Anima]
176
+ def new(attributes)
177
+ self.class.new(*attributes)
203
178
  end
204
- end
179
+ end # Anima
205
180
 
206
181
  require 'anima/error'
207
182
  require 'anima/attribute'
208
- require 'anima/update'
@@ -1,5 +1,4 @@
1
1
  class Anima
2
-
3
2
  # An attribute
4
3
  class Attribute
5
4
  include Adamantium::Flat, Equalizer.new(:name)
@@ -7,38 +6,28 @@ class Anima
7
6
  # Initialize attribute
8
7
  #
9
8
  # @param [Symbol] name
10
- #
11
- # @api private
12
- #
13
9
  def initialize(name)
14
- @name = name
10
+ @name, @instance_variable_name = name, :"@#{name}"
15
11
  end
16
12
 
17
13
  # Return attribute name
18
14
  #
19
15
  # @return [Symbol]
20
- #
21
- # @api private
22
- #
23
16
  attr_reader :name
24
17
 
18
+ # Return instance variable name
19
+ #
20
+ # @return [Symbol]
21
+ attr_reader :instance_variable_name
22
+
25
23
  # Load attribute
26
24
  #
27
25
  # @param [Object] object
28
26
  # @param [Hash] attributes
29
27
  #
30
28
  # @return [self]
31
- #
32
- # @api private
33
- #
34
29
  def load(object, attributes)
35
- attribute_name = name
36
-
37
- value = attributes.fetch(attribute_name) do
38
- raise Error::Missing.new(object.class, attribute_name)
39
- end
40
-
41
- set(object, value)
30
+ set(object, attributes.fetch(name))
42
31
  end
43
32
 
44
33
  # Get attribute value from object
@@ -46,9 +35,6 @@ class Anima
46
35
  # @param [Object] object
47
36
  #
48
37
  # @return [Object]
49
- #
50
- # @api private
51
- #
52
38
  def get(object)
53
39
  object.public_send(name)
54
40
  end
@@ -59,39 +45,10 @@ class Anima
59
45
  # @param [Object] value
60
46
  #
61
47
  # @return [self]
62
- #
63
- # @api private
64
- #
65
48
  def set(object, value)
66
49
  object.instance_variable_set(instance_variable_name, value)
67
50
 
68
51
  self
69
52
  end
70
-
71
- # Return instance variable name
72
- #
73
- # @return [Symbol]
74
- # returns @ prefixed name
75
- #
76
- # @api private
77
- #
78
- def instance_variable_name
79
- :"@#{name}"
80
- end
81
- memoize :instance_variable_name
82
-
83
- # Define reader
84
- #
85
- # @param [Class, Module] scope
86
- #
87
- # @return [self]
88
- #
89
- # @api private
90
- #
91
- def define_reader(scope)
92
- scope.send(:attr_reader, name)
93
- self
94
- end
95
-
96
53
  end # Attribute
97
54
  end # Anima