lotus-model 0.4.1 → 0.5.0
Sign up to get free protection for your applications and to get access to all the features.
- 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 }"
|