mobility 0.7.6 → 0.8.0

Sign up to get free protection for your applications and to get access to all the features.
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]