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,14 @@
1
+ module Mobility
2
+ module Backend
3
+ module Sequel
4
+ autoload :Column, 'mobility/backend/sequel/column'
5
+ autoload :Dirty, 'mobility/backend/sequel/dirty'
6
+ autoload :Hstore, 'mobility/backend/sequel/hstore'
7
+ autoload :Jsonb, 'mobility/backend/sequel/jsonb'
8
+ autoload :KeyValue, 'mobility/backend/sequel/key_value'
9
+ autoload :Serialized, 'mobility/backend/sequel/serialized'
10
+ autoload :Table, 'mobility/backend/sequel/table'
11
+ autoload :QueryMethods, 'mobility/backend/sequel/query_methods'
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,40 @@
1
+ module Mobility
2
+ module Backend
3
+ =begin
4
+
5
+ Implements the {Mobility::Backend::Column} backend for Sequel models.
6
+
7
+ @note This backend disables the +accessor_locales+ option, which would
8
+ otherwise interfere with column methods.
9
+ =end
10
+ class Sequel::Column
11
+ include Backend
12
+ include Mobility::Backend::Column
13
+
14
+ autoload :QueryMethods, 'mobility/backend/sequel/column/query_methods'
15
+
16
+ # @!group Backend Accessors
17
+ # @!macro backend_reader
18
+ # @!method read(locale, **options)
19
+
20
+ # @!group Backend Accessors
21
+ # @!macro backend_writer
22
+ # @!method write(locale, value, **options)
23
+
24
+ # @!group Backend Configuration
25
+ def self.configure!(options)
26
+ options[:locale_accessors] = false
27
+ end
28
+ # @!endgroup
29
+
30
+ setup do |attributes, options|
31
+ extension = Module.new do
32
+ define_method :i18n do
33
+ @mobility_scope ||= super().with_extend(QueryMethods.new(attributes, options))
34
+ end
35
+ end
36
+ extend extension
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,24 @@
1
+ module Mobility
2
+ module Backend
3
+ class Sequel::Column::QueryMethods < Backend::Sequel::QueryMethods
4
+ def initialize(attributes, **options)
5
+ super
6
+ attributes_extractor = @attributes_extractor
7
+
8
+ define_method :_filter_or_exclude do |invert, clause, cond, &block|
9
+ if keys = attributes_extractor.call(cond)
10
+ cond = cond.dup
11
+ keys.each { |attr| cond[Column.column_name_for(attr)] = cond.delete(attr) }
12
+ end
13
+ super(invert, clause, cond, &block)
14
+ end
15
+
16
+ attributes.each do |attribute|
17
+ define_method :"first_by_#{attribute}" do |value|
18
+ where(attribute.to_sym => value).first
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,54 @@
1
+ module Mobility
2
+ module Backend
3
+ =begin
4
+
5
+ Dirty tracking for Sequel models which use the +Sequel::Plugins::Dirty+ plugin.
6
+
7
+ @see http://sequel.jeremyevans.net/rdoc-plugins/index.html Sequel dirty plugin
8
+
9
+ =end
10
+ module Sequel::Dirty
11
+ # @!group Backend Accessors
12
+ # @!macro backend_writer
13
+ def write(locale, value, **options)
14
+ locale_accessor = "#{attribute}_#{locale}".to_sym
15
+ if model.column_changes.has_key?(locale_accessor) && model.initial_values[locale_accessor] == value
16
+ super
17
+ [model.changed_columns, model.initial_values].each { |h| h.delete(locale_accessor) }
18
+ else
19
+ model.will_change_column("#{attribute}_#{locale}".to_sym)
20
+ super
21
+ end
22
+ end
23
+ # @!endgroup
24
+
25
+ # @param [Class] backend_class Class of backend
26
+ def self.included(backend_class)
27
+ backend_class.extend(ClassMethods)
28
+ end
29
+
30
+ # Adds hook after {Backend::Setup#setup_model} to add dirty-tracking
31
+ # methods for translated attributes onto model class.
32
+ module ClassMethods
33
+ # (see Mobility::Backend::Setup#setup_model)
34
+ def setup_model(model_class, attributes, **options)
35
+ super
36
+ model_class.class_eval do
37
+ mod = Module.new do
38
+ %w[initial_value column_change column_changed? reset_column].each do |method_name|
39
+ define_method method_name do |column|
40
+ if attributes.map(&:to_sym).include?(column)
41
+ super("#{column}_#{Mobility.locale}".to_sym)
42
+ else
43
+ super(column)
44
+ end
45
+ end
46
+ end
47
+ end
48
+ include mod
49
+ end
50
+ end
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,51 @@
1
+ module Mobility
2
+ module Backend
3
+ =begin
4
+
5
+ Internal class used by Sequel backends that store values as a hash.
6
+
7
+ =end
8
+ class Sequel::HashValued
9
+ include Backend
10
+
11
+ # @!macro backend_reader
12
+ def read(locale, **options)
13
+ translations[locale.to_s]
14
+ end
15
+
16
+ # @!macro backend_writer
17
+ def write(locale, value, **options)
18
+ translations[locale.to_s] = value
19
+ end
20
+
21
+ # @!group Cache Methods
22
+ def translations
23
+ model.send("#{attribute}_before_mobility")
24
+ end
25
+ alias_method :new_cache, :translations
26
+
27
+ # @return [Boolean]
28
+ def write_to_cache?
29
+ true
30
+ end
31
+ # @!endgroup
32
+
33
+ setup do |attributes, options|
34
+ method_overrides = Module.new do
35
+ define_method :initialize_set do |values|
36
+ attributes.each { |attribute| send(:"#{attribute}_before_mobility=", {}) }
37
+ super(values)
38
+ end
39
+ define_method :before_validation do
40
+ attributes.each do |attribute|
41
+ send("#{attribute}_before_mobility").delete_if { |_, v| v.blank? }
42
+ end
43
+ super()
44
+ end
45
+ end
46
+ include method_overrides
47
+ include Mobility::Sequel::ColumnChanges.new(attributes)
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,36 @@
1
+ require 'mobility/backend/sequel/hash_valued'
2
+
3
+ module Mobility
4
+ module Backend
5
+ =begin
6
+
7
+ Implements the {Mobility::Backend::Hstore} backend for Sequel models.
8
+
9
+ @see Mobility::Backend::Sequel::HashValued
10
+
11
+ =end
12
+ class Sequel::Hstore < Sequel::HashValued
13
+ autoload :QueryMethods, 'mobility/backend/sequel/hstore/query_methods'
14
+
15
+ # @!group Backend Accessors
16
+ # @!macro backend_reader
17
+ # @!method read(locale, **options)
18
+
19
+ # @!group Backend Accessors
20
+ # @!macro backend_writer
21
+ def write(locale, value, **options)
22
+ translations[locale.to_s] = value && value.to_s
23
+ end
24
+ # @!endgroup
25
+
26
+ setup do |attributes, options|
27
+ extension = Module.new do
28
+ define_method :i18n do
29
+ @mobility_scope ||= super().with_extend(QueryMethods.new(attributes, options))
30
+ end
31
+ end
32
+ extend extension
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,42 @@
1
+ Sequel.extension :pg_hstore, :pg_hstore_ops
2
+
3
+ module Mobility
4
+ module Backend
5
+ class Sequel::Hstore::QueryMethods < Sequel::QueryMethods
6
+ def initialize(attributes, **options)
7
+ super
8
+ attributes_extractor = @attributes_extractor
9
+
10
+ define_method :_filter_or_exclude do |invert, clause, *conds, &block|
11
+ if (clause == :where) && i18n_keys = attributes_extractor.call(conds.first)
12
+ locale = Mobility.locale.to_s
13
+ table_name = model.table_name
14
+ cond = conds.first
15
+
16
+ i18n_query = i18n_keys.inject(::Sequel.expr(!invert)) do |expr, attr|
17
+ value = cond.delete(attr)
18
+ attr_hstore = ::Sequel.hstore_op(attr)
19
+ contains_value = attr_hstore.contains({ locale => value.to_s })
20
+ has_key = attr_hstore.has_key?(locale)
21
+ if invert
22
+ expr.|(has_key & ~contains_value)
23
+ else
24
+ expr.&(value.nil? ? ~has_key : contains_value)
25
+ end
26
+ end
27
+ super(invert, clause, *conds, &block).where(i18n_query)
28
+ else
29
+ super(invert, clause, *conds, &block)
30
+ end
31
+ end
32
+
33
+ attributes.each do |attribute|
34
+ define_method :"first_by_#{attribute}" do |value|
35
+ where(::Sequel.hstore(attribute.to_sym).contains(::Sequel.hstore({ Mobility.locale.to_s => value }))).
36
+ select_all(model.table_name).first
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,43 @@
1
+ require 'mobility/backend/sequel/hash_valued'
2
+
3
+ module Mobility
4
+ module Backend
5
+ =begin
6
+
7
+ Implements the {Mobility::Backend::Jsonb} backend for Sequel models.
8
+
9
+ @see Mobility::Backend::Sequel::HashValued
10
+
11
+ =end
12
+ class Sequel::Jsonb < Sequel::HashValued
13
+ autoload :QueryMethods, 'mobility/backend/sequel/jsonb/query_methods'
14
+
15
+ # @!group Backend Accessors
16
+ #
17
+ # @note Translation may be string, integer or boolean-valued since
18
+ # value is stored on a JSON hash.
19
+ # @param [Symbol] locale Locale to read
20
+ # @param [Hash] options
21
+ # @return [String,Integer,Boolean] Value of translation
22
+ # @!method read(locale, **options)
23
+
24
+ # @!group Backend Accessors
25
+ # @note Translation may be string, integer or boolean-valued since
26
+ # value is stored on a JSON hash.
27
+ # @param [Symbol] locale Locale to write
28
+ # @param [String,Integer,Boolean] value Value to write
29
+ # @param [Hash] options
30
+ # @return [String,Integer,Boolean] Updated value
31
+ # @!method write(locale, value, **options)
32
+
33
+ setup do |attributes, options|
34
+ extension = Module.new do
35
+ define_method :i18n do
36
+ @mobility_scope ||= super().with_extend(QueryMethods.new(attributes, options))
37
+ end
38
+ end
39
+ extend extension
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,42 @@
1
+ Sequel.extension :pg_json, :pg_json_ops
2
+
3
+ module Mobility
4
+ module Backend
5
+ class Sequel::Jsonb::QueryMethods < Sequel::QueryMethods
6
+ def initialize(attributes, **options)
7
+ super
8
+ attributes_extractor = @attributes_extractor
9
+
10
+ define_method :_filter_or_exclude do |invert, clause, *conds, &block|
11
+ if (clause == :where) && i18n_keys = attributes_extractor.call(conds.first)
12
+ locale = Mobility.locale.to_s
13
+ table_name = model.table_name
14
+ cond = conds.first
15
+
16
+ i18n_query = i18n_keys.inject(::Sequel.expr(!invert)) do |expr, attr|
17
+ value = cond.delete(attr)
18
+ attr_jsonb = ::Sequel.pg_jsonb_op(attr)
19
+ contains_value = attr_jsonb.contains({ locale => value })
20
+ has_key = attr_jsonb.has_key?(locale)
21
+ if invert
22
+ expr.|(has_key & ~contains_value)
23
+ else
24
+ expr.&(value.nil? ? ~has_key : contains_value)
25
+ end
26
+ end
27
+ super(invert, clause, *conds, &block).where(i18n_query)
28
+ else
29
+ super(invert, clause, *conds, &block)
30
+ end
31
+ end
32
+
33
+ attributes.each do |attribute|
34
+ define_method :"first_by_#{attribute}" do |value|
35
+ where(::Sequel.pg_jsonb_op(attribute).contains({ Mobility.locale => value })).
36
+ select_all(model.table_name).first
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,139 @@
1
+ module Mobility
2
+ module Backend
3
+ =begin
4
+
5
+ Implements the {Mobility::Backend::KeyValue} backend for Sequel models.
6
+
7
+ @note This backend requires the cache to be enabled in order to track
8
+ and store changed translations, since Sequel does not support +build+-type
9
+ methods on associations like ActiveRecord.
10
+
11
+ =end
12
+ class Sequel::KeyValue
13
+ include Backend
14
+
15
+ autoload :QueryMethods, 'mobility/backend/sequel/key_value/query_methods'
16
+
17
+ # @return [Symbol] name of the association
18
+ attr_reader :association_name
19
+
20
+ # @return [Class] translation model class
21
+ attr_reader :class_name
22
+
23
+ # @!macro backend_constructor
24
+ # @option options [Symbol] association_name Name of association
25
+ # @option options [Class] class_name Translation model class
26
+ def initialize(model, attribute, **options)
27
+ super
28
+ @association_name = options[:association_name]
29
+ @class_name = options[:class_name]
30
+ end
31
+
32
+ # @!group Backend Accessors
33
+ # @!macro backend_reader
34
+ def read(locale, **options)
35
+ translation_for(locale).value
36
+ end
37
+
38
+ # @!macro backend_writer
39
+ def write(locale, value, **options)
40
+ translation_for(locale).tap { |t| t.value = value }.value
41
+ end
42
+ # @!endgroup
43
+
44
+ # @!group Backend Configuration
45
+ # @option options [Symbol] type (:text) Column type to use
46
+ # @option options [Symbol] associaiton_name (:mobility_text_translations) Name of association method
47
+ # @option options [Symbol] class_name ({Mobility::Sequel::TextTranslation}) Translation class
48
+ # @raise [CacheRequired] if cache is disabled
49
+ # @raise [ArgumentError] if type is not either :text or :string
50
+ def self.configure!(options)
51
+ raise CacheRequired, "Cache required for Sequel::KeyValue backend" if options[:cache] == false
52
+ options[:type] ||= :text
53
+ case type = options[:type].to_sym
54
+ when :text, :string
55
+ options[:class_name] ||= Mobility::Sequel.const_get("#{type.capitalize}Translation")
56
+ else
57
+ raise ArgumentError, "type must be one of: [text, string]"
58
+ end
59
+ options[:class_name] = options[:class_name].constantize if options[:class_name].is_a?(String)
60
+ options[:association_name] ||= options[:class_name].table_name.to_sym
61
+ %i[type association_name].each { |key| options[key] = options[key].to_sym }
62
+ end
63
+ # @!endgroup
64
+
65
+ setup do |attributes, options|
66
+ association_name = options[:association_name]
67
+ translations_class = options[:class_name]
68
+
69
+ attrs_method_name = :"#{association_name}_attributes"
70
+ association_attributes = (instance_variable_get(:"@#{attrs_method_name}") || []) + attributes
71
+ instance_variable_set(:"@#{attrs_method_name}", association_attributes)
72
+
73
+ one_to_many association_name,
74
+ reciprocal: :translatable,
75
+ key: :translatable_id,
76
+ reciprocal_type: :one_to_many,
77
+ conditions: { translatable_type: self.to_s, key: association_attributes },
78
+ adder: proc { |translation| translation.update(translatable_id: pk, translatable_type: self.class.to_s) },
79
+ remover: proc { |translation| translation.update(translatable_id: nil, translatable_type: nil) },
80
+ clearer: proc { send(:"#{association_name}_dataset").update(translatable_id: nil, translatable_type: nil) },
81
+ class: translations_class
82
+
83
+ plugin :association_dependencies, association_name => :destroy
84
+
85
+ callback_methods = Module.new do
86
+ define_method :before_save do
87
+ super()
88
+ send(association_name).select { |t| attributes.include?(t.key) && t.value.blank? }.each(&:destroy)
89
+ end
90
+ define_method :after_save do
91
+ super()
92
+ attributes.each { |attribute| mobility_backend_for(attribute).save_translations }
93
+ end
94
+ end
95
+ include callback_methods
96
+
97
+ extension = Module.new do
98
+ define_method :i18n do
99
+ @mobility_scope ||= super().with_extend(QueryMethods.new(attributes, options))
100
+ end
101
+ end
102
+ extend extension
103
+
104
+ include Mobility::Sequel::ColumnChanges.new(attributes)
105
+ end
106
+
107
+ # @!group Cache Methods
108
+ # @return [KeyValue::TranslationsCache]
109
+ def new_cache
110
+ KeyValue::TranslationsCache.new(self)
111
+ end
112
+
113
+ # @return [Boolean]
114
+ def write_to_cache?
115
+ true
116
+ end
117
+ # @!endgroup
118
+
119
+ # Returns translation for a given locale, or initializes one if none is present.
120
+ # @param [Symbol] locale
121
+ # @return [Mobility::Sequel::TextTranslation,Mobility::Sequel::StringTranslation]
122
+ def translation_for(locale)
123
+ translation = model.send(association_name).find { |t| t.key == attribute && t.locale == locale.to_s }
124
+ translation ||= class_name.new(locale: locale, key: attribute)
125
+ translation
126
+ end
127
+
128
+ # Saves translation which have been built and which have non-blank values.
129
+ def save_translations
130
+ cache.each_translation do |translation|
131
+ next unless translation.value.present?
132
+ translation.id ? translation.save : model.send("add_#{association_name.to_s.singularize}", translation)
133
+ end
134
+ end
135
+
136
+ class CacheRequired < ::StandardError; end
137
+ end
138
+ end
139
+ end