lotus-model 0.0.0 → 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.gitignore +1 -0
- data/.travis.yml +6 -0
- data/.yardopts +5 -0
- data/EXAMPLE.md +217 -0
- data/Gemfile +14 -2
- data/README.md +303 -3
- data/Rakefile +17 -1
- data/lib/lotus-model.rb +1 -0
- data/lib/lotus/entity.rb +157 -0
- data/lib/lotus/model.rb +23 -2
- data/lib/lotus/model/adapters/abstract.rb +167 -0
- data/lib/lotus/model/adapters/implementation.rb +111 -0
- data/lib/lotus/model/adapters/memory/collection.rb +132 -0
- data/lib/lotus/model/adapters/memory/command.rb +90 -0
- data/lib/lotus/model/adapters/memory/query.rb +457 -0
- data/lib/lotus/model/adapters/memory_adapter.rb +149 -0
- data/lib/lotus/model/adapters/sql/collection.rb +209 -0
- data/lib/lotus/model/adapters/sql/command.rb +67 -0
- data/lib/lotus/model/adapters/sql/query.rb +615 -0
- data/lib/lotus/model/adapters/sql_adapter.rb +154 -0
- data/lib/lotus/model/mapper.rb +101 -0
- data/lib/lotus/model/mapping.rb +23 -0
- data/lib/lotus/model/mapping/coercer.rb +80 -0
- data/lib/lotus/model/mapping/collection.rb +336 -0
- data/lib/lotus/model/version.rb +4 -1
- data/lib/lotus/repository.rb +620 -0
- data/lotus-model.gemspec +15 -11
- data/test/entity_test.rb +126 -0
- data/test/fixtures.rb +81 -0
- data/test/model/adapters/abstract_test.rb +75 -0
- data/test/model/adapters/implementation_test.rb +22 -0
- data/test/model/adapters/memory/query_test.rb +91 -0
- data/test/model/adapters/memory_adapter_test.rb +1044 -0
- data/test/model/adapters/sql/query_test.rb +121 -0
- data/test/model/adapters/sql_adapter_test.rb +1078 -0
- data/test/model/mapper_test.rb +94 -0
- data/test/model/mapping/coercer_test.rb +27 -0
- data/test/model/mapping/collection_test.rb +82 -0
- data/test/repository_test.rb +283 -0
- data/test/test_helper.rb +30 -0
- data/test/version_test.rb +7 -0
- metadata +109 -11
data/Rakefile
CHANGED
@@ -1 +1,17 @@
|
|
1
|
-
require
|
1
|
+
require 'rake'
|
2
|
+
require 'rake/testtask'
|
3
|
+
require 'bundler/gem_tasks'
|
4
|
+
|
5
|
+
Rake::TestTask.new do |t|
|
6
|
+
t.pattern = 'test/**/*_test.rb'
|
7
|
+
t.libs.push 'test'
|
8
|
+
end
|
9
|
+
|
10
|
+
namespace :test do
|
11
|
+
task :coverage do
|
12
|
+
ENV['COVERAGE'] = 'true'
|
13
|
+
Rake::Task['test'].invoke
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
task default: :test
|
data/lib/lotus-model.rb
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require 'lotus/model'
|
data/lib/lotus/entity.rb
ADDED
@@ -0,0 +1,157 @@
|
|
1
|
+
require 'lotus/utils/kernel'
|
2
|
+
|
3
|
+
module Lotus
|
4
|
+
# An object that is defined by its identity.
|
5
|
+
# See Domain Driven Design by Eric Evans.
|
6
|
+
#
|
7
|
+
# 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
|
+
# and meagniful behaviors.
|
10
|
+
#
|
11
|
+
# It deals with one and only one responsibility that is pertinent to the
|
12
|
+
# domain of the application, without caring about details such as persistence
|
13
|
+
# or validations.
|
14
|
+
#
|
15
|
+
# This simplicity of design allows developers to focus on behaviors, or
|
16
|
+
# message passing if you will, which is the quintessence of Object Oriented
|
17
|
+
# Programming.
|
18
|
+
#
|
19
|
+
# @example With Lotus::Entity
|
20
|
+
# require 'lotus/model'
|
21
|
+
#
|
22
|
+
# class Person
|
23
|
+
# include Lotus::Entity
|
24
|
+
# self.attributes = :name, :age
|
25
|
+
# end
|
26
|
+
#
|
27
|
+
# When a class includes `Lotus::Entity` it will receive the following interface:
|
28
|
+
#
|
29
|
+
# * #id
|
30
|
+
# * #id=
|
31
|
+
# * #initialize(attributes = {})
|
32
|
+
#
|
33
|
+
# Also, the usage of `.attributes=` defines accessors for the given attribute
|
34
|
+
# names.
|
35
|
+
#
|
36
|
+
# If we expand the code above in pure Ruby, it would be:
|
37
|
+
#
|
38
|
+
# @example Pure Ruby
|
39
|
+
# class Person
|
40
|
+
# attr_accessor :id, :name, :age
|
41
|
+
#
|
42
|
+
# def initialize(attributes = {})
|
43
|
+
# @id, @name, @age = attributes.values_at(:id, :name, :age)
|
44
|
+
# end
|
45
|
+
# end
|
46
|
+
#
|
47
|
+
# Indeed, **Lotus::Model** ships `Entity` only for developers's convenience, but the
|
48
|
+
# rest of the framework is able to accept any object that implements the interface above.
|
49
|
+
#
|
50
|
+
# However, we suggest to implement this interface by including `Lotus::Entity`,
|
51
|
+
# in case that future versions of the framework will expand it.
|
52
|
+
#
|
53
|
+
# @since 0.1.0
|
54
|
+
#
|
55
|
+
# @see Lotus::Repository
|
56
|
+
module Entity
|
57
|
+
# Inject the public API into the hosting class.
|
58
|
+
#
|
59
|
+
# @since 0.1.0
|
60
|
+
#
|
61
|
+
# @example With Object
|
62
|
+
# require 'lotus/model'
|
63
|
+
#
|
64
|
+
# class User
|
65
|
+
# include Lotus::Entity
|
66
|
+
# end
|
67
|
+
#
|
68
|
+
# @example With Struct
|
69
|
+
# require 'lotus/model'
|
70
|
+
#
|
71
|
+
# User = Struct.new(:id, :name) do
|
72
|
+
# include Lotus::Entity
|
73
|
+
# end
|
74
|
+
def self.included(base)
|
75
|
+
base.class_eval do
|
76
|
+
extend ClassMethods
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
module ClassMethods
|
81
|
+
# (Re)defines getters, setters and initialization for the given attributes.
|
82
|
+
#
|
83
|
+
# These attributes can match the database columns, but this isn't a
|
84
|
+
# requirement. The mapper used by the relative repository will translate
|
85
|
+
# these names automatically.
|
86
|
+
#
|
87
|
+
# An entity can work with attributes not configured in the mapper, but
|
88
|
+
# of course they will be ignored when the entity will be persisted.
|
89
|
+
#
|
90
|
+
# Please notice that the required `id` attribute is automatically defined
|
91
|
+
# and can be omitted in the arguments.
|
92
|
+
#
|
93
|
+
# @param attributes [Array<Symbol>] a set of arbitrary attribute names
|
94
|
+
#
|
95
|
+
# @since 0.1.0
|
96
|
+
#
|
97
|
+
# @see Lotus::Repository
|
98
|
+
# @see Lotus::Model::Mapper
|
99
|
+
#
|
100
|
+
# @example
|
101
|
+
# require 'lotus/model'
|
102
|
+
#
|
103
|
+
# class User
|
104
|
+
# include Lotus::Entity
|
105
|
+
# self.attributes = :name
|
106
|
+
# end
|
107
|
+
def attributes=(*attributes)
|
108
|
+
@attributes = Lotus::Utils::Kernel.Array(attributes.unshift(:id))
|
109
|
+
|
110
|
+
class_eval %{
|
111
|
+
def initialize(attributes = {})
|
112
|
+
#{ @attributes.map {|a| "@#{a}" }.join(', ') }, = *attributes.values_at(#{ @attributes.map {|a| ":#{a}"}.join(', ') })
|
113
|
+
end
|
114
|
+
}
|
115
|
+
|
116
|
+
@attributes.each do |attr|
|
117
|
+
class_eval do
|
118
|
+
attr_accessor attr
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
def attributes
|
124
|
+
@attributes
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
# Defines a generic, inefficient initializer, in case that the attributes
|
129
|
+
# weren't explicitly defined with `.attributes=`.
|
130
|
+
#
|
131
|
+
# @param attributes [Hash] a set of attribute names and values
|
132
|
+
#
|
133
|
+
# @raise NoMethodError in case the given attributes are trying to set unknown
|
134
|
+
# or private methods.
|
135
|
+
#
|
136
|
+
# @since 0.1.0
|
137
|
+
#
|
138
|
+
# @see .attributes
|
139
|
+
def initialize(attributes = {})
|
140
|
+
attributes.each do |k, v|
|
141
|
+
public_send("#{ k }=", v)
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
145
|
+
# Overrides the equality Ruby operator
|
146
|
+
#
|
147
|
+
# Two entities are considered equal if they are instances of the same class
|
148
|
+
# and if they have the same #id.
|
149
|
+
#
|
150
|
+
# @since 0.1.0
|
151
|
+
def ==(other)
|
152
|
+
self.class == other.class &&
|
153
|
+
self.id == other.id
|
154
|
+
end
|
155
|
+
end
|
156
|
+
end
|
157
|
+
|
data/lib/lotus/model.rb
CHANGED
@@ -1,7 +1,28 @@
|
|
1
|
-
require
|
1
|
+
require 'lotus/model/version'
|
2
|
+
require 'lotus/entity'
|
3
|
+
require 'lotus/repository'
|
4
|
+
require 'lotus/model/mapper'
|
2
5
|
|
3
6
|
module Lotus
|
7
|
+
# Model
|
8
|
+
#
|
9
|
+
# @since 0.1.0
|
4
10
|
module Model
|
5
|
-
#
|
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
|
+
# Error for non persisted entity
|
20
|
+
# It's raised when we try to update or delete a non persisted entity.
|
21
|
+
#
|
22
|
+
# @since 0.1.0
|
23
|
+
#
|
24
|
+
# @see Lotus::Repository.update
|
25
|
+
class NonPersistedEntityError < ::StandardError
|
26
|
+
end
|
6
27
|
end
|
7
28
|
end
|
@@ -0,0 +1,167 @@
|
|
1
|
+
module Lotus
|
2
|
+
module Model
|
3
|
+
module Adapters
|
4
|
+
# It's raised when an adapter can't find the underlying database adapter.
|
5
|
+
#
|
6
|
+
# Example: When we try to use the SqlAdapter with a Postgres database
|
7
|
+
# but we didn't loaded the pg gem before.
|
8
|
+
#
|
9
|
+
# @see Lotus::Model::Adapters::SqlAdapter#initialize
|
10
|
+
#
|
11
|
+
# @since 0.1.0
|
12
|
+
class DatabaseAdapterNotFound < ::StandardError
|
13
|
+
end
|
14
|
+
|
15
|
+
# Abstract adapter.
|
16
|
+
#
|
17
|
+
# An adapter is a concrete implementation that allows a repository to
|
18
|
+
# communicate with a single database.
|
19
|
+
#
|
20
|
+
# Lotus::Model is shipped with Memory and SQL adapters.
|
21
|
+
# Third part adapters MUST implement the interface defined here.
|
22
|
+
# For convenience they may inherit from this class.
|
23
|
+
#
|
24
|
+
# These are low level details, and shouldn't be used directly.
|
25
|
+
# Please use a repository for entities persistence.
|
26
|
+
#
|
27
|
+
# @since 0.1.0
|
28
|
+
class Abstract
|
29
|
+
# Initialize the adapter
|
30
|
+
#
|
31
|
+
# @param mapper [Lotus::Model::Mapper] the object that defines the
|
32
|
+
# database to entities mapping
|
33
|
+
#
|
34
|
+
# @param uri [String] the optional connection string to the database
|
35
|
+
#
|
36
|
+
# @since 0.1.0
|
37
|
+
def initialize(mapper, uri = nil)
|
38
|
+
@mapper, @uri = mapper, uri
|
39
|
+
end
|
40
|
+
|
41
|
+
# Creates or updates a record in the database for the given entity.
|
42
|
+
#
|
43
|
+
# @param collection [Symbol] the target collection (it must be mapped).
|
44
|
+
# @param entity [Object] the entity to persist
|
45
|
+
#
|
46
|
+
# @return [Object] the entity
|
47
|
+
#
|
48
|
+
# @since 0.1.0
|
49
|
+
def persist(collection, entity)
|
50
|
+
raise NotImplementedError
|
51
|
+
end
|
52
|
+
|
53
|
+
# Creates a record in the database for the given entity.
|
54
|
+
# It should assign an id (identity) to the entity in case of success.
|
55
|
+
#
|
56
|
+
# @param collection [Symbol] the target collection (it must be mapped).
|
57
|
+
# @param entity [Object] the entity to create
|
58
|
+
#
|
59
|
+
# @return [Object] the entity
|
60
|
+
#
|
61
|
+
# @since 0.1.0
|
62
|
+
def create(collection, entity)
|
63
|
+
raise NotImplementedError
|
64
|
+
end
|
65
|
+
|
66
|
+
# Updates a record in the database corresponding to the given entity.
|
67
|
+
#
|
68
|
+
# @param collection [Symbol] the target collection (it must be mapped).
|
69
|
+
# @param entity [Object] the entity to update
|
70
|
+
#
|
71
|
+
# @return [Object] the entity
|
72
|
+
#
|
73
|
+
# @since 0.1.0
|
74
|
+
def update(collection, entity)
|
75
|
+
raise NotImplementedError
|
76
|
+
end
|
77
|
+
|
78
|
+
# Deletes a record in the database corresponding to the given entity.
|
79
|
+
#
|
80
|
+
# @param collection [Symbol] the target collection (it must be mapped).
|
81
|
+
# @param entity [Object] the entity to delete
|
82
|
+
#
|
83
|
+
# @since 0.1.0
|
84
|
+
def delete(collection, entity)
|
85
|
+
raise NotImplementedError
|
86
|
+
end
|
87
|
+
|
88
|
+
# Returns all the records for the given collection
|
89
|
+
#
|
90
|
+
# @param collection [Symbol] the target collection (it must be mapped).
|
91
|
+
#
|
92
|
+
# @return [Array] all the records
|
93
|
+
#
|
94
|
+
# @since 0.1.0
|
95
|
+
def all(collection)
|
96
|
+
raise NotImplementedError
|
97
|
+
end
|
98
|
+
|
99
|
+
# Returns an unique record from the given collection, with the given
|
100
|
+
# identity.
|
101
|
+
#
|
102
|
+
# @param collection [Symbol] the target collection (it must be mapped).
|
103
|
+
# @param id [Object] the identity of the object.
|
104
|
+
#
|
105
|
+
# @return [Object] the entity
|
106
|
+
#
|
107
|
+
# @since 0.1.0
|
108
|
+
def find(collection, id)
|
109
|
+
raise NotImplementedError
|
110
|
+
end
|
111
|
+
|
112
|
+
# Returns the first record in the given collection.
|
113
|
+
#
|
114
|
+
# @param collection [Symbol] the target collection (it must be mapped).
|
115
|
+
#
|
116
|
+
# @return [Object] the first entity
|
117
|
+
#
|
118
|
+
# @since 0.1.0
|
119
|
+
def first(collection)
|
120
|
+
raise NotImplementedError
|
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
|
+
# @since 0.1.0
|
130
|
+
def last(collection)
|
131
|
+
raise NotImplementedError
|
132
|
+
end
|
133
|
+
|
134
|
+
# Empties the given collection.
|
135
|
+
#
|
136
|
+
# @param collection [Symbol] the target collection (it must be mapped).
|
137
|
+
#
|
138
|
+
# @since 0.1.0
|
139
|
+
def clear(collection)
|
140
|
+
raise NotImplementedError
|
141
|
+
end
|
142
|
+
|
143
|
+
# Executes a command for the given query.
|
144
|
+
#
|
145
|
+
# @param query [Object] the query object to act on.
|
146
|
+
#
|
147
|
+
# @since 0.1.0
|
148
|
+
def command(query)
|
149
|
+
raise NotImplementedError
|
150
|
+
end
|
151
|
+
|
152
|
+
# Returns a query
|
153
|
+
#
|
154
|
+
# @param collection [Symbol] the target collection (it must be mapped).
|
155
|
+
# @param blk [Proc] a block of code to be executed in the context of
|
156
|
+
# the query.
|
157
|
+
#
|
158
|
+
# @return [Object]
|
159
|
+
#
|
160
|
+
# @since 0.1.0
|
161
|
+
def query(collection, &blk)
|
162
|
+
raise NotImplementedError
|
163
|
+
end
|
164
|
+
end
|
165
|
+
end
|
166
|
+
end
|
167
|
+
end
|
@@ -0,0 +1,111 @@
|
|
1
|
+
module Lotus
|
2
|
+
module Model
|
3
|
+
module Adapters
|
4
|
+
# Shared implementation for SqlAdapter and MemoryAdapter
|
5
|
+
#
|
6
|
+
# @api private
|
7
|
+
# @since 0.1.0
|
8
|
+
module Implementation
|
9
|
+
# Creates or updates a record in the database for the given entity.
|
10
|
+
#
|
11
|
+
# @param collection [Symbol] the target collection (it must be mapped).
|
12
|
+
# @param entity [#id, #id=] the entity to persist
|
13
|
+
#
|
14
|
+
# @return [Object] the entity
|
15
|
+
#
|
16
|
+
# @api private
|
17
|
+
# @since 0.1.0
|
18
|
+
def persist(collection, entity)
|
19
|
+
if entity.id
|
20
|
+
update(collection, entity)
|
21
|
+
else
|
22
|
+
create(collection, entity)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
# Returns all the records for the given collection
|
27
|
+
#
|
28
|
+
# @param collection [Symbol] the target collection (it must be mapped).
|
29
|
+
#
|
30
|
+
# @return [Array] all the records
|
31
|
+
#
|
32
|
+
# @api private
|
33
|
+
# @since 0.1.0
|
34
|
+
def all(collection)
|
35
|
+
# TODO consider to make this lazy (aka remove #all)
|
36
|
+
query(collection).all
|
37
|
+
end
|
38
|
+
|
39
|
+
# Returns an unique record from the given collection, with the given
|
40
|
+
# id.
|
41
|
+
#
|
42
|
+
# @param collection [Symbol] the target collection (it must be mapped).
|
43
|
+
# @param id [Object] the identity of the object.
|
44
|
+
#
|
45
|
+
# @return [Object] the entity
|
46
|
+
#
|
47
|
+
# @api private
|
48
|
+
# @since 0.1.0
|
49
|
+
def find(collection, id)
|
50
|
+
_first(
|
51
|
+
_find(collection, id)
|
52
|
+
)
|
53
|
+
end
|
54
|
+
|
55
|
+
# Returns the first record in the given collection.
|
56
|
+
#
|
57
|
+
# @param collection [Symbol] the target collection (it must be mapped).
|
58
|
+
#
|
59
|
+
# @return [Object] the first entity
|
60
|
+
#
|
61
|
+
# @api private
|
62
|
+
# @since 0.1.0
|
63
|
+
def first(collection)
|
64
|
+
_first(
|
65
|
+
query(collection).asc(_identity(collection))
|
66
|
+
)
|
67
|
+
end
|
68
|
+
|
69
|
+
# Returns the last record in the given collection.
|
70
|
+
#
|
71
|
+
# @param collection [Symbol] the target collection (it must be mapped).
|
72
|
+
#
|
73
|
+
# @return [Object] the last entity
|
74
|
+
#
|
75
|
+
# @api private
|
76
|
+
# @since 0.1.0
|
77
|
+
def last(collection)
|
78
|
+
_first(
|
79
|
+
query(collection).desc(_identity(collection))
|
80
|
+
)
|
81
|
+
end
|
82
|
+
|
83
|
+
private
|
84
|
+
def _collection(name)
|
85
|
+
raise NotImplementedError
|
86
|
+
end
|
87
|
+
|
88
|
+
def _mapped_collection(name)
|
89
|
+
@mapper.collection(name)
|
90
|
+
end
|
91
|
+
|
92
|
+
def _find(collection, id)
|
93
|
+
identity = _identity(collection)
|
94
|
+
query(collection).where(identity => _id(collection, identity, id))
|
95
|
+
end
|
96
|
+
|
97
|
+
def _first(query)
|
98
|
+
query.limit(1).first
|
99
|
+
end
|
100
|
+
|
101
|
+
def _identity(collection)
|
102
|
+
_mapped_collection(collection).identity
|
103
|
+
end
|
104
|
+
|
105
|
+
def _id(collection, column, value)
|
106
|
+
_mapped_collection(collection).deserialize_attribute(column, value)
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|