mobility 0.8.13 → 1.0.0.alpha

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 (93) 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 +26 -0
  5. data/Gemfile +5 -2
  6. data/Gemfile.lock +79 -8
  7. data/README.md +183 -91
  8. data/lib/mobility.rb +40 -166
  9. data/lib/mobility/arel/nodes/pg_ops.rb +1 -1
  10. data/lib/mobility/backend.rb +19 -41
  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 +2 -0
  14. data/lib/mobility/backends/active_record/container.rb +4 -2
  15. data/lib/mobility/backends/active_record/hstore.rb +2 -0
  16. data/lib/mobility/backends/active_record/json.rb +2 -0
  17. data/lib/mobility/backends/active_record/jsonb.rb +2 -0
  18. data/lib/mobility/backends/active_record/key_value.rb +5 -3
  19. data/lib/mobility/backends/active_record/pg_hash.rb +1 -1
  20. data/lib/mobility/backends/active_record/serialized.rb +2 -0
  21. data/lib/mobility/backends/active_record/table.rb +5 -3
  22. data/lib/mobility/backends/column.rb +0 -6
  23. data/lib/mobility/backends/container.rb +2 -1
  24. data/lib/mobility/backends/hash.rb +39 -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 +0 -1
  28. data/lib/mobility/backends/key_value.rb +22 -14
  29. data/lib/mobility/backends/null.rb +2 -0
  30. data/lib/mobility/backends/sequel.rb +3 -0
  31. data/lib/mobility/backends/sequel/column.rb +2 -0
  32. data/lib/mobility/backends/sequel/container.rb +3 -1
  33. data/lib/mobility/backends/sequel/hstore.rb +2 -0
  34. data/lib/mobility/backends/sequel/json.rb +2 -0
  35. data/lib/mobility/backends/sequel/jsonb.rb +3 -1
  36. data/lib/mobility/backends/sequel/key_value.rb +8 -6
  37. data/lib/mobility/backends/sequel/serialized.rb +2 -0
  38. data/lib/mobility/backends/sequel/table.rb +5 -2
  39. data/lib/mobility/backends/serialized.rb +1 -3
  40. data/lib/mobility/backends/table.rb +14 -6
  41. data/lib/mobility/pluggable.rb +36 -0
  42. data/lib/mobility/plugin.rb +260 -0
  43. data/lib/mobility/plugins.rb +26 -25
  44. data/lib/mobility/plugins/active_model.rb +17 -0
  45. data/lib/mobility/plugins/active_model/cache.rb +26 -0
  46. data/lib/mobility/plugins/active_model/dirty.rb +112 -77
  47. data/lib/mobility/plugins/active_record.rb +34 -0
  48. data/lib/mobility/plugins/active_record/backend.rb +25 -0
  49. data/lib/mobility/plugins/active_record/cache.rb +28 -0
  50. data/lib/mobility/plugins/active_record/dirty.rb +34 -17
  51. data/lib/mobility/plugins/active_record/query.rb +43 -31
  52. data/lib/mobility/plugins/active_record/uniqueness_validation.rb +60 -0
  53. data/lib/mobility/plugins/attribute_methods.rb +28 -20
  54. data/lib/mobility/plugins/attributes.rb +70 -0
  55. data/lib/mobility/plugins/backend.rb +138 -0
  56. data/lib/mobility/plugins/backend_reader.rb +34 -0
  57. data/lib/mobility/plugins/cache.rb +59 -24
  58. data/lib/mobility/plugins/default.rb +22 -17
  59. data/lib/mobility/plugins/dirty.rb +12 -33
  60. data/lib/mobility/plugins/fallbacks.rb +51 -43
  61. data/lib/mobility/plugins/fallthrough_accessors.rb +20 -23
  62. data/lib/mobility/plugins/locale_accessors.rb +25 -35
  63. data/lib/mobility/plugins/presence.rb +28 -21
  64. data/lib/mobility/plugins/query.rb +8 -17
  65. data/lib/mobility/plugins/reader.rb +50 -0
  66. data/lib/mobility/plugins/sequel.rb +34 -0
  67. data/lib/mobility/plugins/sequel/backend.rb +25 -0
  68. data/lib/mobility/plugins/sequel/cache.rb +24 -0
  69. data/lib/mobility/plugins/sequel/dirty.rb +32 -21
  70. data/lib/mobility/plugins/sequel/query.rb +21 -6
  71. data/lib/mobility/plugins/writer.rb +44 -0
  72. data/lib/mobility/translations.rb +95 -0
  73. data/lib/mobility/version.rb +12 -1
  74. data/lib/rails/generators/mobility/templates/initializer.rb +95 -77
  75. metadata +28 -27
  76. metadata.gz.sig +0 -0
  77. data/lib/mobility/active_model.rb +0 -4
  78. data/lib/mobility/active_model/backend_resetter.rb +0 -26
  79. data/lib/mobility/active_record.rb +0 -23
  80. data/lib/mobility/active_record/backend_resetter.rb +0 -26
  81. data/lib/mobility/active_record/uniqueness_validator.rb +0 -60
  82. data/lib/mobility/attributes.rb +0 -324
  83. data/lib/mobility/backend/orm_delegator.rb +0 -44
  84. data/lib/mobility/backend_resetter.rb +0 -50
  85. data/lib/mobility/configuration.rb +0 -138
  86. data/lib/mobility/fallbacks.rb +0 -28
  87. data/lib/mobility/interface.rb +0 -0
  88. data/lib/mobility/loaded.rb +0 -4
  89. data/lib/mobility/plugins/active_record/attribute_methods.rb +0 -38
  90. data/lib/mobility/plugins/cache/translation_cacher.rb +0 -40
  91. data/lib/mobility/sequel.rb +0 -9
  92. data/lib/mobility/sequel/backend_resetter.rb +0 -23
  93. data/lib/mobility/translates.rb +0 -73
@@ -0,0 +1,60 @@
1
+ module Mobility
2
+ module Plugins
3
+ module ActiveRecord
4
+ module UniquenessValidation
5
+ extend Plugin
6
+
7
+ requires :query, include: false
8
+
9
+ included_hook do |klass|
10
+ klass.class_eval do
11
+ unless const_defined?(:UniquenessValidator, false)
12
+ self.const_set(:UniquenessValidator, Class.new(UniquenessValidator))
13
+ end
14
+ end
15
+ end
16
+
17
+ class UniquenessValidator < ::ActiveRecord::Validations::UniquenessValidator
18
+ # @param [ActiveRecord::Base] record Translated model
19
+ # @param [String] attribute Name of attribute
20
+ # @param [Object] value Attribute value
21
+ def validate_each(record, attribute, value)
22
+ klass = record.class
23
+
24
+ if ([*options[:scope]] + [attribute]).any? { |name| klass.mobility_attribute?(name) }
25
+ return unless value.present?
26
+ relation = klass.unscoped.__mobility_query_scope__ do |m|
27
+ node = m.__send__(attribute)
28
+ options[:case_sensitive] == false ? node.lower.eq(value.downcase) : node.eq(value)
29
+ end
30
+ relation = relation.where.not(klass.primary_key => record.id) if record.persisted?
31
+ relation = mobility_scope_relation(record, relation)
32
+ relation = relation.merge(options[:conditions]) if options[:conditions]
33
+
34
+ if relation.exists?
35
+ error_options = options.except(:case_sensitive, :scope, :conditions)
36
+ error_options[:value] = value
37
+
38
+ record.errors.add(attribute, :taken, error_options)
39
+ end
40
+ else
41
+ super
42
+ end
43
+ end
44
+
45
+ private
46
+
47
+ def mobility_scope_relation(record, relation)
48
+ [*options[:scope]].inject(relation) do |scoped_relation, scope_item|
49
+ scoped_relation.__mobility_query_scope__ do |m|
50
+ m.__send__(scope_item).eq(record.send(scope_item))
51
+ end
52
+ end
53
+ end
54
+ end
55
+ end
56
+ end
57
+
58
+ register_plugin(:active_record_uniqueness_validation, ActiveRecord::UniquenessValidation)
59
+ end
60
+ end
@@ -12,30 +12,38 @@ 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
+
19
+ initialize_hook do |*names|
20
+ include InstanceMethods
21
+
22
+ define_method :translated_attributes do
23
+ super().merge(names.inject({}) do |attributes, name|
24
+ attributes.merge(name.to_s => send(name))
25
+ end)
26
+ end
27
+ end
28
+
29
+ # Applies attribute_methods plugin for a given option value.
30
+ included_hook do
31
+ if options[:attribute_methods]
32
+ define_method :untranslated_attributes, ::ActiveRecord::Base.instance_method(:attributes)
24
33
  end
34
+ end
25
35
 
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)
36
+ module InstanceMethods
37
+ def translated_attributes
38
+ {}
39
+ end
40
+
41
+ def attributes
42
+ super.merge(translated_attributes)
37
43
  end
38
44
  end
39
45
  end
46
+
47
+ register_plugin(:attribute_methods, AttributeMethods)
40
48
  end
41
49
  end
@@ -0,0 +1,70 @@
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
+ klass.extend ClassMethods
36
+ @names.each { |name| klass.register_mobility_attribute(name) }
37
+ end
38
+
39
+ module ClassMethods
40
+ # Return true if attribute name is translated on this model.
41
+ # @param [String, Symbol] Attribute name
42
+ # @return [Boolean]
43
+ def mobility_attribute?(name)
44
+ mobility_attributes.include?(name.to_s)
45
+ end
46
+
47
+ # Register a new attribute name. Public, but treat as internal.
48
+ # @param [String, Symbol] Attribute name
49
+ def register_mobility_attribute(name)
50
+ (self.mobility_attributes << name.to_s).uniq!
51
+ end
52
+
53
+ def inherited(klass)
54
+ super
55
+ mobility_attributes.each { |name| klass.register_mobility_attribute(name) }
56
+ end
57
+
58
+ protected
59
+
60
+ # Return translated attribute names on this model.
61
+ # @return [Array<String>] Attribute names
62
+ def mobility_attributes
63
+ @mobility_attributes ||= []
64
+ end
65
+ end
66
+ end
67
+
68
+ register_plugin(:attributes, Attributes)
69
+ end
70
+ end
@@ -0,0 +1,138 @@
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
+ return unless Plugins::Backend.dependencies_satisfied?(self.class)
36
+
37
+ case options[:backend]
38
+ when String, Symbol, Class
39
+ @backend, @backend_options = options[:backend], options
40
+ when Array
41
+ @backend, @backend_options = options[:backend]
42
+ @backend_options = @backend_options.merge(original_options)
43
+ when NilClass
44
+ @backend = @backend_options = nil
45
+ else
46
+ raise ArgumentError, "backend must be either a backend name, a backend class, or a two-element array"
47
+ end
48
+
49
+ include InstanceMethods
50
+ end
51
+
52
+ # Setup backend class, include modules into model class, include/extend
53
+ # shared modules and setup model with backend setup block (see
54
+ # {Mobility::Backend::Setup#setup_model}).
55
+ def included(klass)
56
+ super
57
+
58
+ klass.extend ClassMethods
59
+
60
+ if backend
61
+ @backend_class = load_backend(backend).
62
+ build_subclass(klass, backend_options)
63
+
64
+ backend_class.setup_model(klass, names)
65
+
66
+ @names.each do |name|
67
+ klass.register_mobility_backend_class(name, @backend_class)
68
+ end
69
+
70
+ backend_class
71
+ end
72
+ end
73
+
74
+ # Include backend name in inspect string.
75
+ # @return [String]
76
+ def inspect
77
+ "#<Translations (#{backend}) @names=#{names.join(", ")}>"
78
+ end
79
+
80
+ def load_backend(backend)
81
+ Backends.load_backend(backend)
82
+ rescue Backends::LoadError => e
83
+ raise e, "could not find a #{backend} backend. Did you forget to include an ORM plugin like active_record or sequel?"
84
+ end
85
+
86
+ # Override default argument-handling in DSL to store kwargs passed along
87
+ # with plugin name.
88
+ def self.configure_default(defaults, key, *args, **kwargs)
89
+ defaults[key] = [args[0], kwargs] unless args.empty?
90
+ end
91
+
92
+ module InstanceMethods
93
+ # Return a new backend for an attribute name.
94
+ # @return [Hash] Hash of attribute names and backend instances
95
+ # @api private
96
+ def mobility_backends
97
+ @mobility_backends ||= ::Hash.new do |hash, name|
98
+ next hash[name.to_sym] if String === name
99
+ hash[name] = self.class.mobility_backend_class(name).new(self, name.to_s)
100
+ end
101
+ end
102
+
103
+ def initialize_dup(other)
104
+ @mobility_backends = nil
105
+ super
106
+ end
107
+ end
108
+
109
+ module ClassMethods
110
+ # Return backend class for a given attribute name.
111
+ # @param [Symbol,String] Name of attribute
112
+ # @return [Class] Backend class
113
+ def mobility_backend_class(name)
114
+ mobility_backend_classes.fetch(name.to_sym)
115
+ rescue KeyError
116
+ raise KeyError, "No backend for: #{name}"
117
+ end
118
+
119
+ def register_mobility_backend_class(name, backend_class)
120
+ mobility_backend_classes[name.to_sym] = backend_class
121
+ end
122
+
123
+ def inherited(klass)
124
+ klass.mobility_backend_classes.merge!(@mobility_backend_classes)
125
+ super
126
+ end
127
+
128
+ protected
129
+
130
+ def mobility_backend_classes
131
+ @mobility_backend_classes ||= {}
132
+ end
133
+ end
134
+ end
135
+
136
+ register_plugin(:backend, Backend)
137
+ end
138
+ 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
@@ -21,34 +20,70 @@ Values are added to the cache in two ways:
21
20
 
22
21
  =end
23
22
  module Cache
23
+ extend Plugin
24
+
25
+ default true
26
+ requires :backend, include: :before
27
+
24
28
  # 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 }
29
+ included_hook do |_, backend_class|
30
+ if options[:cache]
31
+ backend_class.include(BackendMethods) unless backend_class.apply_plugin(:cache)
34
32
  end
35
33
  end
36
34
 
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
35
+ private
36
+
37
+ # Used in ORM cache plugins
38
+ def define_cache_hooks(klass, *reset_methods)
39
+ mod = self
40
+ private_methods = reset_methods & klass.private_instance_methods
41
+ reset_methods.each do |method_name|
42
+ define_method method_name do |*args|
43
+ super(*args).tap do
44
+ mod.names.each { |name| mobility_backends[name].clear_cache }
45
+ end
46
+ end
47
+ end
48
+ klass.class_eval { private(*private_methods) }
49
+ end
50
+
51
+ module BackendMethods
52
+ # @group Backend Accessors
53
+ #
54
+ # @!macro backend_reader
55
+ # @!method read(locale, value, options = {})
56
+ # @option options [Boolean] cache *false* to disable cache.
57
+ def read(locale, **options)
58
+ return super(locale, options) if options.delete(:cache) == false
59
+ if cache.has_key?(locale)
60
+ cache[locale]
61
+ else
62
+ cache[locale] = super(locale, options)
63
+ end
64
+ end
65
+
66
+ # @!macro backend_writer
67
+ # @option options [Boolean] cache
68
+ # *false* to disable cache.
69
+ def write(locale, value, **options)
70
+ return super if options.delete(:cache) == false
71
+ cache[locale] = super
72
+ end
73
+ # @!endgroup
74
+
75
+ def clear_cache
76
+ @cache = {}
77
+ end
78
+
79
+ private
80
+
81
+ def cache
82
+ @cache ||= {}
83
+ end
50
84
  end
51
- # @!endgroup
52
85
  end
86
+
87
+ register_plugin(:cache, Cache)
53
88
  end
54
89
  end