mobility 0.8.10 → 1.0.0.beta2

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 (96) 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 +66 -0
  5. data/Gemfile +50 -18
  6. data/Gemfile.lock +36 -101
  7. data/README.md +183 -91
  8. data/Rakefile +6 -4
  9. data/lib/mobility.rb +44 -166
  10. data/lib/mobility/arel.rb +1 -1
  11. data/lib/mobility/arel/nodes/pg_ops.rb +1 -1
  12. data/lib/mobility/backend.rb +27 -51
  13. data/lib/mobility/backends.rb +20 -0
  14. data/lib/mobility/backends/active_record.rb +4 -0
  15. data/lib/mobility/backends/active_record/column.rb +2 -0
  16. data/lib/mobility/backends/active_record/container.rb +6 -7
  17. data/lib/mobility/backends/active_record/hstore.rb +3 -1
  18. data/lib/mobility/backends/active_record/json.rb +2 -0
  19. data/lib/mobility/backends/active_record/jsonb.rb +2 -0
  20. data/lib/mobility/backends/active_record/key_value.rb +6 -4
  21. data/lib/mobility/backends/active_record/pg_hash.rb +1 -1
  22. data/lib/mobility/backends/active_record/serialized.rb +6 -0
  23. data/lib/mobility/backends/active_record/table.rb +6 -4
  24. data/lib/mobility/backends/column.rb +0 -6
  25. data/lib/mobility/backends/container.rb +10 -1
  26. data/lib/mobility/backends/hash.rb +39 -0
  27. data/lib/mobility/backends/hash_valued.rb +4 -0
  28. data/lib/mobility/backends/hstore.rb +0 -1
  29. data/lib/mobility/backends/json.rb +0 -1
  30. data/lib/mobility/backends/jsonb.rb +1 -2
  31. data/lib/mobility/backends/key_value.rb +31 -26
  32. data/lib/mobility/backends/null.rb +2 -0
  33. data/lib/mobility/backends/sequel.rb +5 -2
  34. data/lib/mobility/backends/sequel/column.rb +2 -0
  35. data/lib/mobility/backends/sequel/container.rb +6 -6
  36. data/lib/mobility/backends/sequel/hstore.rb +3 -1
  37. data/lib/mobility/backends/sequel/json.rb +3 -0
  38. data/lib/mobility/backends/sequel/jsonb.rb +3 -1
  39. data/lib/mobility/backends/sequel/key_value.rb +8 -6
  40. data/lib/mobility/backends/sequel/serialized.rb +6 -0
  41. data/lib/mobility/backends/sequel/table.rb +5 -2
  42. data/lib/mobility/backends/serialized.rb +1 -3
  43. data/lib/mobility/backends/table.rb +29 -26
  44. data/lib/mobility/pluggable.rb +56 -0
  45. data/lib/mobility/plugin.rb +260 -0
  46. data/lib/mobility/plugins.rb +27 -24
  47. data/lib/mobility/plugins/active_model.rb +17 -0
  48. data/lib/mobility/plugins/active_model/cache.rb +26 -0
  49. data/lib/mobility/plugins/active_model/dirty.rb +119 -78
  50. data/lib/mobility/plugins/active_record.rb +34 -0
  51. data/lib/mobility/plugins/active_record/backend.rb +25 -0
  52. data/lib/mobility/plugins/active_record/cache.rb +28 -0
  53. data/lib/mobility/plugins/active_record/dirty.rb +34 -17
  54. data/lib/mobility/plugins/active_record/query.rb +48 -34
  55. data/lib/mobility/plugins/active_record/uniqueness_validation.rb +60 -0
  56. data/lib/mobility/plugins/attribute_methods.rb +29 -20
  57. data/lib/mobility/plugins/attributes.rb +72 -0
  58. data/lib/mobility/plugins/backend.rb +161 -0
  59. data/lib/mobility/plugins/backend_reader.rb +34 -0
  60. data/lib/mobility/plugins/cache.rb +68 -26
  61. data/lib/mobility/plugins/default.rb +22 -17
  62. data/lib/mobility/plugins/dirty.rb +12 -33
  63. data/lib/mobility/plugins/fallbacks.rb +52 -44
  64. data/lib/mobility/plugins/fallthrough_accessors.rb +25 -25
  65. data/lib/mobility/plugins/locale_accessors.rb +22 -35
  66. data/lib/mobility/plugins/presence.rb +28 -21
  67. data/lib/mobility/plugins/query.rb +8 -17
  68. data/lib/mobility/plugins/reader.rb +50 -0
  69. data/lib/mobility/plugins/sequel.rb +34 -0
  70. data/lib/mobility/plugins/sequel/backend.rb +25 -0
  71. data/lib/mobility/plugins/sequel/cache.rb +24 -0
  72. data/lib/mobility/plugins/sequel/dirty.rb +33 -22
  73. data/lib/mobility/plugins/sequel/query.rb +21 -6
  74. data/lib/mobility/plugins/writer.rb +44 -0
  75. data/lib/mobility/translations.rb +95 -0
  76. data/lib/mobility/version.rb +12 -1
  77. data/lib/rails/generators/mobility/templates/initializer.rb +96 -78
  78. metadata +28 -27
  79. metadata.gz.sig +0 -0
  80. data/lib/mobility/active_model.rb +0 -4
  81. data/lib/mobility/active_model/backend_resetter.rb +0 -26
  82. data/lib/mobility/active_record.rb +0 -23
  83. data/lib/mobility/active_record/backend_resetter.rb +0 -26
  84. data/lib/mobility/active_record/uniqueness_validator.rb +0 -60
  85. data/lib/mobility/attributes.rb +0 -324
  86. data/lib/mobility/backend/orm_delegator.rb +0 -44
  87. data/lib/mobility/backend_resetter.rb +0 -50
  88. data/lib/mobility/configuration.rb +0 -138
  89. data/lib/mobility/fallbacks.rb +0 -28
  90. data/lib/mobility/interface.rb +0 -0
  91. data/lib/mobility/loaded.rb +0 -4
  92. data/lib/mobility/plugins/active_record/attribute_methods.rb +0 -38
  93. data/lib/mobility/plugins/cache/translation_cacher.rb +0 -40
  94. data/lib/mobility/sequel.rb +0 -9
  95. data/lib/mobility/sequel/backend_resetter.rb +0 -23
  96. data/lib/mobility/translates.rb +0 -73
@@ -13,47 +13,30 @@ If no locales are passed as an option to the initializer,
13
13
  +Mobility.available_locales+ (i.e. +I18n.available_locales+, or Rails-set
14
14
  available locales for a Rails application) will be used by default.
15
15
 
16
- @example
17
- class Post
18
- def title
19
- "title in #{Mobility.locale}"
20
- end
21
- include Mobility::Plugins::LocaleAccessors.new("title", locales: [:en, :fr])
22
- end
16
+ =end
17
+ module LocaleAccessors
18
+ extend Plugin
23
19
 
24
- Mobility.locale = :en
25
- post = Post.new
26
- post.title
27
- #=> "title in en"
28
- post.title_fr
29
- #=> "title in fr"
20
+ default true
30
21
 
31
- =end
32
- class LocaleAccessors < Module
33
22
  # Apply locale accessors plugin to attributes.
34
- # @param [Attributes] attributes
23
+ # @param [Translations] translations
35
24
  # @param [Boolean] option
36
- def self.apply(attributes, option)
37
- if accessor_locales = option
38
- accessor_locales = Mobility.config.default_accessor_locales if accessor_locales == true
39
- attributes.model_class.include new(*attributes.names, locales: accessor_locales)
40
- end
41
- end
42
-
43
- # @param [String] One or more attribute names
44
- # @param [Array<Symbol>] Locales
45
- def initialize(*attribute_names, locales:)
46
- attribute_names.each do |name|
47
- locales.each do |locale|
48
- define_reader(name, locale)
49
- define_writer(name, locale)
25
+ initialize_hook do |*names|
26
+ if locales = options[:locale_accessors]
27
+ locales = Mobility.available_locales if locales == true
28
+ names.each do |name|
29
+ locales.each do |locale|
30
+ define_locale_reader(name, locale)
31
+ define_locale_writer(name, locale)
32
+ end
50
33
  end
51
34
  end
52
35
  end
53
36
 
54
37
  private
55
38
 
56
- def define_reader(name, locale)
39
+ def define_locale_reader(name, locale)
57
40
  warning_message = "locale passed as option to locale accessor will be ignored"
58
41
  normalized_locale = Mobility.normalize_locale(locale)
59
42
 
@@ -61,18 +44,20 @@ available locales for a Rails application) will be used by default.
61
44
  def #{name}_#{normalized_locale}(options = {})
62
45
  return super() if options.delete(:super)
63
46
  warn "#{warning_message}" if options[:locale]
64
- #{name}(**options, locale: :'#{locale}')
47
+ #{name}(**options, locale: #{locale.inspect})
65
48
  end
49
+ EOM
66
50
 
51
+ module_eval <<-EOM, __FILE__, __LINE__ + 1
67
52
  def #{name}_#{normalized_locale}?(options = {})
68
53
  return super() if options.delete(:super)
69
54
  warn "#{warning_message}" if options[:locale]
70
- #{name}?(**options, locale: :'#{locale}')
55
+ #{name}?(**options, locale: #{locale.inspect})
71
56
  end
72
57
  EOM
73
58
  end
74
59
 
75
- def define_writer(name, locale)
60
+ def define_locale_writer(name, locale)
76
61
  warning_message = "locale passed as option to locale accessor will be ignored"
77
62
  normalized_locale = Mobility.normalize_locale(locale)
78
63
 
@@ -80,10 +65,12 @@ available locales for a Rails application) will be used by default.
80
65
  def #{name}_#{normalized_locale}=(value, options = {})
81
66
  return super(value) if options.delete(:super)
82
67
  warn "#{warning_message}" if options[:locale]
83
- public_send(:#{name}=, value, **options, locale: :'#{locale}')
68
+ public_send(:#{name}=, value, **options, locale: #{locale.inspect})
84
69
  end
85
70
  EOM
86
71
  end
87
72
  end
73
+
74
+ register_plugin(:locale_accessors, LocaleAccessors)
88
75
  end
89
76
  end
@@ -6,43 +6,50 @@ module Mobility
6
6
  =begin
7
7
 
8
8
  Applies presence filter to values fetched from backend and to values set on
9
- backend. Included by default, but can be disabled with +presence: false+ option.
9
+ backend.
10
10
 
11
11
  @note For performance reasons, the presence plugin filters only for empty
12
12
  strings, not other values continued "blank" like empty arrays.
13
13
 
14
14
  =end
15
15
  module Presence
16
+ extend Plugin
17
+
18
+ default true
19
+ requires :backend, include: :before
20
+
16
21
  # Applies presence plugin to attributes.
17
- # @param [Attributes] attributes
18
- # @param [Boolean] option
19
- def self.apply(attributes, option)
20
- attributes.backend_class.include(self) if option
22
+ included_hook do |_, backend_class|
23
+ backend_class.include(BackendMethods) if options[:presence]
21
24
  end
22
25
 
23
- # @!group Backend Accessors
24
- # @!macro backend_reader
25
- # @option options [Boolean] presence
26
- # *false* to disable presence filter.
27
- def read(locale, **options)
28
- options.delete(:presence) == false ? super : Presence[super]
29
- end
26
+ module BackendMethods
27
+ # @!group Backend Accessors
28
+ # @!macro backend_reader
29
+ # @option options [Boolean] presence
30
+ # *false* to disable presence filter.
31
+ def read(locale, **options)
32
+ options.delete(:presence) == false ? super : Presence[super]
33
+ end
30
34
 
31
- # @!macro backend_writer
32
- # @option options [Boolean] presence
33
- # *false* to disable presence filter.
34
- def write(locale, value, **options)
35
- if options.delete(:presence) == false
36
- super
37
- else
38
- super(locale, Presence[value], options)
35
+ # @!macro backend_writer
36
+ # @option options [Boolean] presence
37
+ # *false* to disable presence filter.
38
+ def write(locale, value, **options)
39
+ if options.delete(:presence) == false
40
+ super
41
+ else
42
+ super(locale, Presence[value], **options)
43
+ end
39
44
  end
45
+ # @!endgroup
40
46
  end
41
- # @!endgroup
42
47
 
43
48
  def self.[](value)
44
49
  (value == "") ? nil : value
45
50
  end
46
51
  end
52
+
53
+ register_plugin(:presence, Presence)
47
54
  end
48
55
  end
@@ -3,29 +3,20 @@ module Mobility
3
3
  module Plugins
4
4
  =begin
5
5
 
6
- @see {Mobility::Plugins::ActiveRecord::Query}
6
+ @see {Mobility::Plugins::ActiveRecord::Query} or {Mobility::Plugins::Sequel::Query}.
7
7
 
8
8
  =end
9
9
  module Query
10
- class << self
11
- def apply(attributes, option)
12
- if option
13
- include_query_module(attributes)
14
- end
15
- end
10
+ extend Plugin
16
11
 
17
- private
12
+ default :i18n
13
+ requires :backend, include: :before
18
14
 
19
- def include_query_module(attributes)
20
- if Loaded::ActiveRecord && attributes.model_class < ::ActiveRecord::Base
21
- require "mobility/plugins/active_record/query"
22
- ActiveRecord::Query.apply(attributes)
23
- elsif Loaded::Sequel && attributes.model_class < ::Sequel::Model
24
- require "mobility/plugins/sequel/query"
25
- Sequel::Query.apply(attributes)
26
- end
27
- end
15
+ def query_method
16
+ (options[:query] == true) ? self.class.defaults[:query] : options[:query]
28
17
  end
29
18
  end
19
+
20
+ register_plugin(:query, Query)
30
21
  end
31
22
  end
@@ -0,0 +1,50 @@
1
+ # frozen-string-literal: true
2
+
3
+ module Mobility
4
+ module Plugins
5
+ module Reader
6
+ =begin
7
+
8
+ Defines attribute reader that delegates to +Mobility::Backend#read+.
9
+
10
+ =end
11
+ extend Plugin
12
+
13
+ default true
14
+ requires :backend
15
+
16
+ initialize_hook do |*names, **|
17
+ if options[:reader]
18
+ names.each do |name|
19
+ class_eval <<-EOM, __FILE__, __LINE__ + 1
20
+ def #{name}(locale: nil, **options)
21
+ #{Reader.setup_source}
22
+ mobility_backends[:#{name}].read(locale, **options)
23
+ end
24
+ EOM
25
+ class_eval <<-EOM, __FILE__, __LINE__ + 1
26
+ def #{name}?(locale: nil, **options)
27
+ #{Reader.setup_source}
28
+ mobility_backends[:#{name}].present?(locale, **options)
29
+ end
30
+ EOM
31
+ end
32
+ end
33
+ end
34
+
35
+ def self.setup_source
36
+ <<-EOL
37
+ return super() if options[:super]
38
+ if (locale &&= locale.to_sym)
39
+ #{"Mobility.enforce_available_locales!(locale)" if I18n.enforce_available_locales}
40
+ options[:locale] = true
41
+ else
42
+ locale = Mobility.locale
43
+ end
44
+ EOL
45
+ end
46
+ end
47
+
48
+ register_plugin(:reader, Reader)
49
+ end
50
+ end
@@ -1,6 +1,40 @@
1
+ require "sequel"
2
+ raise VersionNotSupportedError, "Mobility is only compatible with Sequel 4.0 and greater" if ::Sequel::MAJOR < 4
3
+ require "sequel/plugins/mobility"
4
+ unless defined?(ActiveSupport::Inflector)
5
+ # TODO: avoid automatically including the inflector extension
6
+ require "sequel/extensions/inflector"
7
+ end
8
+ require "sequel/plugins/dirty"
9
+ require_relative "./sequel/backend"
10
+ require_relative "./sequel/dirty"
11
+ require_relative "./sequel/cache"
12
+ require_relative "./sequel/query"
13
+
1
14
  module Mobility
2
15
  module Plugins
3
16
  module Sequel
17
+ extend Plugin
18
+
19
+ requires :sequel_backend, include: :after
20
+ requires :sequel_dirty
21
+ requires :sequel_cache
22
+ requires :sequel_query
23
+
24
+ included_hook do |klass|
25
+ unless sequel_class?(klass)
26
+ name = klass.name || klass.to_s
27
+ raise TypeError, "#{name} should be a subclass of Sequel::Model to use the sequel plugin"
28
+ end
29
+ end
30
+
31
+ private
32
+
33
+ def sequel_class?(klass)
34
+ klass < ::Sequel::Model
35
+ end
4
36
  end
37
+
38
+ register_plugin(:sequel, Sequel)
5
39
  end
6
40
  end
@@ -0,0 +1,25 @@
1
+ module Mobility
2
+ module Plugins
3
+ module Sequel
4
+ module Backend
5
+ extend Plugin
6
+
7
+ requires :backend, include: :before
8
+
9
+ def load_backend(backend)
10
+ if Symbol === backend
11
+ require "mobility/backends/sequel/#{backend}"
12
+ Backends.load_backend("sequel_#{backend}".to_sym)
13
+ else
14
+ super
15
+ end
16
+ rescue LoadError => e
17
+ raise unless e.message =~ /sequel\/#{backend}/
18
+ super
19
+ end
20
+ end
21
+ end
22
+
23
+ register_plugin(:sequel_backend, Sequel::Backend)
24
+ end
25
+ end
@@ -0,0 +1,24 @@
1
+ # frozen-string-literal: true
2
+
3
+ module Mobility
4
+ module Plugins
5
+ module Sequel
6
+ =begin
7
+
8
+ Adds hook to clear Mobility cache when +refresh+ is called on Sequel model.
9
+
10
+ =end
11
+ module Cache
12
+ extend Plugin
13
+
14
+ requires :cache, include: false
15
+
16
+ included_hook do |klass|
17
+ define_cache_hooks(klass, :refresh) if options[:cache]
18
+ end
19
+ end
20
+ end
21
+
22
+ register_plugin(:sequel_cache, Sequel::Cache)
23
+ end
24
+ end
@@ -13,29 +13,37 @@ Automatically includes dirty plugin in model class when enabled.
13
13
  =end
14
14
  module Sequel
15
15
  module Dirty
16
- # Builds module which overrides dirty methods to handle translated as
17
- # well as normal (untranslated) attributes.
18
- class MethodsBuilder < Module
19
- def initialize(*attribute_names)
20
- # Although we load the plugin in the included callback method, we
21
- # need to include this module here in advance to ensure that its
22
- # instance methods are included *before* the ones defined here.
23
- include ::Sequel::Plugins::Dirty::InstanceMethods
24
-
25
- %w[initial_value column_change column_changed? reset_column].each do |method_name|
26
- define_method method_name do |column|
27
- if attribute_names.map(&:to_sym).include?(column)
28
- super(Mobility.normalize_locale_accessor(column).to_sym)
29
- else
30
- super(column)
31
- end
32
- end
33
- end
34
- end
16
+ extend Plugin
17
+
18
+ requires :dirty, include: false
35
19
 
36
- def included(model_class)
20
+ initialize_hook do
21
+ # Although we load the plugin in the included callback method, we
22
+ # need to include this module here in advance to ensure that its
23
+ # instance methods are included *before* the ones defined here.
24
+ include ::Sequel::Plugins::Dirty::InstanceMethods
25
+ end
26
+
27
+ included_hook do |klass, backend_class|
28
+ if options[:dirty]
37
29
  # this just adds Sequel::Plugins::Dirty to @plugins
38
- model_class.plugin :dirty
30
+ klass.plugin :dirty
31
+ define_dirty_methods(names)
32
+ backend_class.include BackendMethods
33
+ end
34
+ end
35
+
36
+ private
37
+
38
+ def define_dirty_methods(names)
39
+ %w[initial_value column_change column_changed? reset_column].each do |method_name|
40
+ define_method method_name do |column|
41
+ if names.map(&:to_sym).include?(column)
42
+ super(Mobility.normalize_locale_accessor(column).to_sym)
43
+ else
44
+ super(column)
45
+ end
46
+ end
39
47
  end
40
48
  end
41
49
 
@@ -48,14 +56,17 @@ Automatically includes dirty plugin in model class when enabled.
48
56
  if model.column_changes.has_key?(locale_accessor) && model.initial_values[locale_accessor] == value
49
57
  super
50
58
  [model.changed_columns, model.initial_values].each { |h| h.delete(locale_accessor) }
51
- elsif read(locale, options.merge(fallback: false)) != value
59
+ elsif read(locale, **options.merge(fallback: false)) != value
52
60
  model.will_change_column(locale_accessor)
53
61
  super
54
62
  end
55
63
  end
56
64
  # @!endgroup
57
65
  end
66
+
58
67
  end
59
68
  end
69
+
70
+ register_plugin(:sequel_dirty, Sequel::Dirty)
60
71
  end
61
72
  end
@@ -8,11 +8,18 @@ See ActiveRecord::Query plugin.
8
8
  =end
9
9
  module Sequel
10
10
  module Query
11
- class << self
12
- def apply(attributes)
13
- attributes.model_class.class_eval do
11
+ extend Plugin
12
+
13
+ requires :query, include: false
14
+
15
+ included_hook do |klass, _|
16
+ plugin = self
17
+ if options[:query]
18
+ raise MissingBackend, "backend required for Query plugin" unless backend_class
19
+
20
+ klass.class_eval do
14
21
  extend QueryMethod
15
- singleton_class.send :alias_method, Mobility.query_method, :__mobility_query_dataset__
22
+ singleton_class.send :alias_method, plugin.query_method, :__mobility_query_dataset__
16
23
  end
17
24
  end
18
25
  end
@@ -95,7 +102,7 @@ See ActiveRecord::Query plugin.
95
102
 
96
103
  class << self
97
104
  def build(dataset, query_method, query_conds, &block)
98
- return yield unless Hash === query_conds.first
105
+ return yield unless ::Hash === query_conds.first
99
106
 
100
107
  cond = query_conds.first.dup
101
108
  locale = cond.delete(:locale) || Mobility.locale
@@ -109,7 +116,7 @@ See ActiveRecord::Query plugin.
109
116
  keys, predicates = cond.keys, []
110
117
  model = dataset.model
111
118
 
112
- query_map = model.mobility_modules.inject(IDENTITY) do |qm, mod|
119
+ query_map = attribute_modules(model).inject(IDENTITY) do |qm, mod|
113
120
  i18n_keys = mod.names.map(&:to_sym) & keys
114
121
  next qm if i18n_keys.empty?
115
122
 
@@ -127,6 +134,10 @@ See ActiveRecord::Query plugin.
127
134
  query_map[dataset.public_send(query_method, ::Sequel.&(*predicates))]
128
135
  end
129
136
 
137
+ def attribute_modules(model)
138
+ model.ancestors.grep(::Mobility::Translations)
139
+ end
140
+
130
141
  def build_predicate(op, values)
131
142
  vals = values.is_a?(Array) ? values.uniq: [values]
132
143
  vals = vals.first if vals.size == 1
@@ -135,6 +146,10 @@ See ActiveRecord::Query plugin.
135
146
  end
136
147
  end
137
148
  end
149
+
150
+ class MissingBackend < Mobility::Error; end
138
151
  end
152
+
153
+ register_plugin(:sequel_query, Sequel::Query)
139
154
  end
140
155
  end