mobility 0.6.0 → 0.7.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.
- checksums.yaml +5 -5
- checksums.yaml.gz.sig +3 -2
- data.tar.gz.sig +0 -0
- data/CHANGELOG.md +39 -1
- data/Gemfile.lock +65 -10
- data/README.md +63 -27
- data/lib/mobility.rb +16 -31
- data/lib/mobility/active_record.rb +2 -12
- data/lib/mobility/active_record/uniqueness_validator.rb +9 -8
- data/lib/mobility/arel.rb +20 -0
- data/lib/mobility/arel/nodes.rb +16 -0
- data/lib/mobility/arel/nodes/pg_ops.rb +136 -0
- data/lib/mobility/arel/visitor.rb +61 -0
- data/lib/mobility/attributes.rb +82 -19
- data/lib/mobility/backend.rb +53 -8
- data/lib/mobility/backend_resetter.rb +2 -1
- data/lib/mobility/backends/active_record.rb +31 -11
- data/lib/mobility/backends/active_record/column.rb +7 -3
- data/lib/mobility/backends/active_record/container.rb +23 -21
- data/lib/mobility/backends/active_record/hstore.rb +11 -6
- data/lib/mobility/backends/active_record/json.rb +22 -16
- data/lib/mobility/backends/active_record/jsonb.rb +22 -16
- data/lib/mobility/backends/active_record/key_value.rb +123 -15
- data/lib/mobility/backends/active_record/pg_hash.rb +1 -2
- data/lib/mobility/backends/active_record/serialized.rb +7 -6
- data/lib/mobility/backends/active_record/table.rb +145 -24
- data/lib/mobility/backends/hash_valued.rb +15 -10
- data/lib/mobility/backends/key_value.rb +12 -12
- data/lib/mobility/backends/sequel/container.rb +3 -9
- data/lib/mobility/backends/sequel/hstore.rb +2 -2
- data/lib/mobility/backends/sequel/json.rb +15 -15
- data/lib/mobility/backends/sequel/jsonb.rb +14 -14
- data/lib/mobility/backends/sequel/key_value.rb +0 -11
- data/lib/mobility/backends/sequel/pg_hash.rb +2 -3
- data/lib/mobility/backends/sequel/pg_query_methods.rb +1 -1
- data/lib/mobility/backends/sequel/query_methods.rb +3 -3
- data/lib/mobility/backends/sequel/serialized.rb +2 -2
- data/lib/mobility/backends/sequel/table.rb +10 -11
- data/lib/mobility/backends/table.rb +17 -8
- data/lib/mobility/configuration.rb +4 -1
- data/lib/mobility/interface.rb +0 -0
- data/lib/mobility/plugins.rb +1 -0
- data/lib/mobility/plugins/active_record/query.rb +192 -0
- data/lib/mobility/plugins/cache.rb +1 -2
- data/lib/mobility/plugins/default.rb +28 -14
- data/lib/mobility/plugins/fallbacks.rb +1 -1
- data/lib/mobility/plugins/locale_accessors.rb +13 -9
- data/lib/mobility/plugins/presence.rb +15 -7
- data/lib/mobility/plugins/query.rb +28 -0
- data/lib/mobility/translates.rb +9 -9
- data/lib/mobility/version.rb +1 -1
- data/lib/rails/generators/mobility/templates/initializer.rb +1 -0
- metadata +10 -15
- metadata.gz.sig +0 -0
- data/lib/mobility/accumulator.rb +0 -33
- data/lib/mobility/adapter.rb +0 -20
- data/lib/mobility/backends/active_record/column/query_methods.rb +0 -42
- data/lib/mobility/backends/active_record/container/json_query_methods.rb +0 -36
- data/lib/mobility/backends/active_record/container/jsonb_query_methods.rb +0 -33
- data/lib/mobility/backends/active_record/hstore/query_methods.rb +0 -25
- data/lib/mobility/backends/active_record/json/query_methods.rb +0 -30
- data/lib/mobility/backends/active_record/jsonb/query_methods.rb +0 -26
- data/lib/mobility/backends/active_record/key_value/query_methods.rb +0 -76
- data/lib/mobility/backends/active_record/pg_query_methods.rb +0 -154
- data/lib/mobility/backends/active_record/serialized/query_methods.rb +0 -34
- data/lib/mobility/backends/active_record/table/query_methods.rb +0 -105
@@ -25,17 +25,6 @@ Implements the {Mobility::Backends::KeyValue} backend for Sequel models.
|
|
25
25
|
|
26
26
|
require 'mobility/backends/sequel/key_value/query_methods'
|
27
27
|
|
28
|
-
# @return [Class] translation model class
|
29
|
-
attr_reader :class_name
|
30
|
-
|
31
|
-
# @!macro backend_constructor
|
32
|
-
# @option options [Symbol] association_name Name of association
|
33
|
-
# @option options [Class] class_name Translation model class
|
34
|
-
def initialize(model, attribute, options = {})
|
35
|
-
super
|
36
|
-
@class_name = options[:class_name]
|
37
|
-
end
|
38
|
-
|
39
28
|
# @!group Backend Configuration
|
40
29
|
# @option (see Mobility::Backends::KeyValue::ClassMethods#configure)
|
41
30
|
# @raise (see Mobility::Backends::KeyValue::ClassMethods#configure)
|
@@ -35,8 +35,7 @@ jsonb).
|
|
35
35
|
end
|
36
36
|
|
37
37
|
setup do |attributes, options|
|
38
|
-
|
39
|
-
columns = attributes.map { |attribute| (column_affix % attribute).to_sym }
|
38
|
+
columns = attributes.map { |attribute| (options[:column_affix] % attribute).to_sym }
|
40
39
|
|
41
40
|
before_validation = Module.new do
|
42
41
|
define_method :before_validation do
|
@@ -48,7 +47,7 @@ jsonb).
|
|
48
47
|
end
|
49
48
|
include before_validation
|
50
49
|
include Mobility::Sequel::HashInitializer.new(*columns)
|
51
|
-
include Mobility::Sequel::ColumnChanges.new(attributes, column_affix: column_affix)
|
50
|
+
include Mobility::Sequel::ColumnChanges.new(attributes, column_affix: options[:column_affix])
|
52
51
|
|
53
52
|
plugin :defaults_setter
|
54
53
|
columns.each { |column| default_values[column] = {} }
|
@@ -13,11 +13,11 @@ models. For details see backend-specific subclasses.
|
|
13
13
|
class QueryMethods < Module
|
14
14
|
# @param [Array<String>] attributes Translated attributes
|
15
15
|
def initialize(attributes, _)
|
16
|
-
@attributes = attributes.map
|
16
|
+
@attributes = attributes.map(&:to_sym)
|
17
17
|
|
18
|
-
attributes.each do |attribute|
|
18
|
+
@attributes.each do |attribute|
|
19
19
|
define_method :"first_by_#{attribute}" do |value|
|
20
|
-
where(attribute
|
20
|
+
where(attribute => value).select_all(model.table_name).first
|
21
21
|
end
|
22
22
|
end
|
23
23
|
end
|
@@ -43,14 +43,14 @@ Sequel serialization plugin.
|
|
43
43
|
# @option (see Backends::Serialized.configure)
|
44
44
|
# @raise (see Backends::Serialized.configure)
|
45
45
|
def self.configure(options)
|
46
|
+
super
|
46
47
|
Serialized.configure(options)
|
47
48
|
end
|
48
49
|
# @!endgroup
|
49
50
|
|
50
51
|
setup do |attributes, options|
|
51
52
|
format = options[:format]
|
52
|
-
|
53
|
-
columns = attributes.map { |attribute| (column_affix % attribute).to_sym }
|
53
|
+
columns = attributes.map { |attribute| (options[:column_affix] % attribute).to_sym }
|
54
54
|
|
55
55
|
plugin :serialization
|
56
56
|
plugin :serialization_modification_detection
|
@@ -14,17 +14,16 @@ Implements the {Mobility::Backends::Table} backend for Sequel models.
|
|
14
14
|
class Sequel::Table
|
15
15
|
include Sequel
|
16
16
|
include Table
|
17
|
-
include Util
|
18
17
|
|
19
18
|
require 'mobility/backends/sequel/table/query_methods'
|
20
19
|
|
21
|
-
|
22
|
-
|
20
|
+
def translation_class
|
21
|
+
self.class.translation_class
|
22
|
+
end
|
23
23
|
|
24
|
-
#
|
25
|
-
def
|
26
|
-
|
27
|
-
@translation_class = options[:model_class].const_get(options[:subclass_name])
|
24
|
+
# @return [Symbol] class for translations
|
25
|
+
def self.translation_class
|
26
|
+
@translation_class ||= model_class.const_get(subclass_name)
|
28
27
|
end
|
29
28
|
|
30
29
|
# @!group Backend Configuration
|
@@ -35,11 +34,11 @@ Implements the {Mobility::Backends::Table} backend for Sequel models.
|
|
35
34
|
# @raise [CacheRequired] if cache option is false
|
36
35
|
def self.configure(options)
|
37
36
|
raise CacheRequired, "Cache required for Sequel::Table backend" if options[:cache] == false
|
38
|
-
table_name = options[:model_class].table_name
|
39
|
-
options[:table_name] ||= :"#{
|
40
|
-
options[:foreign_key] ||= foreign_key(camelize(
|
37
|
+
table_name = Util.singularize(options[:model_class].table_name)
|
38
|
+
options[:table_name] ||= :"#{table_name}_translations"
|
39
|
+
options[:foreign_key] ||= Util.foreign_key(Util.camelize(table_name.downcase))
|
41
40
|
if association_name = options[:association_name]
|
42
|
-
options[:subclass_name] ||= camelize(singularize(association_name))
|
41
|
+
options[:subclass_name] ||= Util.camelize(Util.singularize(association_name))
|
43
42
|
else
|
44
43
|
options[:association_name] = :translations
|
45
44
|
options[:subclass_name] ||= :Translation
|
@@ -66,16 +66,21 @@ set.
|
|
66
66
|
=end
|
67
67
|
module Table
|
68
68
|
extend Backend::OrmDelegator
|
69
|
+
# @!method association_name
|
70
|
+
# Returns the name of the translations association.
|
71
|
+
# @return [Symbol] Name of the association
|
69
72
|
|
70
|
-
#
|
71
|
-
|
73
|
+
# @!method subclass_name
|
74
|
+
# Returns translation subclass under model class namespace.
|
75
|
+
# @return [Symbol] Name of translation subclass
|
72
76
|
|
73
|
-
# @!
|
74
|
-
#
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
77
|
+
# @!method foreign_key
|
78
|
+
# Returns foreign_key for translations association.
|
79
|
+
# @return [Symbol] Name of foreign key
|
80
|
+
|
81
|
+
# @!method table_name
|
82
|
+
# Returns name of table where translations are stored.
|
83
|
+
# @return [Symbol] Name of translations table
|
79
84
|
|
80
85
|
# @!group Backend Accessors
|
81
86
|
# @!macro backend_reader
|
@@ -102,6 +107,10 @@ set.
|
|
102
107
|
|
103
108
|
def self.included(backend)
|
104
109
|
backend.extend ClassMethods
|
110
|
+
backend.option_reader :association_name
|
111
|
+
backend.option_reader :subclass_name
|
112
|
+
backend.option_reader :foreign_key
|
113
|
+
backend.option_reader :table_name
|
105
114
|
end
|
106
115
|
|
107
116
|
module ClassMethods
|
@@ -106,10 +106,13 @@ default_fallbacks= will be removed in the next major version of Mobility.
|
|
106
106
|
@fallbacks_generator = lambda { |fallbacks| Mobility::Fallbacks.build(fallbacks) }
|
107
107
|
@default_accessor_locales = lambda { I18n.available_locales }
|
108
108
|
@default_options = Options[{
|
109
|
-
cache:
|
109
|
+
cache: true,
|
110
110
|
presence: true,
|
111
|
+
query: true,
|
112
|
+
default: Plugins::OPTION_UNSET
|
111
113
|
}]
|
112
114
|
@plugins = %i[
|
115
|
+
query
|
113
116
|
cache
|
114
117
|
dirty
|
115
118
|
fallbacks
|
File without changes
|
data/lib/mobility/plugins.rb
CHANGED
@@ -0,0 +1,192 @@
|
|
1
|
+
# frozen-string-literal: true
|
2
|
+
module Mobility
|
3
|
+
module Plugins
|
4
|
+
=begin
|
5
|
+
|
6
|
+
Adds a scope which enables querying on translated attributes using +where+ and
|
7
|
+
+not+ as if they were normal attributes. Under the hood, this plugin uses the
|
8
|
+
generic +build_node+ and +apply_scope+ methods implemented in each backend
|
9
|
+
class to build ActiveRecord queries from Arel nodes. The plugin also adds
|
10
|
+
+find_by_<attribute>+ shortcuts for translated attributes.
|
11
|
+
|
12
|
+
The query scope applies to all translated attributes once the plugin has been
|
13
|
+
enabled for any one attribute on the model.
|
14
|
+
|
15
|
+
=end
|
16
|
+
module ActiveRecord
|
17
|
+
module Query
|
18
|
+
class << self
|
19
|
+
def apply(attributes)
|
20
|
+
model_class = attributes.model_class
|
21
|
+
|
22
|
+
model_class.class_eval do
|
23
|
+
extend QueryMethod
|
24
|
+
extend FindByMethods.new(*attributes.names)
|
25
|
+
singleton_class.send :alias_method, Mobility.query_method, :__mobility_query_scope__
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
module QueryMethod
|
31
|
+
def __mobility_query_scope__(locale: Mobility.locale, &block)
|
32
|
+
if block_given?
|
33
|
+
VirtualRow.build_query(self, locale, &block)
|
34
|
+
else
|
35
|
+
all.extending(QueryExtension)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
# Internal class to create a "clean room" for manipulating translated
|
41
|
+
# attribute nodes in an instance-eval'ed block. Inspired by Sequel's
|
42
|
+
# (much more sophisticated) virtual rows.
|
43
|
+
class VirtualRow < BasicObject
|
44
|
+
attr_reader :__backends
|
45
|
+
|
46
|
+
def initialize(model_class, locale)
|
47
|
+
@model_class, @locale, @__backends = model_class, locale, []
|
48
|
+
end
|
49
|
+
|
50
|
+
def method_missing(m, *)
|
51
|
+
if @model_class.mobility_attributes.include?(m.to_s)
|
52
|
+
@__backends |= [@model_class.mobility_backend_class(m)]
|
53
|
+
@model_class.mobility_backend_class(m).build_node(m, @locale)
|
54
|
+
elsif @model_class.column_names.include?(m.to_s)
|
55
|
+
@model_class.arel_table[m]
|
56
|
+
else
|
57
|
+
super
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
class << self
|
62
|
+
def build_query(klass, locale, &block)
|
63
|
+
row = new(klass, locale)
|
64
|
+
query = block.arity.zero? ? row.instance_eval(&block) : block.call(row)
|
65
|
+
|
66
|
+
if ::ActiveRecord::Relation === query
|
67
|
+
predicates = query.arel.constraints
|
68
|
+
apply_scopes(klass.all, row.__backends, locale, predicates).merge(query)
|
69
|
+
else
|
70
|
+
apply_scopes(klass.all, row.__backends, locale, query).where(query)
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
private
|
75
|
+
|
76
|
+
def apply_scopes(scope, backends, locale, predicates)
|
77
|
+
backends.inject(scope) { |r, b| b.apply_scope(r, predicates, locale) }
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
private_constant :QueryMethod, :VirtualRow
|
82
|
+
|
83
|
+
module QueryExtension
|
84
|
+
def where!(opts, *rest)
|
85
|
+
QueryBuilder.build(self, opts) do |untranslated_opts|
|
86
|
+
untranslated_opts ? super(untranslated_opts, *rest) : super
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
def where(opts = :chain, *rest)
|
91
|
+
opts == :chain ? WhereChain.new(spawn) : super
|
92
|
+
end
|
93
|
+
|
94
|
+
# Return backend node for attribute name.
|
95
|
+
# @param [Symbol,String] name Name of attribute
|
96
|
+
# @param [Symbol] locale Locale
|
97
|
+
# @return [Arel::Node] Arel node for this attribute in given locale
|
98
|
+
def backend_node(name, locale = Mobility.locale)
|
99
|
+
@klass.mobility_backend_class(name)[name, locale]
|
100
|
+
end
|
101
|
+
|
102
|
+
class WhereChain < ::ActiveRecord::QueryMethods::WhereChain
|
103
|
+
def not(opts, *rest)
|
104
|
+
QueryBuilder.build(@scope, opts, invert: true) do |untranslated_opts|
|
105
|
+
untranslated_opts ? super(untranslated_opts, *rest) : super
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
module QueryBuilder
|
111
|
+
class << self
|
112
|
+
def build(scope, where_opts, invert: false)
|
113
|
+
return yield unless Hash === where_opts
|
114
|
+
|
115
|
+
opts = where_opts.with_indifferent_access
|
116
|
+
locale = opts.delete(:locale) || Mobility.locale
|
117
|
+
|
118
|
+
maps = build_maps!(scope, opts, locale, invert: invert)
|
119
|
+
return yield if maps.empty?
|
120
|
+
|
121
|
+
base = opts.empty? ? scope : yield(opts)
|
122
|
+
maps.inject(base) { |rel, map| map[rel] }
|
123
|
+
end
|
124
|
+
|
125
|
+
private
|
126
|
+
|
127
|
+
def build_maps!(scope, opts, locale, invert:)
|
128
|
+
keys = opts.keys.map(&:to_s)
|
129
|
+
|
130
|
+
scope.mobility_modules.map { |mod|
|
131
|
+
next if (i18n_keys = mod.names & keys).empty?
|
132
|
+
|
133
|
+
predicates = i18n_keys.map do |key|
|
134
|
+
build_predicate(scope.backend_node(key.to_sym, locale), opts.delete(key))
|
135
|
+
end
|
136
|
+
|
137
|
+
->(relation) do
|
138
|
+
relation = mod.backend_class.apply_scope(relation, predicates, locale, invert: invert)
|
139
|
+
predicates = predicates.map(&method(:invert_predicate)) if invert
|
140
|
+
relation.where(predicates.inject(&:and))
|
141
|
+
end
|
142
|
+
}.compact
|
143
|
+
end
|
144
|
+
|
145
|
+
def build_predicate(node, values)
|
146
|
+
nils, vals = partition_values(values)
|
147
|
+
|
148
|
+
return node.eq(nil) if vals.empty?
|
149
|
+
|
150
|
+
predicate = vals.length == 1 ? node.eq(vals.first) : node.in(vals)
|
151
|
+
predicate = predicate.or(node.eq(nil)) unless nils.empty?
|
152
|
+
predicate
|
153
|
+
end
|
154
|
+
|
155
|
+
def partition_values(values)
|
156
|
+
Array.wrap(values).uniq.partition(&:nil?)
|
157
|
+
end
|
158
|
+
|
159
|
+
# Adapted from AR::Relation::WhereClause#invert_predicate
|
160
|
+
def invert_predicate(node)
|
161
|
+
case node
|
162
|
+
when ::Arel::Nodes::In
|
163
|
+
::Arel::Nodes::NotIn.new(node.left, node.right)
|
164
|
+
when ::Arel::Nodes::Equality
|
165
|
+
::Arel::Nodes::NotEqual.new(node.left, node.right)
|
166
|
+
else
|
167
|
+
::Arel::Nodes::Not.new(node)
|
168
|
+
end
|
169
|
+
end
|
170
|
+
end
|
171
|
+
end
|
172
|
+
|
173
|
+
private_constant :WhereChain, :QueryBuilder
|
174
|
+
end
|
175
|
+
|
176
|
+
class FindByMethods < Module
|
177
|
+
def initialize(*attributes)
|
178
|
+
attributes.each do |attribute|
|
179
|
+
module_eval <<-EOM, __FILE__, __LINE__ + 1
|
180
|
+
def find_by_#{attribute}(value)
|
181
|
+
find_by(#{attribute}: value)
|
182
|
+
end
|
183
|
+
EOM
|
184
|
+
end
|
185
|
+
end
|
186
|
+
end
|
187
|
+
|
188
|
+
private_constant :QueryExtension, :FindByMethods
|
189
|
+
end
|
190
|
+
end
|
191
|
+
end
|
192
|
+
end
|
@@ -37,9 +37,8 @@ Values are added to the cache in two ways:
|
|
37
37
|
# @group Backend Accessors
|
38
38
|
#
|
39
39
|
# @!macro backend_reader
|
40
|
-
# @option options [Boolean] cache
|
41
|
-
# *false* to disable cache.
|
42
40
|
# @!method read(locale, value, options = {})
|
41
|
+
# @option options [Boolean] cache *false* to disable cache.
|
43
42
|
include TranslationCacher.new(:read)
|
44
43
|
|
45
44
|
# @!macro backend_writer
|
@@ -5,7 +5,9 @@ module Mobility
|
|
5
5
|
=begin
|
6
6
|
|
7
7
|
Defines value or proc to fall through to if return value from getter would
|
8
|
-
otherwise be nil.
|
8
|
+
otherwise be nil. This plugin is disabled by default but will be enabled if any
|
9
|
+
value (other than +Mobility::Plugins::OPTION_UNSET+) is passed as the +default+
|
10
|
+
option key.
|
9
11
|
|
10
12
|
If default is a +Proc+, it will be called with the context of the model, and
|
11
13
|
passed arguments:
|
@@ -60,27 +62,39 @@ The proc can accept zero to three arguments (see examples below)
|
|
60
62
|
post.title(default: lambda { self.class.name.to_s })
|
61
63
|
#=> "Post"
|
62
64
|
=end
|
63
|
-
|
65
|
+
module Default
|
64
66
|
# Applies default plugin to attributes.
|
65
67
|
# @param [Attributes] attributes
|
66
68
|
# @param [Object] option
|
67
69
|
def self.apply(attributes, option)
|
68
|
-
attributes.backend_class.include(
|
70
|
+
attributes.backend_class.include(self) unless option == Plugins::OPTION_UNSET
|
69
71
|
end
|
70
72
|
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
73
|
+
# Generate a default value for given parameters.
|
74
|
+
# @param [Object, Proc] default_value A default value or Proc
|
75
|
+
# @param [Symbol] locale
|
76
|
+
# @param [Hash] accessor_options
|
77
|
+
# @param [String] attribute
|
78
|
+
def self.[](default_value, locale:, accessor_options:, model:, attribute:)
|
79
|
+
return default_value unless default_value.is_a?(Proc)
|
80
|
+
args = [attribute, locale, accessor_options]
|
81
|
+
args = args.first(default_value.arity) unless default_value.arity < 0
|
82
|
+
model.instance_exec(*args, &default_value)
|
83
|
+
end
|
84
|
+
|
85
|
+
# @!group Backend Accessors
|
86
|
+
# @!macro backend_reader
|
87
|
+
# @option accessor_options [Boolean] default
|
88
|
+
# *false* to disable presence filter.
|
89
|
+
def read(locale, accessor_options = {})
|
90
|
+
default = accessor_options.has_key?(:default) ? accessor_options.delete(:default) : options[:default]
|
91
|
+
if (value = super(locale, accessor_options)).nil?
|
92
|
+
Default[default, locale: locale, accessor_options: accessor_options, model: model, attribute: attribute]
|
93
|
+
else
|
94
|
+
value
|
82
95
|
end
|
83
96
|
end
|
97
|
+
# @!endgroup
|
84
98
|
end
|
85
99
|
end
|
86
100
|
end
|