mobility 0.8.13 → 1.0.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
- checksums.yaml.gz.sig +2 -2
- data.tar.gz.sig +0 -0
- data/CHANGELOG.md +63 -0
- data/Gemfile +5 -2
- data/Gemfile.lock +39 -20
- data/README.md +183 -93
- data/lib/mobility.rb +101 -169
- data/lib/mobility/backend.rb +27 -51
- data/lib/mobility/backends.rb +20 -0
- data/lib/mobility/backends/active_record.rb +4 -0
- data/lib/mobility/backends/active_record/column.rb +3 -1
- data/lib/mobility/backends/active_record/container.rb +10 -11
- data/lib/mobility/backends/active_record/hstore.rb +6 -4
- data/lib/mobility/backends/active_record/json.rb +5 -3
- data/lib/mobility/backends/active_record/jsonb.rb +5 -3
- data/lib/mobility/backends/active_record/key_value.rb +31 -13
- data/lib/mobility/backends/active_record/pg_hash.rb +1 -1
- data/lib/mobility/backends/active_record/serialized.rb +6 -0
- data/lib/mobility/backends/active_record/table.rb +17 -10
- data/lib/mobility/backends/column.rb +0 -6
- data/lib/mobility/backends/container.rb +10 -1
- data/lib/mobility/backends/hash.rb +39 -0
- data/lib/mobility/backends/hash_valued.rb +4 -0
- data/lib/mobility/backends/hstore.rb +0 -1
- data/lib/mobility/backends/json.rb +0 -1
- data/lib/mobility/backends/jsonb.rb +1 -2
- data/lib/mobility/backends/key_value.rb +31 -26
- data/lib/mobility/backends/null.rb +2 -0
- data/lib/mobility/backends/sequel.rb +37 -2
- data/lib/mobility/backends/sequel/column.rb +2 -0
- data/lib/mobility/backends/sequel/container.rb +11 -9
- data/lib/mobility/backends/sequel/hstore.rb +3 -1
- data/lib/mobility/backends/sequel/json.rb +3 -0
- data/lib/mobility/backends/sequel/jsonb.rb +3 -1
- data/lib/mobility/backends/sequel/key_value.rb +87 -18
- data/lib/mobility/backends/sequel/pg_hash.rb +6 -6
- data/lib/mobility/backends/sequel/serialized.rb +6 -0
- data/lib/mobility/backends/sequel/table.rb +22 -9
- data/lib/mobility/backends/serialized.rb +1 -3
- data/lib/mobility/backends/table.rb +39 -31
- data/lib/mobility/pluggable.rb +56 -0
- data/lib/mobility/plugin.rb +260 -0
- data/lib/mobility/plugins.rb +27 -24
- 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 +119 -78
- data/lib/mobility/plugins/active_record.rb +37 -0
- data/lib/mobility/plugins/active_record/backend.rb +27 -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 +64 -0
- data/lib/mobility/plugins/arel.rb +125 -0
- data/lib/mobility/plugins/arel/nodes.rb +15 -0
- data/lib/mobility/plugins/arel/nodes/pg_ops.rb +134 -0
- data/lib/mobility/plugins/attribute_methods.rb +29 -20
- data/lib/mobility/plugins/attributes.rb +72 -0
- data/lib/mobility/plugins/backend.rb +161 -0
- data/lib/mobility/plugins/backend_reader.rb +34 -0
- data/lib/mobility/plugins/cache.rb +68 -26
- data/lib/mobility/plugins/default.rb +22 -17
- data/lib/mobility/plugins/dirty.rb +12 -33
- data/lib/mobility/plugins/fallbacks.rb +52 -44
- data/lib/mobility/plugins/fallthrough_accessors.rb +19 -23
- data/lib/mobility/plugins/locale_accessors.rb +22 -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 +34 -23
- 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/create_string_translations.rb +0 -1
- data/lib/rails/generators/mobility/templates/create_text_translations.rb +0 -1
- data/lib/rails/generators/mobility/templates/initializer.rb +104 -78
- metadata +35 -40
- 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/model_translation.rb +0 -14
- data/lib/mobility/active_record/string_translation.rb +0 -10
- data/lib/mobility/active_record/text_translation.rb +0 -10
- data/lib/mobility/active_record/translation.rb +0 -14
- data/lib/mobility/active_record/uniqueness_validator.rb +0 -60
- data/lib/mobility/arel.rb +0 -49
- data/lib/mobility/arel/nodes.rb +0 -13
- data/lib/mobility/arel/nodes/pg_ops.rb +0 -132
- data/lib/mobility/arel/visitor.rb +0 -61
- 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/sequel/column_changes.rb +0 -28
- data/lib/mobility/sequel/hash_initializer.rb +0 -21
- data/lib/mobility/sequel/model_translation.rb +0 -20
- data/lib/mobility/sequel/sql.rb +0 -16
- data/lib/mobility/sequel/string_translation.rb +0 -10
- data/lib/mobility/sequel/text_translation.rb +0 -10
- data/lib/mobility/sequel/translation.rb +0 -53
- data/lib/mobility/translates.rb +0 -73
|
@@ -2,8 +2,6 @@
|
|
|
2
2
|
require "mobility/util"
|
|
3
3
|
require "mobility/backends/sequel"
|
|
4
4
|
require "mobility/backends/hash_valued"
|
|
5
|
-
require "mobility/sequel/column_changes"
|
|
6
|
-
require "mobility/sequel/hash_initializer"
|
|
7
5
|
|
|
8
6
|
module Mobility
|
|
9
7
|
module Backends
|
|
@@ -35,10 +33,12 @@ jsonb).
|
|
|
35
33
|
model[column_name.to_sym]
|
|
36
34
|
end
|
|
37
35
|
|
|
36
|
+
backend = self
|
|
37
|
+
|
|
38
38
|
setup do |attributes, options|
|
|
39
39
|
columns = attributes.map { |attribute| (options[:column_affix] % attribute).to_sym }
|
|
40
40
|
|
|
41
|
-
|
|
41
|
+
mod = Module.new do
|
|
42
42
|
define_method :before_validation do
|
|
43
43
|
columns.each do |column|
|
|
44
44
|
self[column].delete_if { |_, v| Util.blank?(v) }
|
|
@@ -46,9 +46,9 @@ jsonb).
|
|
|
46
46
|
super()
|
|
47
47
|
end
|
|
48
48
|
end
|
|
49
|
-
include
|
|
50
|
-
|
|
51
|
-
|
|
49
|
+
include mod
|
|
50
|
+
backend.define_hash_initializer(mod, columns)
|
|
51
|
+
backend.define_column_changes(mod, attributes, column_affix: options[:column_affix])
|
|
52
52
|
|
|
53
53
|
plugin :defaults_setter
|
|
54
54
|
columns.each { |column| default_values[column] = {} }
|
|
@@ -36,6 +36,10 @@ Sequel serialization plugin.
|
|
|
36
36
|
include Sequel
|
|
37
37
|
include HashValued
|
|
38
38
|
|
|
39
|
+
def self.valid_keys
|
|
40
|
+
super + [:format]
|
|
41
|
+
end
|
|
42
|
+
|
|
39
43
|
# @!group Backend Configuration
|
|
40
44
|
# @param (see Backends::Serialized.configure)
|
|
41
45
|
# @option (see Backends::Serialized.configure)
|
|
@@ -110,5 +114,7 @@ Sequel serialization plugin.
|
|
|
110
114
|
super.to_sym
|
|
111
115
|
end
|
|
112
116
|
end
|
|
117
|
+
|
|
118
|
+
register_backend(:sequel_serialized, Sequel::Serialized)
|
|
113
119
|
end
|
|
114
120
|
end
|
|
@@ -1,9 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
require "mobility/util"
|
|
3
3
|
require "mobility/backends/sequel"
|
|
4
|
-
require "mobility/backends/
|
|
5
|
-
require "mobility/sequel/model_translation"
|
|
6
|
-
require "mobility/sequel/sql"
|
|
4
|
+
require "mobility/backends/table"
|
|
7
5
|
|
|
8
6
|
module Mobility
|
|
9
7
|
module Backends
|
|
@@ -34,7 +32,7 @@ Implements the {Mobility::Backends::Table} backend for Sequel models.
|
|
|
34
32
|
# @raise [CacheRequired] if cache option is false
|
|
35
33
|
def configure(options)
|
|
36
34
|
raise CacheRequired, "Cache required for Sequel::Table backend" if options[:cache] == false
|
|
37
|
-
table_name = Util.singularize(
|
|
35
|
+
table_name = Util.singularize(model_class.table_name)
|
|
38
36
|
options[:table_name] ||= :"#{table_name}_translations"
|
|
39
37
|
options[:foreign_key] ||= Util.foreign_key(Util.camelize(table_name.downcase))
|
|
40
38
|
if association_name = options[:association_name]
|
|
@@ -51,7 +49,7 @@ Implements the {Mobility::Backends::Table} backend for Sequel models.
|
|
|
51
49
|
# @param [Symbol] locale Locale
|
|
52
50
|
# @return [Sequel::SQL::QualifiedIdentifier]
|
|
53
51
|
def build_op(attr, locale)
|
|
54
|
-
::
|
|
52
|
+
::Sequel::SQL::QualifiedIdentifier.new(table_alias(locale), attr || :value)
|
|
55
53
|
end
|
|
56
54
|
|
|
57
55
|
# @param [Sequel::Dataset] dataset Dataset to prepare
|
|
@@ -82,7 +80,7 @@ Implements the {Mobility::Backends::Table} backend for Sequel models.
|
|
|
82
80
|
case predicate
|
|
83
81
|
when Array
|
|
84
82
|
visit_collection(predicate, locale)
|
|
85
|
-
when ::
|
|
83
|
+
when ::Sequel::SQL::QualifiedIdentifier
|
|
86
84
|
visit_sql_identifier(predicate, locale)
|
|
87
85
|
when ::Sequel::SQL::BooleanExpression
|
|
88
86
|
visit_boolean(predicate, locale)
|
|
@@ -118,6 +116,8 @@ Implements the {Mobility::Backends::Table} backend for Sequel models.
|
|
|
118
116
|
end
|
|
119
117
|
end
|
|
120
118
|
|
|
119
|
+
backend = self
|
|
120
|
+
|
|
121
121
|
setup do |attributes, options|
|
|
122
122
|
association_name = options[:association_name]
|
|
123
123
|
subclass_name = options[:subclass_name]
|
|
@@ -127,7 +127,7 @@ Implements the {Mobility::Backends::Table} backend for Sequel models.
|
|
|
127
127
|
const_get(subclass_name, false)
|
|
128
128
|
else
|
|
129
129
|
const_set(subclass_name, Class.new(::Sequel::Model(options[:table_name]))).tap do |klass|
|
|
130
|
-
klass.include
|
|
130
|
+
klass.include Translation
|
|
131
131
|
end
|
|
132
132
|
end
|
|
133
133
|
|
|
@@ -154,10 +154,11 @@ Implements the {Mobility::Backends::Table} backend for Sequel models.
|
|
|
154
154
|
end
|
|
155
155
|
include callback_methods
|
|
156
156
|
|
|
157
|
-
include
|
|
157
|
+
include(mod = Module.new)
|
|
158
|
+
backend.define_column_changes(mod, attributes)
|
|
158
159
|
end
|
|
159
160
|
|
|
160
|
-
def translation_for(locale,
|
|
161
|
+
def translation_for(locale, **)
|
|
161
162
|
translation = model.send(association_name).find { |t| t.locale == locale.to_s }
|
|
162
163
|
translation ||= translation_class.new(locale: locale)
|
|
163
164
|
translation
|
|
@@ -173,7 +174,19 @@ Implements the {Mobility::Backends::Table} backend for Sequel models.
|
|
|
173
174
|
end
|
|
174
175
|
end
|
|
175
176
|
|
|
177
|
+
module Translation
|
|
178
|
+
def self.included(base)
|
|
179
|
+
base.plugin :validation_helpers
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
def validate
|
|
183
|
+
super
|
|
184
|
+
validates_presence [:locale]
|
|
185
|
+
end
|
|
186
|
+
end
|
|
176
187
|
class CacheRequired < ::StandardError; end
|
|
177
188
|
end
|
|
189
|
+
|
|
190
|
+
register_backend(:sequel_table, Sequel::Table)
|
|
178
191
|
end
|
|
179
192
|
end
|
|
@@ -23,8 +23,6 @@ Format for serialization. Either +:yaml+ (default) or +:json+.
|
|
|
23
23
|
|
|
24
24
|
=end
|
|
25
25
|
module Serialized
|
|
26
|
-
extend Backend::OrmDelegator
|
|
27
|
-
|
|
28
26
|
class << self
|
|
29
27
|
|
|
30
28
|
# @!group Backend Configuration
|
|
@@ -40,7 +38,7 @@ Format for serialization. Either +:yaml+ (default) or +:json+.
|
|
|
40
38
|
def serializer_for(format)
|
|
41
39
|
lambda do |obj|
|
|
42
40
|
return if obj.nil?
|
|
43
|
-
if obj.is_a? Hash
|
|
41
|
+
if obj.is_a? ::Hash
|
|
44
42
|
obj = obj.inject({}) do |translations, (locale, value)|
|
|
45
43
|
translations[locale] = value.to_s if Util.present?(value)
|
|
46
44
|
translations
|
|
@@ -10,8 +10,9 @@ Stores attribute translation as rows on a model-specific translation table
|
|
|
10
10
|
the table name for a model +Post+ with table +posts+ will be
|
|
11
11
|
+post_translations+, and the translation class will be +Post::Translation+. The
|
|
12
12
|
translation class is dynamically created when the backend is initialized on the
|
|
13
|
-
model class, and subclasses
|
|
14
|
-
|
|
13
|
+
model class, and subclasses
|
|
14
|
+
{Mobility::Backends::ActiveRecord::Table::Translation} (for AR models) or
|
|
15
|
+
inherits {Mobility::Backends::Sequel::Table::Translation} (for Sequel models).
|
|
15
16
|
|
|
16
17
|
The backend expects the translations table (+post_translations+) to have:
|
|
17
18
|
|
|
@@ -65,7 +66,6 @@ set.
|
|
|
65
66
|
@see Mobility::Backends::Sequel::Table
|
|
66
67
|
=end
|
|
67
68
|
module Table
|
|
68
|
-
extend Backend::OrmDelegator
|
|
69
69
|
# @!method association_name
|
|
70
70
|
# Returns the name of the translations association.
|
|
71
71
|
# @return [Symbol] Name of the association
|
|
@@ -84,13 +84,13 @@ set.
|
|
|
84
84
|
|
|
85
85
|
# @!group Backend Accessors
|
|
86
86
|
# @!macro backend_reader
|
|
87
|
-
def read(locale, options
|
|
88
|
-
translation_for(locale, options).send(attribute)
|
|
87
|
+
def read(locale, **options)
|
|
88
|
+
translation_for(locale, **options).send(attribute)
|
|
89
89
|
end
|
|
90
90
|
|
|
91
91
|
# @!macro backend_writer
|
|
92
|
-
def write(locale, value, options
|
|
93
|
-
translation_for(locale, options).send("#{attribute}=", value)
|
|
92
|
+
def write(locale, value, **options)
|
|
93
|
+
translation_for(locale, **options).send("#{attribute}=", value)
|
|
94
94
|
end
|
|
95
95
|
# @!endgroup
|
|
96
96
|
|
|
@@ -105,25 +105,22 @@ set.
|
|
|
105
105
|
model.send(association_name)
|
|
106
106
|
end
|
|
107
107
|
|
|
108
|
-
def self.included(
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
108
|
+
def self.included(backend_class)
|
|
109
|
+
backend_class.extend ClassMethods
|
|
110
|
+
backend_class.option_reader :association_name
|
|
111
|
+
backend_class.option_reader :subclass_name
|
|
112
|
+
backend_class.option_reader :foreign_key
|
|
113
|
+
backend_class.option_reader :table_name
|
|
114
114
|
end
|
|
115
115
|
|
|
116
116
|
module ClassMethods
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
else
|
|
125
|
-
super
|
|
126
|
-
end
|
|
117
|
+
def valid_keys
|
|
118
|
+
[:association_name, :subclass_name, :foreign_key, :table_name]
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# Apply custom processing for cache plugin
|
|
122
|
+
def include_cache
|
|
123
|
+
include self::Cache
|
|
127
124
|
end
|
|
128
125
|
|
|
129
126
|
def table_alias(locale)
|
|
@@ -134,20 +131,31 @@ set.
|
|
|
134
131
|
# Simple hash cache to memoize translations as a hash so they can be
|
|
135
132
|
# fetched quickly.
|
|
136
133
|
module Cache
|
|
137
|
-
|
|
134
|
+
def translation_for(locale, **options)
|
|
135
|
+
return super(locale, options) if options.delete(:cache) == false
|
|
136
|
+
if cache.has_key?(locale)
|
|
137
|
+
cache[locale]
|
|
138
|
+
else
|
|
139
|
+
cache[locale] = super(locale, **options)
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def clear_cache
|
|
144
|
+
cache.clear
|
|
145
|
+
end
|
|
138
146
|
|
|
139
147
|
private
|
|
140
148
|
|
|
141
149
|
def cache
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
150
|
+
if model.instance_variable_defined?(cache_name)
|
|
151
|
+
model.instance_variable_get(cache_name)
|
|
152
|
+
else
|
|
153
|
+
model.instance_variable_set(cache_name, {})
|
|
154
|
+
end
|
|
147
155
|
end
|
|
148
156
|
|
|
149
|
-
def
|
|
150
|
-
|
|
157
|
+
def cache_name
|
|
158
|
+
@cache_name ||= :"@__mobility_#{association_name}_cache"
|
|
151
159
|
end
|
|
152
160
|
end
|
|
153
161
|
end
|
|
@@ -0,0 +1,56 @@
|
|
|
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 included_plugins
|
|
21
|
+
included_modules.grep(Plugin)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def defaults
|
|
25
|
+
@defaults ||= {}
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def inherited(klass)
|
|
29
|
+
super
|
|
30
|
+
klass.defaults.merge!(defaults)
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def initialize(*, **options)
|
|
35
|
+
initialize_options(options)
|
|
36
|
+
validate_options(@options)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
attr_reader :options
|
|
40
|
+
|
|
41
|
+
private
|
|
42
|
+
|
|
43
|
+
def initialize_options(options)
|
|
44
|
+
@options = self.class.defaults.merge(options)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# This is overridden by backend plugin to exclude mixed-in backend options.
|
|
48
|
+
def validate_options(options)
|
|
49
|
+
plugin_keys = self.class.included_plugins.map { |p| Plugins.lookup_name(p) }
|
|
50
|
+
extra_keys = options.keys - plugin_keys
|
|
51
|
+
raise InvalidOptionKey, "No plugin configured for these keys: #{extra_keys.join(', ')}." unless extra_keys.empty?
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
class InvalidOptionKey < Error; end
|
|
55
|
+
end
|
|
56
|
+
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
|