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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +145 -0
- data/EXAMPLE.md +212 -0
- data/LICENSE.md +22 -0
- data/README.md +600 -7
- data/hanami-model.gemspec +17 -12
- data/lib/hanami-model.rb +1 -0
- data/lib/hanami/entity.rb +298 -0
- data/lib/hanami/entity/dirty_tracking.rb +74 -0
- data/lib/hanami/model.rb +204 -2
- data/lib/hanami/model/adapters/abstract.rb +281 -0
- data/lib/hanami/model/adapters/file_system_adapter.rb +288 -0
- data/lib/hanami/model/adapters/implementation.rb +111 -0
- data/lib/hanami/model/adapters/memory/collection.rb +132 -0
- data/lib/hanami/model/adapters/memory/command.rb +113 -0
- data/lib/hanami/model/adapters/memory/query.rb +653 -0
- data/lib/hanami/model/adapters/memory_adapter.rb +179 -0
- data/lib/hanami/model/adapters/null_adapter.rb +24 -0
- data/lib/hanami/model/adapters/sql/collection.rb +287 -0
- data/lib/hanami/model/adapters/sql/command.rb +73 -0
- data/lib/hanami/model/adapters/sql/console.rb +33 -0
- data/lib/hanami/model/adapters/sql/consoles/mysql.rb +49 -0
- data/lib/hanami/model/adapters/sql/consoles/postgresql.rb +48 -0
- data/lib/hanami/model/adapters/sql/consoles/sqlite.rb +26 -0
- data/lib/hanami/model/adapters/sql/query.rb +788 -0
- data/lib/hanami/model/adapters/sql_adapter.rb +296 -0
- data/lib/hanami/model/coercer.rb +74 -0
- data/lib/hanami/model/config/adapter.rb +116 -0
- data/lib/hanami/model/config/mapper.rb +45 -0
- data/lib/hanami/model/configuration.rb +275 -0
- data/lib/hanami/model/error.rb +7 -0
- data/lib/hanami/model/mapper.rb +124 -0
- data/lib/hanami/model/mapping.rb +48 -0
- data/lib/hanami/model/mapping/attribute.rb +85 -0
- data/lib/hanami/model/mapping/coercers.rb +314 -0
- data/lib/hanami/model/mapping/collection.rb +490 -0
- data/lib/hanami/model/mapping/collection_coercer.rb +79 -0
- data/lib/hanami/model/migrator.rb +324 -0
- data/lib/hanami/model/migrator/adapter.rb +170 -0
- data/lib/hanami/model/migrator/connection.rb +133 -0
- data/lib/hanami/model/migrator/mysql_adapter.rb +72 -0
- data/lib/hanami/model/migrator/postgres_adapter.rb +119 -0
- data/lib/hanami/model/migrator/sqlite_adapter.rb +110 -0
- data/lib/hanami/model/version.rb +4 -1
- data/lib/hanami/repository.rb +872 -0
- metadata +100 -16
- data/.gitignore +0 -9
- data/Gemfile +0 -4
- data/Rakefile +0 -2
- data/bin/console +0 -14
- 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
|