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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +145 -0
- data/EXAMPLE.md +212 -0
- data/LICENSE.md +22 -0
- data/README.md +600 -7
- data/hanami-model.gemspec +17 -12
- data/lib/hanami-model.rb +1 -0
- data/lib/hanami/entity.rb +298 -0
- data/lib/hanami/entity/dirty_tracking.rb +74 -0
- data/lib/hanami/model.rb +204 -2
- data/lib/hanami/model/adapters/abstract.rb +281 -0
- data/lib/hanami/model/adapters/file_system_adapter.rb +288 -0
- data/lib/hanami/model/adapters/implementation.rb +111 -0
- data/lib/hanami/model/adapters/memory/collection.rb +132 -0
- data/lib/hanami/model/adapters/memory/command.rb +113 -0
- data/lib/hanami/model/adapters/memory/query.rb +653 -0
- data/lib/hanami/model/adapters/memory_adapter.rb +179 -0
- data/lib/hanami/model/adapters/null_adapter.rb +24 -0
- data/lib/hanami/model/adapters/sql/collection.rb +287 -0
- data/lib/hanami/model/adapters/sql/command.rb +73 -0
- data/lib/hanami/model/adapters/sql/console.rb +33 -0
- data/lib/hanami/model/adapters/sql/consoles/mysql.rb +49 -0
- data/lib/hanami/model/adapters/sql/consoles/postgresql.rb +48 -0
- data/lib/hanami/model/adapters/sql/consoles/sqlite.rb +26 -0
- data/lib/hanami/model/adapters/sql/query.rb +788 -0
- data/lib/hanami/model/adapters/sql_adapter.rb +296 -0
- data/lib/hanami/model/coercer.rb +74 -0
- data/lib/hanami/model/config/adapter.rb +116 -0
- data/lib/hanami/model/config/mapper.rb +45 -0
- data/lib/hanami/model/configuration.rb +275 -0
- data/lib/hanami/model/error.rb +7 -0
- data/lib/hanami/model/mapper.rb +124 -0
- data/lib/hanami/model/mapping.rb +48 -0
- data/lib/hanami/model/mapping/attribute.rb +85 -0
- data/lib/hanami/model/mapping/coercers.rb +314 -0
- data/lib/hanami/model/mapping/collection.rb +490 -0
- data/lib/hanami/model/mapping/collection_coercer.rb +79 -0
- data/lib/hanami/model/migrator.rb +324 -0
- data/lib/hanami/model/migrator/adapter.rb +170 -0
- data/lib/hanami/model/migrator/connection.rb +133 -0
- data/lib/hanami/model/migrator/mysql_adapter.rb +72 -0
- data/lib/hanami/model/migrator/postgres_adapter.rb +119 -0
- data/lib/hanami/model/migrator/sqlite_adapter.rb +110 -0
- data/lib/hanami/model/version.rb +4 -1
- data/lib/hanami/repository.rb +872 -0
- metadata +100 -16
- data/.gitignore +0 -9
- data/Gemfile +0 -4
- data/Rakefile +0 -2
- data/bin/console +0 -14
- 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,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
|