mobility 0.8.13 → 1.0.0.alpha

Sign up to get free protection for your applications and to get access to all the features.
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