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.
Files changed (114) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +2 -2
  3. data.tar.gz.sig +0 -0
  4. data/CHANGELOG.md +63 -0
  5. data/Gemfile +5 -2
  6. data/Gemfile.lock +39 -20
  7. data/README.md +183 -93
  8. data/lib/mobility.rb +101 -169
  9. data/lib/mobility/backend.rb +27 -51
  10. data/lib/mobility/backends.rb +20 -0
  11. data/lib/mobility/backends/active_record.rb +4 -0
  12. data/lib/mobility/backends/active_record/column.rb +3 -1
  13. data/lib/mobility/backends/active_record/container.rb +10 -11
  14. data/lib/mobility/backends/active_record/hstore.rb +6 -4
  15. data/lib/mobility/backends/active_record/json.rb +5 -3
  16. data/lib/mobility/backends/active_record/jsonb.rb +5 -3
  17. data/lib/mobility/backends/active_record/key_value.rb +31 -13
  18. data/lib/mobility/backends/active_record/pg_hash.rb +1 -1
  19. data/lib/mobility/backends/active_record/serialized.rb +6 -0
  20. data/lib/mobility/backends/active_record/table.rb +17 -10
  21. data/lib/mobility/backends/column.rb +0 -6
  22. data/lib/mobility/backends/container.rb +10 -1
  23. data/lib/mobility/backends/hash.rb +39 -0
  24. data/lib/mobility/backends/hash_valued.rb +4 -0
  25. data/lib/mobility/backends/hstore.rb +0 -1
  26. data/lib/mobility/backends/json.rb +0 -1
  27. data/lib/mobility/backends/jsonb.rb +1 -2
  28. data/lib/mobility/backends/key_value.rb +31 -26
  29. data/lib/mobility/backends/null.rb +2 -0
  30. data/lib/mobility/backends/sequel.rb +37 -2
  31. data/lib/mobility/backends/sequel/column.rb +2 -0
  32. data/lib/mobility/backends/sequel/container.rb +11 -9
  33. data/lib/mobility/backends/sequel/hstore.rb +3 -1
  34. data/lib/mobility/backends/sequel/json.rb +3 -0
  35. data/lib/mobility/backends/sequel/jsonb.rb +3 -1
  36. data/lib/mobility/backends/sequel/key_value.rb +87 -18
  37. data/lib/mobility/backends/sequel/pg_hash.rb +6 -6
  38. data/lib/mobility/backends/sequel/serialized.rb +6 -0
  39. data/lib/mobility/backends/sequel/table.rb +22 -9
  40. data/lib/mobility/backends/serialized.rb +1 -3
  41. data/lib/mobility/backends/table.rb +39 -31
  42. data/lib/mobility/pluggable.rb +56 -0
  43. data/lib/mobility/plugin.rb +260 -0
  44. data/lib/mobility/plugins.rb +27 -24
  45. data/lib/mobility/plugins/active_model.rb +17 -0
  46. data/lib/mobility/plugins/active_model/cache.rb +26 -0
  47. data/lib/mobility/plugins/active_model/dirty.rb +119 -78
  48. data/lib/mobility/plugins/active_record.rb +37 -0
  49. data/lib/mobility/plugins/active_record/backend.rb +27 -0
  50. data/lib/mobility/plugins/active_record/cache.rb +28 -0
  51. data/lib/mobility/plugins/active_record/dirty.rb +34 -17
  52. data/lib/mobility/plugins/active_record/query.rb +43 -31
  53. data/lib/mobility/plugins/active_record/uniqueness_validation.rb +64 -0
  54. data/lib/mobility/plugins/arel.rb +125 -0
  55. data/lib/mobility/plugins/arel/nodes.rb +15 -0
  56. data/lib/mobility/plugins/arel/nodes/pg_ops.rb +134 -0
  57. data/lib/mobility/plugins/attribute_methods.rb +29 -20
  58. data/lib/mobility/plugins/attributes.rb +72 -0
  59. data/lib/mobility/plugins/backend.rb +161 -0
  60. data/lib/mobility/plugins/backend_reader.rb +34 -0
  61. data/lib/mobility/plugins/cache.rb +68 -26
  62. data/lib/mobility/plugins/default.rb +22 -17
  63. data/lib/mobility/plugins/dirty.rb +12 -33
  64. data/lib/mobility/plugins/fallbacks.rb +52 -44
  65. data/lib/mobility/plugins/fallthrough_accessors.rb +19 -23
  66. data/lib/mobility/plugins/locale_accessors.rb +22 -35
  67. data/lib/mobility/plugins/presence.rb +28 -21
  68. data/lib/mobility/plugins/query.rb +8 -17
  69. data/lib/mobility/plugins/reader.rb +50 -0
  70. data/lib/mobility/plugins/sequel.rb +34 -0
  71. data/lib/mobility/plugins/sequel/backend.rb +25 -0
  72. data/lib/mobility/plugins/sequel/cache.rb +24 -0
  73. data/lib/mobility/plugins/sequel/dirty.rb +34 -23
  74. data/lib/mobility/plugins/sequel/query.rb +21 -6
  75. data/lib/mobility/plugins/writer.rb +44 -0
  76. data/lib/mobility/translations.rb +95 -0
  77. data/lib/mobility/version.rb +12 -1
  78. data/lib/rails/generators/mobility/templates/create_string_translations.rb +0 -1
  79. data/lib/rails/generators/mobility/templates/create_text_translations.rb +0 -1
  80. data/lib/rails/generators/mobility/templates/initializer.rb +104 -78
  81. metadata +35 -40
  82. metadata.gz.sig +0 -0
  83. data/lib/mobility/active_model.rb +0 -4
  84. data/lib/mobility/active_model/backend_resetter.rb +0 -26
  85. data/lib/mobility/active_record.rb +0 -23
  86. data/lib/mobility/active_record/backend_resetter.rb +0 -26
  87. data/lib/mobility/active_record/model_translation.rb +0 -14
  88. data/lib/mobility/active_record/string_translation.rb +0 -10
  89. data/lib/mobility/active_record/text_translation.rb +0 -10
  90. data/lib/mobility/active_record/translation.rb +0 -14
  91. data/lib/mobility/active_record/uniqueness_validator.rb +0 -60
  92. data/lib/mobility/arel.rb +0 -49
  93. data/lib/mobility/arel/nodes.rb +0 -13
  94. data/lib/mobility/arel/nodes/pg_ops.rb +0 -132
  95. data/lib/mobility/arel/visitor.rb +0 -61
  96. data/lib/mobility/attributes.rb +0 -324
  97. data/lib/mobility/backend/orm_delegator.rb +0 -44
  98. data/lib/mobility/backend_resetter.rb +0 -50
  99. data/lib/mobility/configuration.rb +0 -138
  100. data/lib/mobility/fallbacks.rb +0 -28
  101. data/lib/mobility/interface.rb +0 -0
  102. data/lib/mobility/loaded.rb +0 -4
  103. data/lib/mobility/plugins/active_record/attribute_methods.rb +0 -38
  104. data/lib/mobility/plugins/cache/translation_cacher.rb +0 -40
  105. data/lib/mobility/sequel.rb +0 -9
  106. data/lib/mobility/sequel/backend_resetter.rb +0 -23
  107. data/lib/mobility/sequel/column_changes.rb +0 -28
  108. data/lib/mobility/sequel/hash_initializer.rb +0 -21
  109. data/lib/mobility/sequel/model_translation.rb +0 -20
  110. data/lib/mobility/sequel/sql.rb +0 -16
  111. data/lib/mobility/sequel/string_translation.rb +0 -10
  112. data/lib/mobility/sequel/text_translation.rb +0 -10
  113. data/lib/mobility/sequel/translation.rb +0 -53
  114. data/lib/mobility/translates.rb +0 -73
@@ -1,10 +0,0 @@
1
- # frozen_string_literal: true
2
- require "mobility/active_record/translation"
3
-
4
- module Mobility
5
- module ActiveRecord
6
- class StringTranslation < Translation
7
- self.table_name = "mobility_string_translations"
8
- end
9
- end
10
- end
@@ -1,10 +0,0 @@
1
- # frozen_string_literal: true
2
- require "mobility/active_record/translation"
3
-
4
- module Mobility
5
- module ActiveRecord
6
- class TextTranslation < Translation
7
- self.table_name = "mobility_text_translations"
8
- end
9
- end
10
- end
@@ -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
@@ -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
@@ -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
@@ -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