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.
data/lib/lotus/entity.rb CHANGED
@@ -1,11 +1,12 @@
1
1
  require 'lotus/utils/kernel'
2
+ require 'lotus/utils/attributes'
2
3
 
3
4
  module Lotus
4
5
  # An object that is defined by its identity.
5
- # See Domain Driven Design by Eric Evans.
6
+ # See "Domain Driven Design" by Eric Evans.
6
7
  #
7
8
  # An entity is the core of an application, where the part of the domain
8
- # logic is implemented. It's a small, cohesive object that express coherent
9
+ # logic is implemented. It's a small, cohesive object that expresses coherent
9
10
  # and meaningful behaviors.
10
11
  #
11
12
  # It deals with one and only one responsibility that is pertinent to the
@@ -21,18 +22,18 @@ module Lotus
21
22
  #
22
23
  # class Person
23
24
  # include Lotus::Entity
24
- # self.attributes = :name, :age
25
+ # attributes :name, :age
25
26
  # end
26
27
  #
27
- # When a class includes `Lotus::Entity` the `.attributes=` method is exposed.
28
- # By then calling the `.attributes=` class method, the following methods are
29
- # added:
28
+ # When a class includes `Lotus::Entity` it receives the following interface:
30
29
  #
31
30
  # * #id
32
31
  # * #id=
33
32
  # * #initialize(attributes = {})
34
33
  #
35
- # If we expand the code above in pure Ruby, it would be:
34
+ # `Lotus::Entity` also provides the `.attributes=` for defining attribute accessors for the given names.
35
+ #
36
+ # If we expand the code above in **pure Ruby**, it would be:
36
37
  #
37
38
  # @example Pure Ruby
38
39
  # class Person
@@ -43,11 +44,18 @@ module Lotus
43
44
  # end
44
45
  # end
45
46
  #
46
- # Indeed, **Lotus::Model** ships `Entity` only for developers's convenience, but the
47
- # rest of the framework is able to accept any object that implements the interface above.
47
+ # **Lotus::Model** ships `Lotus::Entity` for developers's convenience.
48
+ #
49
+ # **Lotus::Model** depends on a narrow and well-defined interface for an
50
+ # Entity - `#id`, `#id=`, `#initialize(attributes={})`.If your object
51
+ # implements that interface then that object can be used as an Entity in the
52
+ # **Lotus::Model** framework.
53
+ #
54
+ # However, we suggest to implement this interface by including
55
+ # `Lotus::Entity`, in case that future versions of the framework will expand
56
+ # it.
48
57
  #
49
- # However, we suggest to implement this interface by including `Lotus::Entity`,
50
- # in case that future versions of the framework will expand it.
58
+ # See Dependency Inversion Principle for more on interfaces.
51
59
  #
52
60
  # @since 0.1.0
53
61
  #
@@ -88,9 +96,9 @@ module Lotus
88
96
  # Please notice that the required `id` attribute is automatically defined
89
97
  # and can be omitted in the arguments.
90
98
  #
91
- # @param attributes [Array<Symbol>] a set of arbitrary attribute names
99
+ # @param attrs [Array<Symbol>] a set of arbitrary attribute names
92
100
  #
93
- # @since 0.1.0
101
+ # @since 0.2.0
94
102
  #
95
103
  # @see Lotus::Repository
96
104
  # @see Lotus::Model::Mapper
@@ -100,22 +108,59 @@ module Lotus
100
108
  #
101
109
  # class User
102
110
  # include Lotus::Entity
103
- # self.attributes = :name
111
+ # attributes :name, :age
112
+ # end
113
+ # User.attributes => #<Set: {:id, :name, :age}>
114
+ #
115
+ # @example Given params is array of attributes
116
+ # require 'lotus/model'
117
+ #
118
+ # class User
119
+ # include Lotus::Entity
120
+ # attributes [:name, :age]
121
+ # end
122
+ # User.attributes => #<Set: {:id, :name, :age}>
123
+ #
124
+ # @example Extend entity
125
+ # require 'lotus/model'
126
+ #
127
+ # class User
128
+ # include Lotus::Entity
129
+ # attributes :name
104
130
  # end
105
- def attributes=(*attributes)
106
- @attributes = Lotus::Utils::Kernel.Array(attributes.unshift(:id))
131
+ #
132
+ # class DeletedUser < User
133
+ # include Lotus::Entity
134
+ # attributes :deleted_at
135
+ # end
136
+ #
137
+ # User.attributes => #<Set: {:id, :name}>
138
+ # DeletedUser.attributes => #<Set: {:id, :name, :deleted_at}>
139
+ def attributes(*attrs)
140
+ if attrs.any?
141
+ self.attributes.merge Lotus::Utils::Kernel.Array(attrs)
107
142
 
108
- class_eval %{
109
- def initialize(attributes = {})
110
- #{ @attributes.map {|a| "@#{a}" }.join(', ') }, = *attributes.values_at(#{ @attributes.map {|a| ":#{a}"}.join(', ') })
111
- end
112
- }
143
+ class_eval <<-END_EVAL, __FILE__, __LINE__
144
+ def initialize(attributes = {})
145
+ attributes = Lotus::Utils::Attributes.new(attributes)
146
+ #{@attributes.map do |a|
147
+ "@#{a} = attributes.get(:#{a})"
148
+ end.join("\n") }
149
+ end
150
+ END_EVAL
113
151
 
114
- attr_accessor *@attributes
152
+ attr_accessor *@attributes
153
+ else
154
+ @attributes ||= Set.new([:id])
155
+ end
115
156
  end
116
157
 
117
- def attributes
118
- @attributes
158
+ protected
159
+
160
+ # @see Class#inherited
161
+ def inherited(subclass)
162
+ subclass.attributes *attributes
163
+ super
119
164
  end
120
165
  end
121
166
 
@@ -146,6 +191,44 @@ module Lotus
146
191
  self.class == other.class &&
147
192
  self.id == other.id
148
193
  end
194
+
195
+ # Return the hash of attributes
196
+ #
197
+ # @since 0.2.0
198
+ #
199
+ # @example
200
+ # require 'lotus/model'
201
+ # class User
202
+ # include Lotus::Entity
203
+ # attributes :name
204
+ # end
205
+ #
206
+ # user = User.new(id: 23, name: 'Luca')
207
+ # user.to_h # => { :id => 23, :name => "Luca" }
208
+ def to_h
209
+ Hash[self.class.attributes.map { |a| [a, public_send(a)] }]
210
+ end
211
+
212
+ # Set attributes for entity
213
+ #
214
+ # @since 0.2.0
215
+ #
216
+ # @example
217
+ # require 'lotus/model'
218
+ # class User
219
+ # include Lotus::Entity
220
+ # attributes :name
221
+ # end
222
+ #
223
+ # user = User.new(name: 'Lucca')
224
+ # user.update(name: 'Luca')
225
+ # user.name # => 'Luca'
226
+ def update(attributes={})
227
+ attributes.each do |attribute, value|
228
+ public_send("#{attribute}=", value)
229
+ end
230
+ end
231
+
149
232
  end
150
233
  end
151
234
 
data/lib/lotus/model.rb CHANGED
@@ -2,20 +2,13 @@ require 'lotus/model/version'
2
2
  require 'lotus/entity'
3
3
  require 'lotus/repository'
4
4
  require 'lotus/model/mapper'
5
+ require 'lotus/model/configuration'
5
6
 
6
7
  module Lotus
7
8
  # Model
8
9
  #
9
10
  # @since 0.1.0
10
11
  module Model
11
- # Error for not found entity
12
- #
13
- # @since 0.1.0
14
- #
15
- # @see Lotus::Repository.find
16
- class EntityNotFound < ::StandardError
17
- end
18
-
19
12
  # Error for non persisted entity
20
13
  # It's raised when we try to update or delete a non persisted entity.
21
14
  #
@@ -24,5 +17,173 @@ module Lotus
24
17
  # @see Lotus::Repository.update
25
18
  class NonPersistedEntityError < ::StandardError
26
19
  end
20
+
21
+ # Error for invalid mapper configuration
22
+ # It's raised when mapping is not configured correctly
23
+ #
24
+ # @since 0.2.0
25
+ #
26
+ # @see Lotus::Configuration#mapping
27
+ class InvalidMappingError < ::StandardError
28
+ end
29
+
30
+ include Utils::ClassAttribute
31
+
32
+ # Framework configuration
33
+ #
34
+ # @since 0.2.0
35
+ # @api private
36
+ class_attribute :configuration
37
+ self.configuration = Configuration.new
38
+
39
+ # Configure the framework.
40
+ # It yields the given block in the context of the configuration
41
+ #
42
+ # @param blk [Proc] the configuration block
43
+ #
44
+ # @since 0.2.0
45
+ #
46
+ # @see Lotus::Model
47
+ #
48
+ # @example
49
+ # require 'lotus/model'
50
+ #
51
+ # Lotus::Model.configure do
52
+ # adapter type: :sql, uri: 'postgres://localhost/database'
53
+ #
54
+ # mapping do
55
+ # collection :users do
56
+ # entity User
57
+ #
58
+ # attribute :id, Integer
59
+ # attribute :name, String
60
+ # end
61
+ # end
62
+ # end
63
+ #
64
+ # Adapter MUST follow the convention in which adapter class is inflection of adapter name
65
+ # The above example has name :sql, thus derived class will be `Lotus::Model::Adapters::SqlAdapter`
66
+ def self.configure(&blk)
67
+ configuration.instance_eval(&blk)
68
+ self
69
+ end
70
+
71
+ # Load the framework
72
+ #
73
+ # @since 0.2.0
74
+ # @api private
75
+ def self.load!
76
+ configuration.load!
77
+ end
78
+
79
+ # Unload the framework
80
+ #
81
+ # @since 0.2.0
82
+ # @api private
83
+ def self.unload!
84
+ configuration.unload!
85
+ end
86
+
87
+ # Duplicate Lotus::Model in order to create a new separated instance
88
+ # of the framework.
89
+ #
90
+ # The new instance of the framework will be completely decoupled from the
91
+ # original. It will inherit the configuration, but all the changes that
92
+ # happen after the duplication, won't be reflected on the other copies.
93
+ #
94
+ # @return [Module] a copy of Lotus::Model
95
+ #
96
+ # @since 0.2.0
97
+ # @api private
98
+ #
99
+ # @example Basic usage
100
+ # require 'lotus/model'
101
+ #
102
+ # module MyApp
103
+ # Model = Lotus::Model.dupe
104
+ # end
105
+ #
106
+ # MyApp::Model == Lotus::Model # => false
107
+ #
108
+ # MyApp::Model.configuration ==
109
+ # Lotus::Model.configuration # => false
110
+ #
111
+ # @example Inheriting configuration
112
+ # require 'lotus/model'
113
+ #
114
+ # Lotus::Model.configure do
115
+ # adapter type: :sql, uri: 'sqlite3://uri'
116
+ # end
117
+ #
118
+ # module MyApp
119
+ # Model = Lotus::Model.dupe
120
+ # end
121
+ #
122
+ # module MyApi
123
+ # Model = Lotus::Model.dupe
124
+ # Model.configure do
125
+ # adapter type: :sql, uri: 'postgresql://uri'
126
+ # end
127
+ # end
128
+ #
129
+ # Lotus::Model.configuration.adapter_config.uri # => 'sqlite3://uri'
130
+ # MyApp::Model.configuration.adapter_config.uri # => 'sqlite3://uri'
131
+ # MyApi::Model.configuration.adapter_config.uri # => 'postgresql://uri'
132
+ def self.dupe
133
+ dup.tap do |duplicated|
134
+ duplicated.configuration = configuration.duplicate
135
+ end
136
+ end
137
+
138
+ # Duplicate the framework and generate modules for the target application
139
+ #
140
+ # @param mod [Module] the Ruby namespace of the application
141
+ # @param blk [Proc] an optional block to configure the framework
142
+ #
143
+ # @return [Module] a copy of Lotus::Model
144
+ #
145
+ # @since 0.2.0
146
+ #
147
+ # @see Lotus::Model#dupe
148
+ # @see Lotus::Model::Configuration
149
+ #
150
+ # @example Basic usage
151
+ # require 'lotus/model'
152
+ #
153
+ # module MyApp
154
+ # Model = Lotus::Model.dupe
155
+ # end
156
+ #
157
+ # # It will:
158
+ # #
159
+ # # 1. Generate MyApp::Model
160
+ # # 2. Generate MyApp::Entity
161
+ # # 3. Generate MyApp::Repository
162
+ #
163
+ # MyApp::Model == Lotus::Model # => false
164
+ # MyApp::Repository == Lotus::Repository # => false
165
+ #
166
+ # @example Block usage
167
+ # require 'lotus/model'
168
+ #
169
+ # module MyApp
170
+ # Model = Lotus::Model.duplicate(self) do
171
+ # adapter type: :memory, uri: 'memory://localhost'
172
+ # end
173
+ # end
174
+ #
175
+ # Lotus::Model.configuration.adapter_config # => nil
176
+ # MyApp::Model.configuration.adapter_config # => #<Lotus::Model::Config::Adapter:0x007ff0ff0244f8 @type=:memory, @uri="memory://localhost", @class_name="MemoryAdapter">
177
+ def self.duplicate(mod, &blk)
178
+ dupe.tap do |duplicated|
179
+ mod.module_eval %{
180
+ Entity = Lotus::Entity.dup
181
+ Repository = Lotus::Repository.dup
182
+ }
183
+
184
+ duplicated.configure(&blk) if block_given?
185
+ end
186
+ end
187
+
27
188
  end
28
189
  end
@@ -35,7 +35,8 @@ module Lotus
35
35
  #
36
36
  # @since 0.1.0
37
37
  def initialize(mapper, uri = nil)
38
- @mapper, @uri = mapper, uri
38
+ @mapper = mapper
39
+ @uri = uri
39
40
  end
40
41
 
41
42
  # Creates or updates a record in the database for the given entity.
@@ -96,7 +97,7 @@ module Lotus
96
97
  raise NotImplementedError
97
98
  end
98
99
 
99
- # Returns an unique record from the given collection, with the given
100
+ # Returns a unique record from the given collection, with the given
100
101
  # identity.
101
102
  #
102
103
  # @param collection [Symbol] the target collection (it must be mapped).
@@ -0,0 +1,272 @@
1
+ require 'thread'
2
+ require 'pathname'
3
+ require 'lotus/model/adapters/memory_adapter'
4
+
5
+ module Lotus
6
+ module Model
7
+ module Adapters
8
+ # In memory adapter with file system persistence.
9
+ # It behaves like the SQL adapter, but it doesn't support all the SQL
10
+ # features offered by that kind of databases.
11
+ #
12
+ # This adapter SHOULD be used only for development or testing purposes.
13
+ # Each read/write operation is wrapped by a `Mutex` and persisted to the
14
+ # disk.
15
+ #
16
+ # For those reasons it's really unefficient, but great for quick
17
+ # prototyping as it's schema-less.
18
+ #
19
+ # It works exactly like the `MemoryAdapter`, with the only difference
20
+ # that it persist data to the disk.
21
+ #
22
+ # The persistence policy uses Ruby `Marshal` `dump` and `load` operations.
23
+ # Please be aware of the limitations this model.
24
+ #
25
+ # @see Lotus::Model::Adapters::Implementation
26
+ # @see Lotus::Model::Adapters::MemoryAdapter
27
+ # @see http://www.ruby-doc.org/core/Marshal.html
28
+ #
29
+ # @api private
30
+ # @since 0.2.0
31
+ class FileSystemAdapter < MemoryAdapter
32
+ # Default writing mode
33
+ #
34
+ # Binary, write only, create file if missing or erase if don't.
35
+ #
36
+ # @see http://ruby-doc.org/core/File/Constants.html
37
+ #
38
+ # @since 0.2.0
39
+ # @api private
40
+ WRITING_MODE = File::WRONLY|File::BINARY|File::CREAT
41
+
42
+ # Default chmod
43
+ #
44
+ # @see http://en.wikipedia.org/wiki/Chmod
45
+ #
46
+ # @since 0.2.0
47
+ # @api private
48
+ CHMOD = 0644
49
+
50
+ # File scheme
51
+ #
52
+ # @see https://tools.ietf.org/html/rfc3986
53
+ #
54
+ # @since 0.2.0
55
+ # @api private
56
+ FILE_SCHEME = 'file:///'.freeze
57
+
58
+ # Initialize the adapter.
59
+ #
60
+ # @param mapper [Object] the database mapper
61
+ # @param uri [String] the connection uri
62
+ #
63
+ # @return [Lotus::Model::Adapters::FileSystemAdapter]
64
+ #
65
+ # @see Lotus::Model::Mapper
66
+ #
67
+ # @api private
68
+ # @since 0.2.0
69
+ def initialize(mapper, uri)
70
+ super
71
+ prepare(uri)
72
+
73
+ @_mutex = Mutex.new
74
+ end
75
+
76
+ # Returns all the records for the given collection
77
+ #
78
+ # @param collection [Symbol] the target collection (it must be mapped).
79
+ #
80
+ # @return [Array] all the records
81
+ #
82
+ # @api private
83
+ # @since 0.2.0
84
+ def all(collection)
85
+ _synchronize do
86
+ read(collection)
87
+ super
88
+ end
89
+ end
90
+
91
+ # Returns a unique record from the given collection, with the given
92
+ # id.
93
+ #
94
+ # @param collection [Symbol] the target collection (it must be mapped).
95
+ # @param id [Object] the identity of the object.
96
+ #
97
+ # @return [Object] the entity
98
+ #
99
+ # @api private
100
+ # @since 0.2.0
101
+ def find(collection, id)
102
+ _synchronize do
103
+ read(collection)
104
+ super
105
+ end
106
+ end
107
+
108
+ # Returns the first record in the given collection.
109
+ #
110
+ # @param collection [Symbol] the target collection (it must be mapped).
111
+ #
112
+ # @return [Object] the first entity
113
+ #
114
+ # @api private
115
+ # @since 0.2.0
116
+ def first(collection)
117
+ _synchronize do
118
+ read(collection)
119
+ super
120
+ end
121
+ end
122
+
123
+ # Returns the last record in the given collection.
124
+ #
125
+ # @param collection [Symbol] the target collection (it must be mapped).
126
+ #
127
+ # @return [Object] the last entity
128
+ #
129
+ # @api private
130
+ # @since 0.2.0
131
+ def last(collection)
132
+ _synchronize do
133
+ read(collection)
134
+ super
135
+ end
136
+ end
137
+
138
+ # Creates a record in the database for the given entity.
139
+ # It assigns the `id` attribute, in case of success.
140
+ #
141
+ # @param collection [Symbol] the target collection (it must be mapped).
142
+ # @param entity [#id=] the entity to create
143
+ #
144
+ # @return [Object] the entity
145
+ #
146
+ # @api private
147
+ # @since 0.2.0
148
+ def create(collection, entity)
149
+ _synchronize do
150
+ super
151
+ write(collection)
152
+ end
153
+ end
154
+
155
+ # Updates a record in the database corresponding to the given entity.
156
+ #
157
+ # @param collection [Symbol] the target collection (it must be mapped).
158
+ # @param entity [#id] the entity to update
159
+ #
160
+ # @return [Object] the entity
161
+ #
162
+ # @api private
163
+ # @since 0.2.0
164
+ def update(collection, entity)
165
+ _synchronize do
166
+ super
167
+ write(collection)
168
+ end
169
+ end
170
+
171
+ # Deletes a record in the database corresponding to the given entity.
172
+ #
173
+ # @param collection [Symbol] the target collection (it must be mapped).
174
+ # @param entity [#id] the entity to delete
175
+ #
176
+ # @api private
177
+ # @since 0.2.0
178
+ def delete(collection, entity)
179
+ _synchronize do
180
+ super
181
+ write(collection)
182
+ end
183
+ end
184
+
185
+ # Deletes all the records from the given collection and resets the
186
+ # identity counter.
187
+ #
188
+ # @param collection [Symbol] the target collection (it must be mapped).
189
+ #
190
+ # @api private
191
+ # @since 0.2.0
192
+ def clear(collection)
193
+ _synchronize do
194
+ super
195
+ write(collection)
196
+ end
197
+ end
198
+
199
+ # Fabricates a query
200
+ #
201
+ # @param collection [Symbol] the target collection (it must be mapped).
202
+ # @param blk [Proc] a block of code to be executed in the context of
203
+ # the query.
204
+ #
205
+ # @return [Lotus::Model::Adapters::Memory::Query]
206
+ #
207
+ # @see Lotus::Model::Adapters::Memory::Query
208
+ #
209
+ # @api private
210
+ # @since 0.2.0
211
+ def query(collection, context = nil, &blk)
212
+ # _synchronize do
213
+ read(collection)
214
+ super
215
+ # end
216
+ end
217
+
218
+ # Database informations
219
+ #
220
+ # @return [Hash] per collection informations
221
+ #
222
+ # @api private
223
+ # @since 0.2.0
224
+ def info
225
+ @collections.each_with_object({}) do |(collection,_), result|
226
+ result[collection] = query(collection).count
227
+ end
228
+ end
229
+
230
+ private
231
+ # @api private
232
+ # @since 0.2.0
233
+ def prepare(uri)
234
+ @root = Pathname.new(uri.sub(FILE_SCHEME, ''))
235
+ @root.mkpath
236
+ end
237
+
238
+ # @api private
239
+ # @since 0.2.0
240
+ def _synchronize
241
+ @_mutex.synchronize { yield }
242
+ end
243
+
244
+ # @api private
245
+ # @since 0.2.0
246
+ def write(collection)
247
+ path = @root.join("#{ collection }")
248
+ path.open(WRITING_MODE, CHMOD) {|f| f.write _dump( @collections.fetch(collection) ) }
249
+ end
250
+
251
+ # @api private
252
+ # @since 0.2.0
253
+ def read(collection)
254
+ path = @root.join("#{ collection }")
255
+ @collections[collection] = _load(path.read) if path.exist?
256
+ end
257
+
258
+ # @api private
259
+ # @since 0.2.0
260
+ def _dump(contents)
261
+ Marshal.dump(contents)
262
+ end
263
+
264
+ # @api private
265
+ # @since 0.2.0
266
+ def _load(contents)
267
+ Marshal.load(contents)
268
+ end
269
+ end
270
+ end
271
+ end
272
+ end