mobility 0.8.9 → 1.0.0.beta1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (98) 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 +63 -0
  5. data/Gemfile +50 -18
  6. data/Gemfile.lock +44 -52
  7. data/Guardfile +23 -1
  8. data/README.md +183 -91
  9. data/Rakefile +6 -4
  10. data/lib/mobility.rb +44 -166
  11. data/lib/mobility/active_record/translation.rb +1 -1
  12. data/lib/mobility/arel.rb +1 -1
  13. data/lib/mobility/arel/nodes/pg_ops.rb +1 -1
  14. data/lib/mobility/backend.rb +27 -51
  15. data/lib/mobility/backends.rb +20 -0
  16. data/lib/mobility/backends/active_record.rb +4 -0
  17. data/lib/mobility/backends/active_record/column.rb +2 -0
  18. data/lib/mobility/backends/active_record/container.rb +6 -7
  19. data/lib/mobility/backends/active_record/hstore.rb +3 -1
  20. data/lib/mobility/backends/active_record/json.rb +2 -0
  21. data/lib/mobility/backends/active_record/jsonb.rb +2 -0
  22. data/lib/mobility/backends/active_record/key_value.rb +6 -4
  23. data/lib/mobility/backends/active_record/pg_hash.rb +1 -1
  24. data/lib/mobility/backends/active_record/serialized.rb +6 -0
  25. data/lib/mobility/backends/active_record/table.rb +6 -4
  26. data/lib/mobility/backends/column.rb +0 -6
  27. data/lib/mobility/backends/container.rb +10 -1
  28. data/lib/mobility/backends/hash.rb +39 -0
  29. data/lib/mobility/backends/hash_valued.rb +4 -0
  30. data/lib/mobility/backends/hstore.rb +0 -1
  31. data/lib/mobility/backends/json.rb +0 -1
  32. data/lib/mobility/backends/jsonb.rb +1 -2
  33. data/lib/mobility/backends/key_value.rb +31 -26
  34. data/lib/mobility/backends/null.rb +2 -0
  35. data/lib/mobility/backends/sequel.rb +5 -2
  36. data/lib/mobility/backends/sequel/column.rb +2 -0
  37. data/lib/mobility/backends/sequel/container.rb +6 -6
  38. data/lib/mobility/backends/sequel/hstore.rb +3 -1
  39. data/lib/mobility/backends/sequel/json.rb +3 -0
  40. data/lib/mobility/backends/sequel/jsonb.rb +3 -1
  41. data/lib/mobility/backends/sequel/key_value.rb +8 -6
  42. data/lib/mobility/backends/sequel/serialized.rb +6 -0
  43. data/lib/mobility/backends/sequel/table.rb +5 -2
  44. data/lib/mobility/backends/serialized.rb +1 -3
  45. data/lib/mobility/backends/table.rb +29 -26
  46. data/lib/mobility/pluggable.rb +56 -0
  47. data/lib/mobility/plugin.rb +260 -0
  48. data/lib/mobility/plugins.rb +27 -24
  49. data/lib/mobility/plugins/active_model.rb +17 -0
  50. data/lib/mobility/plugins/active_model/cache.rb +26 -0
  51. data/lib/mobility/plugins/active_model/dirty.rb +119 -78
  52. data/lib/mobility/plugins/active_record.rb +34 -0
  53. data/lib/mobility/plugins/active_record/backend.rb +25 -0
  54. data/lib/mobility/plugins/active_record/cache.rb +28 -0
  55. data/lib/mobility/plugins/active_record/dirty.rb +34 -17
  56. data/lib/mobility/plugins/active_record/query.rb +48 -34
  57. data/lib/mobility/plugins/active_record/uniqueness_validation.rb +60 -0
  58. data/lib/mobility/plugins/attribute_methods.rb +28 -20
  59. data/lib/mobility/plugins/attributes.rb +70 -0
  60. data/lib/mobility/plugins/backend.rb +161 -0
  61. data/lib/mobility/plugins/backend_reader.rb +34 -0
  62. data/lib/mobility/plugins/cache.rb +68 -26
  63. data/lib/mobility/plugins/default.rb +22 -17
  64. data/lib/mobility/plugins/dirty.rb +12 -33
  65. data/lib/mobility/plugins/fallbacks.rb +52 -44
  66. data/lib/mobility/plugins/fallthrough_accessors.rb +25 -25
  67. data/lib/mobility/plugins/locale_accessors.rb +22 -35
  68. data/lib/mobility/plugins/presence.rb +28 -21
  69. data/lib/mobility/plugins/query.rb +8 -17
  70. data/lib/mobility/plugins/reader.rb +50 -0
  71. data/lib/mobility/plugins/sequel.rb +34 -0
  72. data/lib/mobility/plugins/sequel/backend.rb +25 -0
  73. data/lib/mobility/plugins/sequel/cache.rb +24 -0
  74. data/lib/mobility/plugins/sequel/dirty.rb +33 -22
  75. data/lib/mobility/plugins/sequel/query.rb +21 -6
  76. data/lib/mobility/plugins/writer.rb +44 -0
  77. data/lib/mobility/translations.rb +95 -0
  78. data/lib/mobility/version.rb +12 -1
  79. data/lib/rails/generators/mobility/templates/initializer.rb +96 -78
  80. metadata +51 -51
  81. metadata.gz.sig +0 -0
  82. data/lib/mobility/active_model.rb +0 -4
  83. data/lib/mobility/active_model/backend_resetter.rb +0 -26
  84. data/lib/mobility/active_record.rb +0 -23
  85. data/lib/mobility/active_record/backend_resetter.rb +0 -26
  86. data/lib/mobility/active_record/uniqueness_validator.rb +0 -60
  87. data/lib/mobility/attributes.rb +0 -324
  88. data/lib/mobility/backend/orm_delegator.rb +0 -44
  89. data/lib/mobility/backend_resetter.rb +0 -50
  90. data/lib/mobility/configuration.rb +0 -138
  91. data/lib/mobility/fallbacks.rb +0 -28
  92. data/lib/mobility/interface.rb +0 -0
  93. data/lib/mobility/loaded.rb +0 -4
  94. data/lib/mobility/plugins/active_record/attribute_methods.rb +0 -38
  95. data/lib/mobility/plugins/cache/translation_cacher.rb +0 -40
  96. data/lib/mobility/sequel.rb +0 -9
  97. data/lib/mobility/sequel/backend_resetter.rb +0 -23
  98. 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