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.
Files changed (114) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +2 -2
  3. data.tar.gz.sig +0 -0
  4. data/CHANGELOG.md +63 -0
  5. data/Gemfile +5 -2
  6. data/Gemfile.lock +39 -20
  7. data/README.md +183 -93
  8. data/lib/mobility.rb +101 -169
  9. data/lib/mobility/backend.rb +27 -51
  10. data/lib/mobility/backends.rb +20 -0
  11. data/lib/mobility/backends/active_record.rb +4 -0
  12. data/lib/mobility/backends/active_record/column.rb +3 -1
  13. data/lib/mobility/backends/active_record/container.rb +10 -11
  14. data/lib/mobility/backends/active_record/hstore.rb +6 -4
  15. data/lib/mobility/backends/active_record/json.rb +5 -3
  16. data/lib/mobility/backends/active_record/jsonb.rb +5 -3
  17. data/lib/mobility/backends/active_record/key_value.rb +31 -13
  18. data/lib/mobility/backends/active_record/pg_hash.rb +1 -1
  19. data/lib/mobility/backends/active_record/serialized.rb +6 -0
  20. data/lib/mobility/backends/active_record/table.rb +17 -10
  21. data/lib/mobility/backends/column.rb +0 -6
  22. data/lib/mobility/backends/container.rb +10 -1
  23. data/lib/mobility/backends/hash.rb +39 -0
  24. data/lib/mobility/backends/hash_valued.rb +4 -0
  25. data/lib/mobility/backends/hstore.rb +0 -1
  26. data/lib/mobility/backends/json.rb +0 -1
  27. data/lib/mobility/backends/jsonb.rb +1 -2
  28. data/lib/mobility/backends/key_value.rb +31 -26
  29. data/lib/mobility/backends/null.rb +2 -0
  30. data/lib/mobility/backends/sequel.rb +37 -2
  31. data/lib/mobility/backends/sequel/column.rb +2 -0
  32. data/lib/mobility/backends/sequel/container.rb +11 -9
  33. data/lib/mobility/backends/sequel/hstore.rb +3 -1
  34. data/lib/mobility/backends/sequel/json.rb +3 -0
  35. data/lib/mobility/backends/sequel/jsonb.rb +3 -1
  36. data/lib/mobility/backends/sequel/key_value.rb +87 -18
  37. data/lib/mobility/backends/sequel/pg_hash.rb +6 -6
  38. data/lib/mobility/backends/sequel/serialized.rb +6 -0
  39. data/lib/mobility/backends/sequel/table.rb +22 -9
  40. data/lib/mobility/backends/serialized.rb +1 -3
  41. data/lib/mobility/backends/table.rb +39 -31
  42. data/lib/mobility/pluggable.rb +56 -0
  43. data/lib/mobility/plugin.rb +260 -0
  44. data/lib/mobility/plugins.rb +27 -24
  45. data/lib/mobility/plugins/active_model.rb +17 -0
  46. data/lib/mobility/plugins/active_model/cache.rb +26 -0
  47. data/lib/mobility/plugins/active_model/dirty.rb +119 -78
  48. data/lib/mobility/plugins/active_record.rb +37 -0
  49. data/lib/mobility/plugins/active_record/backend.rb +27 -0
  50. data/lib/mobility/plugins/active_record/cache.rb +28 -0
  51. data/lib/mobility/plugins/active_record/dirty.rb +34 -17
  52. data/lib/mobility/plugins/active_record/query.rb +43 -31
  53. data/lib/mobility/plugins/active_record/uniqueness_validation.rb +64 -0
  54. data/lib/mobility/plugins/arel.rb +125 -0
  55. data/lib/mobility/plugins/arel/nodes.rb +15 -0
  56. data/lib/mobility/plugins/arel/nodes/pg_ops.rb +134 -0
  57. data/lib/mobility/plugins/attribute_methods.rb +29 -20
  58. data/lib/mobility/plugins/attributes.rb +72 -0
  59. data/lib/mobility/plugins/backend.rb +161 -0
  60. data/lib/mobility/plugins/backend_reader.rb +34 -0
  61. data/lib/mobility/plugins/cache.rb +68 -26
  62. data/lib/mobility/plugins/default.rb +22 -17
  63. data/lib/mobility/plugins/dirty.rb +12 -33
  64. data/lib/mobility/plugins/fallbacks.rb +52 -44
  65. data/lib/mobility/plugins/fallthrough_accessors.rb +19 -23
  66. data/lib/mobility/plugins/locale_accessors.rb +22 -35
  67. data/lib/mobility/plugins/presence.rb +28 -21
  68. data/lib/mobility/plugins/query.rb +8 -17
  69. data/lib/mobility/plugins/reader.rb +50 -0
  70. data/lib/mobility/plugins/sequel.rb +34 -0
  71. data/lib/mobility/plugins/sequel/backend.rb +25 -0
  72. data/lib/mobility/plugins/sequel/cache.rb +24 -0
  73. data/lib/mobility/plugins/sequel/dirty.rb +34 -23
  74. data/lib/mobility/plugins/sequel/query.rb +21 -6
  75. data/lib/mobility/plugins/writer.rb +44 -0
  76. data/lib/mobility/translations.rb +95 -0
  77. data/lib/mobility/version.rb +12 -1
  78. data/lib/rails/generators/mobility/templates/create_string_translations.rb +0 -1
  79. data/lib/rails/generators/mobility/templates/create_text_translations.rb +0 -1
  80. data/lib/rails/generators/mobility/templates/initializer.rb +104 -78
  81. metadata +35 -40
  82. metadata.gz.sig +0 -0
  83. data/lib/mobility/active_model.rb +0 -4
  84. data/lib/mobility/active_model/backend_resetter.rb +0 -26
  85. data/lib/mobility/active_record.rb +0 -23
  86. data/lib/mobility/active_record/backend_resetter.rb +0 -26
  87. data/lib/mobility/active_record/model_translation.rb +0 -14
  88. data/lib/mobility/active_record/string_translation.rb +0 -10
  89. data/lib/mobility/active_record/text_translation.rb +0 -10
  90. data/lib/mobility/active_record/translation.rb +0 -14
  91. data/lib/mobility/active_record/uniqueness_validator.rb +0 -60
  92. data/lib/mobility/arel.rb +0 -49
  93. data/lib/mobility/arel/nodes.rb +0 -13
  94. data/lib/mobility/arel/nodes/pg_ops.rb +0 -132
  95. data/lib/mobility/arel/visitor.rb +0 -61
  96. data/lib/mobility/attributes.rb +0 -324
  97. data/lib/mobility/backend/orm_delegator.rb +0 -44
  98. data/lib/mobility/backend_resetter.rb +0 -50
  99. data/lib/mobility/configuration.rb +0 -138
  100. data/lib/mobility/fallbacks.rb +0 -28
  101. data/lib/mobility/interface.rb +0 -0
  102. data/lib/mobility/loaded.rb +0 -4
  103. data/lib/mobility/plugins/active_record/attribute_methods.rb +0 -38
  104. data/lib/mobility/plugins/cache/translation_cacher.rb +0 -40
  105. data/lib/mobility/sequel.rb +0 -9
  106. data/lib/mobility/sequel/backend_resetter.rb +0 -23
  107. data/lib/mobility/sequel/column_changes.rb +0 -28
  108. data/lib/mobility/sequel/hash_initializer.rb +0 -21
  109. data/lib/mobility/sequel/model_translation.rb +0 -20
  110. data/lib/mobility/sequel/sql.rb +0 -16
  111. data/lib/mobility/sequel/string_translation.rb +0 -10
  112. data/lib/mobility/sequel/text_translation.rb +0 -10
  113. data/lib/mobility/sequel/translation.rb +0 -53
  114. data/lib/mobility/translates.rb +0 -73
@@ -12,30 +12,39 @@ attributes only.
12
12
 
13
13
  =end
14
14
  module AttributeMethods
15
- class << self
16
- # Applies attribute_methods plugin for a given option value.
17
- # @param [Attributes] attributes
18
- # @param [Boolean] option Value of option
19
- # @raise [ArgumentError] if model class does not support dirty tracking
20
- def apply(attributes, option)
21
- if option
22
- include_attribute_methods_module(attributes.model_class, *attributes.names)
23
- end
15
+ extend Plugin
16
+
17
+ default true
18
+ requires :attributes
19
+
20
+ initialize_hook do |*names|
21
+ include InstanceMethods
22
+
23
+ define_method :translated_attributes do
24
+ super().merge(names.inject({}) do |attributes, name|
25
+ attributes.merge(name.to_s => send(name))
26
+ end)
27
+ end
28
+ end
29
+
30
+ # Applies attribute_methods plugin for a given option value.
31
+ included_hook do
32
+ if options[:attribute_methods]
33
+ define_method :untranslated_attributes, ::ActiveRecord::Base.instance_method(:attributes)
24
34
  end
35
+ end
25
36
 
26
- private
27
-
28
- def include_attribute_methods_module(model_class, *attribute_names)
29
- module_builder =
30
- if Loaded::ActiveRecord && model_class.ancestors.include?(::ActiveRecord::AttributeMethods)
31
- require "mobility/plugins/active_record/attribute_methods"
32
- Plugins::ActiveRecord::AttributeMethods
33
- else
34
- raise ArgumentError, "#{model_class} does not support AttributeMethods plugin."
35
- end
36
- model_class.include module_builder.new(*attribute_names)
37
+ module InstanceMethods
38
+ def translated_attributes
39
+ {}
40
+ end
41
+
42
+ def attributes
43
+ super.merge(translated_attributes)
37
44
  end
38
45
  end
39
46
  end
47
+
48
+ register_plugin(:attribute_methods, AttributeMethods)
40
49
  end
41
50
  end
@@ -0,0 +1,72 @@
1
+ # frozen-string-literal: true
2
+ module Mobility
3
+ module Plugins
4
+ =begin
5
+
6
+ Takes arguments, converts them to strings, and stores in an array +@names+,
7
+ made available with an +attr_reader+. Also provides some convenience methods
8
+ for aggregating attributes.
9
+
10
+ =end
11
+ module Attributes
12
+ extend Plugin
13
+
14
+ # Attribute names for which accessors will be defined
15
+ # @return [Array<String>] Array of names
16
+ attr_reader :names
17
+
18
+ initialize_hook do |*names|
19
+ @names = names.map(&:to_s).freeze
20
+ end
21
+
22
+ # Yield each attribute name to block
23
+ # @yieldparam [String] Attribute
24
+ def each &block
25
+ names.each(&block)
26
+ end
27
+
28
+ # Show useful information about this module.
29
+ # @return [String]
30
+ def inspect
31
+ "#<Translations @names=#{names.join(", ")}>"
32
+ end
33
+
34
+ included_hook do |klass|
35
+ names = @names
36
+
37
+ klass.class_eval do
38
+ extend ClassMethods
39
+ names.each { |name| mobility_attributes << name.to_s }
40
+ mobility_attributes.uniq!
41
+ rescue FrozenError
42
+ raise FrozenAttributesError, "Attempting to translate these attributes on #{klass}, which has already been subclassed: #{names.join(', ')}."
43
+ end
44
+ end
45
+
46
+ module ClassMethods
47
+ # Return true if attribute name is translated on this model.
48
+ # @param [String, Symbol] Attribute name
49
+ # @return [Boolean]
50
+ def mobility_attribute?(name)
51
+ mobility_attributes.include?(name.to_s)
52
+ end
53
+
54
+ # Return translated attribute names on this model.
55
+ # @return [Array<String>] Attribute names
56
+ def mobility_attributes
57
+ @mobility_attributes ||= []
58
+ end
59
+
60
+ def inherited(klass)
61
+ super
62
+ attrs = mobility_attributes.freeze # ensure attributes are not modified after being inherited
63
+ klass.class_eval { @mobility_attributes = attrs.dup }
64
+ end
65
+ end
66
+
67
+ class FrozenAttributesError < Error; end
68
+ end
69
+
70
+ register_plugin(:attributes, Attributes)
71
+ end
72
+ end
@@ -0,0 +1,161 @@
1
+ # frozen-string-literal: true
2
+ module Mobility
3
+ module Plugins
4
+ =begin
5
+
6
+ Plugin for setting up a backend for a set of model attributes. All backend
7
+ plugins must depend on this.
8
+
9
+ Defines:
10
+ - instance method +mobility_backends+ which returns a hash whose keys are
11
+ attribute names and values a backend for each attribute.
12
+ - class method +mobility_backend_class+ which takes an attribute name and
13
+ returns the backend class for that name.
14
+
15
+ =end
16
+ module Backend
17
+ extend Plugin
18
+
19
+ requires :attributes, include: :before
20
+
21
+ # Backend class
22
+ # @return [Class] Backend class
23
+ attr_reader :backend_class
24
+
25
+ # Backend
26
+ # @return [Symbol,Class,Class] Name of backend, or backend class
27
+ attr_reader :backend
28
+
29
+ # Backend options
30
+ # @return [Hash] Options for backend
31
+ attr_reader :backend_options
32
+
33
+ def initialize(*args, **original_options)
34
+ super
35
+
36
+ # Validate that the default backend from config has valid keys
37
+ if (default = self.class.defaults[:backend])
38
+ name, backend_options = default
39
+ extra_keys = backend_options.keys - backend.valid_keys
40
+ raise InvalidOptionKey, "These are not valid #{name} backend keys: #{extra_keys.join(', ')}." unless extra_keys.empty?
41
+ end
42
+
43
+ include InstanceMethods
44
+ end
45
+
46
+ # Setup backend class, include modules into model class, include/extend
47
+ # shared modules and setup model with backend setup block (see
48
+ # {Mobility::Backend::Setup#setup_model}).
49
+ def included(klass)
50
+ super
51
+
52
+ klass.extend ClassMethods
53
+
54
+ if backend
55
+ @backend_class = backend.build_subclass(klass, backend_options)
56
+
57
+ backend_class.setup_model(klass, names)
58
+
59
+ names = @names
60
+ backend_class = @backend_class
61
+
62
+ klass.class_eval do
63
+ names.each { |name| mobility_backend_classes[name.to_sym] = backend_class }
64
+ end
65
+
66
+ backend_class
67
+ end
68
+ end
69
+
70
+ # Include backend name in inspect string.
71
+ # @return [String]
72
+ def inspect
73
+ "#<Translations (#{backend}) @names=#{names.join(", ")}>"
74
+ end
75
+
76
+ def load_backend(backend)
77
+ Backends.load_backend(backend)
78
+ rescue Backends::LoadError => e
79
+ raise e, "could not find a #{backend} backend. Did you forget to include an ORM plugin like active_record or sequel?"
80
+ end
81
+
82
+ private
83
+
84
+ # Override to extract backend options from options hash.
85
+ def initialize_options(original_options)
86
+ super
87
+
88
+ case options[:backend]
89
+ when String, Symbol, Class
90
+ @backend, @backend_options = options[:backend], options
91
+ when Array
92
+ @backend, @backend_options = options[:backend]
93
+ @backend_options = @backend_options.merge(original_options)
94
+ when NilClass
95
+ @backend = @backend_options = nil
96
+ else
97
+ raise ArgumentError, "backend must be either a backend name, a backend class, or a two-element array"
98
+ end
99
+
100
+ @backend = load_backend(backend)
101
+ end
102
+
103
+ # Override default validation to exclude backend options, which may be
104
+ # mixed in with plugin options.
105
+ def validate_options(options)
106
+ return super unless backend
107
+ super(options.slice(*(options.keys - backend.valid_keys)))
108
+ end
109
+
110
+ # Override default argument-handling in DSL to store kwargs passed along
111
+ # with plugin name.
112
+ def self.configure_default(defaults, key, backend = nil, backend_options = {})
113
+ defaults[key] = [backend, backend_options] if backend
114
+ end
115
+
116
+ module InstanceMethods
117
+ # Return a new backend for an attribute name.
118
+ # @return [Hash] Hash of attribute names and backend instances
119
+ # @api private
120
+ def mobility_backends
121
+ @mobility_backends ||= ::Hash.new do |hash, name|
122
+ next hash[name.to_sym] if String === name
123
+ hash[name] = self.class.mobility_backend_class(name).new(self, name.to_s)
124
+ end
125
+ end
126
+
127
+ def initialize_dup(other)
128
+ @mobility_backends = nil
129
+ super
130
+ end
131
+ end
132
+
133
+ module ClassMethods
134
+ # Return backend class for a given attribute name.
135
+ # @param [Symbol,String] Name of attribute
136
+ # @return [Class] Backend class
137
+ def mobility_backend_class(name)
138
+ mobility_backend_classes.fetch(name.to_sym)
139
+ rescue KeyError
140
+ raise KeyError, "No backend for: #{name}"
141
+ end
142
+
143
+ def inherited(klass)
144
+ parent_classes = mobility_backend_classes.freeze # ensure backend classes are not modified after being inherited
145
+ klass.class_eval { @mobility_backend_classes = parent_classes.dup }
146
+ super
147
+ end
148
+
149
+ protected
150
+
151
+ def mobility_backend_classes
152
+ @mobility_backend_classes ||= {}
153
+ end
154
+ end
155
+
156
+ class InvalidOptionKey < Error; end
157
+ end
158
+
159
+ register_plugin(:backend, Backend)
160
+ end
161
+ end
@@ -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