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.
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,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,10 @@
1
+ =begin
2
+
3
+ Add +blank?+ method to +NilClass+ in case activesupport cannot be loaded.
4
+
5
+ =end
6
+ class NilClass
7
+ def blank?
8
+ true
9
+ end
10
+ 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
@@ -0,0 +1,4 @@
1
+ module Mobility
2
+ module Loaded
3
+ end
4
+ end
@@ -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,7 @@
1
+ module Mobility
2
+ module Sequel
3
+ class StringTranslation < ::Sequel::Model(:mobility_string_translations)
4
+ include Translation
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,7 @@
1
+ module Mobility
2
+ module Sequel
3
+ class TextTranslation < ::Sequel::Model(:mobility_text_translations)
4
+ include Translation
5
+ end
6
+ end
7
+ 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