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.
- checksums.yaml +4 -4
- data/Gemfile +19 -0
- data/Gemfile.lock +153 -0
- data/Guardfile +70 -0
- data/README.md +603 -13
- data/Rakefile +42 -0
- data/lib/generators/mobility/install_generator.rb +45 -0
- data/lib/generators/mobility/templates/create_string_translations.rb +15 -0
- data/lib/generators/mobility/templates/create_text_translations.rb +15 -0
- data/lib/mobility.rb +203 -2
- data/lib/mobility/active_model.rb +6 -0
- data/lib/mobility/active_model/attribute_methods.rb +27 -0
- data/lib/mobility/active_model/backend_resetter.rb +26 -0
- data/lib/mobility/active_record.rb +39 -0
- data/lib/mobility/active_record/backend_resetter.rb +26 -0
- data/lib/mobility/active_record/model_translation.rb +14 -0
- data/lib/mobility/active_record/string_translation.rb +7 -0
- data/lib/mobility/active_record/text_translation.rb +7 -0
- data/lib/mobility/active_record/translation.rb +14 -0
- data/lib/mobility/attributes.rb +210 -0
- data/lib/mobility/backend.rb +152 -0
- data/lib/mobility/backend/active_model.rb +7 -0
- data/lib/mobility/backend/active_model/dirty.rb +84 -0
- data/lib/mobility/backend/active_record.rb +13 -0
- data/lib/mobility/backend/active_record/column.rb +52 -0
- data/lib/mobility/backend/active_record/column/query_methods.rb +40 -0
- data/lib/mobility/backend/active_record/hash_valued.rb +58 -0
- data/lib/mobility/backend/active_record/hstore.rb +36 -0
- data/lib/mobility/backend/active_record/hstore/query_methods.rb +53 -0
- data/lib/mobility/backend/active_record/jsonb.rb +43 -0
- data/lib/mobility/backend/active_record/jsonb/query_methods.rb +53 -0
- data/lib/mobility/backend/active_record/key_value.rb +126 -0
- data/lib/mobility/backend/active_record/key_value/query_methods.rb +63 -0
- data/lib/mobility/backend/active_record/query_methods.rb +36 -0
- data/lib/mobility/backend/active_record/serialized.rb +93 -0
- data/lib/mobility/backend/active_record/serialized/query_methods.rb +32 -0
- data/lib/mobility/backend/active_record/table.rb +197 -0
- data/lib/mobility/backend/active_record/table/query_methods.rb +91 -0
- data/lib/mobility/backend/cache.rb +110 -0
- data/lib/mobility/backend/column.rb +52 -0
- data/lib/mobility/backend/dirty.rb +28 -0
- data/lib/mobility/backend/fallbacks.rb +89 -0
- data/lib/mobility/backend/hstore.rb +21 -0
- data/lib/mobility/backend/jsonb.rb +21 -0
- data/lib/mobility/backend/key_value.rb +71 -0
- data/lib/mobility/backend/null.rb +24 -0
- data/lib/mobility/backend/orm_delegator.rb +33 -0
- data/lib/mobility/backend/sequel.rb +14 -0
- data/lib/mobility/backend/sequel/column.rb +40 -0
- data/lib/mobility/backend/sequel/column/query_methods.rb +24 -0
- data/lib/mobility/backend/sequel/dirty.rb +54 -0
- data/lib/mobility/backend/sequel/hash_valued.rb +51 -0
- data/lib/mobility/backend/sequel/hstore.rb +36 -0
- data/lib/mobility/backend/sequel/hstore/query_methods.rb +42 -0
- data/lib/mobility/backend/sequel/jsonb.rb +43 -0
- data/lib/mobility/backend/sequel/jsonb/query_methods.rb +42 -0
- data/lib/mobility/backend/sequel/key_value.rb +139 -0
- data/lib/mobility/backend/sequel/key_value/query_methods.rb +48 -0
- data/lib/mobility/backend/sequel/query_methods.rb +22 -0
- data/lib/mobility/backend/sequel/serialized.rb +133 -0
- data/lib/mobility/backend/sequel/serialized/query_methods.rb +20 -0
- data/lib/mobility/backend/sequel/table.rb +149 -0
- data/lib/mobility/backend/sequel/table/query_methods.rb +48 -0
- data/lib/mobility/backend/serialized.rb +53 -0
- data/lib/mobility/backend/table.rb +93 -0
- data/lib/mobility/backend_resetter.rb +44 -0
- data/lib/mobility/configuration.rb +31 -0
- data/lib/mobility/core_ext/nil.rb +10 -0
- data/lib/mobility/core_ext/object.rb +19 -0
- data/lib/mobility/core_ext/string.rb +16 -0
- data/lib/mobility/instance_methods.rb +34 -0
- data/lib/mobility/orm.rb +4 -0
- data/lib/mobility/sequel.rb +26 -0
- data/lib/mobility/sequel/backend_resetter.rb +26 -0
- data/lib/mobility/sequel/column_changes.rb +29 -0
- data/lib/mobility/sequel/model_translation.rb +20 -0
- data/lib/mobility/sequel/string_translation.rb +7 -0
- data/lib/mobility/sequel/text_translation.rb +7 -0
- data/lib/mobility/sequel/translation.rb +53 -0
- data/lib/mobility/translates.rb +75 -0
- data/lib/mobility/wrapper.rb +31 -0
- metadata +152 -12
- data/.gitignore +0 -9
- data/.rspec +0 -2
- data/.travis.yml +0 -5
- data/bin/console +0 -14
- data/bin/setup +0 -8
- data/mobility.gemspec +0 -32
@@ -0,0 +1,93 @@
|
|
1
|
+
module Mobility
|
2
|
+
module Backend
|
3
|
+
=begin
|
4
|
+
|
5
|
+
Stores attribute translation as rows on a model-specific translation table
|
6
|
+
(similar to Globalize[https://github.com/globalize/globalize]). By default,
|
7
|
+
the table name for a model +Post+ with table +posts+ will be
|
8
|
+
+post_translations+, and the translation class will be +Post::Translation+. The
|
9
|
+
translation class is dynamically created when the backend is initialized on the
|
10
|
+
model class, and subclasses {Mobility::ActiveRecord::ModelTranslation} (for AR
|
11
|
+
models) or inherits {Mobility::Sequel::ModelTranslation} (for Sequel models).
|
12
|
+
|
13
|
+
The backend expects the translations table (+post_translations+) to have:
|
14
|
+
|
15
|
+
- a string column named +locale+ to store the locale of the translation
|
16
|
+
- columns for each translated attribute that uses the table (in general, this
|
17
|
+
will be all attributes of the model)
|
18
|
+
- an integer column with name +post_id+ (where +post+ is the name of the model class)
|
19
|
+
|
20
|
+
Unlike Globalize, attributes need not all be on one table. Mobility supports
|
21
|
+
any number of translation tables for a given model class (all of the structure
|
22
|
+
described above), provided the +association_name+ option is different for each.
|
23
|
+
Some translations can be stored on one translation table, others on
|
24
|
+
another, and Mobility will handle mapping reads/writes to each. The subclass
|
25
|
+
used in this case will be generated from the +association_name+ by
|
26
|
+
singularizing it and converting it to camelcase.
|
27
|
+
|
28
|
+
For more details, see examples in {Mobility::Backend::ActiveRecord::Table}.
|
29
|
+
|
30
|
+
==Backend Options
|
31
|
+
|
32
|
+
===+association_name+
|
33
|
+
|
34
|
+
Name of association on model. Defaults to +:mobility_model_translations+. If
|
35
|
+
specified, ensure name does not overlap with other methods on model or with the
|
36
|
+
association name used by other backends on model (otherwise one will overwrite
|
37
|
+
the other).
|
38
|
+
|
39
|
+
===+table_name+
|
40
|
+
|
41
|
+
Name of translations table. By default, if the table used by the model is
|
42
|
+
+posts+, the table name used for translations will be +post_translations+.
|
43
|
+
|
44
|
+
===+foreign_key+
|
45
|
+
|
46
|
+
Foreign key to use in defining the association on the model. By default, if the
|
47
|
+
model is a +Post+, this will be +post_id+. Generally this does not need to be
|
48
|
+
set.
|
49
|
+
|
50
|
+
===+subclass_name+
|
51
|
+
|
52
|
+
Subclass to use when dynamically generating translation class for model, by
|
53
|
+
default +:Translation+. Should be a symbol. Generally this does not need to be
|
54
|
+
set.
|
55
|
+
|
56
|
+
@see Mobility::Backend::ActiveRecord::Table
|
57
|
+
@see Mobility::Backend::Sequel::Table
|
58
|
+
=end
|
59
|
+
module Table
|
60
|
+
include OrmDelegator
|
61
|
+
|
62
|
+
# Simple hash cache to memoize translations as a hash so they can be
|
63
|
+
# fetched quickly.
|
64
|
+
class TranslationsCache < Hash
|
65
|
+
|
66
|
+
# @yield [locale] Yields locale to block in case attribute is not yet
|
67
|
+
# cached, expects a new translation for that locale.
|
68
|
+
# @raise [ArgumentError] if block is not given
|
69
|
+
def initialize
|
70
|
+
raise ArgumentError, "missing block" unless block_given?
|
71
|
+
super() { |hash, locale| hash[locale] = yield(locale) }
|
72
|
+
end
|
73
|
+
|
74
|
+
# Return wrapper class which reads and writes to only one attribute of this cache.
|
75
|
+
# @param [String] attribute
|
76
|
+
# @return [Class] Hash-like wrapper object to be used as attribute cache
|
77
|
+
def for(attribute)
|
78
|
+
cache = self
|
79
|
+
|
80
|
+
Class.new do
|
81
|
+
define_singleton_method :[] do |locale|
|
82
|
+
cache[locale].send(attribute)
|
83
|
+
end
|
84
|
+
|
85
|
+
define_singleton_method :[]= do |locale, value|
|
86
|
+
cache[locale].send("#{attribute}=", value)
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
module Mobility
|
2
|
+
=begin
|
3
|
+
|
4
|
+
Resets backend cache when reset events occur.
|
5
|
+
|
6
|
+
@example Add trigger to call a method +my_backend_reset_method+ on backend instance when reset event(s) occurs on model
|
7
|
+
resetter = Mobility::BackendResetter.for(MyModel).new(attributes) { my_backend_reset_method }
|
8
|
+
MyModel.include(resetter)
|
9
|
+
|
10
|
+
@see Mobility::ActiveRecord::BackendResetter
|
11
|
+
@see Mobility::ActiveModel::BackendResetter
|
12
|
+
@see Mobility::Sequel::BackendResetter
|
13
|
+
|
14
|
+
=end
|
15
|
+
class BackendResetter < Module
|
16
|
+
# @param [Array<String>] attributes Attributes whose backends should be reset
|
17
|
+
# @yield Backend to reset as context for block
|
18
|
+
# @raise [ArgumentError] if no block is provided.
|
19
|
+
def initialize(attributes, &block)
|
20
|
+
raise ArgumentError, "block required" unless block_given?
|
21
|
+
@model_reset_method = Proc.new do
|
22
|
+
attributes.each do |attribute|
|
23
|
+
if @mobility_backends && @mobility_backends[attribute]
|
24
|
+
@mobility_backends[attribute].instance_eval &block
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
# Returns backend resetter class for model class
|
31
|
+
# @param [Class] model_class Class of model to which backend resetter will be applied
|
32
|
+
def self.for(model_class)
|
33
|
+
if Loaded::ActiveRecord && model_class < ::ActiveRecord::Base
|
34
|
+
ActiveRecord::BackendResetter
|
35
|
+
elsif Loaded::ActiveRecord && model_class.ancestors.include?(::ActiveModel::Dirty)
|
36
|
+
ActiveModel::BackendResetter
|
37
|
+
elsif Loaded::Sequel && model_class < ::Sequel::Model
|
38
|
+
Sequel::BackendResetter
|
39
|
+
else
|
40
|
+
self
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
module Mobility
|
2
|
+
=begin
|
3
|
+
|
4
|
+
Stores shared Mobility configuration referenced by all backends.
|
5
|
+
|
6
|
+
=end
|
7
|
+
class Configuration
|
8
|
+
# Alias for mobility_accessor (defaults to +translates+)
|
9
|
+
# @return [Symbol]
|
10
|
+
attr_accessor :accessor_method
|
11
|
+
|
12
|
+
# Default fallbacks instance
|
13
|
+
# @return [I18n::Locale::Fallbacks]
|
14
|
+
attr_accessor :default_fallbacks
|
15
|
+
|
16
|
+
# Default backend to use (can be symbol or actual backend class)
|
17
|
+
# @return [Symbol,Class]
|
18
|
+
attr_accessor :default_backend
|
19
|
+
|
20
|
+
# Default set of locales to use when defining accessors (defaults to
|
21
|
+
# +I18n.available_locales+)
|
22
|
+
# @return [Array<Symbol>]
|
23
|
+
attr_accessor :default_accessor_locales
|
24
|
+
|
25
|
+
def initialize
|
26
|
+
@accessor_method = :translates
|
27
|
+
@default_fallbacks = I18n::Locale::Fallbacks.new
|
28
|
+
@default_accessor_locales = I18n.available_locales
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
=begin
|
2
|
+
|
3
|
+
Add +blank?+, +present?+ and +presence+ methods to +Object+ class if
|
4
|
+
activesupport cannot be loaded.
|
5
|
+
|
6
|
+
=end
|
7
|
+
class Object
|
8
|
+
def blank?
|
9
|
+
respond_to?(:empty?) ? !!empty? : !self
|
10
|
+
end
|
11
|
+
|
12
|
+
def present?
|
13
|
+
!blank?
|
14
|
+
end
|
15
|
+
|
16
|
+
def presence
|
17
|
+
self if present?
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
=begin
|
2
|
+
|
3
|
+
Add String methods +camelize+ and +present?+ to +String+ if activesupport
|
4
|
+
cannot be loaded.
|
5
|
+
|
6
|
+
=end
|
7
|
+
class String
|
8
|
+
# paraphrased from activesupport
|
9
|
+
def camelize
|
10
|
+
sub(/^[a-z\d]*/) { $&.capitalize }.gsub(/(?:_|(\/))([a-z\d]*)/) { "#{$1}#{$2.capitalize}" }.gsub('/', '::')
|
11
|
+
end
|
12
|
+
|
13
|
+
def present?
|
14
|
+
!blank?
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
module Mobility
|
2
|
+
=begin
|
3
|
+
|
4
|
+
Instance methods attached to all model classes when model includes or extends
|
5
|
+
{Mobility}.
|
6
|
+
|
7
|
+
=end
|
8
|
+
module InstanceMethods
|
9
|
+
# Fetch backend for an attribute
|
10
|
+
# @param [String] attribute Attribute
|
11
|
+
def mobility_backend_for(attribute)
|
12
|
+
send(Backend.method_name(attribute))
|
13
|
+
end
|
14
|
+
|
15
|
+
private
|
16
|
+
|
17
|
+
def mobility_get(*args)
|
18
|
+
mobility_read(*args).presence
|
19
|
+
end
|
20
|
+
|
21
|
+
def mobility_present?(*args)
|
22
|
+
mobility_read(*args).present?
|
23
|
+
end
|
24
|
+
|
25
|
+
def mobility_set(attribute, value, locale: Mobility.locale)
|
26
|
+
mobility_backend_for(attribute).write(locale.to_sym, value.presence)
|
27
|
+
end
|
28
|
+
|
29
|
+
def mobility_read(attribute, **options)
|
30
|
+
locale = options.delete(:locale) || Mobility.locale
|
31
|
+
mobility_backend_for(attribute).read(locale.to_sym, options)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
data/lib/mobility/orm.rb
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
module Mobility
|
2
|
+
=begin
|
3
|
+
|
4
|
+
Module loading Sequel-specific classes for Mobility models.
|
5
|
+
|
6
|
+
=end
|
7
|
+
module Sequel
|
8
|
+
autoload :BackendResetter, "mobility/sequel/backend_resetter"
|
9
|
+
autoload :ColumnChanges, "mobility/sequel/column_changes"
|
10
|
+
autoload :ModelTranslation, "mobility/sequel/model_translation"
|
11
|
+
autoload :StringTranslation, "mobility/sequel/string_translation"
|
12
|
+
autoload :TextTranslation, "mobility/sequel/text_translation"
|
13
|
+
autoload :Translation, "mobility/sequel/translation"
|
14
|
+
|
15
|
+
def self.included(model_class)
|
16
|
+
model_class.extend(ClassMethods)
|
17
|
+
end
|
18
|
+
|
19
|
+
module ClassMethods
|
20
|
+
# @return [Sequel::Dataset] dataset extended with Mobility query methods.
|
21
|
+
def i18n
|
22
|
+
dataset
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
module Mobility
|
2
|
+
module Sequel
|
3
|
+
=begin
|
4
|
+
|
5
|
+
Backend resetter for Sequel models. Triggers backend reset when +refresh+
|
6
|
+
method is called.
|
7
|
+
|
8
|
+
=end
|
9
|
+
class BackendResetter < Mobility::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 :refresh 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
|
@@ -0,0 +1,29 @@
|
|
1
|
+
module Mobility
|
2
|
+
module Sequel
|
3
|
+
=begin
|
4
|
+
|
5
|
+
Internal class used to force Sequel model to notice changes when +mobility_set+
|
6
|
+
is called.
|
7
|
+
|
8
|
+
=end
|
9
|
+
class ColumnChanges < Module
|
10
|
+
# @param [Array<String>] attributes Backend attributes
|
11
|
+
def initialize(attributes)
|
12
|
+
@attributes = attributes
|
13
|
+
|
14
|
+
define_method :mobility_set do |attribute, value, locale: Mobility.locale|
|
15
|
+
if attributes.include?(attribute)
|
16
|
+
column = attribute.to_sym
|
17
|
+
column_with_locale = :"#{attribute}_#{locale}"
|
18
|
+
if mobility_get(attribute) != value
|
19
|
+
@changed_columns << column_with_locale if !changed_columns.include?(column_with_locale)
|
20
|
+
@changed_columns << column if !changed_columns.include?(column)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
super(attribute, value, locale: locale)
|
24
|
+
end
|
25
|
+
private :mobility_set
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module Mobility
|
2
|
+
module Sequel
|
3
|
+
=begin
|
4
|
+
|
5
|
+
Module included in translation class dynamically generated by
|
6
|
+
{Backend::Sequel::Table} backend.
|
7
|
+
|
8
|
+
=end
|
9
|
+
module ModelTranslation
|
10
|
+
def self.included(base)
|
11
|
+
base.plugin :validation_helpers
|
12
|
+
end
|
13
|
+
|
14
|
+
def validate
|
15
|
+
super
|
16
|
+
validates_presence [:locale]
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
module Mobility
|
2
|
+
module Sequel
|
3
|
+
module Translation
|
4
|
+
def self.included(base)
|
5
|
+
base.class_eval do
|
6
|
+
plugin :validation_helpers
|
7
|
+
|
8
|
+
# Paraphased from sequel_polymorphic gem
|
9
|
+
#
|
10
|
+
model = underscore(self.to_s)
|
11
|
+
plural_model = pluralize(model)
|
12
|
+
many_to_one :translatable,
|
13
|
+
reciprocal: plural_model.to_sym,
|
14
|
+
reciprocal_type: :many_to_one,
|
15
|
+
setter: (proc do |able_instance|
|
16
|
+
self[:translatable_id] = (able_instance.pk if able_instance)
|
17
|
+
self[:translatable_type] = (able_instance.class.name if able_instance)
|
18
|
+
end),
|
19
|
+
dataset: (proc do
|
20
|
+
translatable_type = send :translatable_type
|
21
|
+
translatable_id = send :translatable_id
|
22
|
+
return if translatable_type.nil? || translatable_id.nil?
|
23
|
+
klass = self.class.send(:constantize, translatable_type)
|
24
|
+
klass.where(klass.primary_key => translatable_id)
|
25
|
+
end),
|
26
|
+
eager_loader: (proc do |eo|
|
27
|
+
id_map = {}
|
28
|
+
eo[:rows].each do |model|
|
29
|
+
model_able_type = model.send :translatable_type
|
30
|
+
model_able_id = model.send :translatable_id
|
31
|
+
model.associations[:translatable] = nil
|
32
|
+
((id_map[model_able_type] ||= {})[model_able_id] ||= []) << model if !model_able_type.nil? && !model_able_id.nil?
|
33
|
+
end
|
34
|
+
id_map.each do |klass_name, id_map|
|
35
|
+
klass = constantize(camelize(klass_name))
|
36
|
+
klass.where(klass.primary_key=>id_map.keys).all do |related_obj|
|
37
|
+
id_map[related_obj.pk].each do |model|
|
38
|
+
model.associations[:translatable] = related_obj
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end)
|
43
|
+
|
44
|
+
def validate
|
45
|
+
super
|
46
|
+
validates_presence [:locale, :key, :translatable_id, :translatable_type]
|
47
|
+
validates_unique [:locale, :key, :translatable_id, :translatable_type]
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|