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.
Files changed (43) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +1 -0
  3. data/.travis.yml +6 -0
  4. data/.yardopts +5 -0
  5. data/EXAMPLE.md +217 -0
  6. data/Gemfile +14 -2
  7. data/README.md +303 -3
  8. data/Rakefile +17 -1
  9. data/lib/lotus-model.rb +1 -0
  10. data/lib/lotus/entity.rb +157 -0
  11. data/lib/lotus/model.rb +23 -2
  12. data/lib/lotus/model/adapters/abstract.rb +167 -0
  13. data/lib/lotus/model/adapters/implementation.rb +111 -0
  14. data/lib/lotus/model/adapters/memory/collection.rb +132 -0
  15. data/lib/lotus/model/adapters/memory/command.rb +90 -0
  16. data/lib/lotus/model/adapters/memory/query.rb +457 -0
  17. data/lib/lotus/model/adapters/memory_adapter.rb +149 -0
  18. data/lib/lotus/model/adapters/sql/collection.rb +209 -0
  19. data/lib/lotus/model/adapters/sql/command.rb +67 -0
  20. data/lib/lotus/model/adapters/sql/query.rb +615 -0
  21. data/lib/lotus/model/adapters/sql_adapter.rb +154 -0
  22. data/lib/lotus/model/mapper.rb +101 -0
  23. data/lib/lotus/model/mapping.rb +23 -0
  24. data/lib/lotus/model/mapping/coercer.rb +80 -0
  25. data/lib/lotus/model/mapping/collection.rb +336 -0
  26. data/lib/lotus/model/version.rb +4 -1
  27. data/lib/lotus/repository.rb +620 -0
  28. data/lotus-model.gemspec +15 -11
  29. data/test/entity_test.rb +126 -0
  30. data/test/fixtures.rb +81 -0
  31. data/test/model/adapters/abstract_test.rb +75 -0
  32. data/test/model/adapters/implementation_test.rb +22 -0
  33. data/test/model/adapters/memory/query_test.rb +91 -0
  34. data/test/model/adapters/memory_adapter_test.rb +1044 -0
  35. data/test/model/adapters/sql/query_test.rb +121 -0
  36. data/test/model/adapters/sql_adapter_test.rb +1078 -0
  37. data/test/model/mapper_test.rb +94 -0
  38. data/test/model/mapping/coercer_test.rb +27 -0
  39. data/test/model/mapping/collection_test.rb +82 -0
  40. data/test/repository_test.rb +283 -0
  41. data/test/test_helper.rb +30 -0
  42. data/test/version_test.rb +7 -0
  43. metadata +109 -11
data/Rakefile CHANGED
@@ -1 +1,17 @@
1
- require "bundler/gem_tasks"
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
@@ -0,0 +1 @@
1
+ require 'lotus/model'
@@ -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 "lotus/model/version"
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
- # Your code goes here...
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