mobility 0.8.10 → 1.0.0.beta2
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 +66 -0
- data/Gemfile +50 -18
- data/Gemfile.lock +36 -101
- data/README.md +183 -91
- data/Rakefile +6 -4
- data/lib/mobility.rb +44 -166
- data/lib/mobility/arel.rb +1 -1
- data/lib/mobility/arel/nodes/pg_ops.rb +1 -1
- 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 +2 -0
- data/lib/mobility/backends/active_record/container.rb +6 -7
- data/lib/mobility/backends/active_record/hstore.rb +3 -1
- 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 +6 -4
- 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 +6 -4
- 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 +5 -2
- data/lib/mobility/backends/sequel/column.rb +2 -0
- data/lib/mobility/backends/sequel/container.rb +6 -6
- 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 +8 -6
- data/lib/mobility/backends/sequel/serialized.rb +6 -0
- data/lib/mobility/backends/sequel/table.rb +5 -2
- data/lib/mobility/backends/serialized.rb +1 -3
- data/lib/mobility/backends/table.rb +29 -26
- 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 +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 +48 -34
- data/lib/mobility/plugins/active_record/uniqueness_validation.rb +60 -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 +25 -25
- 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 +33 -22
- 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 +96 -78
- 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
|
@@ -1,9 +1,12 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
require "mobility/backend"
|
|
3
|
+
|
|
1
4
|
module Mobility
|
|
2
5
|
module Backends
|
|
3
6
|
module Sequel
|
|
4
7
|
def self.included(backend_class)
|
|
5
|
-
backend_class.include
|
|
6
|
-
backend_class.extend
|
|
8
|
+
backend_class.include Backend
|
|
9
|
+
backend_class.extend ClassMethods
|
|
7
10
|
end
|
|
8
11
|
|
|
9
12
|
module ClassMethods
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
|
-
require "mobility/backends/sequel
|
|
2
|
+
require "mobility/backends/sequel"
|
|
3
3
|
require "mobility/backends/sequel/jsonb"
|
|
4
|
+
require "mobility/backends/container"
|
|
4
5
|
|
|
5
6
|
module Mobility
|
|
6
7
|
module Backends
|
|
@@ -11,10 +12,7 @@ Implements the {Mobility::Backends::Container} backend for Sequel models.
|
|
|
11
12
|
=end
|
|
12
13
|
class Sequel::Container
|
|
13
14
|
include Sequel
|
|
14
|
-
|
|
15
|
-
# @!method column_name
|
|
16
|
-
# @return [Symbol] (:translations) Name of translations column
|
|
17
|
-
option_reader :column_name
|
|
15
|
+
include Container
|
|
18
16
|
|
|
19
17
|
# @!group Backend Accessors
|
|
20
18
|
#
|
|
@@ -44,7 +42,7 @@ Implements the {Mobility::Backends::Container} backend for Sequel models.
|
|
|
44
42
|
def self.configure(options)
|
|
45
43
|
options[:column_name] ||= :translations
|
|
46
44
|
options[:column_name] = options[:column_name].to_sym
|
|
47
|
-
column_name, db_schema = options[:column_name],
|
|
45
|
+
column_name, db_schema = options[:column_name], model_class.db_schema
|
|
48
46
|
options[:column_type] = db_schema[column_name] && (db_schema[column_name][:db_type]).to_sym
|
|
49
47
|
unless %i[json jsonb].include?(options[:column_type])
|
|
50
48
|
raise InvalidColumnType, "#{options[:column_name]} must be a column of type json or jsonb"
|
|
@@ -114,5 +112,7 @@ Implements the {Mobility::Backends::Container} backend for Sequel models.
|
|
|
114
112
|
end
|
|
115
113
|
end
|
|
116
114
|
end
|
|
115
|
+
|
|
116
|
+
register_backend(:sequel_container, Sequel::Container)
|
|
117
117
|
end
|
|
118
118
|
end
|
|
@@ -20,7 +20,7 @@ Implements the {Mobility::Backends::Hstore} backend for Sequel models.
|
|
|
20
20
|
# @!group Backend Accessors
|
|
21
21
|
# @!macro backend_writer
|
|
22
22
|
def write(locale, value, options = {})
|
|
23
|
-
super(locale, value && value.to_s, options)
|
|
23
|
+
super(locale, value && value.to_s, **options)
|
|
24
24
|
end
|
|
25
25
|
# @!endgroup
|
|
26
26
|
|
|
@@ -35,5 +35,7 @@ Implements the {Mobility::Backends::Hstore} backend for Sequel models.
|
|
|
35
35
|
class HStoreOp < ::Sequel::Postgres::HStoreOp; end
|
|
36
36
|
end
|
|
37
37
|
end
|
|
38
|
+
|
|
39
|
+
register_backend(:sequel_hstore, Sequel::Hstore)
|
|
38
40
|
end
|
|
39
41
|
end
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
1
2
|
require 'mobility/backends/sequel/pg_hash'
|
|
2
3
|
|
|
3
4
|
Sequel.extension :pg_json, :pg_json_ops
|
|
@@ -42,5 +43,7 @@ Implements the {Mobility::Backends::Json} backend for Sequel models.
|
|
|
42
43
|
class JSONOp < ::Sequel::Postgres::JSONOp; end
|
|
43
44
|
end
|
|
44
45
|
end
|
|
46
|
+
|
|
47
|
+
register_backend(:sequel_json, Sequel::Json)
|
|
45
48
|
end
|
|
46
49
|
end
|
|
@@ -55,7 +55,7 @@ Implements the {Mobility::Backends::Jsonb} backend for Sequel models.
|
|
|
55
55
|
|
|
56
56
|
def =~(other)
|
|
57
57
|
case other
|
|
58
|
-
when Integer, Hash
|
|
58
|
+
when Integer, ::Hash
|
|
59
59
|
to_dash_arrow =~ other.to_json
|
|
60
60
|
when NilClass
|
|
61
61
|
~to_question
|
|
@@ -66,5 +66,7 @@ Implements the {Mobility::Backends::Jsonb} backend for Sequel models.
|
|
|
66
66
|
end
|
|
67
67
|
end
|
|
68
68
|
end
|
|
69
|
+
|
|
70
|
+
register_backend(:sequel_jsonb, Sequel::Jsonb)
|
|
69
71
|
end
|
|
70
72
|
end
|
|
@@ -36,7 +36,7 @@ Implements the {Mobility::Backends::KeyValue} backend for Sequel models.
|
|
|
36
36
|
options[:association_name] ||= :"#{options[:type]}_translations"
|
|
37
37
|
options[:class_name] ||= Mobility::Sequel.const_get("#{type.capitalize}Translation")
|
|
38
38
|
end
|
|
39
|
-
options[:table_alias_affix] = "#{
|
|
39
|
+
options[:table_alias_affix] = "#{model_class}_%s_#{options[:association_name]}"
|
|
40
40
|
rescue NameError
|
|
41
41
|
raise ArgumentError, "You must define a Mobility::Sequel::#{type.capitalize}Translation class."
|
|
42
42
|
end
|
|
@@ -93,7 +93,7 @@ Implements the {Mobility::Backends::KeyValue} backend for Sequel models.
|
|
|
93
93
|
join_type = nils.empty? ? :inner : :left_outer
|
|
94
94
|
# TODO: simplify to hash.transform_values { join_type } when
|
|
95
95
|
# support for Ruby 2.3 is deprecated
|
|
96
|
-
Hash[hash.keys.map { |key| [key, join_type] }]
|
|
96
|
+
::Hash[hash.keys.map { |key| [key, join_type] }]
|
|
97
97
|
else
|
|
98
98
|
{}
|
|
99
99
|
end
|
|
@@ -101,13 +101,13 @@ Implements the {Mobility::Backends::KeyValue} backend for Sequel models.
|
|
|
101
101
|
hash = visit(boolean.args, locale)
|
|
102
102
|
# TODO: simplify to hash.transform_values { :inner } when
|
|
103
103
|
# support for Ruby 2.3 is deprecated
|
|
104
|
-
Hash[hash.keys.map { |key| [key, :inner] }]
|
|
104
|
+
::Hash[hash.keys.map { |key| [key, :inner] }]
|
|
105
105
|
elsif boolean.op == :OR
|
|
106
106
|
hash = boolean.args.map { |op| visit(op, locale) }.
|
|
107
|
-
compact.inject(
|
|
107
|
+
compact.inject(:merge)
|
|
108
108
|
# TODO: simplify to hash.transform_values { :left_outer } when
|
|
109
109
|
# support for Ruby 2.3 is deprecated
|
|
110
|
-
Hash[hash.keys.map { |key| [key, :left_outer] }]
|
|
110
|
+
::Hash[hash.keys.map { |key| [key, :left_outer] }]
|
|
111
111
|
else
|
|
112
112
|
visit(boolean.args, locale)
|
|
113
113
|
end
|
|
@@ -153,7 +153,7 @@ Implements the {Mobility::Backends::KeyValue} backend for Sequel models.
|
|
|
153
153
|
end
|
|
154
154
|
define_method :after_save do
|
|
155
155
|
super()
|
|
156
|
-
attributes.each { |attribute|
|
|
156
|
+
attributes.each { |attribute| mobility_backends[attribute].save_translations }
|
|
157
157
|
end
|
|
158
158
|
end
|
|
159
159
|
include callback_methods
|
|
@@ -202,5 +202,7 @@ Implements the {Mobility::Backends::KeyValue} backend for Sequel models.
|
|
|
202
202
|
end
|
|
203
203
|
end
|
|
204
204
|
end
|
|
205
|
+
|
|
206
|
+
register_backend(:sequel_key_value, Sequel::KeyValue)
|
|
205
207
|
end
|
|
206
208
|
end
|
|
@@ -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,7 +1,8 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
require "mobility/util"
|
|
3
3
|
require "mobility/backends/sequel"
|
|
4
|
-
require "mobility/backends/
|
|
4
|
+
require "mobility/backends/table"
|
|
5
|
+
require "mobility/sequel/column_changes"
|
|
5
6
|
require "mobility/sequel/model_translation"
|
|
6
7
|
require "mobility/sequel/sql"
|
|
7
8
|
|
|
@@ -34,7 +35,7 @@ Implements the {Mobility::Backends::Table} backend for Sequel models.
|
|
|
34
35
|
# @raise [CacheRequired] if cache option is false
|
|
35
36
|
def configure(options)
|
|
36
37
|
raise CacheRequired, "Cache required for Sequel::Table backend" if options[:cache] == false
|
|
37
|
-
table_name = Util.singularize(
|
|
38
|
+
table_name = Util.singularize(model_class.table_name)
|
|
38
39
|
options[:table_name] ||= :"#{table_name}_translations"
|
|
39
40
|
options[:foreign_key] ||= Util.foreign_key(Util.camelize(table_name.downcase))
|
|
40
41
|
if association_name = options[:association_name]
|
|
@@ -175,5 +176,7 @@ Implements the {Mobility::Backends::Table} backend for Sequel models.
|
|
|
175
176
|
|
|
176
177
|
class CacheRequired < ::StandardError; end
|
|
177
178
|
end
|
|
179
|
+
|
|
180
|
+
register_backend(:sequel_table, Sequel::Table)
|
|
178
181
|
end
|
|
179
182
|
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
|
|
@@ -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
|
|
@@ -84,13 +83,13 @@ set.
|
|
|
84
83
|
|
|
85
84
|
# @!group Backend Accessors
|
|
86
85
|
# @!macro backend_reader
|
|
87
|
-
def read(locale, options
|
|
88
|
-
translation_for(locale, options).send(attribute)
|
|
86
|
+
def read(locale, **options)
|
|
87
|
+
translation_for(locale, **options).send(attribute)
|
|
89
88
|
end
|
|
90
89
|
|
|
91
90
|
# @!macro backend_writer
|
|
92
|
-
def write(locale, value, options
|
|
93
|
-
translation_for(locale, options).send("#{attribute}=", value)
|
|
91
|
+
def write(locale, value, **options)
|
|
92
|
+
translation_for(locale, **options).send("#{attribute}=", value)
|
|
94
93
|
end
|
|
95
94
|
# @!endgroup
|
|
96
95
|
|
|
@@ -105,25 +104,22 @@ set.
|
|
|
105
104
|
model.send(association_name)
|
|
106
105
|
end
|
|
107
106
|
|
|
108
|
-
def self.included(
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
107
|
+
def self.included(backend_class)
|
|
108
|
+
backend_class.extend ClassMethods
|
|
109
|
+
backend_class.option_reader :association_name
|
|
110
|
+
backend_class.option_reader :subclass_name
|
|
111
|
+
backend_class.option_reader :foreign_key
|
|
112
|
+
backend_class.option_reader :table_name
|
|
114
113
|
end
|
|
115
114
|
|
|
116
115
|
module ClassMethods
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
else
|
|
125
|
-
super
|
|
126
|
-
end
|
|
116
|
+
def valid_keys
|
|
117
|
+
[:association_name, :subclass_name, :foreign_key, :table_name]
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
# Apply custom processing for cache plugin
|
|
121
|
+
def include_cache
|
|
122
|
+
include self::Cache
|
|
127
123
|
end
|
|
128
124
|
|
|
129
125
|
def table_alias(locale)
|
|
@@ -134,7 +130,18 @@ set.
|
|
|
134
130
|
# Simple hash cache to memoize translations as a hash so they can be
|
|
135
131
|
# fetched quickly.
|
|
136
132
|
module Cache
|
|
137
|
-
|
|
133
|
+
def translation_for(locale, **options)
|
|
134
|
+
return super(locale, options) if options.delete(:cache) == false
|
|
135
|
+
if cache.has_key?(locale)
|
|
136
|
+
cache[locale]
|
|
137
|
+
else
|
|
138
|
+
cache[locale] = super(locale, **options)
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def clear_cache
|
|
143
|
+
model_cache && model_cache.clear
|
|
144
|
+
end
|
|
138
145
|
|
|
139
146
|
private
|
|
140
147
|
|
|
@@ -145,10 +152,6 @@ set.
|
|
|
145
152
|
def model_cache
|
|
146
153
|
model.instance_variable_get(:"@__mobility_#{association_name}_cache")
|
|
147
154
|
end
|
|
148
|
-
|
|
149
|
-
def clear_cache
|
|
150
|
-
model_cache && model_cache.clear
|
|
151
|
-
end
|
|
152
155
|
end
|
|
153
156
|
end
|
|
154
157
|
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
|