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.
Files changed (96) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +0 -0
  3. data.tar.gz.sig +0 -0
  4. data/CHANGELOG.md +66 -0
  5. data/Gemfile +50 -18
  6. data/Gemfile.lock +36 -101
  7. data/README.md +183 -91
  8. data/Rakefile +6 -4
  9. data/lib/mobility.rb +44 -166
  10. data/lib/mobility/arel.rb +1 -1
  11. data/lib/mobility/arel/nodes/pg_ops.rb +1 -1
  12. data/lib/mobility/backend.rb +27 -51
  13. data/lib/mobility/backends.rb +20 -0
  14. data/lib/mobility/backends/active_record.rb +4 -0
  15. data/lib/mobility/backends/active_record/column.rb +2 -0
  16. data/lib/mobility/backends/active_record/container.rb +6 -7
  17. data/lib/mobility/backends/active_record/hstore.rb +3 -1
  18. data/lib/mobility/backends/active_record/json.rb +2 -0
  19. data/lib/mobility/backends/active_record/jsonb.rb +2 -0
  20. data/lib/mobility/backends/active_record/key_value.rb +6 -4
  21. data/lib/mobility/backends/active_record/pg_hash.rb +1 -1
  22. data/lib/mobility/backends/active_record/serialized.rb +6 -0
  23. data/lib/mobility/backends/active_record/table.rb +6 -4
  24. data/lib/mobility/backends/column.rb +0 -6
  25. data/lib/mobility/backends/container.rb +10 -1
  26. data/lib/mobility/backends/hash.rb +39 -0
  27. data/lib/mobility/backends/hash_valued.rb +4 -0
  28. data/lib/mobility/backends/hstore.rb +0 -1
  29. data/lib/mobility/backends/json.rb +0 -1
  30. data/lib/mobility/backends/jsonb.rb +1 -2
  31. data/lib/mobility/backends/key_value.rb +31 -26
  32. data/lib/mobility/backends/null.rb +2 -0
  33. data/lib/mobility/backends/sequel.rb +5 -2
  34. data/lib/mobility/backends/sequel/column.rb +2 -0
  35. data/lib/mobility/backends/sequel/container.rb +6 -6
  36. data/lib/mobility/backends/sequel/hstore.rb +3 -1
  37. data/lib/mobility/backends/sequel/json.rb +3 -0
  38. data/lib/mobility/backends/sequel/jsonb.rb +3 -1
  39. data/lib/mobility/backends/sequel/key_value.rb +8 -6
  40. data/lib/mobility/backends/sequel/serialized.rb +6 -0
  41. data/lib/mobility/backends/sequel/table.rb +5 -2
  42. data/lib/mobility/backends/serialized.rb +1 -3
  43. data/lib/mobility/backends/table.rb +29 -26
  44. data/lib/mobility/pluggable.rb +56 -0
  45. data/lib/mobility/plugin.rb +260 -0
  46. data/lib/mobility/plugins.rb +27 -24
  47. data/lib/mobility/plugins/active_model.rb +17 -0
  48. data/lib/mobility/plugins/active_model/cache.rb +26 -0
  49. data/lib/mobility/plugins/active_model/dirty.rb +119 -78
  50. data/lib/mobility/plugins/active_record.rb +34 -0
  51. data/lib/mobility/plugins/active_record/backend.rb +25 -0
  52. data/lib/mobility/plugins/active_record/cache.rb +28 -0
  53. data/lib/mobility/plugins/active_record/dirty.rb +34 -17
  54. data/lib/mobility/plugins/active_record/query.rb +48 -34
  55. data/lib/mobility/plugins/active_record/uniqueness_validation.rb +60 -0
  56. data/lib/mobility/plugins/attribute_methods.rb +29 -20
  57. data/lib/mobility/plugins/attributes.rb +72 -0
  58. data/lib/mobility/plugins/backend.rb +161 -0
  59. data/lib/mobility/plugins/backend_reader.rb +34 -0
  60. data/lib/mobility/plugins/cache.rb +68 -26
  61. data/lib/mobility/plugins/default.rb +22 -17
  62. data/lib/mobility/plugins/dirty.rb +12 -33
  63. data/lib/mobility/plugins/fallbacks.rb +52 -44
  64. data/lib/mobility/plugins/fallthrough_accessors.rb +25 -25
  65. data/lib/mobility/plugins/locale_accessors.rb +22 -35
  66. data/lib/mobility/plugins/presence.rb +28 -21
  67. data/lib/mobility/plugins/query.rb +8 -17
  68. data/lib/mobility/plugins/reader.rb +50 -0
  69. data/lib/mobility/plugins/sequel.rb +34 -0
  70. data/lib/mobility/plugins/sequel/backend.rb +25 -0
  71. data/lib/mobility/plugins/sequel/cache.rb +24 -0
  72. data/lib/mobility/plugins/sequel/dirty.rb +33 -22
  73. data/lib/mobility/plugins/sequel/query.rb +21 -6
  74. data/lib/mobility/plugins/writer.rb +44 -0
  75. data/lib/mobility/translations.rb +95 -0
  76. data/lib/mobility/version.rb +12 -1
  77. data/lib/rails/generators/mobility/templates/initializer.rb +96 -78
  78. metadata +28 -27
  79. metadata.gz.sig +0 -0
  80. data/lib/mobility/active_model.rb +0 -4
  81. data/lib/mobility/active_model/backend_resetter.rb +0 -26
  82. data/lib/mobility/active_record.rb +0 -23
  83. data/lib/mobility/active_record/backend_resetter.rb +0 -26
  84. data/lib/mobility/active_record/uniqueness_validator.rb +0 -60
  85. data/lib/mobility/attributes.rb +0 -324
  86. data/lib/mobility/backend/orm_delegator.rb +0 -44
  87. data/lib/mobility/backend_resetter.rb +0 -50
  88. data/lib/mobility/configuration.rb +0 -138
  89. data/lib/mobility/fallbacks.rb +0 -28
  90. data/lib/mobility/interface.rb +0 -0
  91. data/lib/mobility/loaded.rb +0 -4
  92. data/lib/mobility/plugins/active_record/attribute_methods.rb +0 -38
  93. data/lib/mobility/plugins/cache/translation_cacher.rb +0 -40
  94. data/lib/mobility/sequel.rb +0 -9
  95. data/lib/mobility/sequel/backend_resetter.rb +0 -23
  96. data/lib/mobility/translates.rb +0 -73
@@ -0,0 +1,34 @@
1
+ # frozen-string-literal: true
2
+ module Mobility
3
+ module Plugins
4
+ =begin
5
+
6
+ Defines convenience methods for accessing backends, of the form
7
+ "<name>_backend". The format for this method can be customized by passing a
8
+ different format string as the plugin option.
9
+
10
+ =end
11
+ module BackendReader
12
+ extend Plugin
13
+
14
+ default true
15
+ requires :backend
16
+
17
+ initialize_hook do |*names|
18
+ if backend_reader = options[:backend_reader]
19
+ backend_reader = "%s_backend" if backend_reader == true
20
+
21
+ names.each do |name|
22
+ module_eval <<-EOM, __FILE__, __LINE__ + 1
23
+ def #{backend_reader % name}
24
+ mobility_backends[:#{name}]
25
+ end
26
+ EOM
27
+ end
28
+ end
29
+ end
30
+ end
31
+
32
+ register_plugin(:backend_reader, BackendReader)
33
+ end
34
+ end
@@ -1,5 +1,4 @@
1
1
  # frozen_string_literal: true
2
- require "mobility/plugins/cache/translation_cacher"
3
2
 
4
3
  module Mobility
5
4
  module Plugins
@@ -8,8 +7,7 @@ module Mobility
8
7
  Caches values fetched from the backend so subsequent fetches can be performed
9
8
  more quickly. The cache stores cached values in a simple hash, which is not
10
9
  optimal for some storage strategies, so some backends (KeyValue, Table) use a
11
- custom module through the {Mobility::Backend::Setup#apply_plugin} hook. For
12
- details see the documentation for these backends.
10
+ custom module by defining a method, +include_cache+, on the backend class.
13
11
 
14
12
  The cache is reset when one of a set of events happens (saving, reloading,
15
13
  etc.). See {BackendResetter} for details.
@@ -21,34 +19,78 @@ Values are added to the cache in two ways:
21
19
 
22
20
  =end
23
21
  module Cache
22
+ extend Plugin
23
+
24
+ default true
25
+ requires :backend, include: :before
26
+
24
27
  # Applies cache plugin to attributes.
25
- # @param [Attributes] attributes
26
- # @param [Boolean] option
27
- def self.apply(attributes, option)
28
- if option
29
- backend_class = attributes.backend_class
30
- backend_class.include(self) unless backend_class.apply_plugin(:cache)
31
-
32
- model_class = attributes.model_class
33
- model_class.include BackendResetter.for(model_class).new(attributes.names) { clear_cache }
28
+ included_hook do |_, backend_class|
29
+ if options[:cache]
30
+ if backend_class.respond_to?(:include_cache)
31
+ backend_class.include_cache
32
+ else
33
+ include_cache(backend_class)
34
+ end
34
35
  end
35
36
  end
36
37
 
37
- # @group Backend Accessors
38
- #
39
- # @!macro backend_reader
40
- # @!method read(locale, value, options = {})
41
- # @option options [Boolean] cache *false* to disable cache.
42
- include TranslationCacher.new(:read)
43
-
44
- # @!macro backend_writer
45
- # @option options [Boolean] cache
46
- # *false* to disable cache.
47
- def write(locale, value, **options)
48
- return super if options.delete(:cache) == false
49
- cache[locale] = super
38
+ private
39
+
40
+ def include_cache(backend_class)
41
+ backend_class.include BackendMethods
42
+ end
43
+
44
+ # Used in ORM cache plugins
45
+ def define_cache_hooks(klass, *reset_methods)
46
+ mod = self
47
+ private_methods = reset_methods & klass.private_instance_methods
48
+ reset_methods.each do |method_name|
49
+ define_method method_name do |*args|
50
+ super(*args).tap do
51
+ mod.names.each { |name| mobility_backends[name].clear_cache }
52
+ end
53
+ end
54
+ end
55
+ klass.class_eval { private(*private_methods) }
56
+ end
57
+
58
+ module BackendMethods
59
+ # @group Backend Accessors
60
+ #
61
+ # @!macro backend_reader
62
+ # @!method read(locale, value, options = {})
63
+ # @option options [Boolean] cache *false* to disable cache.
64
+ def read(locale, **options)
65
+ return super(locale, **options) if options.delete(:cache) == false
66
+ if cache.has_key?(locale)
67
+ cache[locale]
68
+ else
69
+ cache[locale] = super(locale, **options)
70
+ end
71
+ end
72
+
73
+ # @!macro backend_writer
74
+ # @option options [Boolean] cache
75
+ # *false* to disable cache.
76
+ def write(locale, value, **options)
77
+ return super if options.delete(:cache) == false
78
+ cache[locale] = super
79
+ end
80
+ # @!endgroup
81
+
82
+ def clear_cache
83
+ @cache = {}
84
+ end
85
+
86
+ private
87
+
88
+ def cache
89
+ @cache ||= {}
90
+ end
50
91
  end
51
- # @!endgroup
52
92
  end
93
+
94
+ register_plugin(:cache, Cache)
53
95
  end
54
96
  end
@@ -6,8 +6,7 @@ module Mobility
6
6
 
7
7
  Defines value or proc to fall through to if return value from getter would
8
8
  otherwise be nil. This plugin is disabled by default but will be enabled if any
9
- value (other than +Mobility::Plugins::OPTION_UNSET+) is passed as the +default+
10
- option key.
9
+ value is passed as the +default+ option key.
11
10
 
12
11
  If default is a +Proc+, it will be called with the context of the model, and
13
12
  passed arguments:
@@ -63,11 +62,13 @@ The proc can accept zero to three arguments (see examples below)
63
62
  #=> "Post"
64
63
  =end
65
64
  module Default
65
+ extend Plugin
66
+
67
+ requires :backend, include: :before
68
+
66
69
  # Applies default plugin to attributes.
67
- # @param [Attributes] attributes
68
- # @param [Object] option
69
- def self.apply(attributes, option)
70
- attributes.backend_class.include(self) unless option == Plugins::OPTION_UNSET
70
+ included_hook do |_klass, backend_class|
71
+ backend_class.include(BackendMethods)
71
72
  end
72
73
 
73
74
  # Generate a default value for given parameters.
@@ -82,19 +83,23 @@ The proc can accept zero to three arguments (see examples below)
82
83
  model.instance_exec(*args, &default_value)
83
84
  end
84
85
 
85
- # @!group Backend Accessors
86
- # @!macro backend_reader
87
- # @option accessor_options [Boolean] default
88
- # *false* to disable presence filter.
89
- def read(locale, accessor_options = {})
90
- default = accessor_options.has_key?(:default) ? accessor_options.delete(:default) : options[:default]
91
- if (value = super(locale, accessor_options)).nil?
92
- Default[default, locale: locale, accessor_options: accessor_options, model: model, attribute: attribute]
93
- else
94
- value
86
+ module BackendMethods
87
+ # @!group Backend Accessors
88
+ # @!macro backend_reader
89
+ # @option accessor_options [Boolean] default
90
+ # *false* to disable presence filter.
91
+ def read(locale, accessor_options = {})
92
+ default = accessor_options.has_key?(:default) ? accessor_options.delete(:default) : options[:default]
93
+ if (value = super(locale, **accessor_options)).nil?
94
+ Default[default, locale: locale, accessor_options: accessor_options, model: model, attribute: attribute]
95
+ else
96
+ value
97
+ end
95
98
  end
99
+ # @!endgroup
96
100
  end
97
- # @!endgroup
98
101
  end
102
+
103
+ register_plugin(:default, Default)
99
104
  end
100
105
  end
@@ -1,6 +1,4 @@
1
1
  # frozen_string_literal: true
2
- require "mobility/backend_resetter"
3
- require "mobility/plugins/fallthrough_accessors"
4
2
 
5
3
  module Mobility
6
4
  module Plugins
@@ -20,40 +18,21 @@ details.
20
18
 
21
19
  =end
22
20
  module Dirty
23
- class << self
24
- # Applies dirty plugin to attributes for a given option value.
25
- # @param [Attributes] attributes
26
- # @param [Boolean] option Value of option
27
- # @raise [ArgumentError] if model class does not support dirty tracking
28
- def apply(attributes, option)
29
- if option
30
- FallthroughAccessors.apply(attributes, true)
31
- include_dirty_module(attributes.backend_class, attributes.model_class, *attributes.names)
32
- end
33
- end
21
+ extend Plugin
22
+
23
+ default true
34
24
 
35
- private
36
-
37
- def include_dirty_module(backend_class, model_class, *attribute_names)
38
- dirty_module =
39
- if Loaded::ActiveRecord && model_class.ancestors.include?(::ActiveModel::Dirty)
40
- if (model_class < ::ActiveRecord::Base)
41
- require "mobility/plugins/active_record/dirty"
42
- Plugins::ActiveRecord::Dirty
43
- else
44
- require "mobility/plugins/active_model/dirty"
45
- Plugins::ActiveModel::Dirty
46
- end
47
- elsif Loaded::Sequel && model_class < ::Sequel::Model
48
- require "mobility/plugins/sequel/dirty"
49
- Plugins::Sequel::Dirty
50
- else
51
- raise ArgumentError, "#{model_class} does not support Dirty module."
52
- end
53
- backend_class.include dirty_module.const_get(:BackendMethods)
54
- model_class.include dirty_module.const_get(:MethodsBuilder).new(*attribute_names)
25
+ requires :backend, include: :before
26
+ requires :fallthrough_accessors
27
+
28
+ initialize_hook do
29
+ if options[:dirty] && !options[:fallthrough_accessors]
30
+ warn 'The Dirty plugin depends on Fallthrough Accessors being enabled, '\
31
+ 'but fallthrough_accessors option is falsey'
55
32
  end
56
33
  end
57
34
  end
35
+
36
+ register_plugin(:dirty, Dirty)
58
37
  end
59
38
  end
@@ -8,15 +8,14 @@ module Mobility
8
8
  Falls back to one or more alternative locales in case no value is defined for a
9
9
  given locale.
10
10
 
11
- For +fallbacks: true+, Mobility will use the value of
12
- {Mobility::Configuration#new_fallbacks} for the fallbacks instance. This
13
- defaults to an instance of +I18n::Locale::Fallbacks+, but can be
14
- configured (see {Mobility::Configuration}).
11
+ For +fallbacks: true+, Mobility will use an instance of
12
+ +I18n::Locale::Fallbacks+, but this can be configured by overriding
13
+ +generate_fallbacks+ in the translations class.
15
14
 
16
15
  If a hash is passed to the +fallbacks+ option, a new fallbacks instance will be
17
16
  created for the model with the hash defining additional fallbacks. To set a
18
- default value for this hash, use set the value of `default_options[:fallbacks]`
19
- in your Mobility configuration (see below).
17
+ default value for this hash, pass this value to the plugin in your Mobility
18
+ configuration.
20
19
 
21
20
  In addition, fallbacks are disabled in certain situations. To explicitly disable
22
21
  fallbacks when reading and writing, you can pass the <tt>fallback: false</tt>
@@ -48,7 +47,7 @@ the current locale was +nil+.
48
47
  Mobility.locale = :ja
49
48
  post.title
50
49
  #=> "foo"
51
-
50
+
52
51
  post.title = "bar"
53
52
  post.title
54
53
  #=> "bar"
@@ -108,58 +107,67 @@ the current locale was +nil+.
108
107
  #=> nil
109
108
  post.title_fr
110
109
  #=> nil
110
+ =end
111
+ module Fallbacks
112
+ extend Plugin
111
113
 
112
- @example Setting default fallbacks across all models
113
- Mobility.configure do |config|
114
- # ...
115
- config.default_options[:fallbacks] = { :'fr' => 'en' }
116
- # ...
117
- end
114
+ default true
115
+ requires :backend, include: :before
118
116
 
119
- class Post
120
- # Post will fallback from French to English by default
121
- translates :title, fallbacks: true
122
- end
123
-
124
- =end
125
- class Fallbacks < Module
126
117
  # Applies fallbacks plugin to attributes. Completely disables fallbacks
127
118
  # on model if option is +false+.
128
- # @param [Attributes] attributes
129
- # @param [Boolean] option
130
- def self.apply(attributes, option)
131
- attributes.backend_class.include(new(option)) unless option == false
119
+ included_hook do |_, backend_class|
120
+ fallbacks = options[:fallbacks]
121
+ backend_class.include(BackendReader.new(fallbacks, method(:generate_fallbacks))) unless fallbacks == false
132
122
  end
133
123
 
134
- def initialize(fallbacks_option)
135
- define_read(convert_option_to_fallbacks(fallbacks_option))
124
+ private
125
+
126
+ def generate_fallbacks(fallbacks)
127
+ fallbacks_class = I18n.respond_to?(:fallbacks) ? I18nFallbacks : I18n::Locale::Fallbacks
128
+ fallbacks_class.new(fallbacks)
136
129
  end
137
130
 
138
- private
131
+ class I18nFallbacks < ::I18n::Locale::Fallbacks
132
+ def [](locale)
133
+ super | I18n.fallbacks[locale]
134
+ end
135
+ end
139
136
 
140
- def define_read(fallbacks)
141
- define_method :read do |locale, fallback: true, **options|
142
- return super(locale, options) if !fallback || options[:locale]
137
+ class BackendReader < Module
138
+ def initialize(fallbacks_option, fallbacks_generator)
139
+ @fallbacks_generator = fallbacks_generator
140
+ define_read(convert_option_to_fallbacks(fallbacks_option))
141
+ end
143
142
 
144
- locales = fallback == true ? fallbacks[locale] : [locale, *fallback]
145
- locales.each do |fallback_locale|
146
- value = super(fallback_locale, options)
147
- return value if Util.present?(value)
148
- end
143
+ private
144
+
145
+ def define_read(fallbacks)
146
+ define_method :read do |locale, fallback: true, **options|
147
+ return super(locale, **options) if !fallback || options[:locale]
149
148
 
150
- super(locale, options)
149
+ locales = fallback == true ? fallbacks[locale] : [locale, *fallback]
150
+ locales.each do |fallback_locale|
151
+ value = super(fallback_locale, **options)
152
+ return value if Util.present?(value)
153
+ end
154
+
155
+ super(locale, **options)
156
+ end
151
157
  end
152
- end
153
158
 
154
- def convert_option_to_fallbacks(option)
155
- if option.is_a?(Hash)
156
- Mobility.new_fallbacks(option)
157
- elsif option == true
158
- Mobility.new_fallbacks
159
- else
160
- Hash.new { [] }
159
+ def convert_option_to_fallbacks(option)
160
+ if option.is_a?(::Hash)
161
+ @fallbacks_generator[option]
162
+ elsif option == true
163
+ @fallbacks_generator[{}]
164
+ else
165
+ ::Hash.new { [] }
166
+ end
161
167
  end
162
168
  end
163
169
  end
170
+
171
+ register_plugin(:fallbacks, Fallbacks)
164
172
  end
165
173
  end
@@ -18,42 +18,40 @@ This is a less efficient (but more open-ended) implementation of locale
18
18
  accessors, for use in cases where the locales to be used are not known when the
19
19
  model class is generated.
20
20
 
21
- @example Using fallthrough locales on a plain old ruby class
22
- class Post
23
- def title
24
- "title in #{Mobility.locale}"
25
- end
26
- include Mobility::FallthroughAccessors.new("title")
27
- end
21
+ =end
22
+ module FallthroughAccessors
23
+ extend Plugin
28
24
 
29
- Mobility.locale = :en
30
- post = Post.new
31
- post.title
32
- #=> "title in en"
33
- post.title_fr
34
- #=> "title in fr"
25
+ default true
35
26
 
36
- =end
37
- class FallthroughAccessors < Module
38
27
  # Apply fallthrough accessors plugin to attributes.
39
- # @param [Attributes] attributes
28
+ # @param [Translations] translations
40
29
  # @param [Boolean] option
41
- def self.apply(attributes, option)
42
- attributes.model_class.include new(*attributes.names) if option
30
+ initialize_hook do
31
+ if options[:fallthrough_accessors]
32
+ define_fallthrough_accessors(names)
33
+ end
43
34
  end
44
35
 
45
- # @param [String] One or more attributes
46
- def initialize(*attributes)
47
- method_name_regex = /\A(#{attributes.join('|')})_([a-z]{2}(_[a-z]{2})?)(=?|\??)\z/.freeze
36
+ private
37
+
38
+ def define_fallthrough_accessors(*names)
39
+ method_name_regex = /\A(#{names.join('|')})_([a-z]{2}(_[a-z]{2})?)(=?|\??)\z/.freeze
48
40
 
49
- define_method :method_missing do |method_name, *arguments, **options, &block|
41
+ define_method :method_missing do |method_name, *args, &block|
50
42
  if method_name =~ method_name_regex
51
- attribute = $1.to_sym
43
+ attribute_method = "#{$1}#{$4}"
52
44
  locale, suffix = $2.split('_')
53
45
  locale = "#{locale}-#{suffix.upcase}" if suffix
54
- public_send("#{attribute}#{$4}", *arguments, **options, locale: locale.to_sym)
46
+ if $4 == '=' # writer
47
+ kwargs = args[1].is_a?(Hash) ? args[1] : {}
48
+ public_send(attribute_method, args[0], **kwargs, locale: locale)
49
+ else # reader
50
+ kwargs = args[0].is_a?(Hash) ? args[0] : {}
51
+ public_send(attribute_method, **kwargs, locale: locale)
52
+ end
55
53
  else
56
- super(method_name, *arguments, &block)
54
+ super(method_name, *args, &block)
57
55
  end
58
56
  end
59
57
 
@@ -62,5 +60,7 @@ model class is generated.
62
60
  end
63
61
  end
64
62
  end
63
+
64
+ register_plugin :fallthrough_accessors, FallthroughAccessors
65
65
  end
66
66
  end