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,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