mobility 0.0.1 → 0.1.0

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