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