mobility 0.8.13 → 1.0.0.alpha
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
- checksums.yaml.gz.sig +0 -0
- data.tar.gz.sig +0 -0
- data/CHANGELOG.md +26 -0
- data/Gemfile +5 -2
- data/Gemfile.lock +79 -8
- data/README.md +183 -91
- data/lib/mobility.rb +40 -166
- data/lib/mobility/arel/nodes/pg_ops.rb +1 -1
- data/lib/mobility/backend.rb +19 -41
- data/lib/mobility/backends.rb +20 -0
- data/lib/mobility/backends/active_record.rb +4 -0
- data/lib/mobility/backends/active_record/column.rb +2 -0
- data/lib/mobility/backends/active_record/container.rb +4 -2
- data/lib/mobility/backends/active_record/hstore.rb +2 -0
- data/lib/mobility/backends/active_record/json.rb +2 -0
- data/lib/mobility/backends/active_record/jsonb.rb +2 -0
- data/lib/mobility/backends/active_record/key_value.rb +5 -3
- data/lib/mobility/backends/active_record/pg_hash.rb +1 -1
- data/lib/mobility/backends/active_record/serialized.rb +2 -0
- data/lib/mobility/backends/active_record/table.rb +5 -3
- data/lib/mobility/backends/column.rb +0 -6
- data/lib/mobility/backends/container.rb +2 -1
- data/lib/mobility/backends/hash.rb +39 -0
- data/lib/mobility/backends/hstore.rb +0 -1
- data/lib/mobility/backends/json.rb +0 -1
- data/lib/mobility/backends/jsonb.rb +0 -1
- data/lib/mobility/backends/key_value.rb +22 -14
- data/lib/mobility/backends/null.rb +2 -0
- data/lib/mobility/backends/sequel.rb +3 -0
- data/lib/mobility/backends/sequel/column.rb +2 -0
- data/lib/mobility/backends/sequel/container.rb +3 -1
- data/lib/mobility/backends/sequel/hstore.rb +2 -0
- data/lib/mobility/backends/sequel/json.rb +2 -0
- data/lib/mobility/backends/sequel/jsonb.rb +3 -1
- data/lib/mobility/backends/sequel/key_value.rb +8 -6
- data/lib/mobility/backends/sequel/serialized.rb +2 -0
- data/lib/mobility/backends/sequel/table.rb +5 -2
- data/lib/mobility/backends/serialized.rb +1 -3
- data/lib/mobility/backends/table.rb +14 -6
- data/lib/mobility/pluggable.rb +36 -0
- data/lib/mobility/plugin.rb +260 -0
- data/lib/mobility/plugins.rb +26 -25
- data/lib/mobility/plugins/active_model.rb +17 -0
- data/lib/mobility/plugins/active_model/cache.rb +26 -0
- data/lib/mobility/plugins/active_model/dirty.rb +112 -77
- data/lib/mobility/plugins/active_record.rb +34 -0
- data/lib/mobility/plugins/active_record/backend.rb +25 -0
- data/lib/mobility/plugins/active_record/cache.rb +28 -0
- data/lib/mobility/plugins/active_record/dirty.rb +34 -17
- data/lib/mobility/plugins/active_record/query.rb +43 -31
- data/lib/mobility/plugins/active_record/uniqueness_validation.rb +60 -0
- data/lib/mobility/plugins/attribute_methods.rb +28 -20
- data/lib/mobility/plugins/attributes.rb +70 -0
- data/lib/mobility/plugins/backend.rb +138 -0
- data/lib/mobility/plugins/backend_reader.rb +34 -0
- data/lib/mobility/plugins/cache.rb +59 -24
- data/lib/mobility/plugins/default.rb +22 -17
- data/lib/mobility/plugins/dirty.rb +12 -33
- data/lib/mobility/plugins/fallbacks.rb +51 -43
- data/lib/mobility/plugins/fallthrough_accessors.rb +20 -23
- data/lib/mobility/plugins/locale_accessors.rb +25 -35
- data/lib/mobility/plugins/presence.rb +28 -21
- data/lib/mobility/plugins/query.rb +8 -17
- data/lib/mobility/plugins/reader.rb +50 -0
- data/lib/mobility/plugins/sequel.rb +34 -0
- data/lib/mobility/plugins/sequel/backend.rb +25 -0
- data/lib/mobility/plugins/sequel/cache.rb +24 -0
- data/lib/mobility/plugins/sequel/dirty.rb +32 -21
- data/lib/mobility/plugins/sequel/query.rb +21 -6
- data/lib/mobility/plugins/writer.rb +44 -0
- data/lib/mobility/translations.rb +95 -0
- data/lib/mobility/version.rb +12 -1
- data/lib/rails/generators/mobility/templates/initializer.rb +95 -77
- metadata +28 -27
- metadata.gz.sig +0 -0
- data/lib/mobility/active_model.rb +0 -4
- data/lib/mobility/active_model/backend_resetter.rb +0 -26
- data/lib/mobility/active_record.rb +0 -23
- data/lib/mobility/active_record/backend_resetter.rb +0 -26
- data/lib/mobility/active_record/uniqueness_validator.rb +0 -60
- data/lib/mobility/attributes.rb +0 -324
- data/lib/mobility/backend/orm_delegator.rb +0 -44
- data/lib/mobility/backend_resetter.rb +0 -50
- data/lib/mobility/configuration.rb +0 -138
- data/lib/mobility/fallbacks.rb +0 -28
- data/lib/mobility/interface.rb +0 -0
- data/lib/mobility/loaded.rb +0 -4
- data/lib/mobility/plugins/active_record/attribute_methods.rb +0 -38
- data/lib/mobility/plugins/cache/translation_cacher.rb +0 -40
- data/lib/mobility/sequel.rb +0 -9
- data/lib/mobility/sequel/backend_resetter.rb +0 -23
- data/lib/mobility/translates.rb +0 -73
@@ -65,7 +65,6 @@ set.
|
|
65
65
|
@see Mobility::Backends::Sequel::Table
|
66
66
|
=end
|
67
67
|
module Table
|
68
|
-
extend Backend::OrmDelegator
|
69
68
|
# @!method association_name
|
70
69
|
# Returns the name of the translations association.
|
71
70
|
# @return [Symbol] Name of the association
|
@@ -134,7 +133,18 @@ set.
|
|
134
133
|
# Simple hash cache to memoize translations as a hash so they can be
|
135
134
|
# fetched quickly.
|
136
135
|
module Cache
|
137
|
-
|
136
|
+
def translation_for(locale, **options)
|
137
|
+
return super(locale, options) if options.delete(:cache) == false
|
138
|
+
if cache.has_key?(locale)
|
139
|
+
cache[locale]
|
140
|
+
else
|
141
|
+
cache[locale] = super(locale, options)
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
145
|
+
def clear_cache
|
146
|
+
model_cache && model_cache.clear
|
147
|
+
end
|
138
148
|
|
139
149
|
private
|
140
150
|
|
@@ -145,11 +155,9 @@ set.
|
|
145
155
|
def model_cache
|
146
156
|
model.instance_variable_get(:"@__mobility_#{association_name}_cache")
|
147
157
|
end
|
148
|
-
|
149
|
-
def clear_cache
|
150
|
-
model_cache && model_cache.clear
|
151
|
-
end
|
152
158
|
end
|
153
159
|
end
|
160
|
+
|
161
|
+
register_backend(:table, Table)
|
154
162
|
end
|
155
163
|
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Mobility
|
4
|
+
=begin
|
5
|
+
|
6
|
+
Abstract Module subclass with methods to define plugins and defaults.
|
7
|
+
Works with {Mobility::Plugin}. (Subclassed by {Mobility::Translations}.)
|
8
|
+
|
9
|
+
=end
|
10
|
+
class Pluggable < Module
|
11
|
+
class << self
|
12
|
+
def plugin(name, *args)
|
13
|
+
Plugin.configure(self, defaults) { __send__ name, *args }
|
14
|
+
end
|
15
|
+
|
16
|
+
def plugins(&block)
|
17
|
+
Plugin.configure(self, defaults, &block)
|
18
|
+
end
|
19
|
+
|
20
|
+
def defaults
|
21
|
+
@defaults ||= {}
|
22
|
+
end
|
23
|
+
|
24
|
+
def inherited(klass)
|
25
|
+
super
|
26
|
+
klass.defaults.merge!(defaults)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def initialize(*, **options)
|
31
|
+
@options = self.class.defaults.merge(options)
|
32
|
+
end
|
33
|
+
|
34
|
+
attr_reader :options
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,260 @@
|
|
1
|
+
# frozen-string-literal: true
|
2
|
+
require "tsort"
|
3
|
+
require "mobility/util"
|
4
|
+
|
5
|
+
module Mobility
|
6
|
+
=begin
|
7
|
+
|
8
|
+
Defines convenience methods on plugin module to hook into initialize/included
|
9
|
+
method calls on +Mobility::Pluggable+ instance.
|
10
|
+
|
11
|
+
- #initialize_hook: called after {Mobility::Pluggable#initialize}, with
|
12
|
+
attribute names.
|
13
|
+
- #included_hook: called after {Mobility::Pluggable#included}. (This can be
|
14
|
+
used to include any module(s) into the backend class, see
|
15
|
+
{Mobility::Plugins::Backend}.)
|
16
|
+
|
17
|
+
Also includes a +configure+ class method to apply plugins to a pluggable
|
18
|
+
({Mobility::Pluggable} instance), with a block.
|
19
|
+
|
20
|
+
@example Defining a plugin
|
21
|
+
module MyPlugin
|
22
|
+
extend Mobility::Plugin
|
23
|
+
|
24
|
+
initialize_hook do |*names|
|
25
|
+
names.each do |name|
|
26
|
+
define_method "#{name}_foo" do
|
27
|
+
# method body
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
included_hook do |klass, backend_class|
|
33
|
+
backend_class.include MyBackendMethods
|
34
|
+
klass.include MyModelMethods
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
@example Configure an attributes class with plugins
|
39
|
+
class Translations < Mobility::Translations
|
40
|
+
end
|
41
|
+
|
42
|
+
Mobility::Plugin.configure(Translations) do
|
43
|
+
cache
|
44
|
+
fallbacks
|
45
|
+
end
|
46
|
+
|
47
|
+
Translations.included_modules
|
48
|
+
#=> [Mobility::Plugins::Fallbacks, Mobility::Plugins::Cache, ...]
|
49
|
+
=end
|
50
|
+
module Plugin
|
51
|
+
class << self
|
52
|
+
# Configure a pluggable {Mobility::Pluggable} with a block. Yields to a
|
53
|
+
# clean room where plugin names define plugins on the module. Plugin
|
54
|
+
# dependencies are resolved before applying them.
|
55
|
+
#
|
56
|
+
# @param [Class, Module] pluggable
|
57
|
+
# @param [Hash] defaults Plugin defaults hash to update
|
58
|
+
# @yield Block to define plugins
|
59
|
+
# @return [Hash] Updated plugin defaults
|
60
|
+
# @raise [Mobility::Plugin::CyclicDependency] if dependencies cannot be met
|
61
|
+
# @example
|
62
|
+
# Mobility::Plugin.configure(Translations) do
|
63
|
+
# cache
|
64
|
+
# fallbacks [:en, :de]
|
65
|
+
# end
|
66
|
+
def configure(pluggable, defaults = pluggable.defaults, &block)
|
67
|
+
DependencyResolver.new(pluggable, defaults).call(&block)
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
def initialize_hook(&block)
|
72
|
+
plugin = self
|
73
|
+
|
74
|
+
define_method :initialize do |*args, **options|
|
75
|
+
super(*args, **options)
|
76
|
+
|
77
|
+
class_exec(*args, &block) if plugin.dependencies_satisfied?(self.class)
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
def included_hook(&block)
|
82
|
+
plugin = self
|
83
|
+
|
84
|
+
define_method :included do |klass|
|
85
|
+
super(klass).tap do |backend_class|
|
86
|
+
if plugin.dependencies_satisfied?(self.class)
|
87
|
+
class_exec(klass, backend_class, &block)
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
def included(pluggable)
|
94
|
+
if defined?(@default) && !pluggable.defaults.has_key?(name = Plugins.lookup_name(self))
|
95
|
+
pluggable.defaults[name] = @default
|
96
|
+
end
|
97
|
+
super
|
98
|
+
end
|
99
|
+
|
100
|
+
def dependencies
|
101
|
+
@dependencies ||= {}
|
102
|
+
end
|
103
|
+
|
104
|
+
def default(value)
|
105
|
+
@default = value
|
106
|
+
end
|
107
|
+
|
108
|
+
# Method called when defining plugins to assign a default based on
|
109
|
+
# arguments and keyword arguments to the plugin method. By default, we
|
110
|
+
# simply assign the first argument, but plugins can opt to customize this
|
111
|
+
# if additional arguments or keyword arguments are required.
|
112
|
+
# (The backend plugin uses keyword arguments to set backend options.)
|
113
|
+
#
|
114
|
+
# @param [Hash] defaults
|
115
|
+
# @param [Symbol] key Plugin key on hash
|
116
|
+
# @param [Array] args Method arguments
|
117
|
+
def configure_default(defaults, key, *args)
|
118
|
+
defaults[key] = args[0] unless args.empty?
|
119
|
+
end
|
120
|
+
|
121
|
+
# Does this class include all plugins this plugin depends (directly) on?
|
122
|
+
# @param [Class] klass Pluggable class
|
123
|
+
def dependencies_satisfied?(klass)
|
124
|
+
required_plugins = dependencies.keys.map { |name| Plugins.load_plugin(name) }
|
125
|
+
(required_plugins - klass.included_modules).none?
|
126
|
+
end
|
127
|
+
|
128
|
+
# Specifies a dependency of this plugin.
|
129
|
+
#
|
130
|
+
# By default, the dependency is included (include: true). Passing +:before+
|
131
|
+
# or +:after+ will ensure the dependency is included before or after this
|
132
|
+
# plugin.
|
133
|
+
#
|
134
|
+
# Passing +false+ does not include the dependency, but checks that it has
|
135
|
+
# been included when running include and initialize hooks (so hooks will
|
136
|
+
# not run for this plugin if it has not been included). In other words:
|
137
|
+
# disable this plugin unless this dependency has been included elsewhere.
|
138
|
+
# (Note that this check is not applied recursively.)
|
139
|
+
#
|
140
|
+
# @param [Symbol] plugin Name of plugin dependency
|
141
|
+
# @option [TrueClass, FalseClass, Symbol] include
|
142
|
+
def requires(plugin, include: true)
|
143
|
+
unless [true, false, :before, :after].include?(include)
|
144
|
+
raise ArgumentError, "requires 'include' keyword argument must be one of: true, false, :before or :after"
|
145
|
+
end
|
146
|
+
dependencies[plugin] = include
|
147
|
+
end
|
148
|
+
|
149
|
+
DependencyResolver = Struct.new(:pluggable, :defaults) do
|
150
|
+
def call(&block)
|
151
|
+
plugins = DSL.call(defaults, &block)
|
152
|
+
tree = create_tree(plugins)
|
153
|
+
|
154
|
+
pluggable.include(*tree.tsort.reverse) unless tree.empty?
|
155
|
+
rescue TSort::Cyclic => e
|
156
|
+
raise_cyclic_dependency!(e.message)
|
157
|
+
end
|
158
|
+
|
159
|
+
private
|
160
|
+
|
161
|
+
def create_tree(plugins)
|
162
|
+
DependencyTree.new.tap do |tree|
|
163
|
+
visited = included_plugins
|
164
|
+
plugins.each { |plugin| traverse(tree, plugin, visited) }
|
165
|
+
end
|
166
|
+
end
|
167
|
+
|
168
|
+
def included_plugins
|
169
|
+
pluggable.included_modules.grep(Plugin)
|
170
|
+
end
|
171
|
+
|
172
|
+
# Recursively traverse dependencies and add their dependencies to tree
|
173
|
+
def traverse(tree, plugin, visited)
|
174
|
+
return if visited.include?(plugin)
|
175
|
+
|
176
|
+
tree.add(plugin)
|
177
|
+
|
178
|
+
plugin.dependencies.each do |dep_name, include_order|
|
179
|
+
next unless include_order
|
180
|
+
dep = Plugins.load_plugin(dep_name)
|
181
|
+
add_dependency(plugin, dep, tree, include_order)
|
182
|
+
|
183
|
+
traverse(tree, dep, visited << plugin)
|
184
|
+
end
|
185
|
+
end
|
186
|
+
|
187
|
+
def add_dependency(plugin, dep, tree, include_order)
|
188
|
+
case include_order
|
189
|
+
when :before
|
190
|
+
tree[plugin] += [dep]
|
191
|
+
when :after
|
192
|
+
check_after_dependency!(plugin, dep)
|
193
|
+
tree.add(dep)
|
194
|
+
tree[dep] += [plugin]
|
195
|
+
end
|
196
|
+
end
|
197
|
+
|
198
|
+
def check_after_dependency!(plugin, dep)
|
199
|
+
if included_plugins.include?(dep)
|
200
|
+
message = "'#{name(dep)}' plugin must come after '#{name(plugin)}' plugin"
|
201
|
+
raise DependencyConflict, append_pluggable_name(message)
|
202
|
+
end
|
203
|
+
end
|
204
|
+
|
205
|
+
def raise_cyclic_dependency!(error_message)
|
206
|
+
components = error_message.scan(/(?<=\[).*(?=\])/).first
|
207
|
+
names = components.split(', ').map! do |plugin|
|
208
|
+
name(Object.const_get(plugin)).to_s
|
209
|
+
end
|
210
|
+
message = "Dependencies cannot be resolved between: #{names.sort.join(', ')}"
|
211
|
+
raise CyclicDependency, append_pluggable_name(message)
|
212
|
+
end
|
213
|
+
|
214
|
+
def append_pluggable_name(message)
|
215
|
+
pluggable.name ? "#{message} in #{pluggable}" : message
|
216
|
+
end
|
217
|
+
|
218
|
+
def name(plugin)
|
219
|
+
Plugins.lookup_name(plugin)
|
220
|
+
end
|
221
|
+
|
222
|
+
class DependencyTree < Hash
|
223
|
+
include ::TSort
|
224
|
+
NO_DEPENDENCIES = Set.new.freeze
|
225
|
+
|
226
|
+
def add(key)
|
227
|
+
self[key] ||= NO_DEPENDENCIES
|
228
|
+
end
|
229
|
+
|
230
|
+
alias tsort_each_node each_key
|
231
|
+
|
232
|
+
def tsort_each_child(dep, &block)
|
233
|
+
self.fetch(dep, []).each(&block)
|
234
|
+
end
|
235
|
+
end
|
236
|
+
|
237
|
+
class DSL < BasicObject
|
238
|
+
def self.call(defaults, &block)
|
239
|
+
new(plugins = ::Set.new, defaults).instance_eval(&block)
|
240
|
+
plugins
|
241
|
+
end
|
242
|
+
|
243
|
+
def initialize(plugins, defaults)
|
244
|
+
@plugins = plugins
|
245
|
+
@defaults = defaults
|
246
|
+
end
|
247
|
+
|
248
|
+
def method_missing(m, *args)
|
249
|
+
plugin = Plugins.load_plugin(m)
|
250
|
+
@plugins << plugin
|
251
|
+
plugin.configure_default(@defaults, m, *args)
|
252
|
+
end
|
253
|
+
end
|
254
|
+
end
|
255
|
+
private_constant :DependencyResolver
|
256
|
+
|
257
|
+
class DependencyConflict < Mobility::Error; end
|
258
|
+
class CyclicDependency < DependencyConflict; end
|
259
|
+
end
|
260
|
+
end
|
data/lib/mobility/plugins.rb
CHANGED
@@ -2,35 +2,36 @@ module Mobility
|
|
2
2
|
=begin
|
3
3
|
|
4
4
|
Plugins allow modular customization of backends independent of the backend
|
5
|
-
itself. They are enabled through
|
6
|
-
|
7
|
-
|
8
|
-
plugins will be applied.
|
5
|
+
itself. They are enabled through {Mobility::Translations.plugins} (delegated to
|
6
|
+
from {Mobility.configure}), which takes a block within which plugins can be
|
7
|
+
declared in any order (dependencies will be resolved).
|
9
8
|
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
translates :title, foo: true
|
15
|
-
end
|
9
|
+
=end
|
10
|
+
module Plugins
|
11
|
+
@plugins = {}
|
12
|
+
@names = {}
|
16
13
|
|
17
|
-
|
18
|
-
|
19
|
-
|
14
|
+
class << self
|
15
|
+
# @param [Symbol] name Name of plugin to load.
|
16
|
+
def load_plugin(name)
|
17
|
+
unless (plugin = @plugins[name])
|
18
|
+
require "mobility/plugins/#{name}"
|
19
|
+
raise LoadError, "plugin #{name} did not register itself correctly in Mobility::Plugins" unless (plugin = @plugins[name])
|
20
|
+
end
|
21
|
+
plugin
|
22
|
+
end
|
20
23
|
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
- the value of the +option+ passed into the model with +translates+ (in this
|
26
|
-
case, +true+).
|
24
|
+
# @param [Module] plugin Plugin module to lookup. Plugin must already be loaded.
|
25
|
+
def lookup_name(plugin)
|
26
|
+
@names.fetch(plugin)
|
27
|
+
end
|
27
28
|
|
28
|
-
|
29
|
-
|
30
|
-
|
29
|
+
def register_plugin(name, plugin)
|
30
|
+
@plugins[name] = plugin
|
31
|
+
@names[plugin] = name
|
32
|
+
end
|
31
33
|
|
32
|
-
|
33
|
-
|
34
|
-
OPTION_UNSET = Object.new
|
34
|
+
class LoadError < Error; end
|
35
|
+
end
|
35
36
|
end
|
36
37
|
end
|
@@ -1,6 +1,23 @@
|
|
1
|
+
require_relative "./active_model/dirty"
|
2
|
+
require_relative "./active_model/cache"
|
3
|
+
|
1
4
|
module Mobility
|
2
5
|
module Plugins
|
6
|
+
=begin
|
7
|
+
|
8
|
+
Plugin for ActiveModel models. In practice, this is simply a wrapper to include
|
9
|
+
a few plugins which apply to models which include ActiveModel::Dirty but are
|
10
|
+
not ActiveRecord models.
|
11
|
+
|
12
|
+
=end
|
3
13
|
module ActiveModel
|
14
|
+
extend Plugin
|
15
|
+
|
16
|
+
requires :active_model_dirty
|
17
|
+
requires :active_model_cache
|
18
|
+
requires :backend, include: :before
|
4
19
|
end
|
20
|
+
|
21
|
+
register_plugin(:active_model, ActiveModel)
|
5
22
|
end
|
6
23
|
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
# frozen-string-literal: true
|
2
|
+
|
3
|
+
module Mobility
|
4
|
+
module Plugins
|
5
|
+
module ActiveModel
|
6
|
+
=begin
|
7
|
+
|
8
|
+
Adds hooks to clear Mobility cache when AM dirty reset methods are called.
|
9
|
+
|
10
|
+
=end
|
11
|
+
module Cache
|
12
|
+
extend Plugin
|
13
|
+
|
14
|
+
requires :cache, include: false
|
15
|
+
|
16
|
+
included_hook do |klass, _|
|
17
|
+
if options[:cache]
|
18
|
+
define_cache_hooks(klass, :changes_applied, :clear_changes_information)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
register_plugin(:active_model_cache, ActiveModel::Cache)
|
25
|
+
end
|
26
|
+
end
|