lotus-model 0.1.2 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,45 @@
1
+ require 'lotus/utils/kernel'
2
+
3
+ module Lotus
4
+ module Model
5
+ module Config
6
+ # Read mapping file for mapping DSL
7
+ #
8
+ # @since 0.2.0
9
+ # @api private
10
+ class Mapper
11
+ EXTNAME = '.rb'
12
+
13
+ def initialize(path=nil, &blk)
14
+ if block_given?
15
+ @blk = blk
16
+ elsif path
17
+ @path = root.join(path)
18
+ else
19
+ raise Lotus::Model::InvalidMappingError.new('You must specify a block or a file.')
20
+ end
21
+ end
22
+
23
+ def to_proc
24
+ unless @blk
25
+ code = realpath.read
26
+ @blk = Proc.new { eval(code) }
27
+ end
28
+
29
+ @blk
30
+ end
31
+
32
+ private
33
+ def realpath
34
+ Utils::Kernel.Pathname("#{ @path }#{ EXTNAME }").realpath
35
+ rescue Errno::ENOENT
36
+ raise ArgumentError, 'You must specify a valid filepath.'
37
+ end
38
+
39
+ def root
40
+ Utils::Kernel.Pathname(Dir.pwd).realpath
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,187 @@
1
+ require 'lotus/model/config/adapter'
2
+ require 'lotus/model/config/mapper'
3
+
4
+ module Lotus
5
+ module Model
6
+ # Configuration for the framework, models and adapters.
7
+ #
8
+ # Lotus::Model has its own global configuration that can be manipulated
9
+ # via `Lotus::Model.configure`.
10
+ #
11
+ # @since 0.2.0
12
+ class Configuration
13
+
14
+ # The persistence mapper
15
+ #
16
+ # @return [Lotus::Model::Mapper]
17
+ #
18
+ # @since 0.2.0
19
+ attr_reader :mapper
20
+
21
+ # An adapter configuration template
22
+ #
23
+ # @return [Lotus::Model::Config::Adapter]
24
+ #
25
+ # @since 0.2.0
26
+ attr_reader :adapter_config
27
+
28
+ # Initialize a configuration instance
29
+ #
30
+ # @return [Lotus::Model::Configuration] a new configuration's
31
+ # instance
32
+ #
33
+ # @since 0.2.0
34
+ def initialize
35
+ reset!
36
+ end
37
+
38
+ # Reset all the values to the defaults
39
+ #
40
+ # @return void
41
+ #
42
+ # @since 0.2.0
43
+ def reset!
44
+ @adapter = nil
45
+ @adapter_config = nil
46
+ @mapper = NullMapper.new
47
+ @mapper_config = nil
48
+ end
49
+
50
+ alias_method :unload!, :reset!
51
+
52
+ # Load the configuration for the current framework
53
+ #
54
+ # @return void
55
+ #
56
+ # @since 0.2.0
57
+ def load!
58
+ _build_mapper
59
+ _build_adapter
60
+
61
+ mapper.load!(@adapter)
62
+ end
63
+
64
+ # Register adapter
65
+ #
66
+ # There could only 1 adapter can be registered per application
67
+ #
68
+ # @overload adapter
69
+ # Retrieves the configured adapter
70
+ # @return [Lotus::Model::Config::Adapter,NilClass] the adapter, if
71
+ # present
72
+ #
73
+ # @overload adapter
74
+ # Register the adapter
75
+ # @param @options [Hash] A set of options to register an adapter
76
+ # @option options [Symbol] :type The adapter type. Eg. :sql, :memory
77
+ # (mandatory)
78
+ # @option options [String] :uri The database uri string (mandatory)
79
+ #
80
+ # @return void
81
+ #
82
+ # @raise [ArgumentError] if one of the mandatory options is omitted
83
+ #
84
+ # @see Lotus::Model.configure
85
+ # @see Lotus::Model::Config::Adapter
86
+ #
87
+ # @example Register the adapter
88
+ # require 'lotus/model'
89
+ #
90
+ # Lotus::Model.configure do
91
+ # adapter type: :sql, uri: 'sqlite3://localhost/database'
92
+ # end
93
+ #
94
+ # Lotus::Model.adapter_config
95
+ #
96
+ # @since 0.2.0
97
+ def adapter(options = nil)
98
+ if options.nil?
99
+ @adapter_config
100
+ else
101
+ _check_adapter_options!(options)
102
+ @adapter_config ||= Lotus::Model::Config::Adapter.new(options)
103
+ end
104
+ end
105
+
106
+ # Set global persistence mapping
107
+ #
108
+ # @overload mapping(blk)
109
+ # Specify a set of mapping in the given block
110
+ # @param blk [Proc] the mapping definitions
111
+ #
112
+ # @overload mapping(path)
113
+ # Specify a relative path where to find the mapping file
114
+ # @param path [String] the relative path
115
+ #
116
+ # @return void
117
+ #
118
+ # @see Lotus::Model.configure
119
+ # @see Lotus::Model::Mapper
120
+ #
121
+ # @example Set global persistence mapper
122
+ # require 'lotus/model'
123
+ #
124
+ # Lotus::Model.configure do
125
+ # mapping do
126
+ # collection :users do
127
+ # entity User
128
+ #
129
+ # attribute :id, Integer
130
+ # attribute :name, String
131
+ # end
132
+ # end
133
+ # end
134
+ #
135
+ # @since 0.2.0
136
+ def mapping(path=nil, &blk)
137
+ @mapper_config = Lotus::Model::Config::Mapper.new(path, &blk)
138
+ end
139
+
140
+ # Duplicate by copying the settings in a new instance.
141
+ #
142
+ # @return [Lotus::Model::Configuration] a copy of the configuration
143
+ #
144
+ # @since 0.2.0
145
+ # @api private
146
+ def duplicate
147
+ Configuration.new.tap do |c|
148
+ c.instance_variable_set(:@adapter_config, @adapter_config)
149
+ c.instance_variable_set(:@mapper, @mapper)
150
+ end
151
+ end
152
+
153
+ private
154
+
155
+ # Instantiate mapper from mapping block
156
+ #
157
+ # @see Lotus::Model::Configuration#mapping
158
+ #
159
+ # @api private
160
+ # @since 0.2.0
161
+ def _build_mapper
162
+ @mapper = Lotus::Model::Mapper.new(&@mapper_config.to_proc) if @mapper_config
163
+ end
164
+
165
+ # @api private
166
+ # @since 0.1.0
167
+ def _build_adapter
168
+ @adapter = adapter_config.build(mapper)
169
+ end
170
+
171
+ # @api private
172
+ # @since 0.2.0
173
+ #
174
+ # NOTE Drop this manual check when Ruby 2.0 will not be supported anymore.
175
+ # Use keyword arguments instead.
176
+ def _check_adapter_options!(options)
177
+ # TODO Maybe this is a candidate for Lotus::Utils::Options
178
+ # We already have two similar cases:
179
+ # 1. Lotus::Router :only/:except for RESTful resources
180
+ # 2. Lotus::Validations.validate_options!
181
+ [:type, :uri].each do |keyword|
182
+ raise ArgumentError.new("missing keyword: #{keyword}") if !options.keys.include?(keyword)
183
+ end
184
+ end
185
+ end
186
+ end
187
+ end
@@ -2,6 +2,26 @@ require 'lotus/model/mapping'
2
2
 
3
3
  module Lotus
4
4
  module Model
5
+ # Error for missing mapper
6
+ # It's raised when loading model without mapper
7
+ #
8
+ # @since 0.2.0
9
+ #
10
+ # @see Lotus::Model::Configuration#mapping
11
+ class NoMappingError < ::StandardError
12
+ def initialize
13
+ super("Mapping is missing. Please check your framework configuration.")
14
+ end
15
+ end
16
+
17
+ # @since 0.2.0
18
+ # @api private
19
+ class NullMapper
20
+ def method_missing(m, *args)
21
+ raise NoMappingError.new
22
+ end
23
+ end
24
+
5
25
  # A persistence mapper that keeps entities independent from database details.
6
26
  #
7
27
  # This is database independent. It can work with SQL, document, and even
@@ -61,6 +81,7 @@ module Lotus
61
81
  def initialize(coercer = nil, &blk)
62
82
  @coercer = coercer || Mapping::Coercer
63
83
  @collections = {}
84
+
64
85
  instance_eval(&blk) if block_given?
65
86
  end
66
87
 
@@ -81,7 +102,6 @@ module Lotus
81
102
  if block_given?
82
103
  @collections[name] = Mapping::Collection.new(name, @coercer, &blk)
83
104
  else
84
- # TODO implement a getter with a private API.
85
105
  @collections[name] or raise Mapping::UnmappedCollectionError.new(name)
86
106
  end
87
107
  end
@@ -92,8 +112,11 @@ module Lotus
92
112
  # application.
93
113
  #
94
114
  # @since 0.1.0
95
- def load!
96
- @collections.each_value { |collection| collection.load! }
115
+ def load!(adapter = nil)
116
+ @collections.each_value do |collection|
117
+ collection.adapter = adapter
118
+ collection.load!
119
+ end
97
120
  self
98
121
  end
99
122
  end
@@ -19,6 +19,30 @@ module Lotus
19
19
  super("Cannot find collection: #{ name }")
20
20
  end
21
21
  end
22
+
23
+ # Invalid entity error.
24
+ #
25
+ # It gets raised when the application tries to access to a existing
26
+ # entity.
27
+ #
28
+ # @since 0.2.0
29
+ class EntityNotFound < ::StandardError
30
+ def initialize(name)
31
+ super("Cannot find class for entity: #{ name }")
32
+ end
33
+ end
34
+
35
+ # Invalid repository error.
36
+ #
37
+ # It gets raised when the application tries to access to a existing
38
+ # repository.
39
+ #
40
+ # @since 0.2.0
41
+ class RepositoryNotFound < ::StandardError
42
+ def initialize(name)
43
+ super("Cannot find class for repository: #{ name }")
44
+ end
45
+ end
22
46
  end
23
47
  end
24
48
  end
@@ -58,15 +58,15 @@ module Lotus
58
58
  instance_eval %{
59
59
  def to_record(entity)
60
60
  if entity.id
61
- Hash[*[#{ @collection.attributes.map{|name,(_,mapped)| ":#{mapped},entity.#{name}"}.join(',') }]]
61
+ Hash[#{ @collection.attributes.map{|name,(_,mapped)| ":#{mapped},entity.#{name}"}.join(',') }]
62
62
  else
63
- Hash[*[#{ @collection.attributes.reject{|name,_| name == @collection.identity }.map{|name,(_,mapped)| ":#{mapped},entity.#{name}"}.join(',') }]]
63
+ Hash[#{ @collection.attributes.reject{|name,_| name == @collection.identity }.map{|name,(_,mapped)| ":#{mapped},entity.#{name}"}.join(',') }]
64
64
  end
65
65
  end
66
66
 
67
67
  def from_record(record)
68
68
  #{ @collection.entity }.new(
69
- Hash[*[#{ @collection.attributes.map{|name,(klass,mapped)| ":#{name},Lotus::Model::Mapping::Coercions.#{klass}(record[:#{mapped}])"}.join(',') }]]
69
+ Hash[#{ @collection.attributes.map{|name,(klass,mapped)| ":#{name},Lotus::Model::Mapping::Coercions.#{klass}(record[:#{mapped}])"}.join(',') }]
70
70
  )
71
71
  end
72
72
 
@@ -112,6 +112,21 @@ module Lotus
112
112
  Utils::Kernel.Integer(arg) unless arg.nil?
113
113
  end
114
114
 
115
+ # Coerce into a BigDecimal, unless the argument is nil
116
+ #
117
+ # @param arg [Object] the object to coerce
118
+ #
119
+ # @return [BigDecimal] the result of the coercion
120
+ #
121
+ # @raise [TypeError] if the argument can't be coerced
122
+ #
123
+ # @since 0.2.0
124
+ #
125
+ # @see http://rdoc.info/gems/lotus-utils/Lotus/Utils/Kernel#BigDecimal-class_method
126
+ def self.BigDecimal(arg)
127
+ Utils::Kernel.BigDecimal(arg) unless arg.nil?
128
+ end
129
+
115
130
  # Coerce into a Set, unless the argument is nil
116
131
  #
117
132
  # @param arg [Object] the object to coerce
@@ -142,6 +157,21 @@ module Lotus
142
157
  Utils::Kernel.String(arg) unless arg.nil?
143
158
  end
144
159
 
160
+ # Coerce into a Symbol, unless the argument is nil
161
+ #
162
+ # @param arg [Object] the object to coerce
163
+ #
164
+ # @return [Symbol] the result of the coercion
165
+ #
166
+ # @raise [TypeError] if the argument can't be coerced
167
+ #
168
+ # @since 0.2.0
169
+ #
170
+ # @see http://rdoc.info/gems/lotus-utils/Lotus/Utils/Kernel#Symbol-class_method
171
+ def self.Symbol(arg)
172
+ Utils::Kernel.Symbol(arg) unless arg.nil?
173
+ end
174
+
145
175
  # Coerce into a Time, unless the argument is nil
146
176
  #
147
177
  # @param arg [Object] the object to coerce
@@ -1,3 +1,5 @@
1
+ require 'lotus/utils/class'
2
+
1
3
  module Lotus
2
4
  module Model
3
5
  module Mapping
@@ -70,18 +72,27 @@ module Lotus
70
72
  # @api private
71
73
  attr_reader :attributes
72
74
 
75
+ # @attr_reader adapter [Lotus::Model::Adapters] the instance of adapter
76
+ #
77
+ # @since 0.1.0
78
+ # @api private
79
+ attr_accessor :adapter
80
+
73
81
  # Instantiate a new collection
74
82
  #
75
83
  # @param name [Symbol] the name of the mapped collection. If used with a
76
84
  # SQL database it's the table name.
77
85
  #
86
+ # @param coercer_class [Class] the coercer class
78
87
  # @param blk [Proc] the block that maps the attributes of that collection.
79
88
  #
80
89
  # @since 0.1.0
81
90
  #
82
91
  # @see Lotus::Model::Mapper#collection
83
92
  def initialize(name, coercer_class, &blk)
84
- @name, @coercer_class, @attributes = name, coercer_class, {}
93
+ @name = name
94
+ @coercer_class = coercer_class
95
+ @attributes = {}
85
96
  instance_eval(&blk) if block_given?
86
97
  end
87
98
 
@@ -90,11 +101,34 @@ module Lotus
90
101
  # The entity can be any kind of object as long as it implements the
91
102
  # following interface: `#initialize(attributes = {})`.
92
103
  #
93
- # @param klass [Class] the entity persisted with this collection.
104
+ # @param klass [Class, String] the entity persisted with this collection.
94
105
  #
95
106
  # @since 0.1.0
96
107
  #
97
108
  # @see Lotus::Entity
109
+ #
110
+ # @example Set entity with class name
111
+ # require 'lotus/model'
112
+ #
113
+ # mapper = Lotus::Model::Mapper.new do
114
+ # collection :articles do
115
+ # entity Article
116
+ # end
117
+ # end
118
+ #
119
+ # mapper.entity #=> Article
120
+ #
121
+ # @example Set entity with class name string
122
+ # require 'lotus/model'
123
+ #
124
+ # mapper = Lotus::Model::Mapper.new do
125
+ # collection :articles do
126
+ # entity 'Article'
127
+ # end
128
+ # end
129
+ #
130
+ # mapper.entity #=> Article
131
+ #
98
132
  def entity(klass = nil)
99
133
  if klass
100
134
  @entity = klass
@@ -103,9 +137,50 @@ module Lotus
103
137
  end
104
138
  end
105
139
 
140
+ # Defines the repository that interacts with this collection.
141
+ #
142
+ # @param klass [Class, String] the repository that interacts with this collection.
143
+ #
144
+ # @since 0.2.0
145
+ #
146
+ # @see Lotus::Repository
147
+ #
148
+ # @example Set repository with class name
149
+ # require 'lotus/model'
150
+ #
151
+ # mapper = Lotus::Model::Mapper.new do
152
+ # collection :articles do
153
+ # entity Article
154
+ #
155
+ # repository RemoteArticleRepository
156
+ # end
157
+ # end
158
+ #
159
+ # mapper.repository #=> RemoteArticleRepository
160
+ #
161
+ # @example Set repository with class name string
162
+ # require 'lotus/model'
163
+ #
164
+ # mapper = Lotus::Model::Mapper.new do
165
+ # collection :articles do
166
+ # entity Article
167
+ #
168
+ # repository 'RemoteArticleRepository'
169
+ # end
170
+ # end
171
+ #
172
+ # mapper.repository #=> RemoteArticleRepository
173
+ def repository(klass = nil)
174
+ if klass
175
+ @repository = klass
176
+ else
177
+ @repository ||= default_repository_klass
178
+ end
179
+ end
180
+
106
181
  # Defines the identity for a collection.
107
182
  #
108
- # An identity is an unique value that identifies a record.
183
+ # An identity is a unique value that identifies a record.
109
184
  # If used with an SQL table it corresponds to the primary key.
110
185
  #
111
186
  # This is an optional feature.
@@ -182,6 +257,7 @@ module Lotus
182
257
  # * Integer
183
258
  # * Set
184
259
  # * String
260
+ # * Symbol
185
261
  # * Time
186
262
  #
187
263
  # @param name [Symbol] the name of the attribute, as we want it to be
@@ -208,7 +284,7 @@ module Lotus
208
284
  # #
209
285
  # # class User
210
286
  # # include Lotus::Entity
211
- # # self.attributes = :name
287
+ # # attributes :name
212
288
  # # end
213
289
  #
214
290
  # mapper = Lotus::Model::Mapper.new do
@@ -245,7 +321,7 @@ module Lotus
245
321
  # #
246
322
  # # class Article
247
323
  # # include Lotus::Entity
248
- # # self.attributes = :user_id, :title, :comments_count
324
+ # # attributes :user_id, :title, :comments_count
249
325
  # # end
250
326
  #
251
327
  # mapper = Lotus::Model::Mapper.new do
@@ -314,22 +390,64 @@ module Lotus
314
390
  # @api private
315
391
  # @since 0.1.0
316
392
  def load!
317
- @coercer = coercer_class.new(self)
318
- configure_repository!
393
+ _load_entity!
394
+ _load_repository!
395
+ _load_coercer!
396
+
397
+ _configure_repository!
319
398
  end
320
399
 
321
400
  private
401
+
322
402
  # Assigns a repository to an entity
323
403
  #
324
404
  # @see Lotus::Repository
325
405
  #
326
406
  # @api private
327
407
  # @since 0.1.0
328
- def configure_repository!
329
- repository = Object.const_get("#{ entity.name }#{ REPOSITORY_SUFFIX }")
408
+ def _configure_repository!
330
409
  repository.collection = name
410
+ repository.adapter = adapter if adapter
411
+ end
412
+
413
+ # Convert repository string to repository class
414
+ #
415
+ # @api private
416
+ # @since 0.2.0
417
+ def _load_repository!
418
+ @repository = Utils::Class.load!(repository)
419
+ rescue NameError
420
+ raise Lotus::Model::Mapping::RepositoryNotFound.new(repository.to_s)
421
+ end
422
+
423
+ # Convert entity string to entity class
424
+ #
425
+ # @api private
426
+ # @since 0.2.0
427
+ def _load_entity!
428
+ @entity = Utils::Class.load!(entity)
331
429
  rescue NameError
430
+ raise Lotus::Model::Mapping::EntityNotFound.new(entity.to_s)
332
431
  end
432
+
433
+ # Load coercer
434
+ #
435
+ # @api private
436
+ # @since 0.1.0
437
+ def _load_coercer!
438
+ @coercer = coercer_class.new(self)
439
+ end
440
+
441
+ # Retrieves the default repository class
442
+ #
443
+ # @see Lotus::Repository
444
+ #
445
+ # @api private
446
+ # @since 0.2.0
447
+ def default_repository_klass
448
+ "#{ entity }#{ REPOSITORY_SUFFIX }"
449
+ end
450
+
333
451
  end
334
452
  end
335
453
  end