mobility 0.7.6 → 0.8.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 (52) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +0 -0
  3. data.tar.gz.sig +0 -0
  4. data/CHANGELOG.md +14 -3
  5. data/Gemfile.lock +57 -0
  6. data/README.md +15 -4
  7. data/lib/mobility.rb +17 -1
  8. data/lib/mobility/active_record/uniqueness_validator.rb +1 -1
  9. data/lib/mobility/arel/nodes.rb +0 -3
  10. data/lib/mobility/arel/nodes/pg_ops.rb +0 -4
  11. data/lib/mobility/attributes.rb +42 -25
  12. data/lib/mobility/backends/active_record.rb +2 -2
  13. data/lib/mobility/backends/active_record/column.rb +2 -2
  14. data/lib/mobility/backends/active_record/key_value.rb +6 -9
  15. data/lib/mobility/backends/active_record/table.rb +6 -7
  16. data/lib/mobility/backends/column.rb +2 -2
  17. data/lib/mobility/backends/key_value.rb +5 -0
  18. data/lib/mobility/backends/sequel.rb +24 -11
  19. data/lib/mobility/backends/sequel/column.rb +4 -3
  20. data/lib/mobility/backends/sequel/container.rb +21 -12
  21. data/lib/mobility/backends/sequel/hstore.rb +11 -3
  22. data/lib/mobility/backends/sequel/json.rb +11 -3
  23. data/lib/mobility/backends/sequel/jsonb.rb +35 -3
  24. data/lib/mobility/backends/sequel/key_value.rb +97 -17
  25. data/lib/mobility/backends/sequel/serialized.rb +5 -4
  26. data/lib/mobility/backends/sequel/table.rb +95 -26
  27. data/lib/mobility/backends/table.rb +4 -0
  28. data/lib/mobility/configuration.rb +1 -1
  29. data/lib/mobility/plugins/active_record/query.rb +52 -26
  30. data/lib/mobility/plugins/locale_accessors.rb +3 -2
  31. data/lib/mobility/plugins/query.rb +3 -0
  32. data/lib/mobility/plugins/sequel/query.rb +140 -0
  33. data/lib/mobility/sequel.rb +0 -14
  34. data/lib/mobility/sequel/sql.rb +16 -0
  35. data/lib/mobility/version.rb +1 -1
  36. data/lib/rails/generators/mobility/templates/column_translations.rb +1 -1
  37. data/lib/rails/generators/mobility/templates/initializer.rb +3 -2
  38. data/lib/rails/generators/mobility/translations_generator.rb +1 -1
  39. metadata +27 -46
  40. metadata.gz.sig +1 -1
  41. data/lib/mobility/backends/active_record/query_methods.rb +0 -50
  42. data/lib/mobility/backends/sequel/column/query_methods.rb +0 -29
  43. data/lib/mobility/backends/sequel/container/json_query_methods.rb +0 -41
  44. data/lib/mobility/backends/sequel/container/jsonb_query_methods.rb +0 -41
  45. data/lib/mobility/backends/sequel/hstore/query_methods.rb +0 -34
  46. data/lib/mobility/backends/sequel/json/query_methods.rb +0 -34
  47. data/lib/mobility/backends/sequel/jsonb/query_methods.rb +0 -34
  48. data/lib/mobility/backends/sequel/key_value/query_methods.rb +0 -58
  49. data/lib/mobility/backends/sequel/pg_query_methods.rb +0 -114
  50. data/lib/mobility/backends/sequel/query_methods.rb +0 -36
  51. data/lib/mobility/backends/sequel/serialized/query_methods.rb +0 -22
  52. data/lib/mobility/backends/sequel/table/query_methods.rb +0 -58
@@ -126,10 +126,10 @@ columns to that table.
126
126
  # query.
127
127
  # @param [ActiveRecord::Relation] relation Relation to scope
128
128
  # @param [Object] predicate Arel predicate
129
- # @param [Symbol] locale Locale
129
+ # @param [Symbol] locale (Mobility.locale) Locale
130
130
  # @option [Boolean] invert
131
131
  # @return [ActiveRecord::Relation] relation Relation with joins applied (if needed)
132
- def apply_scope(relation, predicate, locale, invert: false)
132
+ def apply_scope(relation, predicate, locale = Mobility.locale, invert: false)
133
133
  visitor = Visitor.new(self, locale)
134
134
  if join_type = visitor.accept(predicate)
135
135
  join_type &&= Visitor::INNER_JOIN if invert
@@ -141,10 +141,6 @@ columns to that table.
141
141
 
142
142
  private
143
143
 
144
- def table_alias(locale)
145
- "#{table_name}_#{Mobility.normalize_locale(locale)}"
146
- end
147
-
148
144
  def join_translations(relation, locale, join_type)
149
145
  return relation if already_joined?(relation, locale, join_type)
150
146
  m = model_class.arel_table
@@ -179,7 +175,10 @@ columns to that table.
179
175
  # end
180
176
  #
181
177
  # backend_class = Post.mobility_backend_class(:title)
182
- # visitor = Mobility::Backends::ActiveRecord::Table::Visitor.new(backend_class)
178
+ # visitor = Mobility::Backends::ActiveRecord::Table::Visitor.new(backend_class, :en)
179
+ #
180
+ # title = backend_class.build_node("title", :en) # arel node for title
181
+ # content = backend_class.build_node("content", :en) # arel node for content
183
182
  #
184
183
  # visitor.accept(title.eq(nil).and(content.eq(nil)))
185
184
  # #=> Arel::Nodes::OuterJoin
@@ -6,7 +6,7 @@ module Mobility
6
6
 
7
7
  Stores translated attribute as a column on the model table. To use this
8
8
  backend, ensure that the model table has columns named +<attribute>_<locale>+
9
- for every locale in +I18n.available_locales+.
9
+ for every locale in +Mobility.available_locales+ (i.e. +I18n.available_locales+).
10
10
 
11
11
  If you are using Rails, you can use the +mobility:translations+ generator to
12
12
  create a migration adding these columns to the model table with:
@@ -14,7 +14,7 @@ create a migration adding these columns to the model table with:
14
14
  rails generate mobility:translations post title:string
15
15
 
16
16
  The generated migration will add columns +title_<locale>+ for every locale in
17
- +I18n.available_locales+. (The generator can be run again to add new attributes
17
+ +Mobility.available_locales+. (The generator can be run again to add new attributes
18
18
  or locales.)
19
19
 
20
20
  ==Backend Options
@@ -81,6 +81,7 @@ other backends on model (otherwise one will overwrite the other).
81
81
  backend_class.extend ClassMethods
82
82
  backend_class.option_reader :association_name
83
83
  backend_class.option_reader :class_name
84
+ backend_class.option_reader :table_alias_affix
84
85
  end
85
86
 
86
87
  module ClassMethods
@@ -119,6 +120,10 @@ each translated model, or set a default option in your configuration.
119
120
  super
120
121
  end
121
122
  end
123
+
124
+ def table_alias(attr, locale)
125
+ table_alias_affix % "#{attr}_#{Mobility.normalize_locale(locale)}"
126
+ end
122
127
  end
123
128
 
124
129
  module Cache
@@ -1,19 +1,32 @@
1
1
  module Mobility
2
2
  module Backends
3
3
  module Sequel
4
- def setup_query_methods(query_methods)
5
- setup do |attributes, options|
6
- extend(Module.new do
7
- define_method ::Mobility.query_method do
8
- super().with_extend(query_methods.new(attributes, options))
9
- end
10
- end)
11
- end
12
- end
13
-
14
4
  def self.included(backend_class)
15
5
  backend_class.include(Backend)
16
- backend_class.extend(self)
6
+ backend_class.extend(ClassMethods)
7
+ end
8
+
9
+ module ClassMethods
10
+ # @param [Symbol] name Attribute name
11
+ # @param [Symbol] locale Locale
12
+ def [](name, locale)
13
+ build_op(name.to_s, locale)
14
+ end
15
+
16
+ # @param [String] _attr Attribute name
17
+ # @param [Symbol] _locale Locale
18
+ # @return Op for this translated attribute
19
+ def build_op(_attr, _locale)
20
+ raise NotImplementedError
21
+ end
22
+
23
+ # @param [Sequel::Dataset] dataset Dataset to prepare
24
+ # @param [Object] predicate Predicate
25
+ # @param [Symbol] locale Locale
26
+ # @return [Sequel::Dataset] Prepared dataset
27
+ def prepare_dataset(dataset, _predicate, _locale)
28
+ dataset
29
+ end
17
30
  end
18
31
  end
19
32
  end
@@ -13,8 +13,6 @@ Implements the {Mobility::Backends::Column} backend for Sequel models.
13
13
  include Sequel
14
14
  include Column
15
15
 
16
- require 'mobility/backends/sequel/column/query_methods'
17
-
18
16
  # @!group Backend Accessors
19
17
  # @!macro backend_reader
20
18
  def read(locale, _options = nil)
@@ -34,7 +32,10 @@ Implements the {Mobility::Backends::Column} backend for Sequel models.
34
32
  available_locales.each { |l| yield(l) if present?(l) }
35
33
  end
36
34
 
37
- setup_query_methods(QueryMethods)
35
+ def self.build_op(attr, locale)
36
+ ::Sequel::SQL::QualifiedIdentifier.new(model_class.table_name,
37
+ Column.column_name_for(attr, locale))
38
+ end
38
39
 
39
40
  private
40
41
 
@@ -1,3 +1,7 @@
1
+ # frozen_string_literal: true
2
+ require "mobility/backends/sequel/json"
3
+ require "mobility/backends/sequel/jsonb"
4
+
1
5
  module Mobility
2
6
  module Backends
3
7
  =begin
@@ -8,9 +12,6 @@ Implements the {Mobility::Backends::Container} backend for Sequel models.
8
12
  class Sequel::Container
9
13
  include Sequel
10
14
 
11
- require 'mobility/backends/sequel/container/json_query_methods'
12
- require 'mobility/backends/sequel/container/jsonb_query_methods'
13
-
14
15
  # @!method column_name
15
16
  # @return [Symbol] (:translations) Name of translations column
16
17
  option_reader :column_name
@@ -58,8 +59,6 @@ Implements the {Mobility::Backends::Container} backend for Sequel models.
58
59
  end
59
60
  end
60
61
 
61
- backend_class = self
62
-
63
62
  setup do |attributes, options|
64
63
  column_name = options[:column_name]
65
64
  before_validation = Module.new do
@@ -76,13 +75,6 @@ Implements the {Mobility::Backends::Container} backend for Sequel models.
76
75
 
77
76
  plugin :defaults_setter
78
77
  attributes.each { |attribute| default_values[attribute.to_sym] = {} }
79
-
80
- query_methods = backend_class.const_get("#{options[:column_type].capitalize}QueryMethods")
81
- extend(Module.new do
82
- define_method ::Mobility.query_method do
83
- super().with_extend(query_methods.new(attributes, options))
84
- end
85
- end)
86
78
  end
87
79
 
88
80
  private
@@ -104,6 +96,23 @@ Implements the {Mobility::Backends::Container} backend for Sequel models.
104
96
  end
105
97
 
106
98
  class InvalidColumnType < StandardError; end
99
+
100
+ # @param [Symbol] name Attribute name
101
+ # @param [Symbol] locale Locale
102
+ # @return [Mobility::Backends::Sequel::Container::JSONOp,Mobility::Backends::Sequel::Container::JSONBOp]
103
+ def self.build_op(attr, locale)
104
+ klass = const_get("#{options[:column_type].upcase}Op")
105
+ klass.new(klass.new(column_name.to_sym)[locale.to_s]).get_text(attr)
106
+ end
107
+
108
+ class JSONOp < ::Sequel::Postgres::JSONOp; end
109
+
110
+ class JSONBOp < Jsonb::JSONBOp
111
+ def to_question
112
+ left = @value.args[0].value
113
+ JSONBOp === left ? ::Sequel.&(super, left.to_question) : super
114
+ end
115
+ end
107
116
  end
108
117
  end
109
118
  end
@@ -1,5 +1,7 @@
1
1
  require 'mobility/backends/sequel/pg_hash'
2
2
 
3
+ Sequel.extension :pg_hstore, :pg_hstore_ops
4
+
3
5
  module Mobility
4
6
  module Backends
5
7
  =begin
@@ -11,8 +13,6 @@ Implements the {Mobility::Backends::Hstore} backend for Sequel models.
11
13
  =end
12
14
  module Sequel
13
15
  class Hstore < PgHash
14
- require 'mobility/backends/sequel/hstore/query_methods'
15
-
16
16
  # @!group Backend Accessors
17
17
  # @!macro backend_reader
18
18
  # @!method read(locale, options = {})
@@ -24,7 +24,15 @@ Implements the {Mobility::Backends::Hstore} backend for Sequel models.
24
24
  end
25
25
  # @!endgroup
26
26
 
27
- setup_query_methods(QueryMethods)
27
+ # @param [Symbol] name Attribute name
28
+ # @param [Symbol] locale Locale
29
+ # @return [Mobility::Backends::Sequel::Hstore::HStoreOp]
30
+ def self.build_op(attr, locale)
31
+ column_name = column_affix % attr
32
+ HStoreOp.new(column_name.to_sym)[locale.to_s]
33
+ end
34
+
35
+ class HStoreOp < ::Sequel::Postgres::HStoreOp; end
28
36
  end
29
37
  end
30
38
  end
@@ -1,5 +1,7 @@
1
1
  require 'mobility/backends/sequel/pg_hash'
2
2
 
3
+ Sequel.extension :pg_json, :pg_json_ops
4
+
3
5
  module Mobility
4
6
  module Backends
5
7
  =begin
@@ -11,8 +13,6 @@ Implements the {Mobility::Backends::Json} backend for Sequel models.
11
13
  =end
12
14
  module Sequel
13
15
  class Json < PgHash
14
- require 'mobility/backends/sequel/json/query_methods'
15
-
16
16
  # @!group Backend Accessors
17
17
  #
18
18
  # @!method read(locale, options = {})
@@ -31,7 +31,15 @@ Implements the {Mobility::Backends::Json} backend for Sequel models.
31
31
  # @return [String,Integer,Boolean] Updated value
32
32
  # @!endgroup
33
33
 
34
- setup_query_methods(QueryMethods)
34
+ # @param [Symbol] name Attribute name
35
+ # @param [Symbol] locale Locale
36
+ # @return [Mobility::Backends::Sequel::Json::JSONOp]
37
+ def self.build_op(attr, locale)
38
+ column_name = column_affix % attr
39
+ JSONOp.new(column_name.to_sym).get_text(locale.to_s)
40
+ end
41
+
42
+ class JSONOp < ::Sequel::Postgres::JSONOp; end
35
43
  end
36
44
  end
37
45
  end
@@ -1,5 +1,8 @@
1
+ # frozen_string_literal: true
1
2
  require 'mobility/backends/sequel/pg_hash'
2
3
 
4
+ Sequel.extension :pg_json, :pg_json_ops
5
+
3
6
  module Mobility
4
7
  module Backends
5
8
  =begin
@@ -11,8 +14,6 @@ Implements the {Mobility::Backends::Jsonb} backend for Sequel models.
11
14
  =end
12
15
  module Sequel
13
16
  class Jsonb < PgHash
14
- require 'mobility/backends/sequel/jsonb/query_methods'
15
-
16
17
  # @!group Backend Accessors
17
18
  #
18
19
  # @!method read(locale, **options)
@@ -31,7 +32,38 @@ Implements the {Mobility::Backends::Jsonb} backend for Sequel models.
31
32
  # @return [String,Integer,Boolean] Updated value
32
33
  # @!endgroup
33
34
 
34
- setup_query_methods(QueryMethods)
35
+ # @param [Symbol] name Attribute name
36
+ # @param [Symbol] locale Locale
37
+ # @return [Mobility::Backends::Sequel::Jsonb::JSONBOp]
38
+ def self.build_op(attr, locale)
39
+ column_name = column_affix % attr
40
+ JSONBOp.new(column_name.to_sym).get_text(locale.to_s)
41
+ end
42
+
43
+ class JSONBOp < ::Sequel::Postgres::JSONBOp
44
+ def to_dash_arrow
45
+ column = @value.args[0].value
46
+ locale = @value.args[1]
47
+ ::Sequel.pg_jsonb_op(column)[locale]
48
+ end
49
+
50
+ def to_question
51
+ column = @value.args[0].value
52
+ locale = @value.args[1]
53
+ ::Sequel.pg_jsonb_op(column).has_key?(locale)
54
+ end
55
+
56
+ def =~(other)
57
+ case other
58
+ when Integer, Hash
59
+ to_dash_arrow =~ other.to_json
60
+ when NilClass
61
+ ~to_question
62
+ else
63
+ super
64
+ end
65
+ end
66
+ end
35
67
  end
36
68
  end
37
69
  end
@@ -6,6 +6,7 @@ require "mobility/sequel/column_changes"
6
6
  require "mobility/sequel/hash_initializer"
7
7
  require "mobility/sequel/string_translation"
8
8
  require "mobility/sequel/text_translation"
9
+ require "mobility/sequel/sql"
9
10
 
10
11
  module Mobility
11
12
  module Backends
@@ -23,23 +24,104 @@ Implements the {Mobility::Backends::KeyValue} backend for Sequel models.
23
24
  include KeyValue
24
25
  include Util
25
26
 
26
- require 'mobility/backends/sequel/key_value/query_methods'
27
-
28
- # @!group Backend Configuration
29
- # @option (see Mobility::Backends::KeyValue::ClassMethods#configure)
30
- # @raise (see Mobility::Backends::KeyValue::ClassMethods#configure)
31
- # @raise [CacheRequired] if cache is disabled
32
- def self.configure(options)
33
- raise CacheRequired, "Cache required for Sequel::KeyValue backend" if options[:cache] == false
34
- super
35
- if type = options[:type]
36
- options[:association_name] ||= :"#{options[:type]}_translations"
37
- options[:class_name] ||= Mobility::Sequel.const_get("#{type.capitalize}Translation")
27
+ class << self
28
+ # @!group Backend Configuration
29
+ # @option (see Mobility::Backends::KeyValue::ClassMethods#configure)
30
+ # @raise (see Mobility::Backends::KeyValue::ClassMethods#configure)
31
+ # @raise [CacheRequired] if cache is disabled
32
+ def configure(options)
33
+ raise CacheRequired, "Cache required for Sequel::KeyValue backend" if options[:cache] == false
34
+ super
35
+ if type = options[:type]
36
+ options[:association_name] ||= :"#{options[:type]}_translations"
37
+ options[:class_name] ||= Mobility::Sequel.const_get("#{type.capitalize}Translation")
38
+ end
39
+ options[:table_alias_affix] = "#{options[:model_class]}_%s_#{options[:association_name]}"
40
+ rescue NameError
41
+ raise ArgumentError, "You must define a Mobility::Sequel::#{type.capitalize}Translation class."
42
+ end
43
+ # @!endgroup
44
+
45
+ def build_op(attr, locale)
46
+ ::Mobility::Sequel::SQL::QualifiedIdentifier.new(table_alias(attr, locale), :value, locale, self, attribute_name: attr)
47
+ end
48
+
49
+ # @param [Sequel::Dataset] dataset Dataset to prepare
50
+ # @param [Object] predicate Predicate
51
+ # @param [Symbol] locale Locale
52
+ # @return [Sequel::Dataset] Prepared dataset
53
+ def prepare_dataset(dataset, predicate, locale)
54
+ visit(predicate, locale).inject(dataset) do |ds, (attr, join_type)|
55
+ join_translations(ds, attr, locale, join_type)
56
+ end
57
+ end
58
+
59
+ private
60
+
61
+ def join_translations(dataset, attr, locale, join_type)
62
+ dataset.join_table(join_type,
63
+ class_name.table_name,
64
+ {
65
+ key: attr.to_s,
66
+ locale: locale.to_s,
67
+ translatable_type: model_class.name,
68
+ translatable_id: ::Sequel[:"#{model_class.table_name}"][:id]
69
+ },
70
+ table_alias: table_alias(attr, locale))
71
+ end
72
+
73
+ # @return [Hash] Hash of attribute/join_type pairs
74
+ def visit(predicate, locale)
75
+ case predicate
76
+ when Array
77
+ visit_collection(predicate, locale)
78
+ when ::Mobility::Sequel::SQL::QualifiedIdentifier
79
+ visit_sql_identifier(predicate, locale)
80
+ when ::Sequel::SQL::BooleanExpression
81
+ visit_boolean(predicate, locale)
82
+ when ::Sequel::SQL::Expression
83
+ visit(predicate.args, locale)
84
+ else
85
+ {}
86
+ end
87
+ end
88
+
89
+ def visit_boolean(boolean, locale)
90
+ if boolean.op == :IS
91
+ nils, ops = boolean.args.partition(&:nil?)
92
+ if hash = visit(ops, locale)
93
+ join_type = nils.empty? ? :inner : :left_outer
94
+ # TODO: simplify to hash.transform_values { join_type } when
95
+ # support for Ruby 2.3 is deprecated
96
+ Hash[hash.keys.map { |key| [key, join_type] }]
97
+ else
98
+ {}
99
+ end
100
+ elsif boolean.op == :OR
101
+ hash = boolean.args.map { |op| visit(op, locale) }.
102
+ compact.inject(&:merge)
103
+ # TODO: simplify to hash.transform_values { :left_outer } when
104
+ # support for Ruby 2.3 is deprecated
105
+ Hash[hash.keys.map { |key| [key, :left_outer] }]
106
+ else
107
+ visit(boolean.args, locale)
108
+ end
109
+ end
110
+
111
+ def visit_collection(collection, locale)
112
+ collection.map { |p| visit(p, locale) }.compact.inject do |hash, visited|
113
+ visited.merge(hash) { |_, old, new| old == :inner ? old : new }
114
+ end
115
+ end
116
+
117
+ def visit_sql_identifier(identifier, locale)
118
+ if identifier.backend_class == self && identifier.locale == locale
119
+ { identifier.attribute_name => :inner }
120
+ else
121
+ {}
122
+ end
38
123
  end
39
- rescue NameError
40
- raise ArgumentError, "You must define a Mobility::Sequel::#{type.capitalize}Translation class."
41
124
  end
42
- # @!endgroup
43
125
 
44
126
  setup do |attributes, options|
45
127
  association_name = options[:association_name]
@@ -75,8 +157,6 @@ Implements the {Mobility::Backends::KeyValue} backend for Sequel models.
75
157
  include Mobility::Sequel::ColumnChanges.new(attributes)
76
158
  end
77
159
 
78
- setup_query_methods(QueryMethods)
79
-
80
160
  # Returns translation for a given locale, or initializes one if none is present.
81
161
  # @param [Symbol] locale
82
162
  # @return [Mobility::Sequel::TextTranslation,Mobility::Sequel::StringTranslation]