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,13 @@
1
+ module Mobility
2
+ module Backend
3
+ module ActiveRecord
4
+ autoload :Column, 'mobility/backend/active_record/column'
5
+ autoload :Hstore, 'mobility/backend/active_record/hstore'
6
+ autoload :Jsonb, 'mobility/backend/active_record/jsonb'
7
+ autoload :KeyValue, 'mobility/backend/active_record/key_value'
8
+ autoload :Serialized, 'mobility/backend/active_record/serialized'
9
+ autoload :QueryMethods, 'mobility/backend/active_record/query_methods'
10
+ autoload :Table, 'mobility/backend/active_record/table'
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,52 @@
1
+ module Mobility
2
+ module Backend
3
+ =begin
4
+
5
+ Implements the {Mobility::Backend::Column} backend for ActiveRecord models.
6
+
7
+ @note This backend disables the +accessor_locales+ option, which would
8
+ otherwise interfere with column methods.
9
+
10
+ @example
11
+ class Post < ActiveRecord::Base
12
+ translates :title, backend: :column
13
+ end
14
+
15
+ Mobility.locale = :en
16
+ post = Post.create(title: "foo")
17
+ post.title
18
+ #=> "foo"
19
+ post.title_en
20
+ #=> "foo"
21
+ =end
22
+ class ActiveRecord::Column
23
+ include Backend
24
+ include Mobility::Backend::Column
25
+
26
+ autoload :QueryMethods, 'mobility/backend/active_record/column/query_methods'
27
+
28
+ # @!group Backend Accessors
29
+ # @!macro backend_reader
30
+ # @!method read(locale, **options)
31
+
32
+ # @!group Backend Accessors
33
+ # @!macro backend_writer
34
+ # @!method write(locale, value, **options)
35
+
36
+ # @!group Backend Configuration
37
+ def self.configure!(options)
38
+ options[:locale_accessors] = false
39
+ end
40
+ # @!endgroup
41
+
42
+ setup do |attributes, options|
43
+ mod = Module.new do
44
+ define_method :i18n do
45
+ @mobility_scope ||= super().extending(QueryMethods.new(attributes, options))
46
+ end
47
+ end
48
+ extend mod
49
+ end
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,40 @@
1
+ module Mobility
2
+ module Backend
3
+ class ActiveRecord::Column::QueryMethods < Backend::ActiveRecord::QueryMethods
4
+ def initialize(attributes, **options)
5
+ super
6
+ attributes_extractor = @attributes_extractor
7
+
8
+ define_method :where! do |opts, *rest|
9
+ if i18n_keys = attributes_extractor.call(opts)
10
+ opts = opts.with_indifferent_access
11
+ i18n_keys.each { |attr| opts[Column.column_name_for(attr)] = opts.delete(attr) }
12
+ end
13
+ super(opts, *rest)
14
+ end
15
+
16
+ attributes.each do |attribute|
17
+ define_method :"find_by_#{attribute}" do |value|
18
+ find_by(Column.column_name_for(attribute) => value)
19
+ end
20
+ end
21
+ end
22
+
23
+ def extended(relation)
24
+ super
25
+ attributes_extractor = @attributes_extractor
26
+
27
+ mod = Module.new do
28
+ define_method :not do |opts, *rest|
29
+ if i18n_keys = attributes_extractor.call(opts)
30
+ opts = opts.with_indifferent_access
31
+ i18n_keys.each { |attr| opts[Column.column_name_for(attr)] = opts.delete(attr) }
32
+ end
33
+ super(opts, *rest)
34
+ end
35
+ end
36
+ relation.model.mobility_where_chain.prepend(mod)
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,58 @@
1
+ module Mobility
2
+ module Backend
3
+ =begin
4
+
5
+ Internal class used by ActiveRecord backends that store values as a hash.
6
+
7
+ =end
8
+ class ActiveRecord::HashValued
9
+ include Backend
10
+
11
+ # @!group Backend Accessors
12
+ #
13
+ # @!macro backend_reader
14
+ def read(locale, **options)
15
+ translations[locale]
16
+ end
17
+
18
+ # @!macro backend_writer
19
+ def write(locale, value, **options)
20
+ translations[locale] = value
21
+ end
22
+ # @!endgroup
23
+
24
+ def translations
25
+ model.read_attribute(attribute)
26
+ end
27
+ alias_method :new_cache, :translations
28
+
29
+ def write_to_cache?
30
+ true
31
+ end
32
+
33
+ setup do |attributes, options|
34
+ attributes.each { |attribute| store attribute, coder: Coder }
35
+ before_validation do
36
+ attributes.each { |attribute| self.send(:"#{attribute}=", {}) if send(attribute).nil? }
37
+ end
38
+ end
39
+
40
+ class Coder
41
+ def self.dump(obj)
42
+ if obj.is_a? Hash
43
+ obj = obj.inject({}) do |translations, (locale, value)|
44
+ translations[locale] = value if value.present?
45
+ translations
46
+ end
47
+ else
48
+ raise ArgumentError, "Attribute is supposed to be a Hash, but was a #{obj.class}. -- #{obj.inspect}"
49
+ end
50
+ end
51
+
52
+ def self.load(obj)
53
+ obj
54
+ end
55
+ end
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,36 @@
1
+ require 'mobility/backend/active_record/hash_valued'
2
+
3
+ module Mobility
4
+ module Backend
5
+ =begin
6
+
7
+ Implements the {Mobility::Backend::Hstore} backend for ActiveRecord models.
8
+
9
+ @see Mobility::Backend::ActiveRecord::HashValued
10
+
11
+ =end
12
+ class ActiveRecord::Hstore < ActiveRecord::HashValued
13
+ autoload :QueryMethods, 'mobility/backend/active_record/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] = value && value.to_s
23
+ end
24
+ # @!endgroup
25
+
26
+ setup do |attributes, options|
27
+ query_methods = Module.new do
28
+ define_method :i18n do
29
+ @mobility_scope ||= super().extending(QueryMethods.new(attributes, options))
30
+ end
31
+ end
32
+ extend query_methods
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,53 @@
1
+ module Mobility
2
+ module Backend
3
+ class ActiveRecord::Hstore::QueryMethods < ActiveRecord::QueryMethods
4
+ def initialize(attributes, **options)
5
+ super
6
+ attributes_extractor = @attributes_extractor
7
+
8
+ define_method :where! do |opts, *rest|
9
+ if i18n_keys = attributes_extractor.call(opts)
10
+ locale = Mobility.locale
11
+ opts = opts.with_indifferent_access
12
+
13
+ result = i18n_keys.inject(all) do |scope, attr|
14
+ value = opts.delete(attr)
15
+ if value.nil?
16
+ scope.where.not("#{table_name}.#{attr} ? '#{locale}'")
17
+ else
18
+ scope.where!("#{table_name}.#{attr} @> hstore('#{locale}', ?)", value.to_s)
19
+ end
20
+ end
21
+ result = result.where!(opts, *rest) if opts.present?
22
+ result
23
+ else
24
+ super(opts, *rest)
25
+ end
26
+ end
27
+ end
28
+
29
+ def extended(relation)
30
+ super
31
+ attributes_extractor = @attributes_extractor
32
+ table_name = relation.model.table_name
33
+
34
+ mod = Module.new do
35
+ define_method :not do |opts, *rest|
36
+ if i18n_keys = attributes_extractor.call(opts)
37
+ locale = Mobility.locale
38
+ opts = opts.with_indifferent_access
39
+
40
+ i18n_keys.inject(relation) { |scope, attr|
41
+ scope.where!("#{table_name}.#{attr} ? '#{locale}'").
42
+ where.not("#{table_name}.#{attr} @> hstore('#{locale}', ?)", opts.delete(attr).to_s)
43
+ }.where.not(opts, *rest)
44
+ else
45
+ super(opts, *rest)
46
+ end
47
+ end
48
+ end
49
+ relation.model.mobility_where_chain.prepend(mod)
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,43 @@
1
+ require 'mobility/backend/active_record/hash_valued'
2
+
3
+ module Mobility
4
+ module Backend
5
+ =begin
6
+
7
+ Implements the {Mobility::Backend::Jsonb} backend for ActiveRecord models.
8
+
9
+ @see Mobility::Backend::ActiveRecord::HashValued
10
+
11
+ =end
12
+ class ActiveRecord::Jsonb < ActiveRecord::HashValued
13
+ autoload :QueryMethods, 'mobility/backend/active_record/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
+ query_methods = Module.new do
35
+ define_method :i18n do
36
+ @mobility_scope ||= super().extending(QueryMethods.new(attributes, options))
37
+ end
38
+ end
39
+ extend query_methods
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,53 @@
1
+ module Mobility
2
+ module Backend
3
+ class ActiveRecord::Jsonb::QueryMethods < ActiveRecord::QueryMethods
4
+ def initialize(attributes, **options)
5
+ super
6
+ attributes_extractor = @attributes_extractor
7
+
8
+ define_method :where! do |opts, *rest|
9
+ if i18n_keys = attributes_extractor.call(opts)
10
+ locale = Mobility.locale
11
+ opts = opts.with_indifferent_access
12
+
13
+ result = i18n_keys.inject(all) do |scope, attr|
14
+ value = opts.delete(attr)
15
+ if value.nil?
16
+ scope.where.not("#{table_name}.#{attr} ? :locale", locale: locale)
17
+ else
18
+ scope.where!("#{table_name}.#{attr} @> (?)::jsonb", { locale => value }.to_json)
19
+ end
20
+ end
21
+ result = result.where!(opts, *rest) if opts.present?
22
+ result
23
+ else
24
+ super(opts, *rest)
25
+ end
26
+ end
27
+ end
28
+
29
+ def extended(relation)
30
+ super
31
+ attributes_extractor = @attributes_extractor
32
+ table_name = relation.model.table_name
33
+
34
+ mod = Module.new do
35
+ define_method :not do |opts, *rest|
36
+ if i18n_keys = attributes_extractor.call(opts)
37
+ locale = Mobility.locale
38
+ opts = opts.with_indifferent_access
39
+
40
+ i18n_keys.inject(relation) { |scope, attr|
41
+ scope.where!("#{table_name}.#{attr} ? :locale", locale: locale).
42
+ where.not("#{table_name}.#{attr} @> (?)::jsonb", { locale => opts.delete(attr) }.to_json)
43
+ }.where.not(opts, *rest)
44
+ else
45
+ super(opts, *rest)
46
+ end
47
+ end
48
+ end
49
+ relation.model.mobility_where_chain.prepend(mod)
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,126 @@
1
+ module Mobility
2
+ module Backend
3
+ =begin
4
+
5
+ Implements the {Mobility::Backend::KeyValue} backend for ActiveRecord models.
6
+
7
+ @example
8
+ class Post < ActiveRecord::Base
9
+ translates :title, backend: :key_value, association_name: :translations, type: :string
10
+ end
11
+
12
+ post = Post.create(title: "foo")
13
+ post.translations
14
+ #=> #<ActiveRecord::Associations::CollectionProxy ... >
15
+ post.translations.first.value
16
+ #=> "foo"
17
+ post.translations.first.class
18
+ #=> Mobility::ActiveRercord::StringTranslation
19
+
20
+ =end
21
+ class ActiveRecord::KeyValue
22
+ include Backend
23
+
24
+ autoload :QueryMethods, 'mobility/backend/active_record/key_value/query_methods'
25
+
26
+ # @return [Symbol] Name of the association
27
+ attr_reader :association_name
28
+
29
+ # @!macro backend_constructor
30
+ # @option options [Symbol] association_name Name of association
31
+ def initialize(model, attribute, **options)
32
+ super
33
+ @association_name = options[:association_name]
34
+ end
35
+
36
+ # @!group Backend Accessors
37
+ # @!macro backend_reader
38
+ def read(locale, **options)
39
+ translation_for(locale).value
40
+ end
41
+
42
+ # @!macro backend_reader
43
+ def write(locale, value, **options)
44
+ translation_for(locale).tap { |t| t.value = value }.value
45
+ end
46
+ # @!endgroup
47
+
48
+ # @!group Backend Configuration
49
+ # @option options [Symbol] type (:text) Column type to use
50
+ # @option options [Symbol] association_name (:mobility_text_translations) Name of association method
51
+ # @option options [String,Class] class_name ({Mobility::ActiveRecord::TextTranslation}) Translation class
52
+ # @raise [ArgumentError] if type is not either :text or :string
53
+ def self.configure!(options)
54
+ options[:type] ||= :text
55
+ case type = options[:type].to_sym
56
+ when :text, :string
57
+ options[:class_name] ||= Mobility::ActiveRecord.const_get("#{type.capitalize}Translation")
58
+ else
59
+ raise ArgumentError, "type must be one of: [text, string]"
60
+ end
61
+ options[:class_name] = options[:class_name].constantize if options[:class_name].is_a?(String)
62
+ options[:association_name] ||= options[:class_name].table_name.to_sym
63
+ %i[type association_name].each { |key| options[key] = options[key].to_sym }
64
+ end
65
+ # @!endgroup
66
+
67
+ setup do |attributes, options|
68
+ association_name = options[:association_name]
69
+ translations_class = options[:class_name]
70
+
71
+ # Track all attributes for this association, so that we can limit the scope
72
+ # of keys for the association to only these attributes. We need to track the
73
+ # attributes assigned to the association in case this setup code is called
74
+ # multiple times, so we don't "forget" earlier attributes.
75
+ #
76
+ attrs_method_name = :"__#{association_name}_attributes"
77
+ association_attributes = (instance_variable_get(:"@#{attrs_method_name}") || []) + attributes
78
+ instance_variable_set(:"@#{attrs_method_name}", association_attributes)
79
+
80
+ has_many association_name, ->{ where key: association_attributes },
81
+ as: :translatable,
82
+ class_name: translations_class,
83
+ dependent: :destroy,
84
+ inverse_of: :translatable,
85
+ autosave: true
86
+ before_save do
87
+ send(association_name).select { |t| t.value.blank? }.each do |translation|
88
+ send(association_name).destroy(translation)
89
+ end
90
+ end
91
+
92
+ mod = Module.new do
93
+ define_method :i18n do
94
+ @mobility_scope ||= super().extending(QueryMethods.new(attributes, options))
95
+ end
96
+ end
97
+ extend mod
98
+ end
99
+
100
+ # @!group Cache Methods
101
+ # @return [KeyValue::TranslationsCache]
102
+ def new_cache
103
+ KeyValue::TranslationsCache.new(self)
104
+ end
105
+
106
+ # @return [Boolean]
107
+ def write_to_cache?
108
+ true
109
+ end
110
+ # @!endgroup
111
+
112
+ # Returns translation for a given locale, or builds one if none is present.
113
+ # @param [Symbol] locale
114
+ # @return [Mobility::ActiveRecord::TextTranslation,Mobility::ActiveRecord::StringTranslation]
115
+ def translation_for(locale)
116
+ translation = translations.find { |t| t.key == attribute && t.locale == locale.to_s }
117
+ translation ||= translations.build(locale: locale, key: attribute)
118
+ translation
119
+ end
120
+
121
+ def translations
122
+ model.send(association_name)
123
+ end
124
+ end
125
+ end
126
+ end