mobility 0.0.1 → 0.1.0

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 (88) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile +19 -0
  3. data/Gemfile.lock +153 -0
  4. data/Guardfile +70 -0
  5. data/README.md +603 -13
  6. data/Rakefile +42 -0
  7. data/lib/generators/mobility/install_generator.rb +45 -0
  8. data/lib/generators/mobility/templates/create_string_translations.rb +15 -0
  9. data/lib/generators/mobility/templates/create_text_translations.rb +15 -0
  10. data/lib/mobility.rb +203 -2
  11. data/lib/mobility/active_model.rb +6 -0
  12. data/lib/mobility/active_model/attribute_methods.rb +27 -0
  13. data/lib/mobility/active_model/backend_resetter.rb +26 -0
  14. data/lib/mobility/active_record.rb +39 -0
  15. data/lib/mobility/active_record/backend_resetter.rb +26 -0
  16. data/lib/mobility/active_record/model_translation.rb +14 -0
  17. data/lib/mobility/active_record/string_translation.rb +7 -0
  18. data/lib/mobility/active_record/text_translation.rb +7 -0
  19. data/lib/mobility/active_record/translation.rb +14 -0
  20. data/lib/mobility/attributes.rb +210 -0
  21. data/lib/mobility/backend.rb +152 -0
  22. data/lib/mobility/backend/active_model.rb +7 -0
  23. data/lib/mobility/backend/active_model/dirty.rb +84 -0
  24. data/lib/mobility/backend/active_record.rb +13 -0
  25. data/lib/mobility/backend/active_record/column.rb +52 -0
  26. data/lib/mobility/backend/active_record/column/query_methods.rb +40 -0
  27. data/lib/mobility/backend/active_record/hash_valued.rb +58 -0
  28. data/lib/mobility/backend/active_record/hstore.rb +36 -0
  29. data/lib/mobility/backend/active_record/hstore/query_methods.rb +53 -0
  30. data/lib/mobility/backend/active_record/jsonb.rb +43 -0
  31. data/lib/mobility/backend/active_record/jsonb/query_methods.rb +53 -0
  32. data/lib/mobility/backend/active_record/key_value.rb +126 -0
  33. data/lib/mobility/backend/active_record/key_value/query_methods.rb +63 -0
  34. data/lib/mobility/backend/active_record/query_methods.rb +36 -0
  35. data/lib/mobility/backend/active_record/serialized.rb +93 -0
  36. data/lib/mobility/backend/active_record/serialized/query_methods.rb +32 -0
  37. data/lib/mobility/backend/active_record/table.rb +197 -0
  38. data/lib/mobility/backend/active_record/table/query_methods.rb +91 -0
  39. data/lib/mobility/backend/cache.rb +110 -0
  40. data/lib/mobility/backend/column.rb +52 -0
  41. data/lib/mobility/backend/dirty.rb +28 -0
  42. data/lib/mobility/backend/fallbacks.rb +89 -0
  43. data/lib/mobility/backend/hstore.rb +21 -0
  44. data/lib/mobility/backend/jsonb.rb +21 -0
  45. data/lib/mobility/backend/key_value.rb +71 -0
  46. data/lib/mobility/backend/null.rb +24 -0
  47. data/lib/mobility/backend/orm_delegator.rb +33 -0
  48. data/lib/mobility/backend/sequel.rb +14 -0
  49. data/lib/mobility/backend/sequel/column.rb +40 -0
  50. data/lib/mobility/backend/sequel/column/query_methods.rb +24 -0
  51. data/lib/mobility/backend/sequel/dirty.rb +54 -0
  52. data/lib/mobility/backend/sequel/hash_valued.rb +51 -0
  53. data/lib/mobility/backend/sequel/hstore.rb +36 -0
  54. data/lib/mobility/backend/sequel/hstore/query_methods.rb +42 -0
  55. data/lib/mobility/backend/sequel/jsonb.rb +43 -0
  56. data/lib/mobility/backend/sequel/jsonb/query_methods.rb +42 -0
  57. data/lib/mobility/backend/sequel/key_value.rb +139 -0
  58. data/lib/mobility/backend/sequel/key_value/query_methods.rb +48 -0
  59. data/lib/mobility/backend/sequel/query_methods.rb +22 -0
  60. data/lib/mobility/backend/sequel/serialized.rb +133 -0
  61. data/lib/mobility/backend/sequel/serialized/query_methods.rb +20 -0
  62. data/lib/mobility/backend/sequel/table.rb +149 -0
  63. data/lib/mobility/backend/sequel/table/query_methods.rb +48 -0
  64. data/lib/mobility/backend/serialized.rb +53 -0
  65. data/lib/mobility/backend/table.rb +93 -0
  66. data/lib/mobility/backend_resetter.rb +44 -0
  67. data/lib/mobility/configuration.rb +31 -0
  68. data/lib/mobility/core_ext/nil.rb +10 -0
  69. data/lib/mobility/core_ext/object.rb +19 -0
  70. data/lib/mobility/core_ext/string.rb +16 -0
  71. data/lib/mobility/instance_methods.rb +34 -0
  72. data/lib/mobility/orm.rb +4 -0
  73. data/lib/mobility/sequel.rb +26 -0
  74. data/lib/mobility/sequel/backend_resetter.rb +26 -0
  75. data/lib/mobility/sequel/column_changes.rb +29 -0
  76. data/lib/mobility/sequel/model_translation.rb +20 -0
  77. data/lib/mobility/sequel/string_translation.rb +7 -0
  78. data/lib/mobility/sequel/text_translation.rb +7 -0
  79. data/lib/mobility/sequel/translation.rb +53 -0
  80. data/lib/mobility/translates.rb +75 -0
  81. data/lib/mobility/wrapper.rb +31 -0
  82. metadata +152 -12
  83. data/.gitignore +0 -9
  84. data/.rspec +0 -2
  85. data/.travis.yml +0 -5
  86. data/bin/console +0 -14
  87. data/bin/setup +0 -8
  88. data/mobility.gemspec +0 -32
@@ -0,0 +1,14 @@
1
+ module Mobility
2
+ module ActiveRecord
3
+ =begin
4
+
5
+ Subclassed dynamically to generate translation class in
6
+ {Backend::ActiveRecord::Table} backend.
7
+
8
+ =end
9
+ class ModelTranslation < ::ActiveRecord::Base
10
+ self.abstract_class = true
11
+ validates :locale, presence: true
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,7 @@
1
+ module Mobility
2
+ module ActiveRecord
3
+ class StringTranslation < Translation
4
+ self.table_name = "mobility_string_translations"
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,7 @@
1
+ module Mobility
2
+ module ActiveRecord
3
+ class TextTranslation < Translation
4
+ self.table_name = "mobility_text_translations"
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,14 @@
1
+ module Mobility
2
+ module ActiveRecord
3
+ # @abstract Subclass and set +table_name+ to implement for a particular column type.
4
+ class Translation < ::ActiveRecord::Base
5
+ self.abstract_class = true
6
+
7
+ belongs_to :translatable, polymorphic: true
8
+
9
+ validates :key, presence: true, uniqueness: { scope: [:translatable_id, :translatable_type, :locale] }
10
+ validates :translatable, presence: true
11
+ validates :locale, presence: true
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,210 @@
1
+ module Mobility
2
+ =begin
3
+
4
+ Defines accessor methods to include on model class. Inspired by Traco's
5
+ +Traco::Attributes+ class.
6
+
7
+ Normally this class will be created through class methods defined using
8
+ {Mobility::Translates} accessor methods, and need not be created directly.
9
+ However, the class is central to how Mobility hooks into models to add
10
+ accessors and other methods, and should be useful as a reference when
11
+ understanding and designing backends.
12
+
13
+ ==Including Attributes in a Class
14
+
15
+ Since {Attributes} is a subclass of +Module+, including an instance of it is
16
+ like including a module. Creating an instance like this:
17
+
18
+ Attributes.new(:accessor, ["title"], backend: :my_backend, locale_accessors: [:en, :ja], cache: true, fallbacks: true)
19
+
20
+ will generate an anonymous module looking something like this:
21
+
22
+ Module.new do
23
+ def title_backend
24
+ # Create a subclass of Mobility::Backend::MyBackend and include in it:
25
+ # - Mobility::Cache (from the cache: true option)
26
+ # - Mobility::Fallbacks (from the fallbacks: true option)
27
+ # Then instantiate the backend, memoize it, and return it.
28
+ end
29
+
30
+ def title(**options)
31
+ title_backend.read(Mobility.locale, **options).presence
32
+ end
33
+
34
+ def title?(**options)
35
+ title_backend.read(Mobility.locale, **options).present?
36
+ end
37
+
38
+ def title=(value)
39
+ title_backend.write(Mobility.locale, value.presence)
40
+ end
41
+
42
+ # Start Locale Accessors
43
+ #
44
+ def title_en(**options)
45
+ title_backend.read(:en, **options).presence
46
+ end
47
+
48
+ def title_en?(**options)
49
+ title_backend.read(:en, **options).present?
50
+ end
51
+
52
+ def title_en=(value)
53
+ title_backend.write(:en, value.presence)
54
+ end
55
+
56
+ def title_ja(**options)
57
+ title_backend.read(:ja, **options).presence
58
+ end
59
+
60
+ def title_ja?(**options)
61
+ title_backend.read(:ja, **options).present?
62
+ end
63
+
64
+ def title_ja=(value)
65
+ title_backend.write(:ja, value.presence)
66
+ end
67
+ # End Locale Accessors
68
+ end
69
+
70
+ Including this module into a model class will then add the backend method, the
71
+ reader, writer and presence methods, and the locale accessor so the model
72
+ class.
73
+
74
+ ==Setting up the Model Class
75
+
76
+ Accessor methods alone are of limited use without a hook to actually modify the
77
+ model class. This hook is provided by the {Backend::Setup#setup_model} method,
78
+ which is added to every backend class when it includes the {Backend} module.
79
+
80
+ Assuming the backend has defined a setup block by calling +setup+, this block
81
+ will be called when {Attributes} is {#included} in the model class, passed
82
+ attributes and options defined when the backend was defined on the model class.
83
+ This allows a backend to do things like (for example) define associations on a
84
+ model class required by the backend, as happens in the {Backend::KeyValue} and
85
+ {Backend::Table} backends.
86
+
87
+ The +setup+ block is also used to extend the +i18n+ scope/dataset with
88
+ backend-specific query method support.
89
+
90
+ Since setup blocks are evaluated on the model class, it is possible that
91
+ backends can conflict (for example, overwriting previously defined methods).
92
+ Care should be taken to avoid defining methods on the model class, or where
93
+ necessary, ensure that names are defined in such a way as to avoid conflicts
94
+ with other backends.
95
+
96
+ =end
97
+ class Attributes < Module
98
+ # Attributes for which accessors will be defined
99
+ # @return [Array<String>] Array of attributes
100
+ attr_reader :attributes
101
+
102
+ # Backend options
103
+ # @return [Hash] Backend options
104
+ attr_reader :options
105
+
106
+ # Backend class
107
+ # @return [Class] Backend class
108
+ attr_reader :backend_class
109
+
110
+ # Name of backend
111
+ # @return [Symbol,Class] Name of backend, or backend class
112
+ attr_reader :backend_name
113
+
114
+ # @param [Symbol] method One of: [reader, writer, accessor]
115
+ # @param [Array<String>] _attributes Attributes to define backend for
116
+ # @param [Hash] _options Backend options hash
117
+ # @option _options [Class] model_class Class of model
118
+ # @option _options [Boolean, Array<Symbol>] locale_accessors Enable locale
119
+ # accessors or specify locales for which accessors should be defined on
120
+ # this model backend. Will default to +true+ if +dirty+ option is +true+.
121
+ # @option _options [Boolean] cache (true) Enable cache for this model backend
122
+ # @option _options [Boolean, Hash] fallbacks Enable fallbacks or specify fallbacks for this model backend
123
+ # @option _options [Boolean] dirty Enable dirty tracking for this model backend
124
+ # @raise [ArgumentError] if method is not reader, writer or accessor
125
+ def initialize(method, *_attributes, **_options)
126
+ raise ArgumentError, "method must be one of: reader, writer, accessor" unless %i[reader writer accessor].include?(method)
127
+ @options = _options
128
+ @attributes = _attributes.map &:to_s
129
+ model_class = options[:model_class]
130
+ @backend_name = options.delete(:backend) || Mobility.config.default_backend
131
+ @backend_class = Class.new(get_backend_class(backend: @backend_name,
132
+ model_class: model_class))
133
+
134
+ options[:locale_accessors] ||= true if options[:dirty]
135
+
136
+ @backend_class.configure!(options) if @backend_class.respond_to?(:configure!)
137
+
138
+ @backend_class.include Backend::Cache unless options[:cache] == false
139
+ @backend_class.include Backend::Dirty.for(model_class) if options[:dirty]
140
+ @backend_class.include Backend::Fallbacks if options[:fallbacks]
141
+ @accessor_locales = options[:locale_accessors]
142
+ @accessor_locales = Mobility.config.default_accessor_locales if options[:locale_accessors] == true
143
+
144
+ attributes.each do |attribute|
145
+ define_backend(attribute)
146
+
147
+ if %i[accessor reader].include?(method)
148
+ define_method attribute do |**options|
149
+ mobility_get(attribute, options)
150
+ end
151
+
152
+ define_method "#{attribute}?" do |**options|
153
+ mobility_present?(attribute, options)
154
+ end
155
+ end
156
+
157
+ define_method "#{attribute}=" do |value|
158
+ mobility_set(attribute, value)
159
+ end if %i[accessor writer].include?(method)
160
+
161
+ define_locale_accessors(attribute, @accessor_locales) if @accessor_locales
162
+ end
163
+ end
164
+
165
+ # Add this attributes module to shared {Mobility::Wrapper} and setup model
166
+ # with backend setup block (see {Mobility::Backend::Setup#setup_model}).
167
+ # @param model_class [Class] Class of model
168
+ def included(model_class)
169
+ model_class.mobility << self
170
+ backend_class.setup_model(model_class, attributes, options)
171
+ end
172
+
173
+ # Yield each attribute to block
174
+ # @yield [String] Attribute
175
+ def each &block
176
+ attributes.each &block
177
+ end
178
+
179
+ private
180
+
181
+ def define_backend(attribute)
182
+ _backend_class, _options = backend_class, options
183
+ define_method Backend.method_name(attribute) do
184
+ @mobility_backends ||= {}
185
+ @mobility_backends[attribute] ||= _backend_class.new(self, attribute, _options)
186
+ end
187
+ end
188
+
189
+ def define_locale_accessors(attribute, locales)
190
+ locales.each do |locale|
191
+ normalized_locale = Mobility.normalize_locale(locale)
192
+ define_method "#{attribute}_#{normalized_locale}" do |**options|
193
+ mobility_get(attribute, options.merge(locale: locale))
194
+ end
195
+ define_method "#{attribute}_#{normalized_locale}?" do |**options|
196
+ mobility_present?(attribute, options.merge(locale: locale))
197
+ end
198
+ define_method "#{attribute}_#{normalized_locale}=" do |value, **options|
199
+ mobility_set(attribute, value, locale: locale)
200
+ end
201
+ end
202
+ end
203
+
204
+ def get_backend_class(backend: nil, model_class: nil)
205
+ raise Mobility::BackendRequired, "Backend option required if Mobility.config.default_backend is not set." if backend.nil?
206
+ klass = Module === backend ? backend : Mobility::Backend.const_get(backend.to_s.camelize.gsub(/\s+/, ''))
207
+ model_class.nil? ? klass : klass.for(model_class)
208
+ end
209
+ end
210
+ end
@@ -0,0 +1,152 @@
1
+ module Mobility
2
+ =begin
3
+
4
+ Defines a minimum set of shared components included in any backend. These are:
5
+
6
+ - a reader returning the +model+ on which the backend is defined ({#model})
7
+ - a reader returning the +attribute+ for which the backend is defined
8
+ ({#attribute})
9
+ - a reader returning +options+ configuring the backend ({#options})
10
+ - a constructor setting these three elements (+model+, +attribute+, +options+),
11
+ and extracting fallbacks from the options hash ({#initialize})
12
+ - a +setup+ method adding any configuration code to the model class
13
+ ({Setup#setup})
14
+
15
+ On top of this, a backend will normally:
16
+
17
+ - implement a +read+ instance method to read from the backend
18
+ - implement a +write+ instance method to write to the backend
19
+ - implement a +configure!+ class method to apply any normalization to the
20
+ options hash
21
+ - call the +setup+ method yielding attributes and options to configure the
22
+ model class
23
+
24
+ @example Defining a Backend
25
+ class MyBackend
26
+ include Backend
27
+
28
+ def read(locale, **options)
29
+ # ...
30
+ end
31
+
32
+ def write(locale, value, **options)
33
+ # ...
34
+ end
35
+
36
+ def self.configure!(options)
37
+ # ...
38
+ end
39
+
40
+ setup do |attributes, options|
41
+ # Do something with attributes and options in context of model class.
42
+ end
43
+ end
44
+
45
+ @see Mobility::Attributes
46
+
47
+ =end
48
+
49
+ module Backend
50
+ autoload :ActiveModel, 'mobility/backend/active_model'
51
+ autoload :ActiveRecord, 'mobility/backend/active_record'
52
+ autoload :Cache, 'mobility/backend/cache'
53
+ autoload :Column, 'mobility/backend/column'
54
+ autoload :Dirty, 'mobility/backend/dirty'
55
+ autoload :Fallbacks, 'mobility/backend/fallbacks'
56
+ autoload :Hstore, 'mobility/backend/hstore'
57
+ autoload :Jsonb, 'mobility/backend/jsonb'
58
+ autoload :KeyValue, 'mobility/backend/key_value'
59
+ autoload :Null, 'mobility/backend/null'
60
+ autoload :OrmDelegator, 'mobility/backend/orm_delegator'
61
+ autoload :Sequel, 'mobility/backend/sequel'
62
+ autoload :Serialized, 'mobility/backend/serialized'
63
+ autoload :Table, 'mobility/backend/table'
64
+
65
+ # @return [String] Backend attribute
66
+ attr_reader :attribute
67
+
68
+ # @return [Object] Model on which backend is defined
69
+ attr_reader :model
70
+
71
+ # @return [Hash] Backend options
72
+ attr_reader :options
73
+
74
+ # @!macro [new] backend_constructor
75
+ # @param model Model on which backend is defined
76
+ # @param [String] attribute Backend attribute
77
+ # @option options [Hash] fallbacks Fallbacks hash
78
+ def initialize(model, attribute, **options)
79
+ @model = model
80
+ @attribute = attribute
81
+ @options = options
82
+ fallbacks = options[:fallbacks]
83
+ @fallbacks = I18n::Locale::Fallbacks.new(fallbacks) if fallbacks.is_a?(Hash)
84
+ end
85
+
86
+ # @!macro [new] backend_reader
87
+ # @param [Symbol] locale Locale to read
88
+ # @param [Hash] options
89
+ # @return [Object] Value of translation
90
+ #
91
+ # @!macro [new] backend_writer
92
+ # @param [Symbol] locale Locale to write
93
+ # @param [Object] value Value to write
94
+ # @param [Hash] options
95
+ # @return [Object] Updated value
96
+
97
+ # Extend included class with +setup+ method
98
+ def self.included(base)
99
+ base.extend(Setup)
100
+ end
101
+
102
+ # @param [String] attribute
103
+ # @return [String] name of backend reader method
104
+ def self.method_name(attribute)
105
+ "#{attribute}_backend"
106
+ end
107
+
108
+ # Defines setup hooks for backend to customize model class.
109
+ module Setup
110
+ # Assign block to be called on model class.
111
+ # @yield [attributes, options]
112
+ # @note When called multiple times, setup blocks will be appended
113
+ # so that they are run together consecutively on class.
114
+ def setup &block
115
+ if @setup_block
116
+ setup_block = @setup_block
117
+ @setup_block = lambda do |*args|
118
+ class_exec(*args, &setup_block)
119
+ class_exec(*args, &block)
120
+ end
121
+ else
122
+ @setup_block = block
123
+ end
124
+ end
125
+
126
+ def inherited(subclass)
127
+ subclass.instance_variable_set(:@setup_block, @setup_block)
128
+ end
129
+
130
+ # Call setup block on a class with attributes and options.
131
+ # @param model_class Class to be setup-ed
132
+ # @param [Array<String>] attributes
133
+ # @param [Hash] options
134
+ def setup_model(model_class, attributes, **options)
135
+ return unless setup_block = @setup_block
136
+ model_class.class_exec(attributes, options, &setup_block)
137
+ end
138
+
139
+ # {Attributes} uses this method to get a backend class specific to the
140
+ # model using the backend. Backend classes can override this method to
141
+ # return a class specific to the model class using the backend (e.g.
142
+ # either an ActiveRecord or Sequel backend class depending on whether the
143
+ # model is an ActiveRecord model or a Sequel model.)
144
+ # @see OrmDelegator
145
+ # @see Attributes
146
+ # @return [self] returns itself
147
+ def for(_)
148
+ self
149
+ end
150
+ end
151
+ end
152
+ end
@@ -0,0 +1,7 @@
1
+ module Mobility
2
+ module Backend
3
+ module ActiveModel
4
+ autoload :Dirty, 'mobility/backend/active_model/dirty'
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,84 @@
1
+ module Mobility
2
+ module Backend
3
+ =begin
4
+
5
+ Dirty tracking for models which include the +ActiveModel::Dirty+ module.
6
+
7
+ Assuming we have an attribute +title+, this module will add support for the
8
+ following methods:
9
+ - +title_changed?+
10
+ - +title_change+
11
+ - +title_was+
12
+ - +title_will_change!+
13
+ - +title_previously_changed?+
14
+ - +title_previous_change+
15
+ - +restore_title!+
16
+
17
+ In addition, the private method +restore_attribute!+ will also restore the
18
+ value of the translated attribute if passed to it.
19
+
20
+ @see http://api.rubyonrails.org/classes/ActiveModel/Dirty.html Rails documentation for Active Model Dirty module
21
+
22
+ =end
23
+ module ActiveModel::Dirty
24
+ # @!group Backend Accessors
25
+ # @!macro backend_writer
26
+ def write(locale, value, **options)
27
+ locale_accessor = "#{attribute}_#{locale}"
28
+ if model.changed_attributes.has_key?(locale_accessor) && model.changed_attributes[locale_accessor] == value
29
+ model.attributes_changed_by_setter.except!(locale_accessor)
30
+ else
31
+ model.send(:attribute_will_change!, "#{attribute}_#{locale}")
32
+ end
33
+ super
34
+ end
35
+ # @!endgroup
36
+
37
+ # @param [Class] backend_class Class of backend
38
+ def self.included(backend_class)
39
+ backend_class.extend(ClassMethods)
40
+ end
41
+
42
+ # Adds hook after {Backend::Setup#setup_model} to add dirty-tracking
43
+ # methods for translated attributes onto model class.
44
+ module ClassMethods
45
+ # (see Mobility::Backend::Setup#setup_model)
46
+ def setup_model(model_class, attributes, **options)
47
+ super
48
+ model_class.class_eval do
49
+ %w[changed? change was will_change! previously_changed? previous_change].each do |suffix|
50
+ attributes.each do |attribute|
51
+ class_eval <<-EOM, __FILE__, __LINE__ + 1
52
+ def #{attribute}_#{suffix}
53
+ attribute_#{suffix}("#{attribute}_#\{Mobility.locale\}")
54
+ end
55
+ EOM
56
+ end
57
+ end
58
+ end
59
+
60
+ restore_methods = Module.new do
61
+ attributes.each do |attribute|
62
+ locale_accessor = "#{attribute}_#{Mobility.locale}"
63
+ define_method "restore_#{attribute}!" do
64
+ if attribute_changed?(locale_accessor)
65
+ __send__("#{attribute}=", changed_attributes[locale_accessor])
66
+ end
67
+ end
68
+ end
69
+
70
+ define_method :restore_attribute! do |attr|
71
+ if attributes.include?(attr.to_s)
72
+ send("restore_#{attr}!")
73
+ else
74
+ super(attr)
75
+ end
76
+ end
77
+ private :restore_attribute!
78
+ end
79
+ model_class.include restore_methods
80
+ end
81
+ end
82
+ end
83
+ end
84
+ end