lotus-model 0.0.0 → 0.1.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.
- 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
|