hanami-model 0.0.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +145 -0
  3. data/EXAMPLE.md +212 -0
  4. data/LICENSE.md +22 -0
  5. data/README.md +600 -7
  6. data/hanami-model.gemspec +17 -12
  7. data/lib/hanami-model.rb +1 -0
  8. data/lib/hanami/entity.rb +298 -0
  9. data/lib/hanami/entity/dirty_tracking.rb +74 -0
  10. data/lib/hanami/model.rb +204 -2
  11. data/lib/hanami/model/adapters/abstract.rb +281 -0
  12. data/lib/hanami/model/adapters/file_system_adapter.rb +288 -0
  13. data/lib/hanami/model/adapters/implementation.rb +111 -0
  14. data/lib/hanami/model/adapters/memory/collection.rb +132 -0
  15. data/lib/hanami/model/adapters/memory/command.rb +113 -0
  16. data/lib/hanami/model/adapters/memory/query.rb +653 -0
  17. data/lib/hanami/model/adapters/memory_adapter.rb +179 -0
  18. data/lib/hanami/model/adapters/null_adapter.rb +24 -0
  19. data/lib/hanami/model/adapters/sql/collection.rb +287 -0
  20. data/lib/hanami/model/adapters/sql/command.rb +73 -0
  21. data/lib/hanami/model/adapters/sql/console.rb +33 -0
  22. data/lib/hanami/model/adapters/sql/consoles/mysql.rb +49 -0
  23. data/lib/hanami/model/adapters/sql/consoles/postgresql.rb +48 -0
  24. data/lib/hanami/model/adapters/sql/consoles/sqlite.rb +26 -0
  25. data/lib/hanami/model/adapters/sql/query.rb +788 -0
  26. data/lib/hanami/model/adapters/sql_adapter.rb +296 -0
  27. data/lib/hanami/model/coercer.rb +74 -0
  28. data/lib/hanami/model/config/adapter.rb +116 -0
  29. data/lib/hanami/model/config/mapper.rb +45 -0
  30. data/lib/hanami/model/configuration.rb +275 -0
  31. data/lib/hanami/model/error.rb +7 -0
  32. data/lib/hanami/model/mapper.rb +124 -0
  33. data/lib/hanami/model/mapping.rb +48 -0
  34. data/lib/hanami/model/mapping/attribute.rb +85 -0
  35. data/lib/hanami/model/mapping/coercers.rb +314 -0
  36. data/lib/hanami/model/mapping/collection.rb +490 -0
  37. data/lib/hanami/model/mapping/collection_coercer.rb +79 -0
  38. data/lib/hanami/model/migrator.rb +324 -0
  39. data/lib/hanami/model/migrator/adapter.rb +170 -0
  40. data/lib/hanami/model/migrator/connection.rb +133 -0
  41. data/lib/hanami/model/migrator/mysql_adapter.rb +72 -0
  42. data/lib/hanami/model/migrator/postgres_adapter.rb +119 -0
  43. data/lib/hanami/model/migrator/sqlite_adapter.rb +110 -0
  44. data/lib/hanami/model/version.rb +4 -1
  45. data/lib/hanami/repository.rb +872 -0
  46. metadata +100 -16
  47. data/.gitignore +0 -9
  48. data/Gemfile +0 -4
  49. data/Rakefile +0 -2
  50. data/bin/console +0 -14
  51. data/bin/setup +0 -8
@@ -0,0 +1,281 @@
1
+ require 'hanami/utils/basic_object'
2
+
3
+ module Hanami
4
+ module Model
5
+ module Adapters
6
+ # It's raised when an adapter can't find the underlying database adapter.
7
+ #
8
+ # Example: When we try to use the SqlAdapter with a Postgres database
9
+ # but we didn't loaded the pg gem before.
10
+ #
11
+ # @see Hanami::Model::Adapters::SqlAdapter#initialize
12
+ #
13
+ # @since 0.1.0
14
+ class DatabaseAdapterNotFound < Hanami::Model::Error
15
+ end
16
+
17
+ # It's raised when an adapter does not support a feature.
18
+ #
19
+ # Example: When we try to get a connection string for the current database
20
+ # but the adapter has not implemented it.
21
+ #
22
+ # @see Hanami::Model::Adapters::Abstract#connection_string
23
+ #
24
+ # @since 0.3.0
25
+ class NotSupportedError < Hanami::Model::Error
26
+ end
27
+
28
+ # It's raised when an operation is requested to an adapter after it was
29
+ # disconnected.
30
+ #
31
+ # @since 0.5.0
32
+ class DisconnectedAdapterError < Hanami::Model::Error
33
+ def initialize
34
+ super "You have tried to perform an operation on a disconnected adapter"
35
+ end
36
+ end
37
+
38
+ # Represents a disconnected resource.
39
+ #
40
+ # When we use <tt>#disconnect</tt> for <tt>MemoryAdapter</tt> and
41
+ # </tt>FileSystemAdapter</tt>, we want to free underlying resources such
42
+ # as a mutex or a file descriptor.
43
+ #
44
+ # These adapters use to use anonymous descriptors that are destroyed by
45
+ # Ruby VM after each operation. Sometimes we need to clean the state and
46
+ # start fresh (eg. during a test suite or a deploy).
47
+ #
48
+ # Instead of assign <tt>nil</tt> to these instance variables, we assign this
49
+ # special type: <tt>DisconnectedResource</tt>.
50
+ #
51
+ # In case an operation is still performed after the adapter was disconnected,
52
+ # instead of see a generic <tt>NoMethodError</tt> for <tt>nil</tt>, a developer
53
+ # will face a specific message relative to the state of the adapter.
54
+ #
55
+ # @api private
56
+ # @since 0.5.0
57
+ #
58
+ # @see Hanami::Model::Adapters::Abstract#disconnect
59
+ # @see Hanami::Model::Adapters::MemoryAdapter#disconnect
60
+ # @see Hanami::Model::Adapters::FileSystemAdapter#disconnect
61
+ class DisconnectedResource < Utils::BasicObject
62
+ def method_missing(method_name, *)
63
+ ::Kernel.raise DisconnectedAdapterError.new
64
+ end
65
+ end
66
+
67
+ # Abstract adapter.
68
+ #
69
+ # An adapter is a concrete implementation that allows a repository to
70
+ # communicate with a single database.
71
+ #
72
+ # Hanami::Model is shipped with Memory and SQL adapters.
73
+ # Third part adapters MUST implement the interface defined here.
74
+ # For convenience they may inherit from this class.
75
+ #
76
+ # These are low level details, and shouldn't be used directly.
77
+ # Please use a repository for entities persistence.
78
+ #
79
+ # @since 0.1.0
80
+ class Abstract
81
+ # Initialize the adapter
82
+ #
83
+ # @param mapper [Hanami::Model::Mapper] the object that defines the
84
+ # database to entities mapping
85
+ #
86
+ # @param uri [String] the optional connection string to the database
87
+ #
88
+ # @param options [Hash] a list of non-mandatory adapter options
89
+ #
90
+ # @since 0.1.0
91
+ def initialize(mapper, uri = nil, options = {})
92
+ @mapper = mapper
93
+ @uri = uri
94
+ @options = options
95
+ end
96
+
97
+ # Creates or updates a record in the database for the given entity.
98
+ #
99
+ # @param collection [Symbol] the target collection (it must be mapped).
100
+ # @param entity [Object] the entity to persist
101
+ #
102
+ # @return [Object] the entity
103
+ #
104
+ # @since 0.1.0
105
+ def persist(collection, entity)
106
+ raise NotImplementedError
107
+ end
108
+
109
+ # Creates a record in the database for the given entity.
110
+ # It should assign an id (identity) to the entity in case of success.
111
+ #
112
+ # @param collection [Symbol] the target collection (it must be mapped).
113
+ # @param entity [Object] the entity to create
114
+ #
115
+ # @return [Object] the entity
116
+ #
117
+ # @since 0.1.0
118
+ def create(collection, entity)
119
+ raise NotImplementedError
120
+ end
121
+
122
+ # Updates a record in the database corresponding to the given entity.
123
+ #
124
+ # @param collection [Symbol] the target collection (it must be mapped).
125
+ # @param entity [Object] the entity to update
126
+ #
127
+ # @return [Object] the entity
128
+ #
129
+ # @since 0.1.0
130
+ def update(collection, entity)
131
+ raise NotImplementedError
132
+ end
133
+
134
+ # Deletes a record in the database corresponding to the given entity.
135
+ #
136
+ # @param collection [Symbol] the target collection (it must be mapped).
137
+ # @param entity [Object] the entity to delete
138
+ #
139
+ # @since 0.1.0
140
+ def delete(collection, entity)
141
+ raise NotImplementedError
142
+ end
143
+
144
+ # Returns all the records for the given collection
145
+ #
146
+ # @param collection [Symbol] the target collection (it must be mapped).
147
+ #
148
+ # @return [Array] all the records
149
+ #
150
+ # @since 0.1.0
151
+ def all(collection)
152
+ raise NotImplementedError
153
+ end
154
+
155
+ # Returns a unique record from the given collection, with the given
156
+ # identity.
157
+ #
158
+ # @param collection [Symbol] the target collection (it must be mapped).
159
+ # @param id [Object] the identity of the object.
160
+ #
161
+ # @return [Object] the entity
162
+ #
163
+ # @since 0.1.0
164
+ def find(collection, id)
165
+ raise NotImplementedError
166
+ end
167
+
168
+ # Returns the first record in the given collection.
169
+ #
170
+ # @param collection [Symbol] the target collection (it must be mapped).
171
+ #
172
+ # @return [Object] the first entity
173
+ #
174
+ # @since 0.1.0
175
+ def first(collection)
176
+ raise NotImplementedError
177
+ end
178
+
179
+ # Returns the last record in the given collection.
180
+ #
181
+ # @param collection [Symbol] the target collection (it must be mapped).
182
+ #
183
+ # @return [Object] the last entity
184
+ #
185
+ # @since 0.1.0
186
+ def last(collection)
187
+ raise NotImplementedError
188
+ end
189
+
190
+ # Empties the given collection.
191
+ #
192
+ # @param collection [Symbol] the target collection (it must be mapped).
193
+ #
194
+ # @since 0.1.0
195
+ def clear(collection)
196
+ raise NotImplementedError
197
+ end
198
+
199
+ # Executes a command for the given query.
200
+ #
201
+ # @param query [Object] the query object to act on.
202
+ #
203
+ # @since 0.1.0
204
+ def command(query)
205
+ raise NotImplementedError
206
+ end
207
+
208
+ # Returns a query
209
+ #
210
+ # @param collection [Symbol] the target collection (it must be mapped).
211
+ # @param blk [Proc] a block of code to be executed in the context of
212
+ # the query.
213
+ #
214
+ # @return [Object]
215
+ #
216
+ # @since 0.1.0
217
+ def query(collection, &blk)
218
+ raise NotImplementedError
219
+ end
220
+
221
+ # Wraps the given block in a transaction.
222
+ #
223
+ # For performance reasons the block isn't in the signature of the method,
224
+ # but it's yielded at the lower level.
225
+ #
226
+ # Please note that it's only supported by some databases.
227
+ # For this reason, the options may vary from adapter to adapter.
228
+ #
229
+ # @param options [Hash] options for transaction
230
+ #
231
+ # @see Hanami::Model::Adapters::SqlAdapter#transaction
232
+ # @see Hanami::Model::Adapters::MemoryAdapter#transaction
233
+ #
234
+ # @since 0.2.3
235
+ def transaction(options = {})
236
+ raise NotImplementedError
237
+ end
238
+
239
+ # Returns a string which can be executed to start a console suitable
240
+ # for the configured database.
241
+ #
242
+ # @return [String] to be executed to start a database console
243
+ #
244
+ # @since 0.3.0
245
+ def connection_string
246
+ raise NotSupportedError
247
+ end
248
+
249
+ # Executes a raw command
250
+ #
251
+ # @param raw [String] the raw statement to execute on the connection
252
+ #
253
+ # @return [NilClass]
254
+ #
255
+ # @since 0.3.1
256
+ def execute(raw)
257
+ raise NotImplementedError
258
+ end
259
+
260
+ # Fetches raw records from
261
+ #
262
+ # @param raw [String] the raw query
263
+ # @param blk [Proc] an optional block that is yielded for each record
264
+ #
265
+ # @return [Enumerable<Hash>, Array<Hash>]
266
+ #
267
+ # @since 0.5.0
268
+ def fetch(raw, &blk)
269
+ raise NotImplementedError
270
+ end
271
+
272
+ # Disconnects the connection by freeing low level resources
273
+ #
274
+ # @since 0.5.0
275
+ def disconnect
276
+ raise NotImplementedError
277
+ end
278
+ end
279
+ end
280
+ end
281
+ end
@@ -0,0 +1,288 @@
1
+ require 'thread'
2
+ require 'pathname'
3
+ require 'hanami/model/adapters/memory_adapter'
4
+
5
+ module Hanami
6
+ module Model
7
+ module Adapters
8
+ # In memory adapter with file system persistence.
9
+ # It behaves like the SQL adapter, but it doesn't support all the SQL
10
+ # features offered by that kind of databases.
11
+ #
12
+ # This adapter SHOULD be used only for development or testing purposes.
13
+ # Each read/write operation is wrapped by a `Mutex` and persisted to the
14
+ # disk.
15
+ #
16
+ # For those reasons it's really unefficient, but great for quick
17
+ # prototyping as it's schema-less.
18
+ #
19
+ # It works exactly like the `MemoryAdapter`, with the only difference
20
+ # that it persist data to the disk.
21
+ #
22
+ # The persistence policy uses Ruby `Marshal` `dump` and `load` operations.
23
+ # Please be aware of the limitations this model.
24
+ #
25
+ # @see Hanami::Model::Adapters::Implementation
26
+ # @see Hanami::Model::Adapters::MemoryAdapter
27
+ # @see http://www.ruby-doc.org/core/Marshal.html
28
+ #
29
+ # @api private
30
+ # @since 0.2.0
31
+ class FileSystemAdapter < MemoryAdapter
32
+ # Default writing mode
33
+ #
34
+ # Binary, write only, create file if missing or erase if don't.
35
+ #
36
+ # @see http://ruby-doc.org/core/File/Constants.html
37
+ #
38
+ # @since 0.2.0
39
+ # @api private
40
+ WRITING_MODE = File::WRONLY|File::BINARY|File::CREAT
41
+
42
+ # Default chmod
43
+ #
44
+ # @see http://en.wikipedia.org/wiki/Chmod
45
+ #
46
+ # @since 0.2.0
47
+ # @api private
48
+ CHMOD = 0644
49
+
50
+ # File scheme
51
+ #
52
+ # @see https://tools.ietf.org/html/rfc3986
53
+ #
54
+ # @since 0.2.0
55
+ # @api private
56
+ FILE_SCHEME = 'file:///'.freeze
57
+
58
+ # Initialize the adapter.
59
+ #
60
+ # @param mapper [Object] the database mapper
61
+ # @param uri [String] the connection uri
62
+ # @param options [Hash] a hash of non-mandatory adapter options
63
+ #
64
+ # @return [Hanami::Model::Adapters::FileSystemAdapter]
65
+ #
66
+ # @see Hanami::Model::Mapper
67
+ #
68
+ # @api private
69
+ # @since 0.2.0
70
+ def initialize(mapper, uri, options = {})
71
+ super
72
+ prepare(uri)
73
+
74
+ @_mutex = Mutex.new
75
+ end
76
+
77
+ # Returns all the records for the given collection
78
+ #
79
+ # @param collection [Symbol] the target collection (it must be mapped).
80
+ #
81
+ # @return [Array] all the records
82
+ #
83
+ # @api private
84
+ # @since 0.2.0
85
+ def all(collection)
86
+ _synchronize do
87
+ read(collection)
88
+ super
89
+ end
90
+ end
91
+
92
+ # Returns a unique record from the given collection, with the given
93
+ # id.
94
+ #
95
+ # @param collection [Symbol] the target collection (it must be mapped).
96
+ # @param id [Object] the identity of the object.
97
+ #
98
+ # @return [Object] the entity
99
+ #
100
+ # @api private
101
+ # @since 0.2.0
102
+ def find(collection, id)
103
+ _synchronize do
104
+ read(collection)
105
+ super
106
+ end
107
+ end
108
+
109
+ # Returns the first record in the given collection.
110
+ #
111
+ # @param collection [Symbol] the target collection (it must be mapped).
112
+ #
113
+ # @return [Object] the first entity
114
+ #
115
+ # @api private
116
+ # @since 0.2.0
117
+ def first(collection)
118
+ _synchronize do
119
+ read(collection)
120
+ super
121
+ end
122
+ end
123
+
124
+ # Returns the last record in the given collection.
125
+ #
126
+ # @param collection [Symbol] the target collection (it must be mapped).
127
+ #
128
+ # @return [Object] the last entity
129
+ #
130
+ # @api private
131
+ # @since 0.2.0
132
+ def last(collection)
133
+ _synchronize do
134
+ read(collection)
135
+ super
136
+ end
137
+ end
138
+
139
+ # Creates a record in the database for the given entity.
140
+ # It assigns the `id` attribute, in case of success.
141
+ #
142
+ # @param collection [Symbol] the target collection (it must be mapped).
143
+ # @param entity [#id=] the entity to create
144
+ #
145
+ # @return [Object] the entity
146
+ #
147
+ # @api private
148
+ # @since 0.2.0
149
+ def create(collection, entity)
150
+ _synchronize do
151
+ super.tap { write(collection) }
152
+ end
153
+ end
154
+
155
+ # Updates a record in the database corresponding to the given entity.
156
+ #
157
+ # @param collection [Symbol] the target collection (it must be mapped).
158
+ # @param entity [#id] the entity to update
159
+ #
160
+ # @return [Object] the entity
161
+ #
162
+ # @api private
163
+ # @since 0.2.0
164
+ def update(collection, entity)
165
+ _synchronize do
166
+ super.tap { write(collection) }
167
+ end
168
+ end
169
+
170
+ # Deletes a record in the database corresponding to the given entity.
171
+ #
172
+ # @param collection [Symbol] the target collection (it must be mapped).
173
+ # @param entity [#id] the entity to delete
174
+ #
175
+ # @api private
176
+ # @since 0.2.0
177
+ def delete(collection, entity)
178
+ _synchronize do
179
+ super
180
+ write(collection)
181
+ end
182
+ end
183
+
184
+ # Deletes all the records from the given collection and resets the
185
+ # identity counter.
186
+ #
187
+ # @param collection [Symbol] the target collection (it must be mapped).
188
+ #
189
+ # @api private
190
+ # @since 0.2.0
191
+ def clear(collection)
192
+ _synchronize do
193
+ super
194
+ write(collection)
195
+ end
196
+ end
197
+
198
+ # Fabricates a query
199
+ #
200
+ # @param collection [Symbol] the target collection (it must be mapped).
201
+ # @param blk [Proc] a block of code to be executed in the context of
202
+ # the query.
203
+ #
204
+ # @return [Hanami::Model::Adapters::Memory::Query]
205
+ #
206
+ # @see Hanami::Model::Adapters::Memory::Query
207
+ #
208
+ # @api private
209
+ # @since 0.2.0
210
+ def query(collection, context = nil, &blk)
211
+ # _synchronize do
212
+ read(collection)
213
+ super
214
+ # end
215
+ end
216
+
217
+ # Database informations
218
+ #
219
+ # @return [Hash] per collection informations
220
+ #
221
+ # @api private
222
+ # @since 0.2.0
223
+ def info
224
+ @collections.each_with_object({}) do |(collection,_), result|
225
+ result[collection] = query(collection).count
226
+ end
227
+ end
228
+
229
+ # @api private
230
+ # @since 0.5.0
231
+ #
232
+ # @see Hanami::Model::Adapters::Abstract#disconnect
233
+ def disconnect
234
+ super
235
+
236
+ @_mutex = DisconnectedResource.new
237
+ @root = DisconnectedResource.new
238
+ end
239
+
240
+ private
241
+ # @api private
242
+ # @since 0.2.0
243
+ def prepare(uri)
244
+ @root = Pathname.new(uri.sub(FILE_SCHEME, ''))
245
+ @root.mkpath
246
+
247
+ # Eager load previously persisted data.
248
+ @root.each_child do |collection|
249
+ collection = collection.basename.to_s.to_sym
250
+ read(collection)
251
+ end
252
+ end
253
+
254
+ # @api private
255
+ # @since 0.2.0
256
+ def _synchronize
257
+ @_mutex.synchronize { yield }
258
+ end
259
+
260
+ # @api private
261
+ # @since 0.2.0
262
+ def write(collection)
263
+ path = @root.join("#{ collection }")
264
+ path.open(WRITING_MODE, CHMOD) {|f| f.write _dump( @collections.fetch(collection) ) }
265
+ end
266
+
267
+ # @api private
268
+ # @since 0.2.0
269
+ def read(collection)
270
+ path = @root.join("#{ collection }")
271
+ @collections[collection] = _load(path.read) if path.exist?
272
+ end
273
+
274
+ # @api private
275
+ # @since 0.2.0
276
+ def _dump(contents)
277
+ Marshal.dump(contents)
278
+ end
279
+
280
+ # @api private
281
+ # @since 0.2.0
282
+ def _load(contents)
283
+ Marshal.load(contents)
284
+ end
285
+ end
286
+ end
287
+ end
288
+ end