hanami-model 0.0.0 → 0.6.0

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.
Files changed (51) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +145 -0
  3. data/EXAMPLE.md +212 -0
  4. data/LICENSE.md +22 -0
  5. data/README.md +600 -7
  6. data/hanami-model.gemspec +17 -12
  7. data/lib/hanami-model.rb +1 -0
  8. data/lib/hanami/entity.rb +298 -0
  9. data/lib/hanami/entity/dirty_tracking.rb +74 -0
  10. data/lib/hanami/model.rb +204 -2
  11. data/lib/hanami/model/adapters/abstract.rb +281 -0
  12. data/lib/hanami/model/adapters/file_system_adapter.rb +288 -0
  13. data/lib/hanami/model/adapters/implementation.rb +111 -0
  14. data/lib/hanami/model/adapters/memory/collection.rb +132 -0
  15. data/lib/hanami/model/adapters/memory/command.rb +113 -0
  16. data/lib/hanami/model/adapters/memory/query.rb +653 -0
  17. data/lib/hanami/model/adapters/memory_adapter.rb +179 -0
  18. data/lib/hanami/model/adapters/null_adapter.rb +24 -0
  19. data/lib/hanami/model/adapters/sql/collection.rb +287 -0
  20. data/lib/hanami/model/adapters/sql/command.rb +73 -0
  21. data/lib/hanami/model/adapters/sql/console.rb +33 -0
  22. data/lib/hanami/model/adapters/sql/consoles/mysql.rb +49 -0
  23. data/lib/hanami/model/adapters/sql/consoles/postgresql.rb +48 -0
  24. data/lib/hanami/model/adapters/sql/consoles/sqlite.rb +26 -0
  25. data/lib/hanami/model/adapters/sql/query.rb +788 -0
  26. data/lib/hanami/model/adapters/sql_adapter.rb +296 -0
  27. data/lib/hanami/model/coercer.rb +74 -0
  28. data/lib/hanami/model/config/adapter.rb +116 -0
  29. data/lib/hanami/model/config/mapper.rb +45 -0
  30. data/lib/hanami/model/configuration.rb +275 -0
  31. data/lib/hanami/model/error.rb +7 -0
  32. data/lib/hanami/model/mapper.rb +124 -0
  33. data/lib/hanami/model/mapping.rb +48 -0
  34. data/lib/hanami/model/mapping/attribute.rb +85 -0
  35. data/lib/hanami/model/mapping/coercers.rb +314 -0
  36. data/lib/hanami/model/mapping/collection.rb +490 -0
  37. data/lib/hanami/model/mapping/collection_coercer.rb +79 -0
  38. data/lib/hanami/model/migrator.rb +324 -0
  39. data/lib/hanami/model/migrator/adapter.rb +170 -0
  40. data/lib/hanami/model/migrator/connection.rb +133 -0
  41. data/lib/hanami/model/migrator/mysql_adapter.rb +72 -0
  42. data/lib/hanami/model/migrator/postgres_adapter.rb +119 -0
  43. data/lib/hanami/model/migrator/sqlite_adapter.rb +110 -0
  44. data/lib/hanami/model/version.rb +4 -1
  45. data/lib/hanami/repository.rb +872 -0
  46. metadata +100 -16
  47. data/.gitignore +0 -9
  48. data/Gemfile +0 -4
  49. data/Rakefile +0 -2
  50. data/bin/console +0 -14
  51. data/bin/setup +0 -8
@@ -0,0 +1,48 @@
1
+ require 'hanami/model/mapping/collection'
2
+ require 'hanami/model/mapping/collection_coercer'
3
+ require 'hanami/model/mapping/coercers'
4
+
5
+ module Hanami
6
+ module Model
7
+ # Mapping internal utilities
8
+ #
9
+ # @since 0.1.0
10
+ module Mapping
11
+ # Unmapped collection error.
12
+ #
13
+ # It gets raised when the application tries to access to a non-mapped
14
+ # collection.
15
+ #
16
+ # @since 0.1.0
17
+ class UnmappedCollectionError < Hanami::Model::Error
18
+ def initialize(name)
19
+ super("Cannot find collection: #{ name }")
20
+ end
21
+ end
22
+
23
+ # Invalid entity error.
24
+ #
25
+ # It gets raised when the application tries to access to a existing
26
+ # entity.
27
+ #
28
+ # @since 0.2.0
29
+ class EntityNotFound < Hanami::Model::Error
30
+ def initialize(name)
31
+ super("Cannot find class for entity: #{ name }")
32
+ end
33
+ end
34
+
35
+ # Invalid repository error.
36
+ #
37
+ # It gets raised when the application tries to access to a existing
38
+ # repository.
39
+ #
40
+ # @since 0.2.0
41
+ class RepositoryNotFound < Hanami::Model::Error
42
+ def initialize(name)
43
+ super("Cannot find class for repository: #{ name }")
44
+ end
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,85 @@
1
+ require 'hanami/utils/class'
2
+
3
+ module Hanami
4
+ module Model
5
+ module Mapping
6
+ # Mapping attribute
7
+ #
8
+ # @api private
9
+ # @since 0.5.0
10
+ class Attribute
11
+ # @api private
12
+ # @since 0.5.0
13
+ COERCERS_NAMESPACE = "Hanami::Model::Mapping::Coercers".freeze
14
+
15
+ # Initialize a new attribute
16
+ #
17
+ # @param name [#to_sym] attribute name
18
+ # @param coercer [.load, .dump] a coercer
19
+ # @param options [Hash] a set of options
20
+ #
21
+ # @option options [#to_sym] :as Resolve mismatch between database column
22
+ # name and entity attribute name
23
+ #
24
+ # @return [Hanami::Model::Mapping::Attribute]
25
+ #
26
+ # @api private
27
+ # @since 0.5.0
28
+ #
29
+ # @see Hanami::Model::Coercer
30
+ # @see Hanami::Model::Mapping::Coercers
31
+ # @see Hanami::Model::Mapping::Collection#attribute
32
+ def initialize(name, coercer, options)
33
+ @name = name.to_sym
34
+ @coercer = coercer
35
+ @options = options
36
+ end
37
+
38
+ # Returns the mapped name
39
+ #
40
+ # @return [Symbol] the mapped name
41
+ #
42
+ # @api private
43
+ # @since 0.5.0
44
+ #
45
+ # @see Hanami::Model::Mapping::Collection#attribute
46
+ def mapped
47
+ (@options.fetch(:as) { name }).to_sym
48
+ end
49
+
50
+ # @api private
51
+ # @since 0.5.0
52
+ def load_coercer
53
+ "#{ coercer }.load"
54
+ end
55
+
56
+ # @api private
57
+ # @since 0.5.0
58
+ def dump_coercer
59
+ "#{ coercer }.dump"
60
+ end
61
+
62
+ # @api private
63
+ # @since 0.5.0
64
+ def ==(other)
65
+ self.class == other.class &&
66
+ self.name == other.name &&
67
+ self.mapped == other.mapped &&
68
+ self.coercer == other.coercer
69
+ end
70
+
71
+ protected
72
+
73
+ # @api private
74
+ # @since 0.5.0
75
+ attr_reader :name
76
+
77
+ # @api private
78
+ # @since 0.5.0
79
+ def coercer
80
+ Utils::Class.load_from_pattern!("(#{ COERCERS_NAMESPACE }::#{ @coercer }|#{ @coercer })")
81
+ end
82
+ end
83
+ end
84
+ end
85
+ end
@@ -0,0 +1,314 @@
1
+ require 'hanami/model/coercer'
2
+ require 'hanami/utils/kernel'
3
+
4
+ module Hanami
5
+ module Model
6
+ module Mapping
7
+ # Default coercers
8
+ #
9
+ # @since 0.5.0
10
+ # @api private
11
+ module Coercers
12
+ # Array coercer
13
+ #
14
+ # @since 0.5.0
15
+ # @api private
16
+ #
17
+ # @see Hanami::Model::Coercer
18
+ class Array < Coercer
19
+ # Transform a value from the database into a Ruby Array, unless nil
20
+ #
21
+ # @param value [Object] the object to coerce
22
+ #
23
+ # @return [Array] the result of the coercion
24
+ #
25
+ # @raise [TypeError] if the value can't be coerced
26
+ #
27
+ # @since 0.5.0
28
+ # @api private
29
+ #
30
+ # @see Hanami::Model::Coercer.load
31
+ # @see http://ruby-doc.org/core/Kernel.html#method-i-Array
32
+ def self.load(value)
33
+ ::Kernel.Array(value) unless value.nil?
34
+ end
35
+ end
36
+
37
+ # Boolean coercer
38
+ #
39
+ # @since 0.5.0
40
+ # @api private
41
+ #
42
+ # @see Hanami::Model::Coercer
43
+ class Boolean < Coercer
44
+ # Transform a value from the database into a Boolean, unless nil
45
+ #
46
+ # @param value [Object] the object to coerce
47
+ #
48
+ # @return [Boolean] the result of the coercion
49
+ #
50
+ # @raise [TypeError] if the value can't be coerced
51
+ #
52
+ # @since 0.5.0
53
+ # @api private
54
+ #
55
+ # @see Hanami::Model::Coercer.load
56
+ # @see http://rdoc.info/gems/hanami-utils/Hanami/Utils/Kernel#Boolean-class_method
57
+ def self.load(value)
58
+ Utils::Kernel.Boolean(value) unless value.nil?
59
+ end
60
+ end
61
+
62
+ # Date coercer
63
+ #
64
+ # @since 0.5.0
65
+ # @api private
66
+ #
67
+ # @see Hanami::Model::Coercer
68
+ class Date < Coercer
69
+ # Transform a value from the database into a Date, unless nil
70
+ #
71
+ # @param value [Object] the object to coerce
72
+ #
73
+ # @return [Date] the result of the coercion
74
+ #
75
+ # @raise [TypeError] if the value can't be coerced
76
+ #
77
+ # @since 0.5.0
78
+ # @api private
79
+ #
80
+ # @see Hanami::Model::Coercer.load
81
+ # @see http://rdoc.info/gems/hanami-utils/Hanami/Utils/Kernel#Date-class_method
82
+ def self.load(value)
83
+ Utils::Kernel.Date(value) unless value.nil?
84
+ end
85
+ end
86
+
87
+ # DateTime coercer
88
+ #
89
+ # @since 0.5.0
90
+ # @api private
91
+ #
92
+ # @see Hanami::Model::Coercer
93
+ class DateTime < Coercer
94
+ # Transform a value from the database into a DateTime, unless nil
95
+ #
96
+ # @param value [Object] the object to coerce
97
+ #
98
+ # @return [DateTime] the result of the coercion
99
+ #
100
+ # @raise [TypeError] if the value can't be coerced
101
+ #
102
+ # @since 0.5.0
103
+ # @api private
104
+ #
105
+ # @see Hanami::Model::Coercer.load
106
+ # @see http://rdoc.info/gems/hanami-utils/Hanami/Utils/Kernel#DateTime-class_method
107
+ def self.load(value)
108
+ Utils::Kernel.DateTime(value) unless value.nil?
109
+ end
110
+ end
111
+
112
+ # Float coercer
113
+ #
114
+ # @since 0.5.0
115
+ # @api private
116
+ #
117
+ # @see Hanami::Model::Coercer
118
+ class Float < Coercer
119
+ # Transform a value from the database into a Float, unless nil
120
+ #
121
+ # @param value [Object] the object to coerce
122
+ #
123
+ # @return [Float] the result of the coercion
124
+ #
125
+ # @raise [TypeError] if the value can't be coerced
126
+ #
127
+ # @since 0.5.0
128
+ # @api private
129
+ #
130
+ # @see Hanami::Model::Coercer.load
131
+ # @see http://rdoc.info/gems/hanami-utils/Hanami/Utils/Kernel#Float-class_method
132
+ def self.load(value)
133
+ Utils::Kernel.Float(value) unless value.nil?
134
+ end
135
+ end
136
+
137
+ # Hash coercer
138
+ #
139
+ # @since 0.5.0
140
+ # @api private
141
+ #
142
+ # @see Hanami::Model::Coercer
143
+ class Hash < Coercer
144
+ # Transform a value from the database into a Hash, unless nil
145
+ #
146
+ # @param value [Object] the object to coerce
147
+ #
148
+ # @return [Hash] the result of the coercion
149
+ #
150
+ # @raise [TypeError] if the value can't be coerced
151
+ #
152
+ # @since 0.5.0
153
+ # @api private
154
+ #
155
+ # @see Hanami::Model::Coercer.load
156
+ # @see http://rdoc.info/gems/hanami-utils/Hanami/Utils/Kernel#Hash-class_method
157
+ def self.load(value)
158
+ Utils::Kernel.Hash(value) unless value.nil?
159
+ end
160
+ end
161
+
162
+ # Integer coercer
163
+ #
164
+ # @since 0.5.0
165
+ # @api private
166
+ #
167
+ # @see Hanami::Model::Coercer
168
+ class Integer < Coercer
169
+ # Transform a value from the database into a Integer, unless nil
170
+ #
171
+ # @param value [Object] the object to coerce
172
+ #
173
+ # @return [Integer] the result of the coercion
174
+ #
175
+ # @raise [TypeError] if the value can't be coerced
176
+ #
177
+ # @since 0.5.0
178
+ # @api private
179
+ #
180
+ # @see Hanami::Model::Coercer.load
181
+ # @see http://rdoc.info/gems/hanami-utils/Hanami/Utils/Kernel#Integer-class_method
182
+ def self.load(value)
183
+ Utils::Kernel.Integer(value) unless value.nil?
184
+ end
185
+ end
186
+
187
+ # BigDecimal coercer
188
+ #
189
+ # @since 0.5.0
190
+ # @api private
191
+ #
192
+ # @see Hanami::Model::Coercer
193
+ class BigDecimal < Coercer
194
+ # Transform a value from the database into a BigDecimal, unless nil
195
+ #
196
+ # @param value [Object] the object to coerce
197
+ #
198
+ # @return [BigDecimal] the result of the coercion
199
+ #
200
+ # @raise [TypeError] if the value can't be coerced
201
+ #
202
+ # @since 0.5.0
203
+ # @api private
204
+ #
205
+ # @see Hanami::Model::Coercer.load
206
+ # @see http://rdoc.info/gems/hanami-utils/Hanami/Utils/Kernel#BigDecimal-class_method
207
+ def self.load(value)
208
+ Utils::Kernel.BigDecimal(value) unless value.nil?
209
+ end
210
+ end
211
+
212
+ # Set coercer
213
+ #
214
+ # @since 0.5.0
215
+ # @api private
216
+ #
217
+ # @see Hanami::Model::Coercer
218
+ class Set < Coercer
219
+ # Transform a value from the database into a Set, unless nil
220
+ #
221
+ # @param value [Object] the object to coerce
222
+ #
223
+ # @return [Set] the result of the coercion
224
+ #
225
+ # @raise [TypeError] if the value can't be coerced
226
+ #
227
+ # @since 0.5.0
228
+ # @api private
229
+ #
230
+ # @see Hanami::Model::Coercer.load
231
+ # @see http://rdoc.info/gems/hanami-utils/Hanami/Utils/Kernel#Set-class_method
232
+ def self.load(value)
233
+ Utils::Kernel.Set(value) unless value.nil?
234
+ end
235
+ end
236
+
237
+ # String coercer
238
+ #
239
+ # @since 0.5.0
240
+ # @api private
241
+ #
242
+ # @see Hanami::Model::Coercer
243
+ class String < Coercer
244
+ # Transform a value from the database into a String, unless nil
245
+ #
246
+ # @param value [Object] the object to coerce
247
+ #
248
+ # @return [String] the result of the coercion
249
+ #
250
+ # @raise [TypeError] if the value can't be coerced
251
+ #
252
+ # @since 0.5.0
253
+ # @api private
254
+ #
255
+ # @see Hanami::Model::Coercer.load
256
+ # @see http://rdoc.info/gems/hanami-utils/Hanami/Utils/Kernel#String-class_method
257
+ def self.load(value)
258
+ Utils::Kernel.String(value) unless value.nil?
259
+ end
260
+ end
261
+
262
+ # Symbol coercer
263
+ #
264
+ # @since 0.5.0
265
+ # @api private
266
+ #
267
+ # @see Hanami::Model::Coercer
268
+ class Symbol < Coercer
269
+ # Transform a value from the database into a Symbol, unless nil
270
+ #
271
+ # @param value [Object] the object to coerce
272
+ #
273
+ # @return [Symbol] the result of the coercion
274
+ #
275
+ # @raise [TypeError] if the value can't be coerced
276
+ #
277
+ # @since 0.5.0
278
+ # @api private
279
+ #
280
+ # @see Hanami::Model::Coercer.load
281
+ # @see http://rdoc.info/gems/hanami-utils/Hanami/Utils/Kernel#Symbol-class_method
282
+ def self.load(value)
283
+ Utils::Kernel.Symbol(value) unless value.nil?
284
+ end
285
+ end
286
+
287
+ # Time coercer
288
+ #
289
+ # @since 0.5.0
290
+ # @api private
291
+ #
292
+ # @see Hanami::Model::Coercer
293
+ class Time < Coercer
294
+ # Transform a value from the database into a Time, unless nil
295
+ #
296
+ # @param value [Object] the object to coerce
297
+ #
298
+ # @return [Time] the result of the coercion
299
+ #
300
+ # @raise [TypeError] if the value can't be coerced
301
+ #
302
+ # @since 0.5.0
303
+ # @api private
304
+ #
305
+ # @see Hanami::Model::Coercer.load
306
+ # @see http://rdoc.info/gems/hanami-utils/Hanami/Utils/Kernel#Time-class_method
307
+ def self.load(value)
308
+ Utils::Kernel.Time(value) unless value.nil?
309
+ end
310
+ end
311
+ end
312
+ end
313
+ end
314
+ end
@@ -0,0 +1,490 @@
1
+ require 'hanami/utils/class'
2
+ require 'hanami/model/mapping/attribute'
3
+
4
+ module Hanami
5
+ module Model
6
+ module Mapping
7
+ # Maps a collection and its attributes.
8
+ #
9
+ # A collection is a set of homogeneous records. Think of a table of a SQL
10
+ # database or about collection of MongoDB.
11
+ #
12
+ # This is database independent. It can work with SQL, document, and even
13
+ # with key/value stores.
14
+ #
15
+ # @since 0.1.0
16
+ #
17
+ # @see Hanami::Model::Mapper
18
+ #
19
+ # @example
20
+ # require 'hanami/model'
21
+ #
22
+ # mapper = Hanami::Model::Mapper.new do
23
+ # collection :users do
24
+ # entity User
25
+ #
26
+ # attribute :id, Integer
27
+ # attribute :name, String
28
+ # end
29
+ # end
30
+ class Collection
31
+ # Repository name suffix
32
+ #
33
+ # @api private
34
+ # @since 0.1.0
35
+ #
36
+ # @see Hanami::Repository
37
+ REPOSITORY_SUFFIX = 'Repository'.freeze
38
+
39
+ # @attr_reader name [Symbol] the name of the collection
40
+ #
41
+ # @since 0.1.0
42
+ # @api private
43
+ attr_reader :name
44
+
45
+ # @attr_reader coercer_class [Class] the coercer class
46
+ #
47
+ # @since 0.1.0
48
+ # @api private
49
+ attr_reader :coercer_class
50
+
51
+ # @attr_reader attributes [Hash] the set of attributes
52
+ #
53
+ # @since 0.1.0
54
+ # @api private
55
+ attr_reader :attributes
56
+
57
+ # @attr_reader adapter [Hanami::Model::Adapters] the instance of adapter
58
+ #
59
+ # @since 0.1.0
60
+ # @api private
61
+ attr_accessor :adapter
62
+
63
+ # Instantiate a new collection
64
+ #
65
+ # @param name [Symbol] the name of the mapped collection. If used with a
66
+ # SQL database it's the table name.
67
+ #
68
+ # @param coercer_class [Class] the coercer class
69
+ # @param blk [Proc] the block that maps the attributes of that collection.
70
+ #
71
+ # @since 0.1.0
72
+ #
73
+ # @see Hanami::Model::Mapper#collection
74
+ def initialize(name, coercer_class, &blk)
75
+ @name = name
76
+ @coercer_class = coercer_class
77
+ @attributes = {}
78
+ instance_eval(&blk) if block_given?
79
+ end
80
+
81
+ # Defines the entity that is persisted with this collection.
82
+ #
83
+ # The entity can be any kind of object as long as it implements the
84
+ # following interface: `#initialize(attributes = {})`.
85
+ #
86
+ # @param klass [Class, String] the entity persisted with this collection.
87
+ #
88
+ # @since 0.1.0
89
+ #
90
+ # @see Hanami::Entity
91
+ #
92
+ # @example Set entity with class name
93
+ # require 'hanami/model'
94
+ #
95
+ # mapper = Hanami::Model::Mapper.new do
96
+ # collection :articles do
97
+ # entity Article
98
+ # end
99
+ # end
100
+ #
101
+ # mapper.entity #=> Article
102
+ #
103
+ # @example Set entity with class name string
104
+ # require 'hanami/model'
105
+ #
106
+ # mapper = Hanami::Model::Mapper.new do
107
+ # collection :articles do
108
+ # entity 'Article'
109
+ # end
110
+ # end
111
+ #
112
+ # mapper.entity #=> Article
113
+ #
114
+ def entity(klass = nil)
115
+ if klass
116
+ @entity = klass
117
+ else
118
+ @entity
119
+ end
120
+ end
121
+
122
+ # Defines the repository that interacts with this collection.
123
+ #
124
+ # @param klass [Class, String] the repository that interacts with this collection.
125
+ #
126
+ # @since 0.2.0
127
+ #
128
+ # @see Hanami::Repository
129
+ #
130
+ # @example Set repository with class name
131
+ # require 'hanami/model'
132
+ #
133
+ # mapper = Hanami::Model::Mapper.new do
134
+ # collection :articles do
135
+ # entity Article
136
+ #
137
+ # repository RemoteArticleRepository
138
+ # end
139
+ # end
140
+ #
141
+ # mapper.repository #=> RemoteArticleRepository
142
+ #
143
+ # @example Set repository with class name string
144
+ # require 'hanami/model'
145
+ #
146
+ # mapper = Hanami::Model::Mapper.new do
147
+ # collection :articles do
148
+ # entity Article
149
+ #
150
+ # repository 'RemoteArticleRepository'
151
+ # end
152
+ # end
153
+ #
154
+ # mapper.repository #=> RemoteArticleRepository
155
+ def repository(klass = nil)
156
+ if klass
157
+ @repository = klass
158
+ else
159
+ @repository ||= default_repository_klass
160
+ end
161
+ end
162
+
163
+ # Defines the identity for a collection.
164
+ #
165
+ # An identity is a unique value that identifies a record.
166
+ # If used with an SQL table it corresponds to the primary key.
167
+ #
168
+ # This is an optional feature.
169
+ # By default the system assumes that your identity is `:id`.
170
+ # If this is the case, you can omit the value, otherwise you have to
171
+ # specify it.
172
+ #
173
+ # @param name [Symbol] the name of the identity
174
+ #
175
+ # @since 0.1.0
176
+ #
177
+ # @example Default
178
+ # require 'hanami/model'
179
+ #
180
+ # # We have an SQL table `users` with a primary key `id`.
181
+ # #
182
+ # # This this is compliant to the mapper default, we can omit
183
+ # # `#identity`.
184
+ #
185
+ # mapper = Hanami::Model::Mapper.new do
186
+ # collection :users do
187
+ # entity User
188
+ #
189
+ # # attribute definitions..
190
+ # end
191
+ # end
192
+ #
193
+ # @example Custom identity
194
+ # require 'hanami/model'
195
+ #
196
+ # # We have an SQL table `articles` with a primary key `i_id`.
197
+ # #
198
+ # # This schema diverges from the expected default: `id`, that's why
199
+ # # we need to use #identity to let the mapper to recognize the
200
+ # # primary key.
201
+ #
202
+ # mapper = Hanami::Model::Mapper.new do
203
+ # collection :articles do
204
+ # entity Article
205
+ #
206
+ # # attribute definitions..
207
+ #
208
+ # identity :i_id
209
+ # end
210
+ # end
211
+ def identity(name = nil)
212
+ if name
213
+ @identity = name
214
+ else
215
+ @identity || :id
216
+ end
217
+ end
218
+
219
+ # Map an attribute.
220
+ #
221
+ # An attribute defines a property of an object.
222
+ # This is storage independent. For instance, it can map an SQL column,
223
+ # a MongoDB attribute or everything that makes sense for your database.
224
+ #
225
+ # Each attribute defines a Ruby type, to coerce that value from the
226
+ # database. This fixes a huge problem, because database types don't
227
+ # match Ruby types.
228
+ # Think of Redis, where everything is stored as a string or integer,
229
+ # the mapper translates values from/to the database.
230
+ #
231
+ # It supports the following types (coercers):
232
+ #
233
+ # * Array
234
+ # * Boolean
235
+ # * Date
236
+ # * DateTime
237
+ # * Float
238
+ # * Hash
239
+ # * Integer
240
+ # * BigDecimal
241
+ # * Set
242
+ # * String
243
+ # * Symbol
244
+ # * Time
245
+ #
246
+ # @param name [Symbol] the name of the attribute, as we want it to be
247
+ # mapped in the object
248
+ #
249
+ # @param coercer [.load, .dump] a class that implements coercer interface
250
+ #
251
+ # @param options [Hash] a set of options to customize the mapping
252
+ # @option options [Symbol] :as the name of the original column
253
+ #
254
+ # @raise [NameError] if coercer cannot be found
255
+ #
256
+ # @since 0.1.0
257
+ #
258
+ # @see Hanami::Model::Coercer
259
+ #
260
+ # @example Default schema
261
+ # require 'hanami/model'
262
+ #
263
+ # # Given the following schema:
264
+ # #
265
+ # # CREATE TABLE users (
266
+ # # id integer NOT NULL,
267
+ # # name varchar(64),
268
+ # # );
269
+ # #
270
+ # # And the following entity:
271
+ # #
272
+ # # class User
273
+ # # include Hanami::Entity
274
+ # # attributes :name
275
+ # # end
276
+ #
277
+ # mapper = Hanami::Model::Mapper.new do
278
+ # collection :users do
279
+ # entity User
280
+ #
281
+ # attribute :id, Integer
282
+ # attribute :name, String
283
+ # end
284
+ # end
285
+ #
286
+ # # The first argument (`:name`) always corresponds to the `User`
287
+ # # attribute.
288
+ #
289
+ # # The second one (`:coercer`) is the Ruby type coercer that we want
290
+ # # for our attribute.
291
+ #
292
+ # # We don't need to use `:as` because the database columns match the
293
+ # # `User` attributes.
294
+ #
295
+ # @example Customized schema
296
+ # require 'hanami/model'
297
+ #
298
+ # # Given the following schema:
299
+ # #
300
+ # # CREATE TABLE articles (
301
+ # # i_id integer NOT NULL,
302
+ # # i_user_id integer NOT NULL,
303
+ # # s_title varchar(64),
304
+ # # comments_count varchar(8) # Not an error: it's for String => Integer coercion
305
+ # # );
306
+ # #
307
+ # # And the following entity:
308
+ # #
309
+ # # class Article
310
+ # # include Hanami::Entity
311
+ # # attributes :user_id, :title, :comments_count
312
+ # # end
313
+ #
314
+ # mapper = Hanami::Model::Mapper.new do
315
+ # collection :articles do
316
+ # entity Article
317
+ #
318
+ # attribute :id, Integer, as: :i_id
319
+ # attribute :user_id, Integer, as: :i_user_id
320
+ # attribute :title, String, as: :s_title
321
+ # attribute :comments_count, Integer
322
+ #
323
+ # identity :i_id
324
+ # end
325
+ # end
326
+ #
327
+ # # The first argument (`:name`) always corresponds to the `Article`
328
+ # # attribute.
329
+ #
330
+ # # The second one (`:coercer`) is the Ruby type that we want for our
331
+ # # attribute.
332
+ #
333
+ # # The third option (`:as`) is mandatory only when the database
334
+ # # column doesn't match the name of the mapped attribute.
335
+ # #
336
+ # # For instance: we need to use it for translate `:s_title` to
337
+ # # `:title`, but not for `:comments_count`.
338
+ #
339
+ # @example Custom coercer
340
+ # require 'hanami/model'
341
+ #
342
+ # # Given the following schema:
343
+ # #
344
+ # # CREATE TABLE articles (
345
+ # # id integer NOT NULL,
346
+ # # title varchar(128),
347
+ # # tags text[],
348
+ # # );
349
+ # #
350
+ # # The following entity:
351
+ # #
352
+ # # class Article
353
+ # # include Hanami::Entity
354
+ # # attributes :title, :tags
355
+ # # end
356
+ # #
357
+ # # And the following custom coercer:
358
+ # #
359
+ # # require 'hanami/model/coercer'
360
+ # # require 'sequel/extensions/pg_array'
361
+ # #
362
+ # # class PGArray < Hanami::Model::Coercer
363
+ # # def self.dump(value)
364
+ # # ::Sequel.pg_array(value) rescue nil
365
+ # # end
366
+ # #
367
+ # # def self.load(value)
368
+ # # ::Kernel.Array(value) unless value.nil?
369
+ # # end
370
+ # # end
371
+ #
372
+ # mapper = Hanami::Model::Mapper.new do
373
+ # collection :articles do
374
+ # entity Article
375
+ #
376
+ # attribute :id, Integer
377
+ # attribute :title, String
378
+ # attribute :tags, PGArray
379
+ # end
380
+ # end
381
+ #
382
+ # # When an entity is persisted as record into the database,
383
+ # # `PGArray.dump` is invoked.
384
+ #
385
+ # # When an entity is retrieved from the database, it will be
386
+ # # deserialized as an Array via `PGArray.load`.
387
+ def attribute(name, coercer, options = {})
388
+ @attributes[name] = Attribute.new(name, coercer, options)
389
+ end
390
+
391
+ # Serializes an entity to be persisted in the database.
392
+ #
393
+ # @param entity [Object] an entity
394
+ #
395
+ # @api private
396
+ # @since 0.1.0
397
+ def serialize(entity)
398
+ @coercer.to_record(entity)
399
+ end
400
+
401
+ # Deserialize a set of records fetched from the database.
402
+ #
403
+ # @param records [Array] a set of raw records
404
+ #
405
+ # @api private
406
+ # @since 0.1.0
407
+ def deserialize(records)
408
+ records.map do |record|
409
+ @coercer.from_record(record)
410
+ end
411
+ end
412
+
413
+ # Deserialize only one attribute from a raw value.
414
+ #
415
+ # @param attribute [Symbol] the attribute name
416
+ # @param value [Object,nil] the value to be coerced
417
+ #
418
+ # @api private
419
+ # @since 0.1.0
420
+ def deserialize_attribute(attribute, value)
421
+ @coercer.public_send(:"deserialize_#{ attribute }", value)
422
+ end
423
+
424
+ # Loads the internals of the mapper, in order to guarantee thread safety.
425
+ #
426
+ # @api private
427
+ # @since 0.1.0
428
+ def load!
429
+ _load_entity!
430
+ _load_repository!
431
+ _load_coercer!
432
+
433
+ _configure_repository!
434
+ end
435
+
436
+ private
437
+
438
+ # Assigns a repository to an entity
439
+ #
440
+ # @see Hanami::Repository
441
+ #
442
+ # @api private
443
+ # @since 0.1.0
444
+ def _configure_repository!
445
+ repository.collection = name
446
+ repository.adapter = adapter if adapter
447
+ end
448
+
449
+ # Convert repository string to repository class
450
+ #
451
+ # @api private
452
+ # @since 0.2.0
453
+ def _load_repository!
454
+ @repository = Utils::Class.load!(repository)
455
+ rescue NameError
456
+ raise Hanami::Model::Mapping::RepositoryNotFound.new(repository.to_s)
457
+ end
458
+
459
+ # Convert entity string to entity class
460
+ #
461
+ # @api private
462
+ # @since 0.2.0
463
+ def _load_entity!
464
+ @entity = Utils::Class.load!(entity)
465
+ rescue NameError
466
+ raise Hanami::Model::Mapping::EntityNotFound.new(entity.to_s)
467
+ end
468
+
469
+ # Load coercer
470
+ #
471
+ # @api private
472
+ # @since 0.1.0
473
+ def _load_coercer!
474
+ @coercer = coercer_class.new(self)
475
+ end
476
+
477
+ # Retrieves the default repository class
478
+ #
479
+ # @see Hanami::Repository
480
+ #
481
+ # @api private
482
+ # @since 0.2.0
483
+ def default_repository_klass
484
+ "#{ entity }#{ REPOSITORY_SUFFIX }"
485
+ end
486
+
487
+ end
488
+ end
489
+ end
490
+ end