mobility 0.5.1 → 0.6.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 +4 -4
- checksums.yaml.gz.sig +2 -2
- data.tar.gz.sig +0 -0
- data/CHANGELOG.md +41 -1
- data/Gemfile.lock +3 -58
- data/README.md +22 -21
- data/lib/mobility.rb +3 -2
- data/lib/mobility/accumulator.rb +1 -2
- data/lib/mobility/active_model/backend_resetter.rb +1 -1
- data/lib/mobility/active_record.rb +12 -9
- data/lib/mobility/active_record/backend_resetter.rb +6 -7
- data/lib/mobility/active_record/uniqueness_validator.rb +12 -2
- data/lib/mobility/adapter.rb +1 -0
- data/lib/mobility/attributes.rb +3 -13
- data/lib/mobility/backends/active_record/column.rb +1 -0
- data/lib/mobility/backends/active_record/column/query_methods.rb +25 -20
- data/lib/mobility/backends/active_record/container/json_query_methods.rb +22 -16
- data/lib/mobility/backends/active_record/container/jsonb_query_methods.rb +19 -19
- data/lib/mobility/backends/active_record/hstore.rb +14 -12
- data/lib/mobility/backends/active_record/hstore/query_methods.rb +14 -5
- data/lib/mobility/backends/active_record/json.rb +21 -19
- data/lib/mobility/backends/active_record/json/query_methods.rb +16 -11
- data/lib/mobility/backends/active_record/jsonb.rb +21 -19
- data/lib/mobility/backends/active_record/jsonb/query_methods.rb +14 -5
- data/lib/mobility/backends/active_record/key_value.rb +9 -9
- data/lib/mobility/backends/active_record/key_value/query_methods.rb +53 -46
- data/lib/mobility/backends/active_record/pg_hash.rb +29 -25
- data/lib/mobility/backends/active_record/pg_query_methods.rb +76 -40
- data/lib/mobility/backends/active_record/query_methods.rb +17 -10
- data/lib/mobility/backends/active_record/serialized.rb +4 -2
- data/lib/mobility/backends/active_record/serialized/query_methods.rb +18 -15
- data/lib/mobility/backends/active_record/table.rb +21 -12
- data/lib/mobility/backends/active_record/table/query_methods.rb +82 -83
- data/lib/mobility/backends/hash_valued.rb +19 -0
- data/lib/mobility/backends/hstore.rb +3 -1
- data/lib/mobility/backends/json.rb +3 -1
- data/lib/mobility/backends/jsonb.rb +3 -1
- data/lib/mobility/backends/key_value.rb +32 -15
- data/lib/mobility/backends/sequel/column/query_methods.rb +16 -12
- data/lib/mobility/backends/sequel/container/json_query_methods.rb +25 -18
- data/lib/mobility/backends/sequel/container/jsonb_query_methods.rb +25 -18
- data/lib/mobility/backends/sequel/hstore.rb +14 -12
- data/lib/mobility/backends/sequel/hstore/query_methods.rb +18 -11
- data/lib/mobility/backends/sequel/json.rb +21 -19
- data/lib/mobility/backends/sequel/json/query_methods.rb +18 -11
- data/lib/mobility/backends/sequel/jsonb.rb +21 -19
- data/lib/mobility/backends/sequel/jsonb/query_methods.rb +18 -11
- data/lib/mobility/backends/sequel/key_value.rb +10 -11
- data/lib/mobility/backends/sequel/key_value/query_methods.rb +39 -34
- data/lib/mobility/backends/sequel/pg_hash.rb +37 -25
- data/lib/mobility/backends/sequel/pg_query_methods.rb +45 -20
- data/lib/mobility/backends/sequel/query_methods.rb +5 -0
- data/lib/mobility/backends/sequel/serialized.rb +18 -13
- data/lib/mobility/backends/sequel/serialized/query_methods.rb +10 -7
- data/lib/mobility/backends/sequel/table.rb +1 -1
- data/lib/mobility/backends/sequel/table/query_methods.rb +40 -35
- data/lib/mobility/plugins/cache/translation_cacher.rb +15 -15
- data/lib/mobility/plugins/default.rb +0 -7
- data/lib/mobility/plugins/fallbacks.rb +4 -0
- data/lib/mobility/sequel.rb +11 -5
- data/lib/mobility/sequel/backend_resetter.rb +6 -7
- data/lib/mobility/sequel/column_changes.rb +4 -4
- data/lib/mobility/version.rb +1 -1
- data/lib/rails/generators/mobility/backend_generators/base.rb +4 -0
- data/lib/rails/generators/mobility/backend_generators/table_backend.rb +0 -12
- data/lib/rails/generators/mobility/templates/column_translations.rb +2 -2
- data/lib/rails/generators/mobility/templates/create_string_translations.rb +5 -5
- data/lib/rails/generators/mobility/templates/create_text_translations.rb +5 -5
- data/lib/rails/generators/mobility/templates/initializer.rb +8 -0
- data/lib/rails/generators/mobility/templates/table_migration.rb +2 -3
- data/lib/rails/generators/mobility/templates/table_translations.rb +3 -4
- data/lib/rails/generators/mobility/translations_generator.rb +6 -5
- metadata +2 -3
- metadata.gz.sig +0 -0
- data/lib/mobility/backend/stringify_locale.rb +0 -18
@@ -3,29 +3,32 @@ require "mobility/backends/active_record/query_methods"
|
|
3
3
|
|
4
4
|
module Mobility
|
5
5
|
module Backends
|
6
|
-
|
7
|
-
|
6
|
+
module ActiveRecord
|
7
|
+
class Serialized::QueryMethods < QueryMethods
|
8
|
+
include Backends::Serialized
|
8
9
|
|
9
|
-
|
10
|
-
|
11
|
-
|
10
|
+
def initialize(attributes, _)
|
11
|
+
super
|
12
|
+
q = self
|
12
13
|
|
13
|
-
|
14
|
-
|
14
|
+
define_method :where! do |opts, *rest|
|
15
|
+
q.check_opts(opts) || super(opts, *rest)
|
16
|
+
end
|
15
17
|
end
|
16
|
-
end
|
17
18
|
|
18
|
-
|
19
|
-
|
20
|
-
|
19
|
+
def extended(relation)
|
20
|
+
super
|
21
|
+
q = self
|
21
22
|
|
22
|
-
|
23
|
-
|
24
|
-
|
23
|
+
mod = Module.new do
|
24
|
+
define_method :not do |opts, *rest|
|
25
|
+
q.check_opts(opts) || super(opts, *rest)
|
26
|
+
end
|
25
27
|
end
|
28
|
+
relation.mobility_where_chain.include(mod)
|
26
29
|
end
|
27
|
-
relation.mobility_where_chain.include(mod)
|
28
30
|
end
|
31
|
+
Serialized.private_constant :QueryMethods
|
29
32
|
end
|
30
33
|
end
|
31
34
|
end
|
@@ -20,6 +20,7 @@ columns to that table.
|
|
20
20
|
|
21
21
|
@example Model with table backend
|
22
22
|
class Post < ActiveRecord::Base
|
23
|
+
extend Mobility
|
23
24
|
translates :title, backend: :table
|
24
25
|
end
|
25
26
|
|
@@ -45,6 +46,7 @@ columns to that table.
|
|
45
46
|
|
46
47
|
@example Model with multiple translation tables
|
47
48
|
class Post < ActiveRecord::Base
|
49
|
+
extend Mobility
|
48
50
|
translates :title, backend: :table, table_name: :post_title_translations, association_name: :title_translations
|
49
51
|
translates :content, backend: :table, table_name: :post_content_translations, association_name: :content_translations
|
50
52
|
end
|
@@ -131,7 +133,8 @@ columns to that table.
|
|
131
133
|
foreign_key: options[:foreign_key],
|
132
134
|
dependent: :destroy,
|
133
135
|
autosave: true,
|
134
|
-
inverse_of: :translated_model
|
136
|
+
inverse_of: :translated_model,
|
137
|
+
extend: TranslationsHasManyExtension
|
135
138
|
|
136
139
|
translation_class.belongs_to :translated_model,
|
137
140
|
class_name: name,
|
@@ -139,7 +142,10 @@ columns to that table.
|
|
139
142
|
inverse_of: association_name,
|
140
143
|
touch: true
|
141
144
|
|
142
|
-
before_save
|
145
|
+
before_save do
|
146
|
+
required_attributes = self.class.translated_attribute_names & translation_class.attribute_names
|
147
|
+
send(association_name).destroy_empty_translations(required_attributes)
|
148
|
+
end
|
143
149
|
|
144
150
|
module_name = "MobilityArTable#{association_name.to_s.camelcase}"
|
145
151
|
unless const_defined?(module_name)
|
@@ -151,26 +157,29 @@ columns to that table.
|
|
151
157
|
end
|
152
158
|
include const_set(module_name, dupable)
|
153
159
|
end
|
154
|
-
|
155
|
-
include DestroyEmptyTranslations
|
156
160
|
end
|
157
161
|
|
158
162
|
setup_query_methods(QueryMethods)
|
159
163
|
|
164
|
+
# Returns translation for a given locale, or builds one if none is present.
|
165
|
+
# @param [Symbol] locale
|
160
166
|
def translation_for(locale, _)
|
161
|
-
translation = translations.
|
167
|
+
translation = translations.in_locale(locale)
|
162
168
|
translation ||= translations.build(locale: locale)
|
163
169
|
translation
|
164
170
|
end
|
165
171
|
|
166
|
-
module
|
167
|
-
|
172
|
+
module TranslationsHasManyExtension
|
173
|
+
# Returns translation in a given locale, or nil if none exist
|
174
|
+
# @param [Symbol, String] locale
|
175
|
+
def in_locale(locale)
|
176
|
+
locale = locale.to_s
|
177
|
+
find { |t| t.locale == locale }
|
178
|
+
end
|
168
179
|
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
send(association_name).destroy(t) if attrs.map(&t.method(:send)).none?
|
173
|
-
end
|
180
|
+
# Destroys translations with all empty values
|
181
|
+
def destroy_empty_translations(required_attributes)
|
182
|
+
each { |t| destroy(t) if required_attributes.map(&t.method(:send)).none? }
|
174
183
|
end
|
175
184
|
end
|
176
185
|
end
|
@@ -3,104 +3,103 @@ require "mobility/backends/active_record/query_methods"
|
|
3
3
|
|
4
4
|
module Mobility
|
5
5
|
module Backends
|
6
|
-
|
7
|
-
|
8
|
-
|
6
|
+
module ActiveRecord
|
7
|
+
class Table::QueryMethods < QueryMethods
|
8
|
+
def initialize(attributes, association_name: nil, model_class: nil, subclass_name: nil, **options)
|
9
|
+
super
|
9
10
|
|
10
|
-
|
11
|
-
|
11
|
+
@association_name = association_name
|
12
|
+
@translation_class = translation_class = model_class.const_get(subclass_name)
|
12
13
|
|
13
|
-
|
14
|
-
|
15
|
-
|
14
|
+
define_join_method(association_name, translation_class, **options)
|
15
|
+
define_query_methods(association_name, translation_class, **options)
|
16
|
+
end
|
16
17
|
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
18
|
+
def extended(relation)
|
19
|
+
super
|
20
|
+
association_name = @association_name
|
21
|
+
translation_class = @translation_class
|
22
|
+
q = self
|
22
23
|
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
24
|
+
mod = Module.new do
|
25
|
+
define_method :not do |opts, *rest|
|
26
|
+
if i18n_keys = q.extract_attributes(opts)
|
27
|
+
opts = opts.with_indifferent_access
|
28
|
+
i18n_keys.each do |attr|
|
29
|
+
opts["#{translation_class.table_name}.#{attr}"] = q.collapse opts.delete(attr)
|
30
|
+
end
|
31
|
+
super(opts, *rest).send("join_#{association_name}")
|
32
|
+
else
|
33
|
+
super(opts, *rest)
|
34
|
+
end
|
31
35
|
end
|
32
36
|
end
|
37
|
+
relation.mobility_where_chain.include(mod)
|
33
38
|
end
|
34
|
-
relation.mobility_where_chain.include(mod)
|
35
|
-
end
|
36
39
|
|
37
|
-
|
40
|
+
private
|
38
41
|
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
42
|
+
def define_join_method(association_name, translation_class, foreign_key: nil, table_name: nil, **)
|
43
|
+
define_method :"join_#{association_name}" do |**options|
|
44
|
+
if join = joins_values.find { |v| (Arel::Nodes::Join === v) && (v.left.name == table_name.to_s) }
|
45
|
+
return self if (options[:outer_join] || Arel::Nodes::InnerJoin === join)
|
46
|
+
self.joins_values = joins_values - [join]
|
47
|
+
end
|
48
|
+
t = translation_class.arel_table
|
49
|
+
m = arel_table
|
50
|
+
join_type = options[:outer_join] ? Arel::Nodes::OuterJoin : Arel::Nodes::InnerJoin
|
51
|
+
joins(m.join(t, join_type).
|
52
|
+
on(t[foreign_key].eq(m[:id]).
|
53
|
+
and(t[:locale].eq(Mobility.locale))).join_sources)
|
54
|
+
end
|
48
55
|
end
|
49
|
-
end
|
50
56
|
|
51
|
-
|
52
|
-
|
57
|
+
def define_query_methods(association_name, translation_class, **)
|
58
|
+
q = self
|
53
59
|
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
# We only need an OUTER JOIN if every value is either nil, or an
|
94
|
-
# array with at least one nil value.
|
95
|
-
outer_join: opts.values_at(*i18n_keys).compact.all? { |v| !Array(v).all? }
|
96
|
-
}
|
97
|
-
i18n_keys.each { |attr| opts["#{translation_class.table_name}.#{attr}"] = opts.delete(attr) }
|
98
|
-
super(opts, *rest).send("join_#{association_name}", options)
|
99
|
-
else
|
100
|
-
super(opts, *rest)
|
60
|
+
# Note that Mobility will try to use inner/outer joins appropriate to the query,
|
61
|
+
# so for example:
|
62
|
+
#
|
63
|
+
# Article.where(title: nil, content: nil) #=> OUTER JOIN (all nils)
|
64
|
+
# Article.where(title: "foo", content: nil) #=> INNER JOIN (one non-nil)
|
65
|
+
#
|
66
|
+
# In the first case, if we are in (say) the "en" locale, then we should match articles
|
67
|
+
# that have *no* article_translations with English locales (since no translation is
|
68
|
+
# equivalent to a nil value). If we used an inner join in the first case, an article
|
69
|
+
# with no English translations would be filtered out, so we use an outer join.
|
70
|
+
#
|
71
|
+
# When deciding whether to use an outer or inner join, array-valued
|
72
|
+
# conditions are treated as nil if they have any values.
|
73
|
+
#
|
74
|
+
# Article.where(title: nil, content: ["foo", nil]) #=> OUTER JOIN (all nil or array with nil)
|
75
|
+
# Article.where(title: "foo", content: ["foo", nil]) #=> INNER JOIN (one non-nil)
|
76
|
+
# Article.where(title: ["foo", "bar"], content: ["foo", nil]) #=> INNER JOIN (one non-nil array)
|
77
|
+
#
|
78
|
+
# The logic also applies when a query has more than one where clause.
|
79
|
+
#
|
80
|
+
# Article.where(title: nil).where(content: nil) #=> OUTER JOIN (all nils)
|
81
|
+
# Article.where(title: nil).where(content: "foo") #=> INNER JOIN (one non-nil)
|
82
|
+
# Article.where(title: "foo").where(content: nil) #=> INNER JOIN (one non-nil)
|
83
|
+
#
|
84
|
+
define_method :where! do |opts, *rest|
|
85
|
+
if i18n_keys = q.extract_attributes(opts)
|
86
|
+
opts = opts.with_indifferent_access
|
87
|
+
options = {
|
88
|
+
# We only need an OUTER JOIN if every value is either nil, or an
|
89
|
+
# array with at least one nil value.
|
90
|
+
outer_join: opts.values_at(*i18n_keys).compact.all? { |v| ![*v].all? }
|
91
|
+
}
|
92
|
+
i18n_keys.each do |attr|
|
93
|
+
opts["#{translation_class.table_name}.#{attr}"] = q.collapse opts.delete(attr)
|
94
|
+
end
|
95
|
+
super(opts, *rest).send("join_#{association_name}", options)
|
96
|
+
else
|
97
|
+
super(opts, *rest)
|
98
|
+
end
|
101
99
|
end
|
102
100
|
end
|
103
101
|
end
|
102
|
+
Table.private_constant :QueryMethods
|
104
103
|
end
|
105
104
|
end
|
106
105
|
end
|
@@ -1,3 +1,4 @@
|
|
1
|
+
# frozen_string_literal: true
|
1
2
|
module Mobility
|
2
3
|
module Backends
|
3
4
|
=begin
|
@@ -7,6 +8,16 @@ Defines read and write methods that access the value at a key with value
|
|
7
8
|
|
8
9
|
=end
|
9
10
|
module HashValued
|
11
|
+
# @!macro backend_constructor
|
12
|
+
# @option options [Symbol] column_prefix Prefix added to generate column
|
13
|
+
# name from attribute name
|
14
|
+
# @option options [Symbol] column_suffix Suffix added to generate column
|
15
|
+
# name from attribute name
|
16
|
+
def initialize(_model, _attribute, options = {})
|
17
|
+
super
|
18
|
+
@column_affix = "#{options[:column_prefix]}%s#{options[:column_suffix]}"
|
19
|
+
end
|
20
|
+
|
10
21
|
# @!group Backend Accessors
|
11
22
|
#
|
12
23
|
# @!macro backend_reader
|
@@ -24,6 +35,14 @@ Defines read and write methods that access the value at a key with value
|
|
24
35
|
def each_locale
|
25
36
|
translations.each { |l, _| yield l }
|
26
37
|
end
|
38
|
+
|
39
|
+
private
|
40
|
+
|
41
|
+
def column_name
|
42
|
+
@column_name ||= (@column_affix % attribute)
|
43
|
+
end
|
27
44
|
end
|
45
|
+
|
46
|
+
private_constant :HashValued
|
28
47
|
end
|
29
48
|
end
|
@@ -7,7 +7,9 @@ Stores translations as hash on Postgres hstore column.
|
|
7
7
|
|
8
8
|
==Backend Options
|
9
9
|
|
10
|
-
|
10
|
+
===+column_prefix+ and +column_suffix+
|
11
|
+
|
12
|
+
Prefix and suffix to add to attribute name to generate hstore column name.
|
11
13
|
|
12
14
|
@see Mobility::Backends::ActiveRecord::Hstore
|
13
15
|
@see Mobility::Backends::Sequel::Hstore
|
@@ -6,7 +6,9 @@ Stores translations as hash on Postgres json column.
|
|
6
6
|
|
7
7
|
==Backend Options
|
8
8
|
|
9
|
-
|
9
|
+
===+column_prefix+ and +column_suffix+
|
10
|
+
|
11
|
+
Prefix and suffix to add to attribute name to generate json column name.
|
10
12
|
|
11
13
|
@see Mobility::Backends::ActiveRecord::Json
|
12
14
|
@see Mobility::Backends::Sequel::Json
|
@@ -6,7 +6,9 @@ Stores translations as hash on Postgres jsonb column.
|
|
6
6
|
|
7
7
|
==Backend Options
|
8
8
|
|
9
|
-
|
9
|
+
===+prefix+ and +suffix+
|
10
|
+
|
11
|
+
Prefix and suffix to add to attribute name to generate jsonb column name.
|
10
12
|
|
11
13
|
@see Mobility::Backends::ActiveRecord::Jsonb
|
12
14
|
@see Mobility::Backends::Sequel::Jsonb
|
@@ -15,19 +15,12 @@ string-valued translations (the only difference being the column type of the
|
|
15
15
|
|
16
16
|
==Backend Options
|
17
17
|
|
18
|
-
===+association_name+
|
19
|
-
|
20
|
-
Name of association on model. Defaults to +text_translations+ (if +type+ is
|
21
|
-
+:text+) or +string_translations+ (if +type+ is +:string+). If specified,
|
22
|
-
ensure name does not overlap with other methods on model or with the
|
23
|
-
association name used by other backends on model (otherwise one will overwrite
|
24
|
-
the other).
|
25
|
-
|
26
18
|
===+type+
|
27
19
|
|
28
|
-
Currently, either +:text+ or +:string+ is supported
|
29
|
-
|
30
|
-
|
20
|
+
Currently, either +:text+ or +:string+ is supported, but any value is allowed
|
21
|
+
as long as a corresponding +class_name+ can be found (see below). Determines
|
22
|
+
which class to use for translations, which in turn determines which table to
|
23
|
+
use to store translations (by default +text_translations+ for text type,
|
31
24
|
+string_translations+ for string type).
|
32
25
|
|
33
26
|
===+class_name+
|
@@ -38,6 +31,14 @@ Class to use for translations when defining association. By default,
|
|
38
31
|
for Sequel models). If string is passed in, it will be constantized to get the
|
39
32
|
class.
|
40
33
|
|
34
|
+
===+association_name+
|
35
|
+
|
36
|
+
Name of association on model. Defaults to +<type>_translations+, which will
|
37
|
+
typically be either +:text_translations+ (if +type+ is +:text+) or
|
38
|
+
+:string_translations (if +type+ is +:string+). If specified, ensure name does
|
39
|
+
not overlap with other methods on model or with the association name used by
|
40
|
+
other backends on model (otherwise one will overwrite the other).
|
41
|
+
|
41
42
|
@see Mobility::Backends::ActiveRecord::KeyValue
|
42
43
|
@see Mobility::Backends::Sequel::KeyValue
|
43
44
|
|
@@ -84,11 +85,27 @@ class.
|
|
84
85
|
|
85
86
|
module ClassMethods
|
86
87
|
# @!group Backend Configuration
|
87
|
-
# @option options [Symbol,String] type
|
88
|
-
# @
|
88
|
+
# @option options [Symbol,String] type Column type to use
|
89
|
+
# @option options [Symbol] associaiton_name (:<type>_translations) Name
|
90
|
+
# of association method, defaults to +<type>_translations+
|
91
|
+
# @option options [Symbol] class_name Translation class, defaults to
|
92
|
+
# +Mobility::<ORM>::<type>Translation+
|
93
|
+
# @raise [ArgumentError] if +type+ is not set, and both +class_name+
|
94
|
+
# and +association_name+ are also not set
|
89
95
|
def configure(options)
|
90
|
-
options[:type]
|
91
|
-
|
96
|
+
options[:type] &&= options[:type].to_sym
|
97
|
+
options[:association_name] &&= options[:association_name].to_sym
|
98
|
+
options[:class_name] &&= Util.constantize(options[:class_name])
|
99
|
+
if !(options[:type] || (options[:class_name] && options[:association_name]))
|
100
|
+
# TODO: Remove warning and raise ArgumentError in v1.0
|
101
|
+
warn %{
|
102
|
+
WARNING: In previous versions, the Mobility KeyValue backend defaulted to a
|
103
|
+
text type column, but this behavior is now deprecated and will be removed in
|
104
|
+
the next release. Either explicitly specify the type by passing type: :text in
|
105
|
+
each translated model, or set a default option in your configuration.
|
106
|
+
}
|
107
|
+
options[:type] = :text
|
108
|
+
end
|
92
109
|
end
|
93
110
|
|
94
111
|
# Apply custom processing for plugin
|