mobility 0.1.10 → 0.1.11

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.
@@ -0,0 +1,69 @@
1
+ # frozen-string-literal: true
2
+ require "rails/generators/active_record/migration/migration_generator"
3
+
4
+ module Mobility
5
+ module BackendGenerators
6
+ class Base < ::Rails::Generators::NamedBase
7
+ argument :attributes, type: :array, default: []
8
+ include ::ActiveRecord::Generators::Migration
9
+
10
+ def create_migration_file
11
+ if self.class.migration_exists?(migration_dir, migration_file)
12
+ ::Kernel.warn "Migration already exists: #{migration_file}"
13
+ else
14
+ migration_template "#{template}.rb", "db/migrate/#{migration_file}.rb"
15
+ end
16
+ end
17
+
18
+ def self.next_migration_number(dirname)
19
+ ::ActiveRecord::Generators::Base.next_migration_number(dirname)
20
+ end
21
+
22
+ def backend
23
+ self.class.name.split('::').last.gsub(/Backend$/,'').underscore
24
+ end
25
+
26
+ protected
27
+
28
+ def attributes_with_index
29
+ attributes.select { |a| !a.reference? && a.has_index? }
30
+ end
31
+
32
+ private
33
+
34
+ def check_data_source!
35
+ unless data_source_exists?
36
+ raise NoTableDefined, "The table #{table_name} does not exist. Create it first before generating translated columns."
37
+ end
38
+ end
39
+
40
+ def data_source_exists?
41
+ connection.data_source_exists?(table_name)
42
+ end
43
+
44
+ delegate :connection, to: ::ActiveRecord::Base
45
+
46
+ def truncate_index_name(index_name)
47
+ if index_name.size < connection.index_name_length
48
+ index_name
49
+ else
50
+ "index_#{Digest::SHA1.hexdigest(index_name)}"[0, connection.index_name_length].freeze
51
+ end
52
+ end
53
+
54
+ def template
55
+ "#{backend}_translations".freeze
56
+ end
57
+
58
+ def migration_dir
59
+ File.expand_path("db/migrate".freeze)
60
+ end
61
+
62
+ def migration_file
63
+ "create_#{file_name}_#{attributes.map(&:name).join('_and_')}_translations_for_mobility_#{backend}_backend".freeze
64
+ end
65
+ end
66
+
67
+ class NoTableDefined < StandardError; end
68
+ end
69
+ end
@@ -0,0 +1,15 @@
1
+ # frozen-string-literal: true
2
+ require "rails/generators"
3
+
4
+ module Mobility
5
+ module BackendGenerators
6
+ class ColumnBackend < Mobility::BackendGenerators::Base
7
+ source_root File.expand_path("../../templates", __FILE__)
8
+
9
+ def initialize(*args)
10
+ super
11
+ check_data_source!
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,42 @@
1
+ # frozen-string-literal: true
2
+ require "rails/generators"
3
+
4
+ module Mobility
5
+ module BackendGenerators
6
+ class TableBackend < Mobility::BackendGenerators::Base
7
+ source_root File.expand_path("../../templates", __FILE__)
8
+
9
+ def create_migration_file
10
+ if data_source_exists? && !self.class.migration_exists?(migration_dir, migration_file)
11
+ migration_template "#{backend}_migration.rb", "db/migrate/#{migration_file}.rb"
12
+ else
13
+ super
14
+ end
15
+ end
16
+
17
+ private
18
+
19
+ alias_method :model_table_name, :table_name
20
+ def table_name
21
+ model_table_name = super
22
+ "#{model_table_name.singularize}_translations"
23
+ end
24
+
25
+ def foreign_key
26
+ "#{model_table_name.singularize}_id"
27
+ end
28
+
29
+ def translation_index_name
30
+ truncate_index_name("index_#{table_name}_on_#{foreign_key}")
31
+ end
32
+
33
+ def translation_locale_index_name
34
+ truncate_index_name("index_#{table_name}_on_locale")
35
+ end
36
+
37
+ def translation_unique_index_name
38
+ truncate_index_name("index_#{table_name}_on_#{foreign_key}_and_locale")
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,5 @@
1
+ require "rails/generators"
2
+
3
+ require_relative "./install_generator"
4
+ require_relative "./translations_generator"
5
+ require_relative "./backend_generators/base"
@@ -0,0 +1,17 @@
1
+ class <%= migration_class_name %> < ActiveRecord::Migration[<%= ActiveRecord::Migration.current_version %>]
2
+ def change
3
+ <% attributes.each do |attribute| -%>
4
+ <% I18n.available_locales.each do |locale| -%>
5
+ <% column_name = Mobility.normalize_locale_accessor(attribute.name, locale) -%>
6
+ <% if connection.column_exists?(table_name, column_name) -%>
7
+ <% warn "#{column_name} already exists, skipping." %>
8
+ <% else -%>
9
+ add_column :<%= table_name %>, :<%= column_name %>, :<%= attribute.type %><%= attribute.inject_options %>
10
+ <%- if attribute.has_index? -%>
11
+ add_index :<%= table_name %>, :<%= column_name %><%= attribute.inject_index_options %>
12
+ <%- end -%>
13
+ <% end -%>
14
+ <% end -%>
15
+ <% end -%>
16
+ end
17
+ end
@@ -0,0 +1,17 @@
1
+ class <%= migration_class_name %> < ActiveRecord::Migration[<%= ActiveRecord::Migration.current_version %>]
2
+ def change
3
+ <% attributes.each do |attribute| -%>
4
+ <%- if attribute.reference? -%>
5
+ add_reference :<%= table_name %>, :<%= attribute.name %><%= attribute.inject_options %>
6
+ <%- elsif attribute.token? -%>
7
+ add_column :<%= table_name %>, :<%= attribute.name %>, :string<%= attribute.inject_options %>
8
+ add_index :<%= table_name %>, :<%= attribute.index_name %><%= attribute.inject_index_options %>, unique: true
9
+ <%- else -%>
10
+ add_column :<%= table_name %>, :<%= attribute.name %>, :<%= attribute.type %><%= attribute.inject_options %>
11
+ <%- if attribute.has_index? -%>
12
+ add_index :<%= table_name %>, :<%= attribute.index_name %><%= attribute.inject_index_options %>
13
+ <%- end -%>
14
+ <%- end -%>
15
+ <% end -%>
16
+ end
17
+ end
@@ -0,0 +1,28 @@
1
+ class <%= migration_class_name %> < ActiveRecord::Migration[<%= ActiveRecord::Migration.current_version %>]
2
+ def change
3
+ create_table :<%= table_name %><%= primary_key_type %> do |t|
4
+
5
+ # Translated attribute(s)
6
+ <% attributes.each do |attribute| -%>
7
+ <% if attribute.token? -%>
8
+ t.string :<%= attribute.name %><%= attribute.inject_options %>
9
+ <% else -%>
10
+ t.<%= attribute.type %> :<%= attribute.name %><%= attribute.inject_options %>
11
+ <% end -%>
12
+ <% end -%>
13
+
14
+ t.string :locale, null: false
15
+ t.integer :<%= foreign_key %>, null: false
16
+
17
+ t.timestamps null: false
18
+ end
19
+
20
+ add_index :<%= table_name %>, :<%= foreign_key %>, name: :<%= translation_index_name %>
21
+ add_index :<%= table_name %>, :locale, name: :<%= translation_locale_index_name %>
22
+ add_index :<%= table_name %>, [:<%= foreign_key %>, :locale], name: :<%= translation_unique_index_name %>, unique: true
23
+
24
+ <%- attributes_with_index.each do |attribute| -%>
25
+ add_index :<%= table_name %>, :<%= attribute.index_name %><%= attribute.inject_index_options %>
26
+ <%- end -%>
27
+ end
28
+ end
@@ -0,0 +1,75 @@
1
+ # frozen-string-literal: true
2
+
3
+ module Mobility
4
+ =begin
5
+
6
+ Generator to create translation tables or add translation columns to a model
7
+ table, for either Table or Column backends.
8
+
9
+ ==Usage
10
+
11
+ To add translations for a string attribute +title+ to a model +Post+, call the
12
+ generator with:
13
+
14
+ rails generate mobility:translations post title:string
15
+
16
+ Here, the backend is implicit in the value of +Mobility.default_backend+, but
17
+ it can be explicitly set using the +backend+ option:
18
+
19
+ rails generate mobility:translations post title:string --backend=table
20
+
21
+ For the +table+ backend, the generator will either create a translation table
22
+ (in this case, +post_translations+) or add columns to the table if it already
23
+ exists.
24
+
25
+ For the +column+ backend, the generator will add columns for all locales in
26
+ +I18n.available_locales+. If some columns already exist, they will simply be
27
+ skipped.
28
+
29
+ Other backends are not supported, for obvious reasons:
30
+ * the +key_value+ backend does not need any model-specific migrations, simply
31
+ run the install generator.
32
+ * +jsonb+, +hstore+ and +serialized+ backends simply require a single column on
33
+ a model table, which can be added with the normal Rails migration generator.
34
+
35
+ =end
36
+ class TranslationsGenerator < ::Rails::Generators::NamedBase
37
+ SUPPORTED_BACKENDS = %w[column table]
38
+ BACKEND_OPTIONS = { type: :string, desc: "Backend to use for translations (defaults to Mobility.default_backend)".freeze }
39
+ argument :attributes, type: :array, default: [], banner: "field[:type][:index] field[:type][:index]"
40
+
41
+ class_option(:backend, BACKEND_OPTIONS)
42
+ invoke_from_option :backend
43
+
44
+ def self.class_options(options = nil)
45
+ super
46
+ @class_options[:backend] = Thor::Option.new(:backend, BACKEND_OPTIONS.merge(default: Mobility.default_backend.to_s.freeze))
47
+ @class_options
48
+ end
49
+
50
+ def self.prepare_for_invocation(name, value)
51
+ if name == :backend
52
+ if SUPPORTED_BACKENDS.include?(value)
53
+ require_relative "./backend_generators/#{value}_backend".freeze
54
+ Mobility::BackendGenerators.const_get("#{value}_backend".camelcase.freeze)
55
+ elsif Mobility::Backend.const_get(value.to_s.camelize.gsub(/\s+/, ''.freeze))
56
+ raise Thor::Error, "The #{value} backend does not have a translations generator."
57
+ else
58
+ raise Thor::Error, "#{value} is not a Mobility backend."
59
+ end
60
+ else
61
+ super
62
+ end
63
+ end
64
+
65
+ protected
66
+
67
+ def say_status(status, message, *args)
68
+ if status == :invoke && SUPPORTED_BACKENDS.include?(message)
69
+ super(status, "#{message}_backend".freeze, *args)
70
+ else
71
+ super
72
+ end
73
+ end
74
+ end
75
+ end
data/lib/mobility.rb CHANGED
@@ -208,6 +208,15 @@ module Mobility
208
208
  "#{attribute}_#{normalize_locale(locale)}".freeze
209
209
  end
210
210
 
211
+ # Raises InvalidLocale exception if the locale passed in is present but not available.
212
+ # @param [String,Symbol] locale
213
+ # @raise [InvalidLocale] if locale is present but not available
214
+ def enforce_available_locales!(locale)
215
+ if I18n.enforce_available_locales
216
+ raise Mobility::InvalidLocale.new(locale) unless (I18n.locale_available?(locale) || locale.nil?)
217
+ end
218
+ end
219
+
211
220
  protected
212
221
 
213
222
  def read_locale
@@ -216,9 +225,7 @@ module Mobility
216
225
 
217
226
  def set_locale(locale)
218
227
  locale = locale.to_sym if locale
219
- if I18n.enforce_available_locales
220
- raise Mobility::InvalidLocale.new(locale) unless (I18n.available_locales.include?(locale) || locale.nil?)
221
- end
228
+ enforce_available_locales!(locale)
222
229
  storage[:mobility_locale] = locale
223
230
  end
224
231
  end
@@ -137,6 +137,7 @@ with other backends.
137
137
  if (options[:dirty] && options[:fallthrough_accessors] != false)
138
138
  options[:fallthrough_accessors] = true
139
139
  end
140
+ include FallthroughAccessors.new(attributes) if options[:fallthrough_accessors]
140
141
 
141
142
  @backend_class.configure!(options) if @backend_class.respond_to?(:configure!)
142
143
 
@@ -158,8 +159,8 @@ with other backends.
158
159
  end
159
160
  end
160
161
 
161
- define_method "#{attribute}=" do |value|
162
- mobility_set(attribute, value)
162
+ define_method "#{attribute}=" do |value, **options|
163
+ mobility_set(attribute, value, **options)
163
164
  end if %i[accessor writer].include?(method)
164
165
 
165
166
  define_locale_accessors(attribute, @accessor_locales) if @accessor_locales
@@ -187,7 +188,6 @@ with other backends.
187
188
  backend_class.include(Backend::Cache) unless options[:cache] == false
188
189
  backend_class.include(Backend::Dirty.for(options[:model_class])) if options[:dirty]
189
190
  backend_class.include(Backend::Fallbacks) unless options[:fallbacks] == false
190
- backend_class.include(FallthroughAccessors.new(attributes)) if options[:fallthrough_accessors]
191
191
  end
192
192
 
193
193
  def define_backend(attribute)
@@ -215,7 +215,7 @@ with other backends.
215
215
 
216
216
  def get_backend_class(backend: nil, model_class: nil)
217
217
  raise Mobility::BackendRequired, "Backend option required if Mobility.config.default_backend is not set." if backend.nil?
218
- klass = Module === backend ? backend : Mobility::Backend.const_get(backend.to_s.camelize.gsub(/\s+/, ''))
218
+ klass = Module === backend ? backend : Mobility::Backend.const_get(backend.to_s.camelize.gsub(/\s+/, ''.freeze).freeze)
219
219
  model_class.nil? ? klass : klass.for(model_class)
220
220
  end
221
221
  end
@@ -78,8 +78,6 @@ value of the translated attribute if passed to it.
78
78
  private :restore_attribute!
79
79
  end
80
80
  model_class.include restore_methods
81
-
82
- model_class.include(FallthroughAccessors.new(*attributes))
83
81
  end
84
82
  end
85
83
  end
@@ -4,6 +4,15 @@ module Mobility
4
4
 
5
5
  Implements the {Mobility::Backend::Column} backend for ActiveRecord models.
6
6
 
7
+ You can use the +mobility:translations+ generator to create a migration adding
8
+ translatable columns to the model table with:
9
+
10
+ rails generate mobility:translations post title:string
11
+
12
+ The generated migration will add columns +title_<locale>+ for every locale in
13
+ +I18n.available_locales+. (The generator can be run again to add new attributes
14
+ or locales.)
15
+
7
16
  @note This backend disables the +locale_accessors+ option, which would
8
17
  otherwise interfere with column methods.
9
18
 
@@ -27,11 +36,15 @@ Implements the {Mobility::Backend::Column} backend for ActiveRecord models.
27
36
 
28
37
  # @!group Backend Accessors
29
38
  # @!macro backend_reader
30
- # @!method read(locale, **options)
39
+ def read(locale, **_)
40
+ model.read_attribute(column(locale))
41
+ end
31
42
 
32
43
  # @!group Backend Accessors
33
44
  # @!macro backend_writer
34
- # @!method write(locale, value, **options)
45
+ def write(locale, value, **_)
46
+ model.write_attribute(column(locale), value)
47
+ end
35
48
 
36
49
  # @!group Backend Configuration
37
50
  def self.configure!(options)
@@ -6,6 +6,15 @@ module Mobility
6
6
 
7
7
  Implements the {Mobility::Backend::Table} backend for ActiveRecord models.
8
8
 
9
+ To generate a translation table for a model +Post+, you can use the included
10
+ +mobility:translations+ generator:
11
+
12
+ rails generate mobility:translations post title:string content:text
13
+
14
+ This will create a migration which can be run to create the translation table.
15
+ If the translation table already exists, it will create a migration adding
16
+ columns to that table.
17
+
9
18
  @example Model with table backend
10
19
  class Post < ActiveRecord::Base
11
20
  translates :title, backend: :table, association_name: :translations
@@ -2,10 +2,18 @@ module Mobility
2
2
  module Backend
3
3
  =begin
4
4
 
5
- Stores translated attribute as a column on the model table.
5
+ Stores translated attribute as a column on the model table. To use this
6
+ backend, ensure that the model table has columns named +<attribute>_<locale>+
7
+ for every locale in +I18n.available_locales+.
6
8
 
7
- To use this backend, ensure that the model table has columns named
8
- +<attribute>_<locale>+ for every locale in +I18n.available_locales+.
9
+ If you are using Rails, you can use the +mobility:translations+ generator to
10
+ create a migration adding these columns to the model table with:
11
+
12
+ rails generate mobility:translations post title:string
13
+
14
+ The generated migration will add columns +title_<locale>+ for every locale in
15
+ +I18n.available_locales+. (The generator can be run again to add new attributes
16
+ or locales.)
9
17
 
10
18
  ==Backend Options
11
19
 
@@ -19,19 +27,6 @@ be ignored if set, since it would cause a conflict with column accessors.
19
27
  module Column
20
28
  include OrmDelegator
21
29
 
22
- # @!group Backend Accessors
23
- #
24
- # @!macro backend_reader
25
- def read(locale, **_)
26
- model.send(column(locale))
27
- end
28
-
29
- # @!macro backend_writer
30
- def write(locale, value, **_)
31
- model.send("#{column(locale)}=", value)
32
- end
33
- # @!endgroup
34
-
35
30
  # Returns name of column where translated attribute is stored
36
31
  # @param [Symbol] locale
37
32
  # @return [String]