anima 0.1.0 → 0.3.2

Sign up to get free protection for your applications and to get access to all the features.
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