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.
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