lotus-model 0.1.2 → 0.2.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.
@@ -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