mobility 0.8.8 → 1.0.0.alpha

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