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,296 @@
1
+ require 'hanami/model/adapters/abstract'
2
+ require 'hanami/model/adapters/implementation'
3
+ require 'hanami/model/adapters/sql/collection'
4
+ require 'hanami/model/adapters/sql/command'
5
+ require 'hanami/model/adapters/sql/query'
6
+ require 'hanami/model/adapters/sql/console'
7
+ require 'sequel'
8
+
9
+ module Hanami
10
+ module Model
11
+ module Adapters
12
+ # Adapter for SQL databases
13
+ #
14
+ # In order to use it with a specific database, you must require the Ruby
15
+ # gem before of loading Hanami::Model.
16
+ #
17
+ # @see Hanami::Model::Adapters::Implementation
18
+ #
19
+ # @api private
20
+ # @since 0.1.0
21
+ class SqlAdapter < Abstract
22
+ include Implementation
23
+
24
+ # Initialize the adapter.
25
+ #
26
+ # Hanami::Model uses Sequel. For a complete reference of the connection
27
+ # URI, please see: http://sequel.jeremyevans.net/rdoc/files/doc/opening_databases_rdoc.html
28
+ #
29
+ # @param mapper [Object] the database mapper
30
+ # @param uri [String] the connection uri for the database
31
+ # @param options [Hash] a hash of non-mandatory adapter options
32
+ #
33
+ # @return [Hanami::Model::Adapters::SqlAdapter]
34
+ #
35
+ # @raise [Hanami::Model::Adapters::DatabaseAdapterNotFound] if the given
36
+ # URI refers to an unknown or not registered adapter.
37
+ #
38
+ # @raise [URI::InvalidURIError] if the given URI is malformed
39
+ #
40
+ # @see Hanami::Model::Mapper
41
+ # @see http://sequel.jeremyevans.net/rdoc/files/doc/opening_databases_rdoc.html
42
+ #
43
+ # @api private
44
+ # @since 0.1.0
45
+ def initialize(mapper, uri, options = {})
46
+ super
47
+ @connection = Sequel.connect(@uri, @options)
48
+ rescue Sequel::AdapterNotFound => e
49
+ raise DatabaseAdapterNotFound.new(e.message)
50
+ end
51
+
52
+ # Creates a record in the database for the given entity.
53
+ # It assigns the `id` attribute, in case of success.
54
+ #
55
+ # @param collection [Symbol] the target collection (it must be mapped).
56
+ # @param entity [#id=] the entity to create
57
+ #
58
+ # @return [Object] the entity
59
+ #
60
+ # @api private
61
+ # @since 0.1.0
62
+ def create(collection, entity)
63
+ command(
64
+ query(collection)
65
+ ).create(entity)
66
+ end
67
+
68
+ # Updates a record in the database corresponding to the given entity.
69
+ #
70
+ # @param collection [Symbol] the target collection (it must be mapped).
71
+ # @param entity [#id] the entity to update
72
+ #
73
+ # @return [Object] the entity
74
+ #
75
+ # @api private
76
+ # @since 0.1.0
77
+ def update(collection, entity)
78
+ command(
79
+ _find(collection, entity.id)
80
+ ).update(entity)
81
+ end
82
+
83
+ # Deletes a record in the database corresponding to the given entity.
84
+ #
85
+ # @param collection [Symbol] the target collection (it must be mapped).
86
+ # @param entity [#id] the entity to delete
87
+ #
88
+ # @api private
89
+ # @since 0.1.0
90
+ def delete(collection, entity)
91
+ command(
92
+ _find(collection, entity.id)
93
+ ).delete
94
+ end
95
+
96
+ # Deletes all the records from the given collection.
97
+ #
98
+ # @param collection [Symbol] the target collection (it must be mapped).
99
+ #
100
+ # @api private
101
+ # @since 0.1.0
102
+ def clear(collection)
103
+ command(query(collection)).clear
104
+ end
105
+
106
+ # Fabricates a command for the given query.
107
+ #
108
+ # @param query [Hanami::Model::Adapters::Sql::Query] the query object to
109
+ # act on.
110
+ #
111
+ # @return [Hanami::Model::Adapters::Sql::Command]
112
+ #
113
+ # @see Hanami::Model::Adapters::Sql::Command
114
+ #
115
+ # @api private
116
+ # @since 0.1.0
117
+ def command(query)
118
+ Sql::Command.new(query)
119
+ end
120
+
121
+ # Fabricates a query
122
+ #
123
+ # @param collection [Symbol] the target collection (it must be mapped).
124
+ # @param blk [Proc] a block of code to be executed in the context of
125
+ # the query.
126
+ #
127
+ # @return [Hanami::Model::Adapters::Sql::Query]
128
+ #
129
+ # @see Hanami::Model::Adapters::Sql::Query
130
+ #
131
+ # @api private
132
+ # @since 0.1.0
133
+ def query(collection, context = nil, &blk)
134
+ Sql::Query.new(_collection(collection), context, &blk)
135
+ end
136
+
137
+ # Wraps the given block in a transaction.
138
+ #
139
+ # For performance reasons the block isn't in the signature of the method,
140
+ # but it's yielded at the lower level.
141
+ #
142
+ # @param options [Hash] options for transaction
143
+ # @option rollback [Symbol] the optional rollback policy: `:always` or
144
+ # `:reraise`.
145
+ #
146
+ # @see Hanami::Repository::ClassMethods#transaction
147
+ #
148
+ # @since 0.2.3
149
+ # @api private
150
+ #
151
+ # @example Basic usage
152
+ # require 'hanami/model'
153
+ #
154
+ # class Article
155
+ # include Hanami::Entity
156
+ # attributes :title, :body
157
+ # end
158
+ #
159
+ # class ArticleRepository
160
+ # include Hanami::Repository
161
+ # end
162
+ #
163
+ # article = Article.new(title: 'Introducing transactions',
164
+ # body: 'lorem ipsum')
165
+ #
166
+ # ArticleRepository.transaction do
167
+ # ArticleRepository.dangerous_operation!(article) # => RuntimeError
168
+ # # !!! ROLLBACK !!!
169
+ # end
170
+ #
171
+ # @example Policy rollback always
172
+ # require 'hanami/model'
173
+ #
174
+ # class Article
175
+ # include Hanami::Entity
176
+ # attributes :title, :body
177
+ # end
178
+ #
179
+ # class ArticleRepository
180
+ # include Hanami::Repository
181
+ # end
182
+ #
183
+ # article = Article.new(title: 'Introducing transactions',
184
+ # body: 'lorem ipsum')
185
+ #
186
+ # ArticleRepository.transaction(rollback: :always) do
187
+ # ArticleRepository.create(article)
188
+ # # !!! ROLLBACK !!!
189
+ # end
190
+ #
191
+ # # The operation is rolled back, even in no exceptions were raised.
192
+ #
193
+ # @example Policy rollback reraise
194
+ # require 'hanami/model'
195
+ #
196
+ # class Article
197
+ # include Hanami::Entity
198
+ # attributes :title, :body
199
+ # end
200
+ #
201
+ # class ArticleRepository
202
+ # include Hanami::Repository
203
+ # end
204
+ #
205
+ # article = Article.new(title: 'Introducing transactions',
206
+ # body: 'lorem ipsum')
207
+ #
208
+ # ArticleRepository.transaction(rollback: :reraise) do
209
+ # ArticleRepository.dangerous_operation!(article) # => RuntimeError
210
+ # # !!! ROLLBACK !!!
211
+ # end # => RuntimeError
212
+ #
213
+ # # The operation is rolled back, but RuntimeError is re-raised.
214
+ def transaction(options = {})
215
+ @connection.transaction(options) do
216
+ yield
217
+ end
218
+ end
219
+
220
+ # Returns a string which can be executed to start a console suitable
221
+ # for the configured database, adding the necessary CLI flags, such as
222
+ # url, password, port number etc.
223
+ #
224
+ # @return [String]
225
+ #
226
+ # @since 0.3.0
227
+ def connection_string
228
+ Sql::Console.new(@uri).connection_string
229
+ end
230
+
231
+ # Executes a raw SQL command
232
+ #
233
+ # @param raw [String] the raw SQL statement to execute on the connection
234
+ #
235
+ # @raise [Hanami::Model::InvalidCommandError] if the raw SQL statement is invalid
236
+ #
237
+ # @return [NilClass]
238
+ #
239
+ # @since 0.3.1
240
+ def execute(raw)
241
+ begin
242
+ @connection.execute(raw)
243
+ nil
244
+ rescue Sequel::DatabaseError => e
245
+ raise Hanami::Model::InvalidCommandError.new(e.message)
246
+ end
247
+ end
248
+
249
+ # Fetches raw result sets for the given SQL query
250
+ #
251
+ # @param raw [String] the raw SQL query
252
+ # @param blk [Proc] optional block that is yielded for each record
253
+ #
254
+ # @return [Array]
255
+ #
256
+ # @raise [Hanami::Model::InvalidQueryError] if the raw SQL statement is invalid
257
+ #
258
+ # @since 0.5.0
259
+ def fetch(raw, &blk)
260
+ if block_given?
261
+ @connection.fetch(raw, &blk)
262
+ else
263
+ @connection.fetch(raw).to_a
264
+ end
265
+ rescue Sequel::DatabaseError => e
266
+ raise Hanami::Model::InvalidQueryError.new(e.message)
267
+ end
268
+
269
+ # @api private
270
+ # @since 0.5.0
271
+ #
272
+ # @see Hanami::Model::Adapters::Abstract#disconnect
273
+ def disconnect
274
+ @connection.disconnect
275
+ @connection = DisconnectedResource.new
276
+ end
277
+
278
+ private
279
+
280
+ # Returns a collection from the given name.
281
+ #
282
+ # @param name [Symbol] a name of the collection (it must be mapped).
283
+ #
284
+ # @return [Hanami::Model::Adapters::Sql::Collection]
285
+ #
286
+ # @see Hanami::Model::Adapters::Sql::Collection
287
+ #
288
+ # @api private
289
+ # @since 0.1.0
290
+ def _collection(name)
291
+ Sql::Collection.new(@connection[name], _mapped_collection(name))
292
+ end
293
+ end
294
+ end
295
+ end
296
+ end
@@ -0,0 +1,74 @@
1
+ module Hanami
2
+ module Model
3
+ # Abstract coercer
4
+ #
5
+ # It can be used as super class for custom mapping coercers.
6
+ #
7
+ # @since 0.5.0
8
+ #
9
+ # @see Hanami::Model::Mapper
10
+ #
11
+ # @example Postgres Array
12
+ # require 'hanami/model/coercer'
13
+ # require 'sequel/extensions/pg_array'
14
+ #
15
+ # class PGArray < Hanami::Model::Coercer
16
+ # def self.dump(value)
17
+ # ::Sequel.pg_array(value) rescue nil
18
+ # end
19
+ #
20
+ # def self.load(value)
21
+ # ::Kernel.Array(value) unless value.nil?
22
+ # end
23
+ # end
24
+ #
25
+ # Hanami::Model.configure do
26
+ # mapping do
27
+ # collection :articles do
28
+ # entity Article
29
+ # repository ArticleRepository
30
+ #
31
+ # attribute :id, Integer
32
+ # attribute :title, String
33
+ # attribute :tags, PGArray
34
+ # end
35
+ # end
36
+ # end.load!
37
+ #
38
+ # # When the entity is serialized, it calls `PGArray.dump` to store `tags`
39
+ # # as a Postgres Array.
40
+ # #
41
+ # # When the record is loaded (unserialized) from the database, it calls
42
+ # # `PGArray.load` and returns a Ruby Array.
43
+ class Coercer
44
+ # Deserialize (load) a value coming from the database into a Ruby object.
45
+ #
46
+ # When inheriting from this class, it's a good practice to return <tt>nil</tt>
47
+ # if the given value it's <tt>nil</tt>.
48
+ #
49
+ # @abstract
50
+ #
51
+ # @raise [TypeError] if the value can't be coerced
52
+ #
53
+ # @since 0.5.0
54
+ #
55
+ # @see Hanami::Model::Mapping::Coercers
56
+ def self.load(value)
57
+ raise NotImplementedError
58
+ end
59
+
60
+ # Serialize (dump) a Ruby object into a value that can be store by the database.
61
+ #
62
+ # @abstract
63
+ #
64
+ # @raise [TypeError] if the value can't be coerced
65
+ #
66
+ # @since 0.5.0
67
+ #
68
+ # @see Hanami::Model::Mapping::Coercers
69
+ def self.dump(value)
70
+ self.load(value)
71
+ end
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,116 @@
1
+ require 'hanami/utils/class'
2
+
3
+ module Hanami
4
+ module Model
5
+ module Config
6
+ # Raised when an adapter class does not exist
7
+ #
8
+ # @since 0.2.0
9
+ class AdapterNotFound < Hanami::Model::Error
10
+ def initialize(adapter_name)
11
+ super "Cannot find Hanami::Model adapter #{adapter_name}"
12
+ end
13
+ end
14
+
15
+ # Configuration for the adapter
16
+ #
17
+ # Hanami::Model has its own global configuration that can be manipulated
18
+ # via `Hanami::Model.configure`.
19
+ #
20
+ # New adapter configuration can be registered via `Hanami::Model.adapter`.
21
+ #
22
+ # @see Hanami::Model.adapter
23
+ #
24
+ # @example
25
+ # require 'hanami/model'
26
+ #
27
+ # Hanami::Model.configure do
28
+ # adapter type: :sql, uri: 'postgres://localhost/database'
29
+ # end
30
+ #
31
+ # Hanami::Model.configuration.adapter_config
32
+ # # => Hanami::Model::Config::Adapter(type: :sql, uri: 'postgres://localhost/database')
33
+ #
34
+ # By convention, Hanami inflects type to find the adapter class
35
+ # For example, if type is :sql, derived class will be `Hanami::Model::Adapters::SqlAdapter`
36
+ #
37
+ # @since 0.2.0
38
+ class Adapter
39
+ # @return [Symbol] the adapter name
40
+ #
41
+ # @since 0.2.0
42
+ attr_reader :type
43
+
44
+ # @return [String] the adapter URI
45
+ #
46
+ # @since 0.2.0
47
+ attr_reader :uri
48
+
49
+ # @return [Hash] a list of non-mandatory options for the adapter
50
+ #
51
+ attr_reader :options
52
+
53
+ # @return [String] the adapter class name
54
+ #
55
+ # @since 0.2.0
56
+ attr_reader :class_name
57
+
58
+ # Initialize an adapter configuration instance
59
+ #
60
+ # @param options [Hash] configuration options
61
+ # @option options [Symbol] :type adapter type name
62
+ # @option options [String] :uri adapter URI
63
+ #
64
+ # @return [Hanami::Model::Config::Adapter] a new apdapter configuration's
65
+ # instance
66
+ #
67
+ # @since 0.2.0
68
+ def initialize(**options)
69
+ opts = options.dup
70
+
71
+ @type = opts.delete(:type)
72
+ @uri = opts.delete(:uri)
73
+ @options = opts
74
+
75
+ @class_name ||= Hanami::Utils::String.new("#{@type}_adapter").classify
76
+ end
77
+
78
+ # Initialize the adapter
79
+ #
80
+ # @param mapper [Hanami::Model::Mapper] the mapper instance
81
+ #
82
+ # @return [Hanami::Model::Adapters::SqlAdapter, Hanami::Model::Adapters::MemoryAdapter] an adapter instance
83
+ #
84
+ # @see Hanami::Model::Adapters
85
+ #
86
+ # @since 0.2.0
87
+ def build(mapper)
88
+ load_adapter
89
+ instantiate_adapter(mapper)
90
+ end
91
+
92
+ private
93
+
94
+ def load_adapter
95
+ begin
96
+ require "hanami/model/adapters/#{type}_adapter"
97
+ rescue LoadError => e
98
+ raise LoadError.new("Cannot find Hanami::Model adapter '#{type}' (#{e.message})")
99
+ end
100
+ end
101
+
102
+ def instantiate_adapter(mapper)
103
+ begin
104
+ klass = Hanami::Utils::Class.load!(class_name, Hanami::Model::Adapters)
105
+ klass.new(mapper, uri, options)
106
+ rescue NameError
107
+ raise AdapterNotFound.new(class_name)
108
+ rescue => e
109
+ raise "Cannot instantiate adapter of #{klass} (#{e.message})"
110
+ end
111
+ end
112
+
113
+ end
114
+ end
115
+ end
116
+ end