hanami-model 0.0.0 → 0.6.0

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