mobility 0.8.13 → 1.0.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 +63 -0
- data/Gemfile +5 -2
- data/Gemfile.lock +39 -20
- data/README.md +183 -93
- data/lib/mobility.rb +101 -169
- data/lib/mobility/backend.rb +27 -51
- data/lib/mobility/backends.rb +20 -0
- data/lib/mobility/backends/active_record.rb +4 -0
- data/lib/mobility/backends/active_record/column.rb +3 -1
- data/lib/mobility/backends/active_record/container.rb +10 -11
- data/lib/mobility/backends/active_record/hstore.rb +6 -4
- data/lib/mobility/backends/active_record/json.rb +5 -3
- data/lib/mobility/backends/active_record/jsonb.rb +5 -3
- data/lib/mobility/backends/active_record/key_value.rb +31 -13
- data/lib/mobility/backends/active_record/pg_hash.rb +1 -1
- data/lib/mobility/backends/active_record/serialized.rb +6 -0
- data/lib/mobility/backends/active_record/table.rb +17 -10
- data/lib/mobility/backends/column.rb +0 -6
- data/lib/mobility/backends/container.rb +10 -1
- data/lib/mobility/backends/hash.rb +39 -0
- data/lib/mobility/backends/hash_valued.rb +4 -0
- data/lib/mobility/backends/hstore.rb +0 -1
- data/lib/mobility/backends/json.rb +0 -1
- data/lib/mobility/backends/jsonb.rb +1 -2
- data/lib/mobility/backends/key_value.rb +31 -26
- data/lib/mobility/backends/null.rb +2 -0
- data/lib/mobility/backends/sequel.rb +37 -2
- data/lib/mobility/backends/sequel/column.rb +2 -0
- data/lib/mobility/backends/sequel/container.rb +11 -9
- data/lib/mobility/backends/sequel/hstore.rb +3 -1
- data/lib/mobility/backends/sequel/json.rb +3 -0
- data/lib/mobility/backends/sequel/jsonb.rb +3 -1
- data/lib/mobility/backends/sequel/key_value.rb +87 -18
- data/lib/mobility/backends/sequel/pg_hash.rb +6 -6
- data/lib/mobility/backends/sequel/serialized.rb +6 -0
- data/lib/mobility/backends/sequel/table.rb +22 -9
- data/lib/mobility/backends/serialized.rb +1 -3
- data/lib/mobility/backends/table.rb +39 -31
- data/lib/mobility/pluggable.rb +56 -0
- data/lib/mobility/plugin.rb +260 -0
- data/lib/mobility/plugins.rb +27 -24
- data/lib/mobility/plugins/active_model.rb +17 -0
- data/lib/mobility/plugins/active_model/cache.rb +26 -0
- data/lib/mobility/plugins/active_model/dirty.rb +119 -78
- data/lib/mobility/plugins/active_record.rb +37 -0
- data/lib/mobility/plugins/active_record/backend.rb +27 -0
- data/lib/mobility/plugins/active_record/cache.rb +28 -0
- data/lib/mobility/plugins/active_record/dirty.rb +34 -17
- data/lib/mobility/plugins/active_record/query.rb +43 -31
- data/lib/mobility/plugins/active_record/uniqueness_validation.rb +64 -0
- data/lib/mobility/plugins/arel.rb +125 -0
- data/lib/mobility/plugins/arel/nodes.rb +15 -0
- data/lib/mobility/plugins/arel/nodes/pg_ops.rb +134 -0
- data/lib/mobility/plugins/attribute_methods.rb +29 -20
- data/lib/mobility/plugins/attributes.rb +72 -0
- data/lib/mobility/plugins/backend.rb +161 -0
- data/lib/mobility/plugins/backend_reader.rb +34 -0
- data/lib/mobility/plugins/cache.rb +68 -26
- data/lib/mobility/plugins/default.rb +22 -17
- data/lib/mobility/plugins/dirty.rb +12 -33
- data/lib/mobility/plugins/fallbacks.rb +52 -44
- data/lib/mobility/plugins/fallthrough_accessors.rb +19 -23
- data/lib/mobility/plugins/locale_accessors.rb +22 -35
- data/lib/mobility/plugins/presence.rb +28 -21
- data/lib/mobility/plugins/query.rb +8 -17
- data/lib/mobility/plugins/reader.rb +50 -0
- data/lib/mobility/plugins/sequel.rb +34 -0
- data/lib/mobility/plugins/sequel/backend.rb +25 -0
- data/lib/mobility/plugins/sequel/cache.rb +24 -0
- data/lib/mobility/plugins/sequel/dirty.rb +34 -23
- data/lib/mobility/plugins/sequel/query.rb +21 -6
- data/lib/mobility/plugins/writer.rb +44 -0
- data/lib/mobility/translations.rb +95 -0
- data/lib/mobility/version.rb +12 -1
- data/lib/rails/generators/mobility/templates/create_string_translations.rb +0 -1
- data/lib/rails/generators/mobility/templates/create_text_translations.rb +0 -1
- data/lib/rails/generators/mobility/templates/initializer.rb +104 -78
- metadata +35 -40
- metadata.gz.sig +0 -0
- data/lib/mobility/active_model.rb +0 -4
- data/lib/mobility/active_model/backend_resetter.rb +0 -26
- data/lib/mobility/active_record.rb +0 -23
- data/lib/mobility/active_record/backend_resetter.rb +0 -26
- data/lib/mobility/active_record/model_translation.rb +0 -14
- data/lib/mobility/active_record/string_translation.rb +0 -10
- data/lib/mobility/active_record/text_translation.rb +0 -10
- data/lib/mobility/active_record/translation.rb +0 -14
- data/lib/mobility/active_record/uniqueness_validator.rb +0 -60
- data/lib/mobility/arel.rb +0 -49
- data/lib/mobility/arel/nodes.rb +0 -13
- data/lib/mobility/arel/nodes/pg_ops.rb +0 -132
- data/lib/mobility/arel/visitor.rb +0 -61
- data/lib/mobility/attributes.rb +0 -324
- data/lib/mobility/backend/orm_delegator.rb +0 -44
- data/lib/mobility/backend_resetter.rb +0 -50
- data/lib/mobility/configuration.rb +0 -138
- data/lib/mobility/fallbacks.rb +0 -28
- data/lib/mobility/interface.rb +0 -0
- data/lib/mobility/loaded.rb +0 -4
- data/lib/mobility/plugins/active_record/attribute_methods.rb +0 -38
- data/lib/mobility/plugins/cache/translation_cacher.rb +0 -40
- data/lib/mobility/sequel.rb +0 -9
- data/lib/mobility/sequel/backend_resetter.rb +0 -23
- data/lib/mobility/sequel/column_changes.rb +0 -28
- data/lib/mobility/sequel/hash_initializer.rb +0 -21
- data/lib/mobility/sequel/model_translation.rb +0 -20
- data/lib/mobility/sequel/sql.rb +0 -16
- data/lib/mobility/sequel/string_translation.rb +0 -10
- data/lib/mobility/sequel/text_translation.rb +0 -10
- data/lib/mobility/sequel/translation.rb +0 -53
- data/lib/mobility/translates.rb +0 -73
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
module Mobility
|
|
2
|
-
module ActiveRecord
|
|
3
|
-
# @abstract Subclass and set +table_name+ to implement for a particular column type.
|
|
4
|
-
class Translation < ::ActiveRecord::Base
|
|
5
|
-
self.abstract_class = true
|
|
6
|
-
|
|
7
|
-
belongs_to :translatable, polymorphic: true, touch: true
|
|
8
|
-
|
|
9
|
-
validates :key, presence: true, uniqueness: { scope: [:translatable_id, :translatable_type, :locale], case_sensitive: true }
|
|
10
|
-
validates :translatable, presence: true
|
|
11
|
-
validates :locale, presence: true
|
|
12
|
-
end
|
|
13
|
-
end
|
|
14
|
-
end
|
|
@@ -1,60 +0,0 @@
|
|
|
1
|
-
module Mobility
|
|
2
|
-
module ActiveRecord
|
|
3
|
-
=begin
|
|
4
|
-
|
|
5
|
-
A backend-agnostic uniqueness validator for ActiveRecord translated attributes.
|
|
6
|
-
To use the validator, you must +extend Mobility+ before calling +validates+
|
|
7
|
-
(see example below).
|
|
8
|
-
|
|
9
|
-
@note This validator does not support case sensitivity, since doing so would
|
|
10
|
-
significantly complicate implementation.
|
|
11
|
-
|
|
12
|
-
@example Validating uniqueness on translated model
|
|
13
|
-
class Post < ActiveRecord::Base
|
|
14
|
-
extend Mobility
|
|
15
|
-
translates :title
|
|
16
|
-
|
|
17
|
-
# This must come *after* extending Mobility.
|
|
18
|
-
validates :title, uniqueness: true
|
|
19
|
-
end
|
|
20
|
-
=end
|
|
21
|
-
class UniquenessValidator < ::ActiveRecord::Validations::UniquenessValidator
|
|
22
|
-
# @param [ActiveRecord::Base] record Translated model
|
|
23
|
-
# @param [String] attribute Name of attribute
|
|
24
|
-
# @param [Object] value Attribute value
|
|
25
|
-
def validate_each(record, attribute, value)
|
|
26
|
-
klass = record.class
|
|
27
|
-
|
|
28
|
-
if (([*options[:scope]] + [attribute]).map(&:to_s) & klass.mobility_attributes).present?
|
|
29
|
-
return unless value.present?
|
|
30
|
-
relation = klass.unscoped.__mobility_query_scope__ do |m|
|
|
31
|
-
node = m.__send__(attribute)
|
|
32
|
-
options[:case_sensitive] == false ? node.lower.eq(value.downcase) : node.eq(value)
|
|
33
|
-
end
|
|
34
|
-
relation = relation.where.not(klass.primary_key => record.id) if record.persisted?
|
|
35
|
-
relation = mobility_scope_relation(record, relation)
|
|
36
|
-
relation = relation.merge(options[:conditions]) if options[:conditions]
|
|
37
|
-
|
|
38
|
-
if relation.exists?
|
|
39
|
-
error_options = options.except(:case_sensitive, :scope, :conditions)
|
|
40
|
-
error_options[:value] = value
|
|
41
|
-
|
|
42
|
-
record.errors.add(attribute, :taken, error_options)
|
|
43
|
-
end
|
|
44
|
-
else
|
|
45
|
-
super
|
|
46
|
-
end
|
|
47
|
-
end
|
|
48
|
-
|
|
49
|
-
private
|
|
50
|
-
|
|
51
|
-
def mobility_scope_relation(record, relation)
|
|
52
|
-
[*options[:scope]].inject(relation) do |scoped_relation, scope_item|
|
|
53
|
-
scoped_relation.__mobility_query_scope__ do |m|
|
|
54
|
-
m.__send__(scope_item).eq(record.send(scope_item))
|
|
55
|
-
end
|
|
56
|
-
end
|
|
57
|
-
end
|
|
58
|
-
end
|
|
59
|
-
end
|
|
60
|
-
end
|
data/lib/mobility/arel.rb
DELETED
|
@@ -1,49 +0,0 @@
|
|
|
1
|
-
# frozen-string-literal: true
|
|
2
|
-
require "mobility/arel/nodes"
|
|
3
|
-
require "mobility/arel/visitor"
|
|
4
|
-
|
|
5
|
-
module Mobility
|
|
6
|
-
module Arel
|
|
7
|
-
module MobilityExpressions
|
|
8
|
-
include ::Arel::Expressions
|
|
9
|
-
|
|
10
|
-
# @note This is necessary in order to ensure that when a translated
|
|
11
|
-
# attribute is selected with an alias using +AS+, the resulting
|
|
12
|
-
# expression can still be counted without blowing up.
|
|
13
|
-
#
|
|
14
|
-
# Extending +::Arel::Expressions+ is necessary to convince ActiveRecord
|
|
15
|
-
# that this node should not be stringified, which otherwise would
|
|
16
|
-
# result in garbage SQL.
|
|
17
|
-
#
|
|
18
|
-
# @see https://github.com/rails/rails/blob/847342c25c61acaea988430dc3ab66a82e3ed486/activerecord/lib/active_record/relation/calculations.rb#L261
|
|
19
|
-
def as(*)
|
|
20
|
-
super
|
|
21
|
-
.extend(::Arel::Expressions)
|
|
22
|
-
.extend(Countable)
|
|
23
|
-
end
|
|
24
|
-
|
|
25
|
-
module Countable
|
|
26
|
-
# @note This allows expressions with selected translated attributes to
|
|
27
|
-
# be counted.
|
|
28
|
-
def count(*args)
|
|
29
|
-
left.count(*args)
|
|
30
|
-
end
|
|
31
|
-
end
|
|
32
|
-
end
|
|
33
|
-
|
|
34
|
-
class Attribute < ::Arel::Attributes::Attribute
|
|
35
|
-
include MobilityExpressions
|
|
36
|
-
|
|
37
|
-
attr_reader :backend_class
|
|
38
|
-
attr_reader :locale
|
|
39
|
-
attr_reader :attribute_name
|
|
40
|
-
|
|
41
|
-
def initialize(relation, column_name, locale, backend_class, attribute_name: nil)
|
|
42
|
-
@backend_class = backend_class
|
|
43
|
-
@locale = locale
|
|
44
|
-
@attribute_name = attribute_name || column_name
|
|
45
|
-
super(relation, column_name)
|
|
46
|
-
end
|
|
47
|
-
end
|
|
48
|
-
end
|
|
49
|
-
end
|
data/lib/mobility/arel/nodes.rb
DELETED
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
# frozen-string-literal: true
|
|
2
|
-
module Mobility
|
|
3
|
-
module Arel
|
|
4
|
-
module Nodes
|
|
5
|
-
class Binary < ::Arel::Nodes::Binary; end
|
|
6
|
-
class Grouping < ::Arel::Nodes::Grouping; end
|
|
7
|
-
|
|
8
|
-
::Arel::Visitors::ToSql.class_eval do
|
|
9
|
-
alias :visit_Mobility_Arel_Nodes_Grouping :visit_Arel_Nodes_Grouping
|
|
10
|
-
end
|
|
11
|
-
end
|
|
12
|
-
end
|
|
13
|
-
end
|
|
@@ -1,132 +0,0 @@
|
|
|
1
|
-
# frozen-string-literal: true
|
|
2
|
-
require "mobility/arel"
|
|
3
|
-
|
|
4
|
-
module Mobility
|
|
5
|
-
module Arel
|
|
6
|
-
module Nodes
|
|
7
|
-
%w[
|
|
8
|
-
JsonDashArrow
|
|
9
|
-
JsonDashDoubleArrow
|
|
10
|
-
JsonbDashArrow
|
|
11
|
-
JsonbDashDoubleArrow
|
|
12
|
-
JsonbQuestion
|
|
13
|
-
HstoreDashArrow
|
|
14
|
-
HstoreQuestion
|
|
15
|
-
].each do |name|
|
|
16
|
-
const_set name, (Class.new(Binary) do
|
|
17
|
-
include ::Arel::Predications
|
|
18
|
-
include ::Arel::OrderPredications
|
|
19
|
-
include ::Arel::AliasPredication
|
|
20
|
-
include ::Mobility::Arel::MobilityExpressions
|
|
21
|
-
|
|
22
|
-
def lower
|
|
23
|
-
super self
|
|
24
|
-
end
|
|
25
|
-
end)
|
|
26
|
-
end
|
|
27
|
-
|
|
28
|
-
# Needed for AR 4.2, can be removed when support is deprecated
|
|
29
|
-
if ::ActiveRecord::VERSION::STRING < '5.0'
|
|
30
|
-
[JsonbDashDoubleArrow, HstoreDashArrow].each do |klass|
|
|
31
|
-
klass.class_eval do
|
|
32
|
-
def quoted_node other
|
|
33
|
-
other && super
|
|
34
|
-
end
|
|
35
|
-
end
|
|
36
|
-
end
|
|
37
|
-
end
|
|
38
|
-
|
|
39
|
-
class Jsonb < JsonbDashDoubleArrow
|
|
40
|
-
def to_dash_arrow
|
|
41
|
-
JsonbDashArrow.new left, right
|
|
42
|
-
end
|
|
43
|
-
|
|
44
|
-
def to_question
|
|
45
|
-
JsonbQuestion.new left, right
|
|
46
|
-
end
|
|
47
|
-
|
|
48
|
-
def eq other
|
|
49
|
-
case other
|
|
50
|
-
when NilClass
|
|
51
|
-
to_question.not
|
|
52
|
-
when Integer, Array, Hash
|
|
53
|
-
to_dash_arrow.eq other.to_json
|
|
54
|
-
when Jsonb
|
|
55
|
-
to_dash_arrow.eq other.to_dash_arrow
|
|
56
|
-
when JsonbDashArrow
|
|
57
|
-
to_dash_arrow.eq other
|
|
58
|
-
else
|
|
59
|
-
super
|
|
60
|
-
end
|
|
61
|
-
end
|
|
62
|
-
end
|
|
63
|
-
|
|
64
|
-
class Hstore < HstoreDashArrow
|
|
65
|
-
def to_question
|
|
66
|
-
HstoreQuestion.new left, right
|
|
67
|
-
end
|
|
68
|
-
|
|
69
|
-
def eq other
|
|
70
|
-
other.nil? ? to_question.not : super
|
|
71
|
-
end
|
|
72
|
-
end
|
|
73
|
-
|
|
74
|
-
class Json < JsonDashDoubleArrow; end
|
|
75
|
-
|
|
76
|
-
class JsonContainer < Json
|
|
77
|
-
def initialize column, locale, attr
|
|
78
|
-
super(Arel::Nodes::JsonDashArrow.new(column, locale), attr)
|
|
79
|
-
end
|
|
80
|
-
end
|
|
81
|
-
|
|
82
|
-
class JsonbContainer < Jsonb
|
|
83
|
-
def initialize column, locale, attr
|
|
84
|
-
@column, @locale = column, locale
|
|
85
|
-
super(JsonbDashArrow.new(column, locale), attr)
|
|
86
|
-
end
|
|
87
|
-
|
|
88
|
-
def eq other
|
|
89
|
-
other.nil? ? super.or(JsonbQuestion.new(@column, @locale).not) : super
|
|
90
|
-
end
|
|
91
|
-
end
|
|
92
|
-
end
|
|
93
|
-
|
|
94
|
-
module Visitors
|
|
95
|
-
def visit_Mobility_Arel_Nodes_JsonDashArrow o, a
|
|
96
|
-
json_infix o, a, '->'
|
|
97
|
-
end
|
|
98
|
-
|
|
99
|
-
def visit_Mobility_Arel_Nodes_JsonDashDoubleArrow o, a
|
|
100
|
-
json_infix o, a, '->>'
|
|
101
|
-
end
|
|
102
|
-
|
|
103
|
-
def visit_Mobility_Arel_Nodes_JsonbDashArrow o, a
|
|
104
|
-
json_infix o, a, '->'
|
|
105
|
-
end
|
|
106
|
-
|
|
107
|
-
def visit_Mobility_Arel_Nodes_JsonbDashDoubleArrow o, a
|
|
108
|
-
json_infix o, a, '->>'
|
|
109
|
-
end
|
|
110
|
-
|
|
111
|
-
def visit_Mobility_Arel_Nodes_JsonbQuestion o, a
|
|
112
|
-
json_infix o, a, '?'
|
|
113
|
-
end
|
|
114
|
-
|
|
115
|
-
def visit_Mobility_Arel_Nodes_HstoreDashArrow o, a
|
|
116
|
-
json_infix o, a, '->'
|
|
117
|
-
end
|
|
118
|
-
|
|
119
|
-
def visit_Mobility_Arel_Nodes_HstoreQuestion o, a
|
|
120
|
-
json_infix o, a, '?'
|
|
121
|
-
end
|
|
122
|
-
|
|
123
|
-
private
|
|
124
|
-
|
|
125
|
-
def json_infix o, a, opr
|
|
126
|
-
visit(Nodes::Grouping.new(::Arel::Nodes::InfixOperation.new(opr, o.left, o.right)), a)
|
|
127
|
-
end
|
|
128
|
-
end
|
|
129
|
-
|
|
130
|
-
::Arel::Visitors::PostgreSQL.include Visitors
|
|
131
|
-
end
|
|
132
|
-
end
|
|
@@ -1,61 +0,0 @@
|
|
|
1
|
-
# frozen-string-literal: true
|
|
2
|
-
module Mobility
|
|
3
|
-
module Arel
|
|
4
|
-
class Visitor < ::Arel::Visitors::Visitor
|
|
5
|
-
INNER_JOIN = ::Arel::Nodes::InnerJoin
|
|
6
|
-
OUTER_JOIN = ::Arel::Nodes::OuterJoin
|
|
7
|
-
|
|
8
|
-
attr_reader :backend_class, :locale
|
|
9
|
-
|
|
10
|
-
def initialize(backend_class, locale)
|
|
11
|
-
super()
|
|
12
|
-
@backend_class, @locale = backend_class, locale
|
|
13
|
-
end
|
|
14
|
-
|
|
15
|
-
private
|
|
16
|
-
|
|
17
|
-
def visit(*args)
|
|
18
|
-
super
|
|
19
|
-
rescue TypeError
|
|
20
|
-
visit_default(*args)
|
|
21
|
-
end
|
|
22
|
-
|
|
23
|
-
def visit_collection(_objects)
|
|
24
|
-
raise NotImplementedError
|
|
25
|
-
end
|
|
26
|
-
alias :visit_Array :visit_collection
|
|
27
|
-
|
|
28
|
-
def visit_Arel_Nodes_Unary(object)
|
|
29
|
-
visit(object.expr)
|
|
30
|
-
end
|
|
31
|
-
|
|
32
|
-
def visit_Arel_Nodes_Binary(object)
|
|
33
|
-
visit_collection([object.left, object.right])
|
|
34
|
-
end
|
|
35
|
-
|
|
36
|
-
def visit_Arel_Nodes_Function(object)
|
|
37
|
-
visit_collection(object.expressions)
|
|
38
|
-
end
|
|
39
|
-
|
|
40
|
-
def visit_Arel_Nodes_Case(object)
|
|
41
|
-
visit_collection([object.case, object.conditions, object.default])
|
|
42
|
-
end
|
|
43
|
-
|
|
44
|
-
def visit_Arel_Nodes_And(object)
|
|
45
|
-
visit_Array(object.children)
|
|
46
|
-
end
|
|
47
|
-
|
|
48
|
-
def visit_Arel_Nodes_Node(object)
|
|
49
|
-
visit_default(object)
|
|
50
|
-
end
|
|
51
|
-
|
|
52
|
-
def visit_Arel_Attributes_Attribute(object)
|
|
53
|
-
visit_default(object)
|
|
54
|
-
end
|
|
55
|
-
|
|
56
|
-
def visit_default(_object)
|
|
57
|
-
nil
|
|
58
|
-
end
|
|
59
|
-
end
|
|
60
|
-
end
|
|
61
|
-
end
|
data/lib/mobility/attributes.rb
DELETED
|
@@ -1,324 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
require "mobility/util"
|
|
3
|
-
|
|
4
|
-
module Mobility
|
|
5
|
-
=begin
|
|
6
|
-
|
|
7
|
-
Defines accessor methods to include on model class. Inspired by Traco's
|
|
8
|
-
+Traco::Attributes+ class.
|
|
9
|
-
|
|
10
|
-
Normally this class will be created through class methods defined using
|
|
11
|
-
{Mobility::Translates} accessor methods, and need not be created directly.
|
|
12
|
-
However, the class is central to how Mobility hooks into models to add
|
|
13
|
-
accessors and other methods, and should be useful as a reference when
|
|
14
|
-
understanding and designing backends.
|
|
15
|
-
|
|
16
|
-
==Including Attributes in a Class
|
|
17
|
-
|
|
18
|
-
Since {Attributes} is a subclass of +Module+, including an instance of it is
|
|
19
|
-
like including a module. Creating an instance like this:
|
|
20
|
-
|
|
21
|
-
Attributes.new("title", backend: :my_backend, locale_accessors: [:en, :ja], cache: true, fallbacks: true)
|
|
22
|
-
|
|
23
|
-
will generate an anonymous module that behaves approximately like this:
|
|
24
|
-
|
|
25
|
-
Module.new do
|
|
26
|
-
def mobility_backends
|
|
27
|
-
# Returns a memoized hash with attribute name keys and backend instance
|
|
28
|
-
# values. When a key is fetched from the hash, the hash calls
|
|
29
|
-
# +self.class.mobility_backend_class(name)+ (where +name+ is the
|
|
30
|
-
# attribute name) to get the backend class, then instantiate it (passing
|
|
31
|
-
# the model instance and attribute name to its initializer) and return it.
|
|
32
|
-
#
|
|
33
|
-
# The backend class returned from the class method
|
|
34
|
-
# +mobility_backend_class+ returns a subclass of
|
|
35
|
-
# +Mobility::Backends::MyBackend+ and includes into it:
|
|
36
|
-
#
|
|
37
|
-
# - Mobility::Plugins::Cache (from the +cache: true+ option)
|
|
38
|
-
# - instance of Mobility::Plugins::Fallbacks (from the +fallbacks: true+ option)
|
|
39
|
-
# - Mobility::Plugins::Presence (by default, disabled by +presence: false+)
|
|
40
|
-
end
|
|
41
|
-
|
|
42
|
-
def title(locale: Mobility.locale)
|
|
43
|
-
mobility_backends[:title].read(locale)
|
|
44
|
-
end
|
|
45
|
-
|
|
46
|
-
def title?(locale: Mobility.locale)
|
|
47
|
-
mobility_backends[:title].read(locale).present?
|
|
48
|
-
end
|
|
49
|
-
|
|
50
|
-
def title=(value, locale: Mobility.locale)
|
|
51
|
-
mobility_backends[:title].write(locale, value)
|
|
52
|
-
end
|
|
53
|
-
|
|
54
|
-
# Start Locale Accessors
|
|
55
|
-
#
|
|
56
|
-
def title_en
|
|
57
|
-
title(locale: :en)
|
|
58
|
-
end
|
|
59
|
-
|
|
60
|
-
def title_en?
|
|
61
|
-
title?(locale: :en)
|
|
62
|
-
end
|
|
63
|
-
|
|
64
|
-
def title_en=(value)
|
|
65
|
-
public_send(:title=, value, locale: :en)
|
|
66
|
-
end
|
|
67
|
-
|
|
68
|
-
def title_ja
|
|
69
|
-
title(locale: :ja)
|
|
70
|
-
end
|
|
71
|
-
|
|
72
|
-
def title_ja?
|
|
73
|
-
title?(locale: :ja)
|
|
74
|
-
end
|
|
75
|
-
|
|
76
|
-
def title_ja=(value)
|
|
77
|
-
public_send(:title=, value, locale: :ja)
|
|
78
|
-
end
|
|
79
|
-
# End Locale Accessors
|
|
80
|
-
end
|
|
81
|
-
|
|
82
|
-
Including this module into a model class will thus add the backend method, the
|
|
83
|
-
reader, writer and presence methods, and the locale accessor so the model
|
|
84
|
-
class. (These methods are in fact added to the model in an +included+ hook.)
|
|
85
|
-
|
|
86
|
-
Note that some simplifications have been made above for readability. (In
|
|
87
|
-
reality, all getters and setters accept an options hash which is passed along
|
|
88
|
-
to the backend instance.)
|
|
89
|
-
|
|
90
|
-
==Setting up the Model Class
|
|
91
|
-
|
|
92
|
-
Accessor methods alone are of limited use without a hook to actually modify the
|
|
93
|
-
model class. This hook is provided by the {Backend::Setup#setup_model} method,
|
|
94
|
-
which is added to every backend class when it includes the {Backend} module.
|
|
95
|
-
|
|
96
|
-
Assuming the backend has defined a setup block by calling +setup+, this block
|
|
97
|
-
will be called when {Attributes} is {#included} in the model class, passed
|
|
98
|
-
attributes and options defined when the backend was defined on the model class.
|
|
99
|
-
This allows a backend to do things like (for example) define associations on a
|
|
100
|
-
model class required by the backend, as happens in the {Backends::KeyValue} and
|
|
101
|
-
{Backends::Table} backends.
|
|
102
|
-
|
|
103
|
-
Since setup blocks are evaluated on the model class, it is possible that
|
|
104
|
-
backends can conflict (for example, overwriting previously defined methods).
|
|
105
|
-
Care should be taken to avoid defining methods on the model class, or where
|
|
106
|
-
necessary, ensure that names are defined in such a way as to avoid conflicts
|
|
107
|
-
with other backends.
|
|
108
|
-
|
|
109
|
-
=end
|
|
110
|
-
class Attributes < Module
|
|
111
|
-
|
|
112
|
-
# Method (accessor, reader or writer)
|
|
113
|
-
# @return [Symbol] method
|
|
114
|
-
attr_reader :method
|
|
115
|
-
|
|
116
|
-
# Attribute names for which accessors will be defined
|
|
117
|
-
# @return [Array<String>] Array of names
|
|
118
|
-
attr_reader :names
|
|
119
|
-
|
|
120
|
-
# Backend options
|
|
121
|
-
# @return [Hash] Backend options
|
|
122
|
-
attr_reader :options
|
|
123
|
-
|
|
124
|
-
# Backend class
|
|
125
|
-
# @return [Class] Backend class
|
|
126
|
-
attr_reader :backend_class
|
|
127
|
-
|
|
128
|
-
# Name of backend
|
|
129
|
-
# @return [Symbol,Class] Name of backend, or backend class
|
|
130
|
-
attr_reader :backend_name
|
|
131
|
-
|
|
132
|
-
# Model class
|
|
133
|
-
# @return [Class] Class of model
|
|
134
|
-
attr_reader :model_class
|
|
135
|
-
|
|
136
|
-
# @param [Symbol] method One of: [reader, writer, accessor]
|
|
137
|
-
# @param [Array<String>] attribute_names Names of attributes to define backend for
|
|
138
|
-
# @param [Hash] backend_options Backend options hash
|
|
139
|
-
# @option backend_options [Class] model_class Class of model
|
|
140
|
-
# @raise [ArgumentError] if method is not reader, writer or accessor
|
|
141
|
-
def initialize(*attribute_names, method: :accessor, backend: Mobility.default_backend, **backend_options)
|
|
142
|
-
raise ArgumentError, "method must be one of: reader, writer, accessor" unless %i[reader writer accessor].include?(method)
|
|
143
|
-
@method = method
|
|
144
|
-
@options = Mobility.default_options.to_h.merge(backend_options)
|
|
145
|
-
@names = attribute_names.map(&:to_s).freeze
|
|
146
|
-
raise BackendRequired, "Backend option required if Mobility.config.default_backend is not set." if backend.nil?
|
|
147
|
-
@backend_name = backend
|
|
148
|
-
end
|
|
149
|
-
|
|
150
|
-
# Setup backend class, include modules into model class, include/extend
|
|
151
|
-
# shared modules and setup model with backend setup block (see
|
|
152
|
-
# {Mobility::Backend::Setup#setup_model}).
|
|
153
|
-
# @param klass [Class] Class of model
|
|
154
|
-
def included(klass)
|
|
155
|
-
@model_class = @options[:model_class] = klass
|
|
156
|
-
@backend_class = get_backend_class(backend_name).for(model_class).with_options(options)
|
|
157
|
-
|
|
158
|
-
Mobility.plugins.each do |name|
|
|
159
|
-
plugin = get_plugin_class(name)
|
|
160
|
-
plugin.apply(self, options[name])
|
|
161
|
-
end
|
|
162
|
-
|
|
163
|
-
each do |name|
|
|
164
|
-
define_backend(name)
|
|
165
|
-
define_reader(name) if %i[accessor reader].include?(method)
|
|
166
|
-
define_writer(name) if %i[accessor writer].include?(method)
|
|
167
|
-
end
|
|
168
|
-
|
|
169
|
-
klass.include InstanceMethods
|
|
170
|
-
klass.extend ClassMethods
|
|
171
|
-
|
|
172
|
-
backend_class.setup_model(model_class, names)
|
|
173
|
-
end
|
|
174
|
-
|
|
175
|
-
# Yield each attribute name to block
|
|
176
|
-
# @yieldparam [String] Attribute
|
|
177
|
-
def each &block
|
|
178
|
-
names.each(&block)
|
|
179
|
-
end
|
|
180
|
-
|
|
181
|
-
# Show useful information about this module.
|
|
182
|
-
# @return [String]
|
|
183
|
-
def inspect
|
|
184
|
-
"#<Attributes (#{backend_name}) @names=#{names.join(", ")}>"
|
|
185
|
-
end
|
|
186
|
-
|
|
187
|
-
private
|
|
188
|
-
|
|
189
|
-
def define_backend(attribute)
|
|
190
|
-
module_eval <<-EOM, __FILE__, __LINE__ + 1
|
|
191
|
-
def #{Backend.method_name(attribute)}
|
|
192
|
-
mobility_backends[:#{attribute}]
|
|
193
|
-
end
|
|
194
|
-
EOM
|
|
195
|
-
end
|
|
196
|
-
|
|
197
|
-
def define_reader(attribute)
|
|
198
|
-
class_eval <<-EOM, __FILE__, __LINE__ + 1
|
|
199
|
-
def #{attribute}(**options)
|
|
200
|
-
return super() if options.delete(:super)
|
|
201
|
-
#{set_locale_from_options_inline}
|
|
202
|
-
mobility_backends[:#{attribute}].read(locale, options)
|
|
203
|
-
end
|
|
204
|
-
|
|
205
|
-
def #{attribute}?(**options)
|
|
206
|
-
return super() if options.delete(:super)
|
|
207
|
-
#{set_locale_from_options_inline}
|
|
208
|
-
mobility_backends[:#{attribute}].present?(locale, options)
|
|
209
|
-
end
|
|
210
|
-
EOM
|
|
211
|
-
end
|
|
212
|
-
|
|
213
|
-
def define_writer(attribute)
|
|
214
|
-
class_eval <<-EOM, __FILE__, __LINE__ + 1
|
|
215
|
-
def #{attribute}=(value, **options)
|
|
216
|
-
return super(value) if options.delete(:super)
|
|
217
|
-
#{set_locale_from_options_inline}
|
|
218
|
-
mobility_backends[:#{attribute}].write(locale, value, options)
|
|
219
|
-
end
|
|
220
|
-
EOM
|
|
221
|
-
end
|
|
222
|
-
|
|
223
|
-
# This string is evaluated inline in order to optimize performance of
|
|
224
|
-
# getters and setters, avoiding extra steps where they are unneeded.
|
|
225
|
-
def set_locale_from_options_inline
|
|
226
|
-
<<-EOL
|
|
227
|
-
if options[:locale]
|
|
228
|
-
#{"Mobility.enforce_available_locales!(options[:locale])" if I18n.enforce_available_locales}
|
|
229
|
-
locale = options[:locale].to_sym
|
|
230
|
-
options[:locale] &&= !!locale
|
|
231
|
-
else
|
|
232
|
-
locale = Mobility.locale
|
|
233
|
-
end
|
|
234
|
-
EOL
|
|
235
|
-
end
|
|
236
|
-
|
|
237
|
-
def get_backend_class(backend)
|
|
238
|
-
return backend if Module === backend
|
|
239
|
-
require "mobility/backends/#{backend}"
|
|
240
|
-
get_class_from_key(Mobility::Backends, backend)
|
|
241
|
-
end
|
|
242
|
-
|
|
243
|
-
def get_plugin_class(plugin)
|
|
244
|
-
require "mobility/plugins/#{plugin}"
|
|
245
|
-
get_class_from_key(Mobility::Plugins, plugin)
|
|
246
|
-
end
|
|
247
|
-
|
|
248
|
-
def get_class_from_key(parent_class, key)
|
|
249
|
-
klass_name = key.to_s.gsub(/(^|_)(.)/){|x| x[-1..-1].upcase}
|
|
250
|
-
parent_class.const_get(klass_name)
|
|
251
|
-
end
|
|
252
|
-
|
|
253
|
-
module InstanceMethods
|
|
254
|
-
# Return a new backend for an attribute name.
|
|
255
|
-
# @return [Hash] Hash of attribute names and backend instances
|
|
256
|
-
# @api private
|
|
257
|
-
def mobility_backends
|
|
258
|
-
@mobility_backends ||= Hash.new do |hash, name|
|
|
259
|
-
next hash[name.to_sym] if String === name
|
|
260
|
-
hash[name] = self.class.mobility_backend_class(name).new(self, name.to_s)
|
|
261
|
-
end
|
|
262
|
-
end
|
|
263
|
-
|
|
264
|
-
def initialize_dup(other)
|
|
265
|
-
@mobility_backends = nil
|
|
266
|
-
super
|
|
267
|
-
end
|
|
268
|
-
end
|
|
269
|
-
|
|
270
|
-
module ClassMethods
|
|
271
|
-
# Return all {Mobility::Attribute} module instances from among ancestors
|
|
272
|
-
# of this model.
|
|
273
|
-
# @return [Array<Mobility::Attributes>] Attribute modules
|
|
274
|
-
def mobility_modules
|
|
275
|
-
ancestors.grep(Attributes)
|
|
276
|
-
end
|
|
277
|
-
|
|
278
|
-
# Return translated attribute names on this model.
|
|
279
|
-
# @return [Array<String>] Attribute names
|
|
280
|
-
def mobility_attributes
|
|
281
|
-
mobility_modules.map(&:names).flatten.uniq
|
|
282
|
-
end
|
|
283
|
-
|
|
284
|
-
# Return true if attribute name is translated on this model.
|
|
285
|
-
# @param [String, Symbol] Attribute name
|
|
286
|
-
# @return [Boolean]
|
|
287
|
-
def mobility_attribute?(name)
|
|
288
|
-
mobility_attributes.include?(name.to_s)
|
|
289
|
-
end
|
|
290
|
-
|
|
291
|
-
# @!method translated_attribute_names
|
|
292
|
-
# @return (see #mobility_attributes)
|
|
293
|
-
alias translated_attribute_names mobility_attributes
|
|
294
|
-
|
|
295
|
-
# Return backend class for a given attribute name.
|
|
296
|
-
# @param [Symbol,String] Name of attribute
|
|
297
|
-
# @return [Class] Backend class
|
|
298
|
-
def mobility_backend_class(name)
|
|
299
|
-
@backends ||= BackendsCache.new(self)
|
|
300
|
-
@backends[name.to_sym]
|
|
301
|
-
end
|
|
302
|
-
|
|
303
|
-
class BackendsCache < Hash
|
|
304
|
-
def initialize(klass)
|
|
305
|
-
# Preload backend mapping
|
|
306
|
-
klass.mobility_modules.each do |mod|
|
|
307
|
-
mod.names.each { |name| self[name.to_sym] = mod.backend_class }
|
|
308
|
-
end
|
|
309
|
-
|
|
310
|
-
super() do |hash, name|
|
|
311
|
-
if mod = klass.mobility_modules.find { |m| m.names.include? name.to_s }
|
|
312
|
-
hash[name] = mod.backend_class
|
|
313
|
-
else
|
|
314
|
-
raise KeyError, "No backend for: #{name}."
|
|
315
|
-
end
|
|
316
|
-
end
|
|
317
|
-
end
|
|
318
|
-
end
|
|
319
|
-
private_constant :BackendsCache
|
|
320
|
-
end
|
|
321
|
-
end
|
|
322
|
-
|
|
323
|
-
class BackendRequired < ArgumentError; end
|
|
324
|
-
end
|