mobility 0.8.8 → 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 (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 +56 -0
  5. data/Gemfile +52 -16
  6. data/Gemfile.lock +113 -52
  7. data/Guardfile +23 -1
  8. data/README.md +184 -92
  9. data/Rakefile +6 -4
  10. data/lib/mobility.rb +40 -166
  11. data/lib/mobility/active_record/translation.rb +1 -1
  12. data/lib/mobility/arel/nodes/pg_ops.rb +1 -1
  13. data/lib/mobility/backend.rb +19 -41
  14. data/lib/mobility/backends.rb +20 -0
  15. data/lib/mobility/backends/active_record.rb +4 -0
  16. data/lib/mobility/backends/active_record/column.rb +2 -0
  17. data/lib/mobility/backends/active_record/container.rb +4 -2
  18. data/lib/mobility/backends/active_record/hstore.rb +2 -0
  19. data/lib/mobility/backends/active_record/json.rb +2 -0
  20. data/lib/mobility/backends/active_record/jsonb.rb +2 -0
  21. data/lib/mobility/backends/active_record/key_value.rb +5 -3
  22. data/lib/mobility/backends/active_record/pg_hash.rb +1 -1
  23. data/lib/mobility/backends/active_record/serialized.rb +2 -0
  24. data/lib/mobility/backends/active_record/table.rb +5 -3
  25. data/lib/mobility/backends/column.rb +0 -6
  26. data/lib/mobility/backends/container.rb +2 -1
  27. data/lib/mobility/backends/hash.rb +39 -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 +0 -1
  31. data/lib/mobility/backends/key_value.rb +22 -14
  32. data/lib/mobility/backends/null.rb +2 -0
  33. data/lib/mobility/backends/sequel.rb +3 -0
  34. data/lib/mobility/backends/sequel/column.rb +2 -0
  35. data/lib/mobility/backends/sequel/container.rb +3 -1
  36. data/lib/mobility/backends/sequel/hstore.rb +2 -0
  37. data/lib/mobility/backends/sequel/json.rb +2 -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 +2 -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 +14 -6
  44. data/lib/mobility/pluggable.rb +36 -0
  45. data/lib/mobility/plugin.rb +260 -0
  46. data/lib/mobility/plugins.rb +26 -25
  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 +310 -54
  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 +72 -101
  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 +28 -20
  57. data/lib/mobility/plugins/attributes.rb +70 -0
  58. data/lib/mobility/plugins/backend.rb +138 -0
  59. data/lib/mobility/plugins/backend_reader.rb +34 -0
  60. data/lib/mobility/plugins/cache.rb +59 -24
  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 +51 -43
  64. data/lib/mobility/plugins/fallthrough_accessors.rb +26 -25
  65. data/lib/mobility/plugins/locale_accessors.rb +25 -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 +45 -32
  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 +95 -77
  78. metadata +51 -51
  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
@@ -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,47 +13,60 @@ Automatically includes dirty plugin in model class when enabled.
13
13
  =end
14
14
  module Sequel
15
15
  module Dirty
16
- # @!group Backend Accessors
17
- # @!macro backend_writer
18
- # @param [Hash] options
19
- def write(locale, value, options = {})
20
- locale_accessor = Mobility.normalize_locale_accessor(attribute, locale).to_sym
21
- if model.column_changes.has_key?(locale_accessor) && model.initial_values[locale_accessor] == value
22
- super
23
- [model.changed_columns, model.initial_values].each { |h| h.delete(locale_accessor) }
24
- elsif read(locale, options.merge(fallback: false)) != value
25
- model.will_change_column(locale_accessor)
26
- super
16
+ extend Plugin
17
+
18
+ requires :dirty, include: false
19
+
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]
29
+ # this just adds Sequel::Plugins::Dirty to @plugins
30
+ klass.plugin :dirty
31
+ define_dirty_methods(names)
32
+ backend_class.include BackendMethods
27
33
  end
28
34
  end
29
- # @!endgroup
30
-
31
- # Builds module which overrides dirty methods to handle translated as
32
- # well as normal (untranslated) attributes.
33
- class MethodsBuilder < Module
34
- def initialize(*attribute_names)
35
- # Although we load the plugin in the included callback method, we
36
- # need to include this module here in advance to ensure that its
37
- # instance methods are included *before* the ones defined here.
38
- include ::Sequel::Plugins::Dirty::InstanceMethods
39
-
40
- %w[initial_value column_change column_changed? reset_column].each do |method_name|
41
- define_method method_name do |column|
42
- if attribute_names.map(&:to_sym).include?(column)
43
- super(Mobility.normalize_locale_accessor(column).to_sym)
44
- else
45
- super(column)
46
- 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)
47
45
  end
48
46
  end
49
47
  end
48
+ end
50
49
 
51
- def included(model_class)
52
- # this just adds Sequel::Plugins::Dirty to @plugins
53
- model_class.plugin :dirty
50
+ module BackendMethods
51
+ # @!group Backend Accessors
52
+ # @!macro backend_writer
53
+ # @param [Hash] options
54
+ def write(locale, value, options = {})
55
+ locale_accessor = Mobility.normalize_locale_accessor(attribute, locale).to_sym
56
+ if model.column_changes.has_key?(locale_accessor) && model.initial_values[locale_accessor] == value
57
+ super
58
+ [model.changed_columns, model.initial_values].each { |h| h.delete(locale_accessor) }
59
+ elsif read(locale, options.merge(fallback: false)) != value
60
+ model.will_change_column(locale_accessor)
61
+ super
62
+ end
54
63
  end
64
+ # @!endgroup
55
65
  end
66
+
56
67
  end
57
68
  end
69
+
70
+ register_plugin(:sequel_dirty, Sequel::Dirty)
58
71
  end
59
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
@@ -0,0 +1,44 @@
1
+ # frozen-string-literal: true
2
+
3
+ module Mobility
4
+ module Plugins
5
+ module Writer
6
+ =begin
7
+
8
+ Defines attribute writer that delegates to +Mobility::Backend#write+.
9
+
10
+ =end
11
+ extend Plugin
12
+
13
+ default true
14
+ requires :backend
15
+
16
+ initialize_hook do |*names|
17
+ if options[:writer]
18
+ names.each do |name|
19
+ class_eval <<-EOM, __FILE__, __LINE__ + 1
20
+ def #{name}=(value, locale: nil, **options)
21
+ #{Writer.setup_source}
22
+ mobility_backends[:#{name}].write(locale, value, options)
23
+ end
24
+ EOM
25
+ end
26
+ end
27
+ end
28
+
29
+ def self.setup_source
30
+ <<-EOL
31
+ return super(value) if options[:super]
32
+ if (locale &&= locale.to_sym)
33
+ #{"Mobility.enforce_available_locales!(locale)" if I18n.enforce_available_locales}
34
+ options[:locale] = true
35
+ else
36
+ locale = Mobility.locale
37
+ end
38
+ EOL
39
+ end
40
+ end
41
+
42
+ register_plugin(:writer, Writer)
43
+ end
44
+ end
@@ -0,0 +1,95 @@
1
+ # frozen_string_literal: true
2
+ require "mobility/pluggable"
3
+
4
+ module Mobility
5
+ =begin
6
+
7
+ Module containing translation accessor methods and other methods for accessing
8
+ translations.
9
+
10
+ Normally this class will be created by calling +translates+ on the model class,
11
+ added when extending {Mobility}, but it can also be used independent of that
12
+ macro.
13
+
14
+ ==Including Translations in a Class
15
+
16
+ Since {Translations} is a subclass of +Module+, including an instance of it is
17
+ like including a module. Options passed to the initializer are passed to
18
+ plugins (if those plugins have been enabled).
19
+
20
+ We can first enable plugins by subclassing `Mobility::Translations`, and
21
+ calling +plugins+ to enable any plugins we want to use. We want reader and
22
+ writer methods for accessing translations, so we enable those plugins:
23
+
24
+ class Translations < Mobility::Translations
25
+ plugins do
26
+ reader
27
+ writer
28
+ end
29
+ end
30
+
31
+ Both `reader` and `writer` depend on the `backend` plugin, so this is also enabled.
32
+
33
+ Then create an instance like this:
34
+
35
+ Translations.new("title", backend: :my_backend)
36
+
37
+ This will generate an anonymous module that behaves approximately like this:
38
+
39
+ Module.new do
40
+ # From Mobility::Plugins::Backend module
41
+ #
42
+ def mobility_backends
43
+ # Returns a memoized hash with attribute name keys and backend instance
44
+ # values. When a key is fetched from the hash, the hash calls
45
+ # +self.class.mobility_backend_class(name)+ (where +name+ is the
46
+ # attribute name) to get the backend class, then instantiate it (passing
47
+ # the model instance and attribute name to its initializer) and return it.
48
+ end
49
+
50
+ # From Mobility::Plugins::Reader module
51
+ #
52
+ def title(locale: Mobility.locale)
53
+ mobility_backends[:title].read(locale)
54
+ end
55
+
56
+ def title?(locale: Mobility.locale)
57
+ mobility_backends[:title].read(locale).present?
58
+ end
59
+
60
+ # From Mobility::Plugins::Writer module
61
+ def title=(value, locale: Mobility.locale)
62
+ mobility_backends[:title].write(locale, value)
63
+ end
64
+ end
65
+
66
+ Including this module into a model class will thus add the backends method and
67
+ reader and writer methods for accessing translations. Other plugins (e.g.
68
+ fallbacks, cache) modify the result returned by the backend, by hooking into
69
+ the +included+ callback method on the module, see {Mobility::Plugin} for
70
+ details.
71
+
72
+ ==Setting up the Model Class
73
+
74
+ Accessor methods alone are of limited use without a hook to actually modify the
75
+ model class. This hook is provided by the {Backend::Setup#setup_model} method,
76
+ which is added to every backend class when it includes the {Backend} module.
77
+
78
+ Assuming the backend has defined a setup block by calling +setup+, this block
79
+ will be called when {Translations} is {#included} in the model class, passed
80
+ attributes and options defined when the backend was defined on the model class.
81
+ This allows a backend to do things like (for example) define associations on a
82
+ model class required by the backend, as happens in the {Backends::KeyValue} and
83
+ {Backends::Table} backends.
84
+
85
+ Since setup blocks are evaluated on the model class, it is possible that
86
+ backends can conflict (for example, overwriting previously defined methods).
87
+ Care should be taken to avoid defining methods on the model class, or where
88
+ necessary, ensure that names are defined in such a way as to avoid conflicts
89
+ with other backends.
90
+
91
+ =end
92
+ class Translations < Pluggable
93
+ include Plugins.load_plugin(:attributes)
94
+ end
95
+ end
@@ -1,5 +1,16 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Mobility
4
- VERSION = "0.8.8"
4
+ def self.gem_version
5
+ Gem::Version.new VERSION::STRING
6
+ end
7
+
8
+ module VERSION
9
+ MAJOR = 1
10
+ MINOR = 0
11
+ TINY = 0
12
+ PRE = "alpha"
13
+
14
+ STRING = [MAJOR, MINOR, TINY, PRE].compact.join(".")
15
+ end
5
16
  end