mobility 0.8.11 → 1.0.0.rc1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (113) 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 +69 -1
  5. data/Gemfile +50 -18
  6. data/Gemfile.lock +32 -75
  7. data/README.md +184 -92
  8. data/Rakefile +6 -4
  9. data/lib/mobility.rb +100 -168
  10. data/lib/mobility/backend.rb +27 -51
  11. data/lib/mobility/backends.rb +20 -0
  12. data/lib/mobility/backends/active_record.rb +4 -0
  13. data/lib/mobility/backends/active_record/column.rb +3 -1
  14. data/lib/mobility/backends/active_record/container.rb +10 -11
  15. data/lib/mobility/backends/active_record/hstore.rb +6 -4
  16. data/lib/mobility/backends/active_record/json.rb +5 -3
  17. data/lib/mobility/backends/active_record/jsonb.rb +5 -3
  18. data/lib/mobility/backends/active_record/key_value.rb +31 -13
  19. data/lib/mobility/backends/active_record/pg_hash.rb +1 -1
  20. data/lib/mobility/backends/active_record/serialized.rb +6 -0
  21. data/lib/mobility/backends/active_record/table.rb +17 -10
  22. data/lib/mobility/backends/column.rb +0 -6
  23. data/lib/mobility/backends/container.rb +10 -1
  24. data/lib/mobility/backends/hash.rb +39 -0
  25. data/lib/mobility/backends/hash_valued.rb +4 -0
  26. data/lib/mobility/backends/hstore.rb +0 -1
  27. data/lib/mobility/backends/json.rb +0 -1
  28. data/lib/mobility/backends/jsonb.rb +1 -2
  29. data/lib/mobility/backends/key_value.rb +31 -26
  30. data/lib/mobility/backends/null.rb +2 -0
  31. data/lib/mobility/backends/sequel.rb +37 -2
  32. data/lib/mobility/backends/sequel/column.rb +2 -0
  33. data/lib/mobility/backends/sequel/container.rb +11 -9
  34. data/lib/mobility/backends/sequel/hstore.rb +3 -1
  35. data/lib/mobility/backends/sequel/json.rb +3 -0
  36. data/lib/mobility/backends/sequel/jsonb.rb +3 -1
  37. data/lib/mobility/backends/sequel/key_value.rb +87 -18
  38. data/lib/mobility/backends/sequel/pg_hash.rb +6 -6
  39. data/lib/mobility/backends/sequel/serialized.rb +6 -0
  40. data/lib/mobility/backends/sequel/table.rb +22 -9
  41. data/lib/mobility/backends/serialized.rb +1 -3
  42. data/lib/mobility/backends/table.rb +39 -31
  43. data/lib/mobility/pluggable.rb +56 -0
  44. data/lib/mobility/plugin.rb +260 -0
  45. data/lib/mobility/plugins.rb +27 -24
  46. data/lib/mobility/plugins/active_model.rb +17 -0
  47. data/lib/mobility/plugins/active_model/cache.rb +26 -0
  48. data/lib/mobility/plugins/active_model/dirty.rb +119 -78
  49. data/lib/mobility/plugins/active_record.rb +37 -0
  50. data/lib/mobility/plugins/active_record/backend.rb +27 -0
  51. data/lib/mobility/plugins/active_record/cache.rb +28 -0
  52. data/lib/mobility/plugins/active_record/dirty.rb +34 -17
  53. data/lib/mobility/plugins/active_record/query.rb +43 -31
  54. data/lib/mobility/plugins/active_record/uniqueness_validation.rb +60 -0
  55. data/lib/mobility/plugins/arel.rb +125 -0
  56. data/lib/mobility/plugins/arel/nodes.rb +15 -0
  57. data/lib/mobility/plugins/arel/nodes/pg_ops.rb +134 -0
  58. data/lib/mobility/plugins/attribute_methods.rb +29 -20
  59. data/lib/mobility/plugins/attributes.rb +72 -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 +34 -23
  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 +31 -42
  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/model_translation.rb +0 -14
  87. data/lib/mobility/active_record/string_translation.rb +0 -10
  88. data/lib/mobility/active_record/text_translation.rb +0 -10
  89. data/lib/mobility/active_record/translation.rb +0 -14
  90. data/lib/mobility/active_record/uniqueness_validator.rb +0 -60
  91. data/lib/mobility/arel.rb +0 -49
  92. data/lib/mobility/arel/nodes.rb +0 -13
  93. data/lib/mobility/arel/nodes/pg_ops.rb +0 -132
  94. data/lib/mobility/arel/visitor.rb +0 -61
  95. data/lib/mobility/attributes.rb +0 -324
  96. data/lib/mobility/backend/orm_delegator.rb +0 -44
  97. data/lib/mobility/backend_resetter.rb +0 -50
  98. data/lib/mobility/configuration.rb +0 -138
  99. data/lib/mobility/fallbacks.rb +0 -28
  100. data/lib/mobility/interface.rb +0 -0
  101. data/lib/mobility/loaded.rb +0 -4
  102. data/lib/mobility/plugins/active_record/attribute_methods.rb +0 -38
  103. data/lib/mobility/plugins/cache/translation_cacher.rb +0 -40
  104. data/lib/mobility/sequel.rb +0 -9
  105. data/lib/mobility/sequel/backend_resetter.rb +0 -23
  106. data/lib/mobility/sequel/column_changes.rb +0 -28
  107. data/lib/mobility/sequel/hash_initializer.rb +0 -21
  108. data/lib/mobility/sequel/model_translation.rb +0 -20
  109. data/lib/mobility/sequel/sql.rb +0 -16
  110. data/lib/mobility/sequel/string_translation.rb +0 -10
  111. data/lib/mobility/sequel/text_translation.rb +0 -10
  112. data/lib/mobility/sequel/translation.rb +0 -53
  113. 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