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.
@@ -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 klass [Class] the Ruby type that we want to assign as value
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 (`:klass`) is the Ruby type that we want for our
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 (`:klass`) is the Ruby type that we want for our
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
- def attribute(name, klass, options = {})
334
- @attributes[name] = [klass, (options.fetch(:as) { name }).to_sym]
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 Coercer
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 |_,(klass,mapped)|
48
+ code = @collection.attributes.map do |_,attr|
51
49
  %{
52
- def deserialize_#{ mapped }(value)
53
- Lotus::Model::Mapping::Coercions.#{klass}(value)
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,(klass,mapped)| ":#{mapped},Lotus::Model::Mapping::Coercions.#{klass}(entity.#{name})"}.join(',') }]
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,(klass,mapped)| "value = Lotus::Model::Mapping::Coercions.#{klass}(entity.#{name}); record[:#{mapped}] = value unless value.nil?"}.join('; ') }
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,(klass,mapped)| ":#{name},Lotus::Model::Mapping::Coercions.#{klass}(record[:#{mapped}])"}.join(',') }]
69
+ Hash[#{ @collection.attributes.map{|name,attr| ":#{name},#{attr.load_coercer}(record[:#{attr.mapped}])"}.join(',') }]
72
70
  )
73
71
  end
74
72
 
@@ -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.children.any?
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 (#{ @connection.database_type }) doesn't support create.")
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 (#{ @connection.database_type }) doesn't support drop.")
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 (#{ @connection.database_type }) doesn't support load.")
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 @connection.tables.include?(MIGRATIONS_TABLE)
90
+ return unless connection.adapter_connection.tables.include?(MIGRATIONS_TABLE)
91
91
 
92
- if record = @connection[MIGRATIONS_TABLE].order(MIGRATIONS_TABLE_VERSION_COLUMN).last
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.4.0
99
+ # @since 0.5.0
100
100
  # @api private
101
- def new_connection
102
- uri = URI.parse(@connection.uri)
103
- scheme, userinfo, host, port = uri.select(:scheme, :userinfo, :host, :port)
101
+ attr_reader :connection
104
102
 
105
- uri = "#{ scheme }://"
106
- uri += "#{ userinfo }@" unless userinfo.nil?
107
- uri += host
108
- uri += ":#{ port }" unless port.nil?
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 options.fetch(:database)
123
+ escape connection.database
117
124
  end
118
125
 
119
126
  # @since 0.4.0
120
127
  # @api private
121
- def host
122
- escape options.fetch(:host)
128
+ def port
129
+ escape connection.port
123
130
  end
124
131
 
125
132
  # @since 0.4.0
126
133
  # @api private
127
- def port
128
- escape options.fetch(:port)
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 options.fetch(:user)
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 options.fetch(:password)
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 }"