hanami-model 0.6.1 → 0.7.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (67) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +44 -0
  3. data/README.md +54 -420
  4. data/hanami-model.gemspec +9 -6
  5. data/lib/hanami/entity.rb +107 -191
  6. data/lib/hanami/entity/schema.rb +236 -0
  7. data/lib/hanami/model.rb +52 -138
  8. data/lib/hanami/model/association.rb +37 -0
  9. data/lib/hanami/model/associations/belongs_to.rb +19 -0
  10. data/lib/hanami/model/associations/dsl.rb +29 -0
  11. data/lib/hanami/model/associations/has_many.rb +200 -0
  12. data/lib/hanami/model/configuration.rb +52 -224
  13. data/lib/hanami/model/configurator.rb +62 -0
  14. data/lib/hanami/model/entity_name.rb +35 -0
  15. data/lib/hanami/model/error.rb +37 -24
  16. data/lib/hanami/model/mapping.rb +29 -35
  17. data/lib/hanami/model/migration.rb +31 -0
  18. data/lib/hanami/model/migrator.rb +111 -88
  19. data/lib/hanami/model/migrator/adapter.rb +39 -16
  20. data/lib/hanami/model/migrator/connection.rb +23 -11
  21. data/lib/hanami/model/migrator/mysql_adapter.rb +38 -17
  22. data/lib/hanami/model/migrator/postgres_adapter.rb +20 -19
  23. data/lib/hanami/model/migrator/sqlite_adapter.rb +9 -8
  24. data/lib/hanami/model/plugins.rb +25 -0
  25. data/lib/hanami/model/plugins/mapping.rb +55 -0
  26. data/lib/hanami/model/plugins/schema.rb +55 -0
  27. data/lib/hanami/model/plugins/timestamps.rb +118 -0
  28. data/lib/hanami/model/relation_name.rb +24 -0
  29. data/lib/hanami/model/sql.rb +161 -0
  30. data/lib/hanami/model/sql/console.rb +41 -0
  31. data/lib/hanami/model/sql/consoles/abstract.rb +33 -0
  32. data/lib/hanami/model/sql/consoles/mysql.rb +63 -0
  33. data/lib/hanami/model/sql/consoles/postgresql.rb +68 -0
  34. data/lib/hanami/model/sql/consoles/sqlite.rb +46 -0
  35. data/lib/hanami/model/sql/entity/schema.rb +125 -0
  36. data/lib/hanami/model/sql/types.rb +95 -0
  37. data/lib/hanami/model/sql/types/schema/coercions.rb +198 -0
  38. data/lib/hanami/model/types.rb +99 -0
  39. data/lib/hanami/model/version.rb +1 -1
  40. data/lib/hanami/repository.rb +287 -723
  41. metadata +77 -40
  42. data/EXAMPLE.md +0 -213
  43. data/lib/hanami/entity/dirty_tracking.rb +0 -74
  44. data/lib/hanami/model/adapters/abstract.rb +0 -281
  45. data/lib/hanami/model/adapters/file_system_adapter.rb +0 -288
  46. data/lib/hanami/model/adapters/implementation.rb +0 -111
  47. data/lib/hanami/model/adapters/memory/collection.rb +0 -132
  48. data/lib/hanami/model/adapters/memory/command.rb +0 -113
  49. data/lib/hanami/model/adapters/memory/query.rb +0 -653
  50. data/lib/hanami/model/adapters/memory_adapter.rb +0 -179
  51. data/lib/hanami/model/adapters/null_adapter.rb +0 -24
  52. data/lib/hanami/model/adapters/sql/collection.rb +0 -287
  53. data/lib/hanami/model/adapters/sql/command.rb +0 -88
  54. data/lib/hanami/model/adapters/sql/console.rb +0 -33
  55. data/lib/hanami/model/adapters/sql/consoles/mysql.rb +0 -49
  56. data/lib/hanami/model/adapters/sql/consoles/postgresql.rb +0 -48
  57. data/lib/hanami/model/adapters/sql/consoles/sqlite.rb +0 -26
  58. data/lib/hanami/model/adapters/sql/query.rb +0 -788
  59. data/lib/hanami/model/adapters/sql_adapter.rb +0 -296
  60. data/lib/hanami/model/coercer.rb +0 -74
  61. data/lib/hanami/model/config/adapter.rb +0 -116
  62. data/lib/hanami/model/config/mapper.rb +0 -45
  63. data/lib/hanami/model/mapper.rb +0 -124
  64. data/lib/hanami/model/mapping/attribute.rb +0 -85
  65. data/lib/hanami/model/mapping/coercers.rb +0 -314
  66. data/lib/hanami/model/mapping/collection.rb +0 -490
  67. data/lib/hanami/model/mapping/collection_coercer.rb +0 -79
@@ -1,490 +0,0 @@
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