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
@@ -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