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