lotus-model 0.4.1 → 0.5.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 +15 -1
- data/README.md +3 -1
- data/lib/lotus/model.rb +10 -2
- data/lib/lotus/model/adapters/abstract.rb +63 -2
- data/lib/lotus/model/adapters/file_system_adapter.rb +11 -0
- data/lib/lotus/model/adapters/memory/query.rb +13 -0
- data/lib/lotus/model/adapters/memory_adapter.rb +9 -0
- data/lib/lotus/model/adapters/sql/collection.rb +63 -11
- data/lib/lotus/model/adapters/sql/query.rb +93 -1
- data/lib/lotus/model/adapters/sql_adapter.rb +35 -5
- data/lib/lotus/model/coercer.rb +74 -0
- data/lib/lotus/model/configuration.rb +1 -1
- data/lib/lotus/model/mapper.rb +2 -2
- data/lib/lotus/model/mapping.rb +2 -2
- data/lib/lotus/model/mapping/attribute.rb +85 -0
- data/lib/lotus/model/mapping/coercers.rb +314 -0
- data/lib/lotus/model/mapping/collection.rb +61 -7
- data/lib/lotus/model/mapping/{coercer.rb → collection_coercer.rb} +7 -9
- data/lib/lotus/model/migrator.rb +2 -1
- data/lib/lotus/model/migrator/adapter.rb +28 -27
- data/lib/lotus/model/migrator/connection.rb +133 -0
- data/lib/lotus/model/migrator/mysql_adapter.rb +2 -2
- data/lib/lotus/model/migrator/postgres_adapter.rb +20 -17
- data/lib/lotus/model/migrator/sqlite_adapter.rb +2 -2
- data/lib/lotus/model/version.rb +1 -1
- data/lib/lotus/repository.rb +134 -18
- data/lotus-model.gemspec +1 -1
- metadata +9 -6
- data/lib/lotus/model/mapping/coercions.rb +0 -192
@@ -1,4 +1,5 @@
|
|
1
1
|
require 'lotus/utils/class'
|
2
|
+
require 'lotus/model/mapping/attribute'
|
2
3
|
|
3
4
|
module Lotus
|
4
5
|
module Model
|
@@ -227,7 +228,7 @@ module Lotus
|
|
227
228
|
# Think of Redis, where everything is stored as a string or integer,
|
228
229
|
# the mapper translates values from/to the database.
|
229
230
|
#
|
230
|
-
# It supports the following types:
|
231
|
+
# It supports the following types (coercers):
|
231
232
|
#
|
232
233
|
# * Array
|
233
234
|
# * Boolean
|
@@ -245,13 +246,17 @@ module Lotus
|
|
245
246
|
# @param name [Symbol] the name of the attribute, as we want it to be
|
246
247
|
# mapped in the object
|
247
248
|
#
|
248
|
-
# @param
|
249
|
+
# @param coercer [.load, .dump] a class that implements coercer interface
|
249
250
|
#
|
250
251
|
# @param options [Hash] a set of options to customize the mapping
|
251
252
|
# @option options [Symbol] :as the name of the original column
|
252
253
|
#
|
254
|
+
# @raise [NameError] if coercer cannot be found
|
255
|
+
#
|
253
256
|
# @since 0.1.0
|
254
257
|
#
|
258
|
+
# @see Lotus::Model::Coercer
|
259
|
+
#
|
255
260
|
# @example Default schema
|
256
261
|
# require 'lotus/model'
|
257
262
|
#
|
@@ -281,8 +286,8 @@ module Lotus
|
|
281
286
|
# # The first argument (`:name`) always corresponds to the `User`
|
282
287
|
# # attribute.
|
283
288
|
#
|
284
|
-
# # The second one (`:
|
285
|
-
# # attribute.
|
289
|
+
# # The second one (`:coercer`) is the Ruby type coercer that we want
|
290
|
+
# # for our attribute.
|
286
291
|
#
|
287
292
|
# # We don't need to use `:as` because the database columns match the
|
288
293
|
# # `User` attributes.
|
@@ -322,7 +327,7 @@ module Lotus
|
|
322
327
|
# # The first argument (`:name`) always corresponds to the `Article`
|
323
328
|
# # attribute.
|
324
329
|
#
|
325
|
-
# # The second one (`:
|
330
|
+
# # The second one (`:coercer`) is the Ruby type that we want for our
|
326
331
|
# # attribute.
|
327
332
|
#
|
328
333
|
# # The third option (`:as`) is mandatory only when the database
|
@@ -330,8 +335,57 @@ module Lotus
|
|
330
335
|
# #
|
331
336
|
# # For instance: we need to use it for translate `:s_title` to
|
332
337
|
# # `:title`, but not for `:comments_count`.
|
333
|
-
|
334
|
-
|
338
|
+
#
|
339
|
+
# @example Custom coercer
|
340
|
+
# require 'lotus/model'
|
341
|
+
#
|
342
|
+
# # Given the following schema:
|
343
|
+
# #
|
344
|
+
# # CREATE TABLE articles (
|
345
|
+
# # id integer NOT NULL,
|
346
|
+
# # title varchar(128),
|
347
|
+
# # tags text[],
|
348
|
+
# # );
|
349
|
+
# #
|
350
|
+
# # The following entity:
|
351
|
+
# #
|
352
|
+
# # class Article
|
353
|
+
# # include Lotus::Entity
|
354
|
+
# # attributes :title, :tags
|
355
|
+
# # end
|
356
|
+
# #
|
357
|
+
# # And the following custom coercer:
|
358
|
+
# #
|
359
|
+
# # require 'lotus/model/coercer'
|
360
|
+
# # require 'sequel/extensions/pg_array'
|
361
|
+
# #
|
362
|
+
# # class PGArray < Lotus::Model::Coercer
|
363
|
+
# # def self.dump(value)
|
364
|
+
# # ::Sequel.pg_array(value) rescue nil
|
365
|
+
# # end
|
366
|
+
# #
|
367
|
+
# # def self.load(value)
|
368
|
+
# # ::Kernel.Array(value) unless value.nil?
|
369
|
+
# # end
|
370
|
+
# # end
|
371
|
+
#
|
372
|
+
# mapper = Lotus::Model::Mapper.new do
|
373
|
+
# collection :articles do
|
374
|
+
# entity Article
|
375
|
+
#
|
376
|
+
# attribute :id, Integer
|
377
|
+
# attribute :title, String
|
378
|
+
# attribute :tags, PGArray
|
379
|
+
# end
|
380
|
+
# end
|
381
|
+
#
|
382
|
+
# # When an entity is persisted as record into the database,
|
383
|
+
# # `PGArray.dump` is invoked.
|
384
|
+
#
|
385
|
+
# # When an entity is retrieved from the database, it will be
|
386
|
+
# # deserialized as an Array via `PGArray.load`.
|
387
|
+
def attribute(name, coercer, options = {})
|
388
|
+
@attributes[name] = Attribute.new(name, coercer, options)
|
335
389
|
end
|
336
390
|
|
337
391
|
# Serializes an entity to be persisted in the database.
|
@@ -1,5 +1,3 @@
|
|
1
|
-
require 'lotus/model/mapping/coercer'
|
2
|
-
|
3
1
|
module Lotus
|
4
2
|
module Model
|
5
3
|
module Mapping
|
@@ -7,7 +5,7 @@ module Lotus
|
|
7
5
|
#
|
8
6
|
# @api private
|
9
7
|
# @since 0.1.0
|
10
|
-
class
|
8
|
+
class CollectionCoercer
|
11
9
|
# Initialize a coercer for the given collection.
|
12
10
|
#
|
13
11
|
# @param collection [Lotus::Model::Mapping::Collection] the collection
|
@@ -47,10 +45,10 @@ module Lotus
|
|
47
45
|
# @api private
|
48
46
|
# @since 0.1.0
|
49
47
|
def _compile!
|
50
|
-
code = @collection.attributes.map do |_,
|
48
|
+
code = @collection.attributes.map do |_,attr|
|
51
49
|
%{
|
52
|
-
def deserialize_#{ mapped }(value)
|
53
|
-
|
50
|
+
def deserialize_#{ attr.mapped }(value)
|
51
|
+
#{ attr.load_coercer }(value)
|
54
52
|
end
|
55
53
|
}
|
56
54
|
end.join("\n")
|
@@ -58,17 +56,17 @@ module Lotus
|
|
58
56
|
instance_eval <<-EVAL, __FILE__, __LINE__
|
59
57
|
def to_record(entity)
|
60
58
|
if entity.id
|
61
|
-
Hash[#{ @collection.attributes.map{|name,
|
59
|
+
Hash[#{ @collection.attributes.map{|name,attr| ":#{ attr.mapped },#{ attr.dump_coercer }(entity.#{name})"}.join(',') }]
|
62
60
|
else
|
63
61
|
Hash[].tap do |record|
|
64
|
-
#{ @collection.attributes.reject{|name,_| name == @collection.identity }.map{|name,
|
62
|
+
#{ @collection.attributes.reject{|name,_| name == @collection.identity }.map{|name,attr| "value = #{ attr.dump_coercer }(entity.#{name}); record[:#{attr.mapped}] = value unless value.nil?"}.join('; ') }
|
65
63
|
end
|
66
64
|
end
|
67
65
|
end
|
68
66
|
|
69
67
|
def from_record(record)
|
70
68
|
::#{ @collection.entity }.new(
|
71
|
-
Hash[#{ @collection.attributes.map{|name,
|
69
|
+
Hash[#{ @collection.attributes.map{|name,attr| ":#{name},#{attr.load_coercer}(record[:#{attr.mapped}])"}.join(',') }]
|
72
70
|
)
|
73
71
|
end
|
74
72
|
|
data/lib/lotus/model/migrator.rb
CHANGED
@@ -1,5 +1,6 @@
|
|
1
1
|
require 'sequel'
|
2
2
|
require 'sequel/extensions/migration'
|
3
|
+
require 'lotus/model/migrator/connection'
|
3
4
|
require 'lotus/model/migrator/adapter'
|
4
5
|
|
5
6
|
module Lotus
|
@@ -314,7 +315,7 @@ module Lotus
|
|
314
315
|
# @since 0.4.0
|
315
316
|
# @api private
|
316
317
|
def self.migrations?
|
317
|
-
migrations.
|
318
|
+
Dir["#{ migrations }/*.rb"].any?
|
318
319
|
end
|
319
320
|
end
|
320
321
|
end
|
@@ -46,7 +46,7 @@ module Lotus
|
|
46
46
|
# @since 0.4.0
|
47
47
|
# @api private
|
48
48
|
def initialize(connection)
|
49
|
-
@connection = connection
|
49
|
+
@connection = Connection.new(connection)
|
50
50
|
end
|
51
51
|
|
52
52
|
# Create database.
|
@@ -57,7 +57,7 @@ module Lotus
|
|
57
57
|
#
|
58
58
|
# @see Lotus::Model::Migrator.create
|
59
59
|
def create
|
60
|
-
raise MigrationError.new("Current adapter (#{
|
60
|
+
raise MigrationError.new("Current adapter (#{ connection.database_type }) doesn't support create.")
|
61
61
|
end
|
62
62
|
|
63
63
|
# Drop database.
|
@@ -68,7 +68,7 @@ module Lotus
|
|
68
68
|
#
|
69
69
|
# @see Lotus::Model::Migrator.drop
|
70
70
|
def drop
|
71
|
-
raise MigrationError.new("Current adapter (#{
|
71
|
+
raise MigrationError.new("Current adapter (#{ connection.database_type }) doesn't support drop.")
|
72
72
|
end
|
73
73
|
|
74
74
|
# Load database schema.
|
@@ -79,7 +79,7 @@ module Lotus
|
|
79
79
|
#
|
80
80
|
# @see Lotus::Model::Migrator.prepare
|
81
81
|
def load
|
82
|
-
raise MigrationError.new("Current adapter (#{
|
82
|
+
raise MigrationError.new("Current adapter (#{ connection.database_type }) doesn't support load.")
|
83
83
|
end
|
84
84
|
|
85
85
|
# Database version.
|
@@ -87,25 +87,32 @@ module Lotus
|
|
87
87
|
# @since 0.4.0
|
88
88
|
# @api private
|
89
89
|
def version
|
90
|
-
return unless
|
90
|
+
return unless connection.adapter_connection.tables.include?(MIGRATIONS_TABLE)
|
91
91
|
|
92
|
-
if record =
|
92
|
+
if record = connection.adapter_connection[MIGRATIONS_TABLE].order(MIGRATIONS_TABLE_VERSION_COLUMN).last
|
93
93
|
record.fetch(MIGRATIONS_TABLE_VERSION_COLUMN).scan(/\A[\d]{14}/).first.to_s
|
94
94
|
end
|
95
95
|
end
|
96
96
|
|
97
97
|
private
|
98
98
|
|
99
|
-
# @since 0.
|
99
|
+
# @since 0.5.0
|
100
100
|
# @api private
|
101
|
-
|
102
|
-
uri = URI.parse(@connection.uri)
|
103
|
-
scheme, userinfo, host, port = uri.select(:scheme, :userinfo, :host, :port)
|
101
|
+
attr_reader :connection
|
104
102
|
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
103
|
+
# Returns a database connection
|
104
|
+
#
|
105
|
+
# Given a DB connection URI we can connect to a specific database or not, we need this when creating
|
106
|
+
# or droping a database. Important to notice that we can't always open a _global_ DB connection,
|
107
|
+
# because most of the times application's DB user has no rights to do so.
|
108
|
+
#
|
109
|
+
# @param global [Boolean] determine whether or not a connection should specify an database.
|
110
|
+
#
|
111
|
+
# @since 0.5.0
|
112
|
+
# @api private
|
113
|
+
#
|
114
|
+
def new_connection(global: false)
|
115
|
+
uri = global ? connection.global_uri : connection.uri
|
109
116
|
|
110
117
|
Sequel.connect(uri)
|
111
118
|
end
|
@@ -113,31 +120,31 @@ module Lotus
|
|
113
120
|
# @since 0.4.0
|
114
121
|
# @api private
|
115
122
|
def database
|
116
|
-
escape
|
123
|
+
escape connection.database
|
117
124
|
end
|
118
125
|
|
119
126
|
# @since 0.4.0
|
120
127
|
# @api private
|
121
|
-
def
|
122
|
-
escape
|
128
|
+
def port
|
129
|
+
escape connection.port
|
123
130
|
end
|
124
131
|
|
125
132
|
# @since 0.4.0
|
126
133
|
# @api private
|
127
|
-
def
|
128
|
-
escape
|
134
|
+
def host
|
135
|
+
escape connection.host
|
129
136
|
end
|
130
137
|
|
131
138
|
# @since 0.4.0
|
132
139
|
# @api private
|
133
140
|
def username
|
134
|
-
escape
|
141
|
+
escape connection.user
|
135
142
|
end
|
136
143
|
|
137
144
|
# @since 0.4.0
|
138
145
|
# @api private
|
139
146
|
def password
|
140
|
-
escape
|
147
|
+
escape connection.password
|
141
148
|
end
|
142
149
|
|
143
150
|
# @since 0.4.0
|
@@ -152,12 +159,6 @@ module Lotus
|
|
152
159
|
escape MIGRATIONS_TABLE
|
153
160
|
end
|
154
161
|
|
155
|
-
# @since 0.4.0
|
156
|
-
# @api private
|
157
|
-
def options
|
158
|
-
@connection.opts
|
159
|
-
end
|
160
|
-
|
161
162
|
# @since 0.4.0
|
162
163
|
# @api private
|
163
164
|
def escape(string)
|
@@ -0,0 +1,133 @@
|
|
1
|
+
module Lotus
|
2
|
+
module Model
|
3
|
+
module Migrator
|
4
|
+
# Sequel connection wrapper
|
5
|
+
#
|
6
|
+
# Normalize external adapters interfaces
|
7
|
+
#
|
8
|
+
# @since 0.5.0
|
9
|
+
# @api private
|
10
|
+
class Connection
|
11
|
+
attr_reader :adapter_connection
|
12
|
+
|
13
|
+
def initialize(adapter_connection)
|
14
|
+
@adapter_connection = adapter_connection
|
15
|
+
end
|
16
|
+
|
17
|
+
# Returns DB connection host
|
18
|
+
#
|
19
|
+
# Even when adapter doesn't provide it explicitly it tries to parse
|
20
|
+
#
|
21
|
+
# @since 0.5.0
|
22
|
+
# @api private
|
23
|
+
def host
|
24
|
+
@host ||= opts.fetch(:host, parsed_uri.host)
|
25
|
+
end
|
26
|
+
|
27
|
+
# Returns DB connection port
|
28
|
+
#
|
29
|
+
# Even when adapter doesn't provide it explicitly it tries to parse
|
30
|
+
#
|
31
|
+
# @since 0.5.0
|
32
|
+
# @api private
|
33
|
+
def port
|
34
|
+
@port ||= opts.fetch(:port, parsed_uri.port)
|
35
|
+
end
|
36
|
+
|
37
|
+
# Returns DB name from conenction
|
38
|
+
#
|
39
|
+
# Even when adapter doesn't provide it explicitly it tries to parse
|
40
|
+
#
|
41
|
+
# @since 0.5.0
|
42
|
+
# @api private
|
43
|
+
def database
|
44
|
+
@database ||= opts.fetch(:database, parsed_uri.path[1..-1])
|
45
|
+
end
|
46
|
+
|
47
|
+
# Returns DB type
|
48
|
+
#
|
49
|
+
# @example
|
50
|
+
# connection.database_type
|
51
|
+
# # => 'postgres'
|
52
|
+
#
|
53
|
+
# @since 0.5.0
|
54
|
+
# @api private
|
55
|
+
def database_type
|
56
|
+
adapter_connection.database_type
|
57
|
+
end
|
58
|
+
|
59
|
+
# Returns user from DB connection
|
60
|
+
#
|
61
|
+
# Even when adapter doesn't provide it explicitly it tries to parse
|
62
|
+
#
|
63
|
+
# @since 0.5.0
|
64
|
+
# @api private
|
65
|
+
def user
|
66
|
+
@user ||= opts.fetch(:user, parsed_opt('user'))
|
67
|
+
end
|
68
|
+
|
69
|
+
# Returns user from DB connection
|
70
|
+
#
|
71
|
+
# Even when adapter doesn't provide it explicitly it tries to parse
|
72
|
+
#
|
73
|
+
# @since 0.5.0
|
74
|
+
# @api private
|
75
|
+
def password
|
76
|
+
@password ||= opts.fetch(:password, parsed_opt('password'))
|
77
|
+
end
|
78
|
+
|
79
|
+
# Returns DB connection URI directly from adapter
|
80
|
+
#
|
81
|
+
# @since 0.5.0
|
82
|
+
# @api private
|
83
|
+
def uri
|
84
|
+
adapter_connection.uri
|
85
|
+
end
|
86
|
+
|
87
|
+
# Returns DB connection wihout specifying database name
|
88
|
+
#
|
89
|
+
# @since 0.5.0
|
90
|
+
# @api private
|
91
|
+
def global_uri
|
92
|
+
adapter_connection.uri.sub(parsed_uri.select(:path).first, '')
|
93
|
+
end
|
94
|
+
|
95
|
+
# Returns a boolean telling if a DB connection is from JDBC or not
|
96
|
+
#
|
97
|
+
# @since 0.5.0
|
98
|
+
# @api private
|
99
|
+
def jdbc?
|
100
|
+
!adapter_connection.uri.scan('jdbc:').empty?
|
101
|
+
end
|
102
|
+
|
103
|
+
# Returns database connection URI instance without JDBC namespace
|
104
|
+
#
|
105
|
+
# @since 0.5.0
|
106
|
+
# @api private
|
107
|
+
def parsed_uri
|
108
|
+
@uri ||= URI.parse(adapter_connection.uri.sub('jdbc:', ''))
|
109
|
+
end
|
110
|
+
|
111
|
+
private
|
112
|
+
|
113
|
+
# Returns a value of a given query string param
|
114
|
+
#
|
115
|
+
# @param option [String] which option from database connection will be extracted from URI
|
116
|
+
#
|
117
|
+
# @since 0.5.0
|
118
|
+
# @api private
|
119
|
+
def parsed_opt(option)
|
120
|
+
parsed_uri.to_s.match(/[\?|\&]#{ option }=(\w+)\&?/).to_a.last
|
121
|
+
end
|
122
|
+
|
123
|
+
# Fetch connection options from adapter
|
124
|
+
#
|
125
|
+
# @since 0.5.0
|
126
|
+
# @api private
|
127
|
+
def opts
|
128
|
+
adapter_connection.opts
|
129
|
+
end
|
130
|
+
end
|
131
|
+
end
|
132
|
+
end
|
133
|
+
end
|
@@ -9,13 +9,13 @@ module Lotus
|
|
9
9
|
# @since 0.4.0
|
10
10
|
# @api private
|
11
11
|
def create
|
12
|
-
new_connection.run %(CREATE DATABASE #{ database };)
|
12
|
+
new_connection(global: true).run %(CREATE DATABASE #{ database };)
|
13
13
|
end
|
14
14
|
|
15
15
|
# @since 0.4.0
|
16
16
|
# @api private
|
17
17
|
def drop
|
18
|
-
new_connection.run %(DROP DATABASE #{ database };)
|
18
|
+
new_connection(global: true).run %(DROP DATABASE #{ database };)
|
19
19
|
rescue Sequel::DatabaseError => e
|
20
20
|
message = if e.message.match(/doesn\'t exist/)
|
21
21
|
"Cannot find database: #{ database }"
|