hanami-model 0.6.1 → 0.7.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 (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