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
data/Rakefile CHANGED
@@ -1,6 +1,48 @@
1
1
  require "bundler/gem_tasks"
2
2
  require "rspec/core/rake_task"
3
+ require "yaml"
3
4
 
4
5
  RSpec::Core::RakeTask.new(:spec)
5
6
 
6
7
  task :default => :spec
8
+
9
+ task :load_path do
10
+ %w(lib spec).each do |path|
11
+ $LOAD_PATH.unshift(File.expand_path("../#{path}", __FILE__))
12
+ end
13
+ end
14
+
15
+ namespace :db do
16
+ desc "Create the database"
17
+ task :create => :load_path do
18
+ require "database"
19
+ driver = Mobility::Test::Database.driver
20
+ config = Mobility::Test::Database.config[driver]
21
+ commands = {
22
+ "mysql" => "mysql -u #{config['username']} -e 'create database #{config["database"]};' >/dev/null",
23
+ "postgres" => "psql -c 'create database #{config['database']};' -U #{config['username']} >/dev/null"
24
+ }
25
+ %x{#{commands[driver] || true}}
26
+ end
27
+
28
+ desc "Drop the database"
29
+ task :drop => :load_path do
30
+ require "database"
31
+ driver = Mobility::Test::Database.driver
32
+ config = Mobility::Test::Database.config[driver]
33
+ commands = {
34
+ "mysql" => "mysql -u #{config['username']} -e 'drop database #{config["database"]};' >/dev/null",
35
+ "postgres" => "psql -c 'drop database #{config['database']};' -U #{config['username']} >/dev/null"
36
+ }
37
+ %x{#{commands[driver] || true}}
38
+ end
39
+
40
+ desc "Set up the database schema"
41
+ task :up => :load_path do
42
+ require "spec_helper"
43
+ Mobility::Test::Schema.up
44
+ end
45
+
46
+ desc "Drop and recreate the database schema"
47
+ task :reset => [:drop, :create]
48
+ end
@@ -0,0 +1,45 @@
1
+ require "rails/generators"
2
+ require "rails/generators/active_record"
3
+
4
+ module Mobility
5
+ class InstallGenerator < ::Rails::Generators::Base
6
+ include ::Rails::Generators::Migration
7
+
8
+ desc "Generates migrations to add translations tables."
9
+
10
+ source_root File.expand_path("../templates", __FILE__)
11
+ class_option(
12
+ :without_tables,
13
+ type: :boolean,
14
+ default: false,
15
+ desc: "Skip creating translations tables."
16
+ )
17
+
18
+ def create_migration_file
19
+ add_mobility_migration("create_text_translations") unless options.without_tables?
20
+ add_mobility_migration("create_string_translations") unless options.without_tables?
21
+ end
22
+
23
+ def create_initializer
24
+ create_file(
25
+ "config/initializers/mobility.rb",
26
+ "Mobility.config.default_backend = :key_value"
27
+ )
28
+ end
29
+
30
+ def self.next_migration_number(dirname)
31
+ ::ActiveRecord::Generators::Base.next_migration_number(dirname)
32
+ end
33
+
34
+ protected
35
+
36
+ def add_mobility_migration(template)
37
+ migration_dir = File.expand_path("db/migrate")
38
+ if self.class.migration_exists?(migration_dir, template)
39
+ ::Kernel.warn "Migration already exists: #{template}"
40
+ else
41
+ migration_template "#{template}.rb", "db/migrate/#{template}.rb"
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,15 @@
1
+ class CreateStringTranslations < ActiveRecord::Migration
2
+
3
+ def change
4
+ create_table :mobility_string_translations do |t|
5
+ t.string :locale
6
+ t.string :key
7
+ t.string :value
8
+ t.integer :translatable_id
9
+ t.string :translatable_type
10
+ t.timestamps
11
+ end
12
+ add_index :mobility_string_translations, [:translatable_id, :translatable_type, :locale, :key], unique: true, name: :index_mobility_string_translations_on_keys
13
+ add_index :mobility_string_translations, [:translatable_id, :translatable_type], name: :index_mobility_string_translations_on_translatable
14
+ end
15
+ end
@@ -0,0 +1,15 @@
1
+ class CreateTextTranslations < ActiveRecord::Migration
2
+
3
+ def change
4
+ create_table :mobility_text_translations do |t|
5
+ t.string :locale
6
+ t.string :key
7
+ t.text :value
8
+ t.integer :translatable_id
9
+ t.string :translatable_type
10
+ t.timestamps
11
+ end
12
+ add_index :mobility_text_translations, [:translatable_id, :translatable_type, :locale, :key], unique: true, name: :index_mobility_text_translations_on_keys
13
+ add_index :mobility_text_translations, [:translatable_id, :translatable_type], name: :index_mobility_text_translations_on_translatable
14
+ end
15
+ end
data/lib/mobility.rb CHANGED
@@ -1,5 +1,206 @@
1
- require "mobility/version"
1
+ require 'i18n'
2
+ require 'request_store'
3
+ require 'mobility/version'
2
4
 
5
+ %w[object nil string].each do |type|
6
+ begin
7
+ require "active_support/core_ext/#{type}"
8
+ rescue LoadError
9
+ require "mobility/core_ext/#{type}"
10
+ end
11
+ end
12
+
13
+ =begin
14
+
15
+ Mobility is a gem for storing and retrieving localized data through attributes
16
+ on a class. The {Mobility} module includes all necessary methods and modules to
17
+ support defining backend accessors on a class.
18
+
19
+ To enable Mobility on a class, simply include or extend the {Mobility} module,
20
+ and define any attribute accessors using {Translates#mobility_accessor} (aliased to the
21
+ value of {Mobility.accessor_method}, which defaults to +translates+).
22
+
23
+ class MyClass
24
+ include Mobility
25
+ translates :title, backend: :key_value
26
+ end
27
+
28
+ When defining this module, Mobility attempts to +require+ various gems (for
29
+ example, +active_record+ and +sequel+) to evaluate which are loaded. Loaded
30
+ gems are tracked with dynamic subclasses of the {Loaded} module and referenced
31
+ in backends to define gem-dependent behavior.
32
+
33
+ =end
3
34
  module Mobility
4
- # Your code goes here...
35
+ autoload :Attributes, "mobility/attributes"
36
+ autoload :Backend, "mobility/backend"
37
+ autoload :BackendResetter, "mobility/backend_resetter"
38
+ autoload :Configuration, "mobility/configuration"
39
+ autoload :InstanceMethods, "mobility/instance_methods"
40
+ autoload :Translates, "mobility/translates"
41
+ autoload :Wrapper, "mobility/wrapper"
42
+
43
+ require "mobility/orm"
44
+
45
+ # General error for version compatibility conflicts
46
+ class VersionNotSupportedError < ArgumentError; end
47
+
48
+ begin
49
+ require "active_record"
50
+ raise VersionNotSupportedError, "Mobility is only compatible with ActiveRecord 5.0 and greater" if ::ActiveRecord::VERSION::MAJOR < 5
51
+ autoload :ActiveModel, "mobility/active_model"
52
+ autoload :ActiveRecord, "mobility/active_record"
53
+ Loaded::ActiveRecord = true
54
+ rescue LoadError
55
+ Loaded::ActiveRecord = false
56
+ end
57
+
58
+ begin
59
+ require "rails"
60
+ autoload :InstallGenerator, "generators/mobility/install_generator"
61
+ Loaded::Rails = true
62
+ rescue LoadError
63
+ class InstallGenerator; end
64
+ Loaded::Rails = false
65
+ end
66
+
67
+ begin
68
+ require "sequel"
69
+ raise VersionNotSupportedError, "Mobility is only compatible with Sequel 4.0 and greater" if ::Sequel::MAJOR < 4
70
+ require "sequel/extensions/inflector"
71
+ require "sequel/plugins/dirty"
72
+ autoload :Sequel, "mobility/sequel"
73
+ Loaded::Sequel = true
74
+ rescue LoadError
75
+ Loaded::Sequel = false
76
+ end
77
+
78
+ class << self
79
+ def extended(model_class)
80
+ return if model_class.respond_to? :mobility_accessor
81
+ model_class.class_eval do
82
+ def self.mobility
83
+ @mobility ||= Mobility::Wrapper.new(self)
84
+ end
85
+ def self.translated_attribute_names
86
+ mobility.translated_attribute_names
87
+ end
88
+
89
+ class << self
90
+ include Translates
91
+ if translates = Mobility.config.accessor_method
92
+ alias_method translates, :mobility_accessor
93
+ end
94
+ end
95
+ end
96
+
97
+ model_class.include(InstanceMethods)
98
+
99
+ if Loaded::ActiveRecord
100
+ model_class.include(ActiveRecord) if model_class < ::ActiveRecord::Base
101
+ model_class.include(ActiveModel::AttributeMethods) if model_class.ancestors.include?(::ActiveModel::AttributeMethods)
102
+ end
103
+
104
+ if Loaded::Sequel
105
+ model_class.include(Sequel) if model_class < ::Sequel::Model
106
+ end
107
+ end
108
+
109
+ # Extends model with this class so that +include Mobility+ is equivalent to
110
+ # +extend Mobility+
111
+ # @param model_class
112
+ def included(model_class)
113
+ model_class.extend self
114
+ end
115
+
116
+ # @!group Locale Accessors
117
+ # @return [Symbol] Mobility locale
118
+ def locale
119
+ read_locale || I18n.locale
120
+ end
121
+
122
+ # Sets Mobility locale
123
+ # @param [Symbol] locale Locale to set
124
+ # @raise [InvalidLocale] if locale is nil or not in +I18n.available_locales
125
+ # @return [Symbol] Locale
126
+ def locale=(locale)
127
+ set_locale(locale)
128
+ end
129
+
130
+ # Sets Mobility locale around block
131
+ # @param [Symbol] locale Locale to set in block
132
+ # @yield [Symbol] Locale
133
+ def with_locale(locale)
134
+ previous_locale = read_locale
135
+ begin
136
+ set_locale(locale)
137
+ yield(locale)
138
+ ensure
139
+ set_locale(previous_locale)
140
+ end
141
+ end
142
+ # @!endgroup
143
+
144
+ # @return [RequestStore] Request store
145
+ def storage
146
+ RequestStore.store
147
+ end
148
+
149
+ # @!group Configuration Methods
150
+ # @return [Mobility::Configuration] Mobility configuration
151
+ def config
152
+ storage[:mobility_configuration] ||= Mobility::Configuration.new
153
+ end
154
+
155
+ # (see Mobility::Configuration#accessor_method)
156
+ # @!method accessor_method
157
+
158
+ # (see Mobility::Configuration#default_fallbacks)
159
+ # @!method default_fallbacks
160
+
161
+ # (see Mobility::Configuration#default_backend)
162
+ # @!method default_backend
163
+
164
+ # (see Mobility::Configuration#default_accessor_locales)
165
+ # @!method default_accessor_locales
166
+ %w[accessor_method default_fallbacks default_backend default_accessor_locales].each do |method_name|
167
+ define_method method_name do
168
+ config.public_send(method_name)
169
+ end
170
+ end
171
+
172
+ # Configure Mobility
173
+ # @yield [Mobility::Configuration] Mobility configuration
174
+ def configure
175
+ yield config
176
+ end
177
+ # @!endgroup
178
+
179
+ # Return normalized locale
180
+ # @param [String,Symbol] locale
181
+ # @return [String] Normalized locale
182
+ # @example
183
+ # Mobility.normalize_locale(:ja)
184
+ # #=> "ja"
185
+ # Mobility.normalize_locale("pt-BR")
186
+ # #=> "pt_br"
187
+ def normalize_locale(locale)
188
+ "#{locale.to_s.downcase.sub("-", "_")}"
189
+ end
190
+
191
+ protected
192
+
193
+ def read_locale
194
+ storage[:mobility_locale]
195
+ end
196
+
197
+ def set_locale(locale)
198
+ locale = locale.to_sym if locale
199
+ raise Mobility::InvalidLocale.new(locale) unless I18n.available_locales.include?(locale) || locale.nil?
200
+ storage[:mobility_locale] = locale
201
+ end
202
+ end
203
+
204
+ class BackendRequired < ArgumentError; end
205
+ class InvalidLocale < I18n::InvalidLocale; end
5
206
  end
@@ -0,0 +1,6 @@
1
+ module Mobility
2
+ module ActiveModel
3
+ autoload :AttributeMethods, "mobility/active_model/attribute_methods"
4
+ autoload :BackendResetter, "mobility/active_model/backend_resetter"
5
+ end
6
+ end
@@ -0,0 +1,27 @@
1
+ module Mobility
2
+ module ActiveModel
3
+ =begin
4
+
5
+ Included into model if model has +ActiveModel::AttributeMethods+ among its
6
+ ancestors.
7
+
8
+ =end
9
+ module AttributeMethods
10
+ delegate :translated_attribute_names, to: :class
11
+
12
+ # Adds translated attributes to +attributes+.
13
+ # @return [Array<String>] Model attributes
14
+ def attributes
15
+ super.merge(translated_attributes)
16
+ end
17
+
18
+ # Translated attributes defined on model.
19
+ # @return [Array<String>] Translated attributes
20
+ def translated_attributes
21
+ translated_attribute_names.inject({}) do |attributes, name|
22
+ attributes.merge(name.to_s => send(name))
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,26 @@
1
+ module Mobility
2
+ module ActiveModel
3
+ =begin
4
+
5
+ Backend resetter for ActiveModel models. Adds hook to reset backend when
6
+ +changes_applied+ or +clear_changes_information+ methods are called on model.
7
+
8
+ =end
9
+ class BackendResetter < Mobility::BackendResetter
10
+
11
+ # (see Mobility::BackendResetter#initialize)
12
+ def initialize(attributes)
13
+ super
14
+
15
+ model_reset_method = @model_reset_method
16
+
17
+ %i[changes_applied clear_changes_information].each do |method|
18
+ define_method method do
19
+ super()
20
+ instance_eval &model_reset_method
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,39 @@
1
+ module Mobility
2
+ =begin
3
+
4
+ Module loading ActiveRecord-specific classes for Mobility models.
5
+
6
+ =end
7
+ module ActiveRecord
8
+ autoload :BackendResetter, "mobility/active_record/backend_resetter"
9
+ autoload :ModelTranslation, "mobility/active_record/model_translation"
10
+ autoload :StringTranslation, "mobility/active_record/string_translation"
11
+ autoload :TextTranslation, "mobility/active_record/text_translation"
12
+ autoload :Translation, "mobility/active_record/translation"
13
+
14
+ def changes_applied
15
+ @previously_changed = changes
16
+ super
17
+ end
18
+
19
+ def clear_changes_information
20
+ @previously_changed = ActiveSupport::HashWithIndifferentAccess.new
21
+ super
22
+ end
23
+
24
+ def previous_changes
25
+ super.merge(@previously_changed ||= ActiveSupport::HashWithIndifferentAccess.new)
26
+ end
27
+
28
+ def self.included(model_class)
29
+ model_class.extend(ClassMethods)
30
+ end
31
+
32
+ module ClassMethods
33
+ # @return [ActiveRecord::Relation] relation extended with Mobility query methods.
34
+ def i18n
35
+ all
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,26 @@
1
+ module Mobility
2
+ module ActiveRecord
3
+ =begin
4
+
5
+ Backend resetter for ActiveRecord models. Adds hook on +reload+ event to
6
+ {Mobility::ActiveModel::BackendResetter}.
7
+
8
+ =end
9
+ class BackendResetter < Mobility::ActiveModel::BackendResetter
10
+
11
+ # @param [Class] model_class Class of model to which backend resetter will be applied
12
+ def included(model_class)
13
+ model_reset_method = @model_reset_method
14
+
15
+ model_class.class_eval do
16
+ mod = Module.new do
17
+ define_method :reload do
18
+ super().tap { instance_eval &model_reset_method }
19
+ end
20
+ end
21
+ include mod
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end