hanami-model 0.0.0 → 0.6.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 (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
@@ -0,0 +1,45 @@
1
+ require 'hanami/utils/kernel'
2
+
3
+ module Hanami
4
+ module Model
5
+ module Config
6
+ # Read mapping file for mapping DSL
7
+ #
8
+ # @since 0.2.0
9
+ # @api private
10
+ class Mapper
11
+ EXTNAME = '.rb'
12
+
13
+ def initialize(path=nil, &blk)
14
+ if block_given?
15
+ @blk = blk
16
+ elsif path
17
+ @path = root.join(path)
18
+ else
19
+ raise Hanami::Model::InvalidMappingError.new('You must specify a block or a file.')
20
+ end
21
+ end
22
+
23
+ def to_proc
24
+ unless @blk
25
+ code = realpath.read
26
+ @blk = Proc.new { eval(code) }
27
+ end
28
+
29
+ @blk
30
+ end
31
+
32
+ private
33
+ def realpath
34
+ Utils::Kernel.Pathname("#{ @path }#{ EXTNAME }").realpath
35
+ rescue Errno::ENOENT
36
+ raise ArgumentError, 'You must specify a valid filepath.'
37
+ end
38
+
39
+ def root
40
+ Utils::Kernel.Pathname(Dir.pwd).realpath
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,275 @@
1
+ require 'hanami/model/config/adapter'
2
+ require 'hanami/model/config/mapper'
3
+
4
+ module Hanami
5
+ module Model
6
+ # Configuration for the framework, models and adapters.
7
+ #
8
+ # Hanami::Model has its own global configuration that can be manipulated
9
+ # via `Hanami::Model.configure`.
10
+ #
11
+ # @since 0.2.0
12
+ class Configuration
13
+ # Default migrations path
14
+ #
15
+ # @since 0.4.0
16
+ # @api private
17
+ #
18
+ # @see Hanami::Model::Configuration#migrations
19
+ DEFAULT_MIGRATIONS_PATH = Pathname.new('db/migrations').freeze
20
+
21
+ # Default schema path
22
+ #
23
+ # @since 0.4.0
24
+ # @api private
25
+ #
26
+ # @see Hanami::Model::Configuration#schema
27
+ DEFAULT_SCHEMA_PATH = Pathname.new('db/schema.sql').freeze
28
+
29
+ # The persistence mapper
30
+ #
31
+ # @return [Hanami::Model::Mapper]
32
+ #
33
+ # @since 0.2.0
34
+ attr_reader :mapper
35
+
36
+ # An adapter configuration template
37
+ #
38
+ # @return [Hanami::Model::Config::Adapter]
39
+ #
40
+ # @since 0.2.0
41
+ attr_reader :adapter_config
42
+
43
+ # Initialize a configuration instance
44
+ #
45
+ # @return [Hanami::Model::Configuration] a new configuration's
46
+ # instance
47
+ #
48
+ # @since 0.2.0
49
+ def initialize
50
+ reset!
51
+ end
52
+
53
+ # Reset all the values to the defaults
54
+ #
55
+ # @return void
56
+ #
57
+ # @since 0.2.0
58
+ def reset!
59
+ @adapter = nil
60
+ @adapter_config = nil
61
+ @mapper = NullMapper.new
62
+ @mapper_config = nil
63
+ @migrations = DEFAULT_MIGRATIONS_PATH
64
+ @schema = DEFAULT_SCHEMA_PATH
65
+ end
66
+
67
+ alias_method :unload!, :reset!
68
+
69
+ # Load the configuration for the current framework
70
+ #
71
+ # @return void
72
+ #
73
+ # @since 0.2.0
74
+ def load!
75
+ _build_mapper
76
+ _build_adapter
77
+
78
+ mapper.load!(@adapter)
79
+ end
80
+
81
+ # Register adapter
82
+ #
83
+ # There could only 1 adapter can be registered per application
84
+ #
85
+ # @overload adapter
86
+ # Retrieves the configured adapter
87
+ # @return [Hanami::Model::Config::Adapter,NilClass] the adapter, if
88
+ # present
89
+ #
90
+ # @overload adapter
91
+ # Register the adapter
92
+ # @param @options [Hash] A set of options to register an adapter
93
+ # @option options [Symbol] :type The adapter type. Eg. :sql, :memory
94
+ # (mandatory)
95
+ # @option options [String] :uri The database uri string (mandatory)
96
+ #
97
+ # @return void
98
+ #
99
+ # @raise [ArgumentError] if one of the mandatory options is omitted
100
+ #
101
+ # @see Hanami::Model.configure
102
+ # @see Hanami::Model::Config::Adapter
103
+ #
104
+ # @example Register the adapter
105
+ # require 'hanami/model'
106
+ #
107
+ # Hanami::Model.configure do
108
+ # adapter type: :sql, uri: 'sqlite3://localhost/database'
109
+ # end
110
+ #
111
+ # Hanami::Model.configuration.adapter_config
112
+ #
113
+ # @since 0.2.0
114
+ def adapter(options = nil)
115
+ if options.nil?
116
+ @adapter_config
117
+ else
118
+ _check_adapter_options!(options)
119
+ @adapter_config ||= Hanami::Model::Config::Adapter.new(options)
120
+ end
121
+ end
122
+
123
+ # Set global persistence mapping
124
+ #
125
+ # @overload mapping(blk)
126
+ # Specify a set of mapping in the given block
127
+ # @param blk [Proc] the mapping definitions
128
+ #
129
+ # @overload mapping(path)
130
+ # Specify a relative path where to find the mapping file
131
+ # @param path [String] the relative path
132
+ #
133
+ # @return void
134
+ #
135
+ # @see Hanami::Model.configure
136
+ # @see Hanami::Model::Mapper
137
+ #
138
+ # @example Set global persistence mapper
139
+ # require 'hanami/model'
140
+ #
141
+ # Hanami::Model.configure do
142
+ # mapping do
143
+ # collection :users do
144
+ # entity User
145
+ #
146
+ # attribute :id, Integer
147
+ # attribute :name, String
148
+ # end
149
+ # end
150
+ # end
151
+ #
152
+ # @since 0.2.0
153
+ def mapping(path=nil, &blk)
154
+ @mapper_config = Hanami::Model::Config::Mapper.new(path, &blk)
155
+ end
156
+
157
+ # Migrations directory
158
+ #
159
+ # It defaults to <tt>db/migrations</tt>.
160
+ #
161
+ # @overload migrations
162
+ # Get migrations directory
163
+ # @return [Pathname] migrations directory
164
+ #
165
+ # @overload migrations(path)
166
+ # Set migrations directory
167
+ # @param path [String,Pathname] the path
168
+ # @raise [Errno::ENOENT] if the given path doesn't exist
169
+ #
170
+ # @since 0.4.0
171
+ #
172
+ # @see Hanami::Model::Migrations::DEFAULT_MIGRATIONS_PATH
173
+ #
174
+ # @example Set Custom Path
175
+ # require 'hanami/model'
176
+ #
177
+ # Hanami::Model.configure do
178
+ # # ...
179
+ # migrations 'path/to/migrations'
180
+ # end
181
+ def migrations(path = nil)
182
+ if path.nil?
183
+ @migrations
184
+ else
185
+ @migrations = root.join(path).realpath
186
+ end
187
+ end
188
+
189
+ # Schema
190
+ #
191
+ # It defaults to <tt>db/schema.sql</tt>.
192
+ #
193
+ # @overload schema
194
+ # Get schema path
195
+ # @return [Pathname] schema path
196
+ #
197
+ # @overload schema(path)
198
+ # Set schema path
199
+ # @param path [String,Pathname] the path
200
+ #
201
+ # @since 0.4.0
202
+ #
203
+ # @see Hanami::Model::Migrations::DEFAULT_SCHEMA_PATH
204
+ #
205
+ # @example Set Custom Path
206
+ # require 'hanami/model'
207
+ #
208
+ # Hanami::Model.configure do
209
+ # # ...
210
+ # schema 'path/to/schema.sql'
211
+ # end
212
+ def schema(path = nil)
213
+ if path.nil?
214
+ @schema
215
+ else
216
+ @schema = root.join(path)
217
+ end
218
+ end
219
+
220
+ # Root directory
221
+ #
222
+ # @since 0.4.0
223
+ # @api private
224
+ def root
225
+ Hanami.respond_to?(:root) ? Hanami.root : Pathname.pwd
226
+ end
227
+
228
+ # Duplicate by copying the settings in a new instance.
229
+ #
230
+ # @return [Hanami::Model::Configuration] a copy of the configuration
231
+ #
232
+ # @since 0.2.0
233
+ # @api private
234
+ def duplicate
235
+ Configuration.new.tap do |c|
236
+ c.instance_variable_set(:@adapter_config, @adapter_config)
237
+ c.instance_variable_set(:@mapper, @mapper)
238
+ end
239
+ end
240
+
241
+ private
242
+
243
+ # Instantiate mapper from mapping block
244
+ #
245
+ # @see Hanami::Model::Configuration#mapping
246
+ #
247
+ # @api private
248
+ # @since 0.2.0
249
+ def _build_mapper
250
+ @mapper = Hanami::Model::Mapper.new(&@mapper_config) if @mapper_config
251
+ end
252
+
253
+ # @api private
254
+ # @since 0.1.0
255
+ def _build_adapter
256
+ @adapter = adapter_config.build(mapper)
257
+ end
258
+
259
+ # @api private
260
+ # @since 0.2.0
261
+ #
262
+ # NOTE Drop this manual check when Ruby 2.0 will not be supported anymore.
263
+ # Use keyword arguments instead.
264
+ def _check_adapter_options!(options)
265
+ # TODO Maybe this is a candidate for Hanami::Utils::Options
266
+ # We already have two similar cases:
267
+ # 1. Hanami::Router :only/:except for RESTful resources
268
+ # 2. Hanami::Validations.validate_options!
269
+ [:type, :uri].each do |keyword|
270
+ raise ArgumentError.new("missing keyword: #{keyword}") if !options.keys.include?(keyword)
271
+ end
272
+ end
273
+ end
274
+ end
275
+ end
@@ -0,0 +1,7 @@
1
+ module Hanami
2
+ module Model
3
+ # @since 0.5.1
4
+ class Error < ::StandardError
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,124 @@
1
+ require 'hanami/model/mapping'
2
+
3
+ module Hanami
4
+ module Model
5
+ # Error for missing mapper
6
+ # It's raised when loading model without mapper
7
+ #
8
+ # @since 0.2.0
9
+ #
10
+ # @see Hanami::Model::Configuration#mapping
11
+ class NoMappingError < Hanami::Model::Error
12
+ def initialize
13
+ super("Mapping is missing. Please check your framework configuration.")
14
+ end
15
+ end
16
+
17
+ # @since 0.2.0
18
+ # @api private
19
+ class NullMapper
20
+ def method_missing(m, *args)
21
+ raise NoMappingError.new
22
+ end
23
+ end
24
+
25
+ # A persistence mapper that keeps entities independent from database details.
26
+ #
27
+ # This is database independent. It can work with SQL, document, and even
28
+ # with key/value stores.
29
+ #
30
+ # @since 0.1.0
31
+ #
32
+ # @see http://martinfowler.com/eaaCatalog/dataMapper.html
33
+ #
34
+ # @example
35
+ # require 'hanami/model'
36
+ #
37
+ # mapper = Hanami::Model::Mapper.new do
38
+ # collection :users do
39
+ # entity User
40
+ #
41
+ # attribute :id, Integer
42
+ # attribute :name, String
43
+ # end
44
+ # end
45
+ #
46
+ # # This guarantees thread-safety and should happen as last thing before
47
+ # # to start the app code.
48
+ # mapper.load!
49
+ class Mapper
50
+ # @attr_reader collections [Hash] all the mapped collections
51
+ #
52
+ # @since 0.1.0
53
+ # @api private
54
+ attr_reader :collections
55
+
56
+ # Instantiate a mapper.
57
+ #
58
+ # It accepts an optional argument (`coercer`) a class that defines the
59
+ # policies for entities translations from/to the database.
60
+ #
61
+ # If provided, this class must implement the following interface:
62
+ #
63
+ # * #initialize(collection) # Hanami::Model::Mapping::Collection
64
+ # * #to_record(entity) # translates an entity to the database type
65
+ # * #from_record(record) # translates a record into an entity
66
+ # * #deserialize_*(value) # a set of methods, one for each database column.
67
+ #
68
+ # If not given, it uses `Hanami::Model::Mapping::CollectionCoercer`, by default.
69
+ #
70
+ #
71
+ #
72
+ # @param coercer [Class] an optional class that defines the policies for
73
+ # entity translations from/to the database.
74
+ #
75
+ # @param blk [Proc] an optional block of code that gets evaluated in the
76
+ # context of the current instance
77
+ #
78
+ # @return [Hanami::Model::Mapper]
79
+ #
80
+ # @since 0.1.0
81
+ def initialize(coercer = nil, &blk)
82
+ @coercer = coercer || Mapping::CollectionCoercer
83
+ @collections = {}
84
+
85
+ instance_eval(&blk) if block_given?
86
+ end
87
+
88
+ # Maps a collection.
89
+ #
90
+ # A collection is a set of homogeneous records. Think of a table of a SQL
91
+ # database or about collection of MongoDB.
92
+ #
93
+ # @param name [Symbol] the name of the mapped collection. If used with a
94
+ # SQL database it's the table name.
95
+ #
96
+ # @param blk [Proc] the block that maps the attributes of that collection.
97
+ #
98
+ # @since 0.1.0
99
+ #
100
+ # @see Hanami::Model::Mapping::Collection
101
+ def collection(name, &blk)
102
+ if block_given?
103
+ @collections[name] = Mapping::Collection.new(name, @coercer, &blk)
104
+ else
105
+ @collections[name] or raise Mapping::UnmappedCollectionError.new(name)
106
+ end
107
+ end
108
+
109
+ # Loads the internals of the mapper, in order to guarantee thread safety.
110
+ #
111
+ # This method MUST be invoked as the last thing before of start using the
112
+ # application.
113
+ #
114
+ # @since 0.1.0
115
+ def load!(adapter = nil)
116
+ @collections.each_value do |collection|
117
+ collection.adapter = adapter
118
+ collection.load!
119
+ end
120
+ self
121
+ end
122
+ end
123
+ end
124
+ end