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