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