hanami-model 0.0.0 → 0.6.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (51) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +145 -0
  3. data/EXAMPLE.md +212 -0
  4. data/LICENSE.md +22 -0
  5. data/README.md +600 -7
  6. data/hanami-model.gemspec +17 -12
  7. data/lib/hanami-model.rb +1 -0
  8. data/lib/hanami/entity.rb +298 -0
  9. data/lib/hanami/entity/dirty_tracking.rb +74 -0
  10. data/lib/hanami/model.rb +204 -2
  11. data/lib/hanami/model/adapters/abstract.rb +281 -0
  12. data/lib/hanami/model/adapters/file_system_adapter.rb +288 -0
  13. data/lib/hanami/model/adapters/implementation.rb +111 -0
  14. data/lib/hanami/model/adapters/memory/collection.rb +132 -0
  15. data/lib/hanami/model/adapters/memory/command.rb +113 -0
  16. data/lib/hanami/model/adapters/memory/query.rb +653 -0
  17. data/lib/hanami/model/adapters/memory_adapter.rb +179 -0
  18. data/lib/hanami/model/adapters/null_adapter.rb +24 -0
  19. data/lib/hanami/model/adapters/sql/collection.rb +287 -0
  20. data/lib/hanami/model/adapters/sql/command.rb +73 -0
  21. data/lib/hanami/model/adapters/sql/console.rb +33 -0
  22. data/lib/hanami/model/adapters/sql/consoles/mysql.rb +49 -0
  23. data/lib/hanami/model/adapters/sql/consoles/postgresql.rb +48 -0
  24. data/lib/hanami/model/adapters/sql/consoles/sqlite.rb +26 -0
  25. data/lib/hanami/model/adapters/sql/query.rb +788 -0
  26. data/lib/hanami/model/adapters/sql_adapter.rb +296 -0
  27. data/lib/hanami/model/coercer.rb +74 -0
  28. data/lib/hanami/model/config/adapter.rb +116 -0
  29. data/lib/hanami/model/config/mapper.rb +45 -0
  30. data/lib/hanami/model/configuration.rb +275 -0
  31. data/lib/hanami/model/error.rb +7 -0
  32. data/lib/hanami/model/mapper.rb +124 -0
  33. data/lib/hanami/model/mapping.rb +48 -0
  34. data/lib/hanami/model/mapping/attribute.rb +85 -0
  35. data/lib/hanami/model/mapping/coercers.rb +314 -0
  36. data/lib/hanami/model/mapping/collection.rb +490 -0
  37. data/lib/hanami/model/mapping/collection_coercer.rb +79 -0
  38. data/lib/hanami/model/migrator.rb +324 -0
  39. data/lib/hanami/model/migrator/adapter.rb +170 -0
  40. data/lib/hanami/model/migrator/connection.rb +133 -0
  41. data/lib/hanami/model/migrator/mysql_adapter.rb +72 -0
  42. data/lib/hanami/model/migrator/postgres_adapter.rb +119 -0
  43. data/lib/hanami/model/migrator/sqlite_adapter.rb +110 -0
  44. data/lib/hanami/model/version.rb +4 -1
  45. data/lib/hanami/repository.rb +872 -0
  46. metadata +100 -16
  47. data/.gitignore +0 -9
  48. data/Gemfile +0 -4
  49. data/Rakefile +0 -2
  50. data/bin/console +0 -14
  51. data/bin/setup +0 -8
@@ -4,20 +4,25 @@ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
4
  require 'hanami/model/version'
5
5
 
6
6
  Gem::Specification.new do |spec|
7
- spec.name = "hanami-model"
7
+ spec.name = 'hanami-model'
8
8
  spec.version = Hanami::Model::VERSION
9
- spec.authors = ["Luca Guidi"]
10
- spec.email = ["me@lucaguidi.com"]
9
+ spec.authors = ['Luca Guidi', 'Trung Lê', 'Alfonso Uceda']
10
+ spec.email = ['me@lucaguidi.com', 'trung.le@ruby-journal.com', 'uceda73@gmail.com']
11
+ spec.summary = %q{A persistence layer for Hanami}
12
+ spec.description = %q{A persistence framework with entities, repositories, data mapper and query objects}
13
+ spec.homepage = 'http://hanamirb.org'
14
+ spec.license = 'MIT'
11
15
 
12
- spec.summary = %q{The web, with simplicity}
13
- spec.description = %q{Hanami is a web framework for Ruby}
14
- spec.homepage = "http://hanamirb.org"
16
+ spec.files = `git ls-files -z -- lib/* CHANGELOG.md EXAMPLE.md LICENSE.md README.md hanami-model.gemspec`.split("\x0")
17
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
+ spec.require_paths = ['lib']
20
+ spec.required_ruby_version = '>= 2.0.0'
15
21
 
16
- spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
17
- spec.bindir = "exe"
18
- spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
19
- spec.require_paths = ["lib"]
22
+ spec.add_runtime_dependency 'hanami-utils', '~> 0.7'
23
+ spec.add_runtime_dependency 'sequel', '~> 4.9'
20
24
 
21
- spec.add_development_dependency "bundler", "~> 1.11"
22
- spec.add_development_dependency "rake", "~> 10.0"
25
+ spec.add_development_dependency 'bundler', '~> 1.6'
26
+ spec.add_development_dependency 'minitest', '~> 5'
27
+ spec.add_development_dependency 'rake', '~> 10'
23
28
  end
@@ -0,0 +1 @@
1
+ require 'hanami/model'
@@ -0,0 +1,298 @@
1
+ require 'hanami/utils/kernel'
2
+ require 'hanami/utils/attributes'
3
+
4
+ module Hanami
5
+ # An object that is defined by its identity.
6
+ # See "Domain Driven Design" by Eric Evans.
7
+ #
8
+ # An entity is the core of an application, where the part of the domain
9
+ # logic is implemented. It's a small, cohesive object that expresses coherent
10
+ # and meaningful behaviors.
11
+ #
12
+ # It deals with one and only one responsibility that is pertinent to the
13
+ # domain of the application, without caring about details such as persistence
14
+ # or validations.
15
+ #
16
+ # This simplicity of design allows developers to focus on behaviors, or
17
+ # message passing if you will, which is the quintessence of Object Oriented
18
+ # Programming.
19
+ #
20
+ # @example With Hanami::Entity
21
+ # require 'hanami/model'
22
+ #
23
+ # class Person
24
+ # include Hanami::Entity
25
+ # attributes :name, :age
26
+ # end
27
+ #
28
+ # When a class includes `Hanami::Entity` it receives the following interface:
29
+ #
30
+ # * #id
31
+ # * #id=
32
+ # * #initialize(attributes = {})
33
+ #
34
+ # `Hanami::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:
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
+ # **Hanami::Model** ships `Hanami::Entity` for developers's convenience.
48
+ #
49
+ # **Hanami::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
+ # **Hanami::Model** framework.
53
+ #
54
+ # However, we suggest to implement this interface by including
55
+ # `Hanami::Entity`, in case that future versions of the framework will expand
56
+ # it.
57
+ #
58
+ # See Dependency Inversion Principle for more on interfaces.
59
+ #
60
+ # @since 0.1.0
61
+ #
62
+ # @see Hanami::Repository
63
+ module Entity
64
+ # Inject the public API into the hosting class.
65
+ #
66
+ # @since 0.1.0
67
+ #
68
+ # @example With Object
69
+ # require 'hanami/model'
70
+ #
71
+ # class User
72
+ # include Hanami::Entity
73
+ # end
74
+ #
75
+ # @example With Struct
76
+ # require 'hanami/model'
77
+ #
78
+ # User = Struct.new(:id, :name) do
79
+ # include Hanami::Entity
80
+ # end
81
+ def self.included(base)
82
+ base.class_eval do
83
+ extend ClassMethods
84
+ attributes :id
85
+ end
86
+ end
87
+
88
+ module ClassMethods
89
+ # (Re)defines getters, setters and initialization for the given attributes.
90
+ #
91
+ # These attributes can match the database columns, but this isn't a
92
+ # requirement. The mapper used by the relative repository will translate
93
+ # these names automatically.
94
+ #
95
+ # An entity can work with attributes not configured in the mapper, but
96
+ # of course they will be ignored when the entity will be persisted.
97
+ #
98
+ # Please notice that the required `id` attribute is automatically defined
99
+ # and can be omitted in the arguments.
100
+ #
101
+ # @param attrs [Array<Symbol>] a set of arbitrary attribute names
102
+ #
103
+ # @since 0.2.0
104
+ #
105
+ # @see Hanami::Repository
106
+ # @see Hanami::Model::Mapper
107
+ #
108
+ # @example
109
+ # require 'hanami/model'
110
+ #
111
+ # class User
112
+ # include Hanami::Entity
113
+ # attributes :name, :age
114
+ # end
115
+ # User.attributes => #<Set: {:id, :name, :age}>
116
+ #
117
+ # @example Given params is array of attributes
118
+ # require 'hanami/model'
119
+ #
120
+ # class User
121
+ # include Hanami::Entity
122
+ # attributes [:name, :age]
123
+ # end
124
+ # User.attributes => #<Set: {:id, :name, :age}>
125
+ #
126
+ # @example Extend entity
127
+ # require 'hanami/model'
128
+ #
129
+ # class User
130
+ # include Hanami::Entity
131
+ # attributes :name
132
+ # end
133
+ #
134
+ # class DeletedUser < User
135
+ # include Hanami::Entity
136
+ # attributes :deleted_at
137
+ # end
138
+ #
139
+ # User.attributes => #<Set: {:id, :name}>
140
+ # DeletedUser.attributes => #<Set: {:id, :name, :deleted_at}>
141
+ #
142
+ def attributes(*attrs)
143
+ return @attributes ||= Set.new unless attrs.any?
144
+
145
+ Hanami::Utils::Kernel.Array(attrs).each do |attr|
146
+ if allowed_attribute_name?(attr)
147
+ define_attr_accessor(attr)
148
+ self.attributes << attr
149
+ end
150
+ end
151
+ end
152
+
153
+ # Define setter/getter methods for attributes.
154
+ #
155
+ # @param attr [Symbol] an attribute name
156
+ #
157
+ # @since 0.3.1
158
+ # @api private
159
+ def define_attr_accessor(attr)
160
+ attr_accessor(attr)
161
+ end
162
+
163
+ # Check if attr_reader define the given attribute
164
+ #
165
+ # @since 0.5.1
166
+ # @api private
167
+ def allowed_attribute_name?(name)
168
+ !instance_methods.include?(name)
169
+ end
170
+
171
+ protected
172
+
173
+ # @see Class#inherited
174
+ def inherited(subclass)
175
+ subclass.attributes(*attributes)
176
+ super
177
+ end
178
+ end
179
+
180
+ # Defines a generic, inefficient initializer, in case that the attributes
181
+ # weren't explicitly defined with `.attributes=`.
182
+ #
183
+ # @param attributes [Hash] a set of attribute names and values
184
+ #
185
+ # @raise NoMethodError in case the given attributes are trying to set unknown
186
+ # or private methods.
187
+ #
188
+ # @since 0.1.0
189
+ #
190
+ # @see .attributes
191
+ def initialize(attributes = {})
192
+ attributes.each do |k, v|
193
+ setter = "#{ k }="
194
+ public_send(setter, v) if respond_to?(setter)
195
+ end
196
+ end
197
+
198
+ # Overrides the equality Ruby operator
199
+ #
200
+ # Two entities are considered equal if they are instances of the same class
201
+ # and if they have the same #id.
202
+ #
203
+ # @since 0.1.0
204
+ def ==(other)
205
+ self.class == other.class &&
206
+ self.id == other.id
207
+ end
208
+
209
+ # Return the hash of attributes
210
+ #
211
+ # @since 0.2.0
212
+ #
213
+ # @example
214
+ # require 'hanami/model'
215
+ # class User
216
+ # include Hanami::Entity
217
+ # attributes :name
218
+ # end
219
+ #
220
+ # user = User.new(id: 23, name: 'Luca')
221
+ # user.to_h # => { :id => 23, :name => "Luca" }
222
+ def to_h
223
+ Hash[attribute_names.map { |a| [a, read_attribute(a)] }]
224
+ end
225
+
226
+ # Return the set of attribute names
227
+ #
228
+ # @since 0.5.1
229
+ #
230
+ # @example
231
+ # require 'hanami/model'
232
+ # class User
233
+ # include Hanami::Entity
234
+ # attributes :name
235
+ # end
236
+ #
237
+ # user = User.new(id: 23, name: 'Luca')
238
+ # user.attribute_names # #<Set: {:id, :name}>
239
+ def attribute_names
240
+ self.class.attributes
241
+ end
242
+
243
+ # Return the contents of the entity as a nicely formatted string.
244
+ #
245
+ # Display all attributes of the entity for inspection (even if they are nil)
246
+ #
247
+ # @since 0.5.1
248
+ #
249
+ # @example
250
+ # require 'hanami/model'
251
+ # class User
252
+ # include Hanami::Entity
253
+ # attributes :name, :email
254
+ # end
255
+ #
256
+ # user = User.new(id: 23, name: 'Luca')
257
+ # user.inspect # #<User:0x007fa7eefe0b58 @id=nil @name="Luca" @email=nil>
258
+ def inspect
259
+ attr_list = attribute_names.inject([]) do |res, name|
260
+ res << "@#{name}=#{read_attribute(name).inspect}"
261
+ end.join(' ')
262
+
263
+ "#<#{self.class.name}:0x00#{(__id__ << 1).to_s(16)} #{attr_list}>"
264
+ end
265
+
266
+ alias_method :to_s, :inspect
267
+
268
+ # Set attributes for entity
269
+ #
270
+ # @since 0.2.0
271
+ #
272
+ # @example
273
+ # require 'hanami/model'
274
+ # class User
275
+ # include Hanami::Entity
276
+ # attributes :name
277
+ # end
278
+ #
279
+ # user = User.new(name: 'Lucca')
280
+ # user.update(name: 'Luca')
281
+ # user.name # => 'Luca'
282
+ def update(attributes={})
283
+ attributes.each do |attribute, value|
284
+ public_send("#{attribute}=", value)
285
+ end
286
+ end
287
+
288
+ private
289
+
290
+ # Return the value by attribute name
291
+ #
292
+ # @since 0.5.1
293
+ # @api private
294
+ def read_attribute(attr_name)
295
+ public_send(attr_name)
296
+ end
297
+ end
298
+ end
@@ -0,0 +1,74 @@
1
+ module Hanami
2
+ module Entity
3
+ # Dirty tracking for entities
4
+ #
5
+ # @since 0.3.1
6
+ #
7
+ # @example Dirty tracking
8
+ # require 'hanami/model'
9
+ #
10
+ # class User
11
+ # include Hanami::Entity
12
+ # include Hanami::Entity::DirtyTracking
13
+ #
14
+ # attributes :name
15
+ # end
16
+ #
17
+ # article = Article.new(title: 'Generation P')
18
+ # article.changed? # => false
19
+ #
20
+ # article.title = 'Master and Margarita'
21
+ # article.changed? # => true
22
+ #
23
+ # article.changed_attributes # => {:title => "Generation P"}
24
+ module DirtyTracking
25
+ # Override initialize process.
26
+ #
27
+ # @param attributes [Hash] a set of attribute names and values
28
+ #
29
+ # @since 0.3.1
30
+ #
31
+ # @see Hanami::Entity#initialize
32
+ def initialize(attributes = {})
33
+ super
34
+ @_initial_state = Utils::Hash.new(to_h).deep_dup
35
+ end
36
+
37
+ # Getter for hash of changed attributes.
38
+ # Return empty hash, if there is no changes
39
+ # Getter for hash of changed attributes. Value in it is the previous one.
40
+ #
41
+ # @return [::Hash] the changed attributes
42
+ #
43
+ # @since 0.3.1
44
+ #
45
+ # @example
46
+ # require 'hanami/model'
47
+ #
48
+ # class Article
49
+ # include Hanami::Entity
50
+ # include Hanami::Entity::DirtyTracking
51
+ #
52
+ # attributes :title
53
+ # end
54
+ #
55
+ # article = Article.new(title: 'The crime and punishment')
56
+ # article.changed_attributes # => {}
57
+ #
58
+ # article.title = 'Master and Margarita'
59
+ # article.changed_attributes # => {:title => "The crime and punishment"}
60
+ def changed_attributes
61
+ Hash[@_initial_state.to_a - to_h.to_a]
62
+ end
63
+
64
+ # Checks if the attributes were changed
65
+ #
66
+ # @return [TrueClass, FalseClass] the result of the check
67
+ #
68
+ # @since 0.3.1
69
+ def changed?
70
+ changed_attributes.any?
71
+ end
72
+ end
73
+ end
74
+ end
@@ -1,7 +1,209 @@
1
- require "hanami/model/version"
1
+ require 'hanami/model/version'
2
+ require 'hanami/entity'
3
+ require 'hanami/entity/dirty_tracking'
4
+ require 'hanami/repository'
5
+ require 'hanami/model/mapper'
6
+ require 'hanami/model/configuration'
7
+ require 'hanami/model/error'
2
8
 
3
9
  module Hanami
10
+ # Model
11
+ #
12
+ # @since 0.1.0
4
13
  module Model
5
- # Your code goes here...
14
+ # Error for non persisted entity
15
+ # It's raised when we try to update or delete a non persisted entity.
16
+ #
17
+ # @since 0.1.0
18
+ #
19
+ # @see Hanami::Repository.update
20
+ class NonPersistedEntityError < Hanami::Model::Error
21
+ end
22
+
23
+ # Error for invalid mapper configuration
24
+ # It's raised when mapping is not configured correctly
25
+ #
26
+ # @since 0.2.0
27
+ #
28
+ # @see Hanami::Configuration#mapping
29
+ class InvalidMappingError < Hanami::Model::Error
30
+ end
31
+
32
+ # Error for invalid raw command syntax
33
+ #
34
+ # @since 0.5.0
35
+ class InvalidCommandError < Hanami::Model::Error
36
+ def initialize(message = "Invalid command")
37
+ super
38
+ end
39
+ end
40
+
41
+ # Error for invalid raw query syntax
42
+ #
43
+ # @since 0.3.1
44
+ class InvalidQueryError < Hanami::Model::Error
45
+ def initialize(message = "Invalid query")
46
+ super
47
+ end
48
+ end
49
+
50
+ include Utils::ClassAttribute
51
+
52
+ # Framework configuration
53
+ #
54
+ # @since 0.2.0
55
+ # @api private
56
+ class_attribute :configuration
57
+ self.configuration = Configuration.new
58
+
59
+ # Configure the framework.
60
+ # It yields the given block in the context of the configuration
61
+ #
62
+ # @param blk [Proc] the configuration block
63
+ #
64
+ # @since 0.2.0
65
+ #
66
+ # @see Hanami::Model
67
+ #
68
+ # @example
69
+ # require 'hanami/model'
70
+ #
71
+ # Hanami::Model.configure do
72
+ # adapter type: :sql, uri: 'postgres://localhost/database'
73
+ #
74
+ # mapping do
75
+ # collection :users do
76
+ # entity User
77
+ #
78
+ # attribute :id, Integer
79
+ # attribute :name, String
80
+ # end
81
+ # end
82
+ # end
83
+ #
84
+ # Adapter MUST follow the convention in which adapter class is inflection of adapter name
85
+ # The above example has name :sql, thus derived class will be `Hanami::Model::Adapters::SqlAdapter`
86
+ def self.configure(&blk)
87
+ configuration.instance_eval(&blk)
88
+ self
89
+ end
90
+
91
+ # Load the framework
92
+ #
93
+ # @since 0.2.0
94
+ # @api private
95
+ def self.load!
96
+ configuration.load!
97
+ end
98
+
99
+ # Unload the framework
100
+ #
101
+ # @since 0.2.0
102
+ # @api private
103
+ def self.unload!
104
+ configuration.unload!
105
+ end
106
+
107
+ # Duplicate Hanami::Model in order to create a new separated instance
108
+ # of the framework.
109
+ #
110
+ # The new instance of the framework will be completely decoupled from the
111
+ # original. It will inherit the configuration, but all the changes that
112
+ # happen after the duplication, won't be reflected on the other copies.
113
+ #
114
+ # @return [Module] a copy of Hanami::Model
115
+ #
116
+ # @since 0.2.0
117
+ # @api private
118
+ #
119
+ # @example Basic usage
120
+ # require 'hanami/model'
121
+ #
122
+ # module MyApp
123
+ # Model = Hanami::Model.dupe
124
+ # end
125
+ #
126
+ # MyApp::Model == Hanami::Model # => false
127
+ #
128
+ # MyApp::Model.configuration ==
129
+ # Hanami::Model.configuration # => false
130
+ #
131
+ # @example Inheriting configuration
132
+ # require 'hanami/model'
133
+ #
134
+ # Hanami::Model.configure do
135
+ # adapter type: :sql, uri: 'sqlite3://uri'
136
+ # end
137
+ #
138
+ # module MyApp
139
+ # Model = Hanami::Model.dupe
140
+ # end
141
+ #
142
+ # module MyApi
143
+ # Model = Hanami::Model.dupe
144
+ # Model.configure do
145
+ # adapter type: :sql, uri: 'postgresql://uri'
146
+ # end
147
+ # end
148
+ #
149
+ # Hanami::Model.configuration.adapter_config.uri # => 'sqlite3://uri'
150
+ # MyApp::Model.configuration.adapter_config.uri # => 'sqlite3://uri'
151
+ # MyApi::Model.configuration.adapter_config.uri # => 'postgresql://uri'
152
+ def self.dupe
153
+ dup.tap do |duplicated|
154
+ duplicated.configuration = Configuration.new
155
+ end
156
+ end
157
+
158
+ # Duplicate the framework and generate modules for the target application
159
+ #
160
+ # @param mod [Module] the Ruby namespace of the application
161
+ # @param blk [Proc] an optional block to configure the framework
162
+ #
163
+ # @return [Module] a copy of Hanami::Model
164
+ #
165
+ # @since 0.2.0
166
+ #
167
+ # @see Hanami::Model#dupe
168
+ # @see Hanami::Model::Configuration
169
+ #
170
+ # @example Basic usage
171
+ # require 'hanami/model'
172
+ #
173
+ # module MyApp
174
+ # Model = Hanami::Model.dupe
175
+ # end
176
+ #
177
+ # # It will:
178
+ # #
179
+ # # 1. Generate MyApp::Model
180
+ # # 2. Generate MyApp::Entity
181
+ # # 3. Generate MyApp::Repository
182
+ #
183
+ # MyApp::Model == Hanami::Model # => false
184
+ # MyApp::Repository == Hanami::Repository # => false
185
+ #
186
+ # @example Block usage
187
+ # require 'hanami/model'
188
+ #
189
+ # module MyApp
190
+ # Model = Hanami::Model.duplicate(self) do
191
+ # adapter type: :memory, uri: 'memory://localhost'
192
+ # end
193
+ # end
194
+ #
195
+ # Hanami::Model.configuration.adapter_config # => nil
196
+ # MyApp::Model.configuration.adapter_config # => #<Hanami::Model::Config::Adapter:0x007ff0ff0244f8 @type=:memory, @uri="memory://localhost", @class_name="MemoryAdapter">
197
+ def self.duplicate(mod, &blk)
198
+ dupe.tap do |duplicated|
199
+ mod.module_eval %{
200
+ Entity = Hanami::Entity.dup
201
+ Repository = Hanami::Repository.dup
202
+ }
203
+
204
+ duplicated.configure(&blk) if block_given?
205
+ end
206
+ end
207
+
6
208
  end
7
209
  end