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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +145 -0
- data/EXAMPLE.md +212 -0
- data/LICENSE.md +22 -0
- data/README.md +600 -7
- data/hanami-model.gemspec +17 -12
- data/lib/hanami-model.rb +1 -0
- data/lib/hanami/entity.rb +298 -0
- data/lib/hanami/entity/dirty_tracking.rb +74 -0
- data/lib/hanami/model.rb +204 -2
- data/lib/hanami/model/adapters/abstract.rb +281 -0
- data/lib/hanami/model/adapters/file_system_adapter.rb +288 -0
- data/lib/hanami/model/adapters/implementation.rb +111 -0
- data/lib/hanami/model/adapters/memory/collection.rb +132 -0
- data/lib/hanami/model/adapters/memory/command.rb +113 -0
- data/lib/hanami/model/adapters/memory/query.rb +653 -0
- data/lib/hanami/model/adapters/memory_adapter.rb +179 -0
- data/lib/hanami/model/adapters/null_adapter.rb +24 -0
- data/lib/hanami/model/adapters/sql/collection.rb +287 -0
- data/lib/hanami/model/adapters/sql/command.rb +73 -0
- data/lib/hanami/model/adapters/sql/console.rb +33 -0
- data/lib/hanami/model/adapters/sql/consoles/mysql.rb +49 -0
- data/lib/hanami/model/adapters/sql/consoles/postgresql.rb +48 -0
- data/lib/hanami/model/adapters/sql/consoles/sqlite.rb +26 -0
- data/lib/hanami/model/adapters/sql/query.rb +788 -0
- data/lib/hanami/model/adapters/sql_adapter.rb +296 -0
- data/lib/hanami/model/coercer.rb +74 -0
- data/lib/hanami/model/config/adapter.rb +116 -0
- data/lib/hanami/model/config/mapper.rb +45 -0
- data/lib/hanami/model/configuration.rb +275 -0
- data/lib/hanami/model/error.rb +7 -0
- data/lib/hanami/model/mapper.rb +124 -0
- data/lib/hanami/model/mapping.rb +48 -0
- data/lib/hanami/model/mapping/attribute.rb +85 -0
- data/lib/hanami/model/mapping/coercers.rb +314 -0
- data/lib/hanami/model/mapping/collection.rb +490 -0
- data/lib/hanami/model/mapping/collection_coercer.rb +79 -0
- data/lib/hanami/model/migrator.rb +324 -0
- data/lib/hanami/model/migrator/adapter.rb +170 -0
- data/lib/hanami/model/migrator/connection.rb +133 -0
- data/lib/hanami/model/migrator/mysql_adapter.rb +72 -0
- data/lib/hanami/model/migrator/postgres_adapter.rb +119 -0
- data/lib/hanami/model/migrator/sqlite_adapter.rb +110 -0
- data/lib/hanami/model/version.rb +4 -1
- data/lib/hanami/repository.rb +872 -0
- metadata +100 -16
- data/.gitignore +0 -9
- data/Gemfile +0 -4
- data/Rakefile +0 -2
- data/bin/console +0 -14
- 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
|