rom-core 4.2.1 → 5.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 (139) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +0 -2
  3. data/lib/rom-core.rb +2 -0
  4. data/lib/rom/array_dataset.rb +2 -0
  5. data/lib/rom/association_set.rb +2 -0
  6. data/lib/rom/associations/abstract.rb +2 -0
  7. data/lib/rom/associations/definitions.rb +2 -0
  8. data/lib/rom/associations/definitions/abstract.rb +2 -0
  9. data/lib/rom/associations/definitions/many_to_many.rb +2 -0
  10. data/lib/rom/associations/definitions/many_to_one.rb +2 -0
  11. data/lib/rom/associations/definitions/one_to_many.rb +2 -0
  12. data/lib/rom/associations/definitions/one_to_one.rb +2 -0
  13. data/lib/rom/associations/definitions/one_to_one_through.rb +2 -0
  14. data/lib/rom/associations/many_to_many.rb +2 -0
  15. data/lib/rom/associations/many_to_one.rb +2 -0
  16. data/lib/rom/associations/one_to_many.rb +2 -0
  17. data/lib/rom/associations/one_to_one.rb +2 -0
  18. data/lib/rom/associations/one_to_one_through.rb +2 -0
  19. data/lib/rom/associations/through_identifier.rb +2 -0
  20. data/lib/rom/attribute.rb +58 -72
  21. data/lib/rom/auto_curry.rb +2 -0
  22. data/lib/rom/cache.rb +2 -0
  23. data/lib/rom/command.rb +7 -5
  24. data/lib/rom/command_compiler.rb +2 -0
  25. data/lib/rom/command_proxy.rb +2 -0
  26. data/lib/rom/command_registry.rb +13 -7
  27. data/lib/rom/commands.rb +2 -0
  28. data/lib/rom/commands/class_interface.rb +4 -2
  29. data/lib/rom/commands/composite.rb +2 -0
  30. data/lib/rom/commands/create.rb +2 -0
  31. data/lib/rom/commands/delete.rb +2 -0
  32. data/lib/rom/commands/graph.rb +2 -0
  33. data/lib/rom/commands/graph/class_interface.rb +2 -0
  34. data/lib/rom/commands/graph/input_evaluator.rb +2 -0
  35. data/lib/rom/commands/lazy.rb +2 -0
  36. data/lib/rom/commands/lazy/create.rb +2 -0
  37. data/lib/rom/commands/lazy/delete.rb +2 -0
  38. data/lib/rom/commands/lazy/update.rb +2 -0
  39. data/lib/rom/commands/update.rb +2 -0
  40. data/lib/rom/configuration.rb +2 -0
  41. data/lib/rom/configuration_dsl.rb +2 -0
  42. data/lib/rom/configuration_dsl/command.rb +2 -0
  43. data/lib/rom/configuration_dsl/command_dsl.rb +2 -0
  44. data/lib/rom/configuration_dsl/relation.rb +2 -0
  45. data/lib/rom/configuration_plugin.rb +2 -0
  46. data/lib/rom/constants.rb +3 -0
  47. data/lib/rom/container.rb +2 -0
  48. data/lib/rom/core.rb +4 -1
  49. data/lib/rom/create_container.rb +2 -0
  50. data/lib/rom/data_proxy.rb +2 -0
  51. data/lib/rom/enumerable_dataset.rb +2 -0
  52. data/lib/rom/environment.rb +2 -0
  53. data/lib/rom/gateway.rb +2 -0
  54. data/lib/rom/global.rb +2 -0
  55. data/lib/rom/global/plugin_dsl.rb +2 -0
  56. data/lib/rom/header.rb +198 -0
  57. data/lib/rom/header/attribute.rb +192 -0
  58. data/lib/rom/initializer.rb +2 -0
  59. data/lib/rom/lint/enumerable_dataset.rb +2 -0
  60. data/lib/rom/lint/gateway.rb +2 -0
  61. data/lib/rom/lint/linter.rb +2 -0
  62. data/lib/rom/lint/spec.rb +2 -0
  63. data/lib/rom/lint/test.rb +2 -0
  64. data/lib/rom/mapper.rb +100 -0
  65. data/lib/rom/mapper/attribute_dsl.rb +480 -0
  66. data/lib/rom/mapper/builder.rb +39 -0
  67. data/lib/rom/mapper/configuration_plugin.rb +28 -0
  68. data/lib/rom/mapper/dsl.rb +123 -0
  69. data/lib/rom/mapper/mapper_dsl.rb +45 -0
  70. data/lib/rom/mapper/model_dsl.rb +60 -0
  71. data/lib/rom/mapper_compiler.rb +84 -0
  72. data/lib/rom/mapper_registry.rb +2 -0
  73. data/lib/rom/memory.rb +2 -0
  74. data/lib/rom/memory/associations.rb +2 -0
  75. data/lib/rom/memory/associations/many_to_many.rb +2 -0
  76. data/lib/rom/memory/associations/many_to_one.rb +2 -0
  77. data/lib/rom/memory/associations/one_to_many.rb +2 -0
  78. data/lib/rom/memory/associations/one_to_one.rb +2 -0
  79. data/lib/rom/memory/commands.rb +2 -0
  80. data/lib/rom/memory/dataset.rb +2 -0
  81. data/lib/rom/memory/gateway.rb +2 -0
  82. data/lib/rom/memory/mapper_compiler.rb +2 -0
  83. data/lib/rom/memory/relation.rb +2 -0
  84. data/lib/rom/memory/schema.rb +2 -0
  85. data/lib/rom/memory/storage.rb +2 -0
  86. data/lib/rom/memory/types.rb +2 -0
  87. data/lib/rom/model_builder.rb +103 -0
  88. data/lib/rom/open_struct.rb +37 -0
  89. data/lib/rom/pipeline.rb +2 -0
  90. data/lib/rom/plugin.rb +2 -0
  91. data/lib/rom/plugin_base.rb +2 -0
  92. data/lib/rom/plugin_registry.rb +2 -0
  93. data/lib/rom/plugins/command/schema.rb +2 -0
  94. data/lib/rom/plugins/command/timestamps.rb +2 -0
  95. data/lib/rom/plugins/relation/instrumentation.rb +2 -0
  96. data/lib/rom/plugins/relation/registry_reader.rb +2 -0
  97. data/lib/rom/plugins/schema/timestamps.rb +8 -1
  98. data/lib/rom/processor.rb +30 -0
  99. data/lib/rom/processor/transproc.rb +417 -0
  100. data/lib/rom/registry.rb +2 -0
  101. data/lib/rom/relation.rb +4 -2
  102. data/lib/rom/relation/class_interface.rb +2 -0
  103. data/lib/rom/relation/combined.rb +2 -0
  104. data/lib/rom/relation/commands.rb +2 -0
  105. data/lib/rom/relation/composite.rb +2 -0
  106. data/lib/rom/relation/curried.rb +3 -1
  107. data/lib/rom/relation/graph.rb +2 -0
  108. data/lib/rom/relation/loaded.rb +2 -0
  109. data/lib/rom/relation/materializable.rb +2 -0
  110. data/lib/rom/relation/name.rb +2 -0
  111. data/lib/rom/relation/view_dsl.rb +2 -0
  112. data/lib/rom/relation/wrap.rb +2 -0
  113. data/lib/rom/relation_registry.rb +2 -0
  114. data/lib/rom/schema.rb +39 -6
  115. data/lib/rom/schema/associations_dsl.rb +5 -3
  116. data/lib/rom/schema/dsl.rb +41 -11
  117. data/lib/rom/schema/inferrer.rb +21 -3
  118. data/lib/rom/schema_plugin.rb +2 -0
  119. data/lib/rom/setup.rb +2 -0
  120. data/lib/rom/setup/auto_registration.rb +2 -0
  121. data/lib/rom/setup/auto_registration_strategies/base.rb +3 -1
  122. data/lib/rom/setup/auto_registration_strategies/custom_namespace.rb +2 -0
  123. data/lib/rom/setup/auto_registration_strategies/no_namespace.rb +2 -0
  124. data/lib/rom/setup/auto_registration_strategies/with_namespace.rb +2 -0
  125. data/lib/rom/setup/finalize.rb +2 -0
  126. data/lib/rom/setup/finalize/finalize_commands.rb +2 -0
  127. data/lib/rom/setup/finalize/finalize_mappers.rb +2 -0
  128. data/lib/rom/setup/finalize/finalize_relations.rb +2 -0
  129. data/lib/rom/struct.rb +108 -0
  130. data/lib/rom/struct_compiler.rb +110 -0
  131. data/lib/rom/support/configurable.rb +2 -0
  132. data/lib/rom/support/inflector.rb +2 -0
  133. data/lib/rom/support/memoizable.rb +2 -0
  134. data/lib/rom/support/notifications.rb +2 -0
  135. data/lib/rom/transaction.rb +2 -0
  136. data/lib/rom/transformer.rb +34 -0
  137. data/lib/rom/types.rb +10 -3
  138. data/lib/rom/version.rb +3 -1
  139. metadata +37 -21
@@ -0,0 +1,192 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'dry/equalizer'
4
+
5
+ module ROM
6
+ class Header
7
+ # An attribute provides information about a specific attribute in a tuple
8
+ #
9
+ # This may include information about how an attribute should be renamed,
10
+ # or how its value should coerced.
11
+ #
12
+ # More complex attributes describe how an attribute should be transformed.
13
+ #
14
+ # @private
15
+ class Attribute
16
+ include Dry::Equalizer(:name, :key, :type)
17
+
18
+ # @return [Symbol] name of an attribute
19
+ #
20
+ # @api private
21
+ attr_reader :name
22
+
23
+ # @return [Symbol] key of an attribute that corresponds to tuple attribute
24
+ #
25
+ # @api private
26
+ attr_reader :key
27
+
28
+ # @return [Symbol] type identifier (defaults to :object)
29
+ #
30
+ # @api private
31
+ attr_reader :type
32
+
33
+ # @return [Hash] additional meta information
34
+ #
35
+ # @api private
36
+ attr_reader :meta
37
+
38
+ # Return attribute class for a given meta hash
39
+ #
40
+ # @param [Hash] meta hash with type information and optional transformation info
41
+ #
42
+ # @return [Class]
43
+ #
44
+ # @api private
45
+ def self.[](meta)
46
+ key = (meta.keys & TYPE_MAP.keys).first
47
+ TYPE_MAP.fetch(key || meta[:type], self)
48
+ end
49
+
50
+ # Coerce an array with attribute meta-data into an attribute object
51
+ #
52
+ # @param [Array<Symbol,Hash>] input attribute name/options pair
53
+ #
54
+ # @return [Attribute]
55
+ #
56
+ # @api private
57
+ def self.coerce(input)
58
+ name = input[0]
59
+ meta = (input[1] || {}).dup
60
+
61
+ meta[:type] ||= :object
62
+
63
+ if meta.key?(:header)
64
+ meta[:header] = Header.coerce(meta[:header], model: meta[:model])
65
+ end
66
+
67
+ self[meta].new(name, meta)
68
+ end
69
+
70
+ # @api private
71
+ def initialize(name, meta)
72
+ @name = name
73
+ @meta = meta
74
+ @key = meta.fetch(:from) { name }
75
+ @type = meta.fetch(:type)
76
+ end
77
+
78
+ # Return if an attribute has a specific type identifier
79
+ #
80
+ # @api private
81
+ def typed?
82
+ type != :object
83
+ end
84
+
85
+ # Return if an attribute should be aliased
86
+ #
87
+ # @api private
88
+ def aliased?
89
+ key != name
90
+ end
91
+
92
+ # Return :key-to-:name mapping hash
93
+ #
94
+ # @return [Hash]
95
+ #
96
+ # @api private
97
+ def mapping
98
+ { key => name }
99
+ end
100
+
101
+ def union?
102
+ key.is_a? ::Array
103
+ end
104
+ end
105
+
106
+ # Embedded attribute is a special attribute type that has a header
107
+ #
108
+ # This is the base of complex attributes like Hash or Group
109
+ #
110
+ # @private
111
+ class Embedded < Attribute
112
+ include Dry::Equalizer(:name, :key, :type, :header)
113
+
114
+ # return [Header] header of an attribute
115
+ #
116
+ # @api private
117
+ attr_reader :header
118
+
119
+ # @api private
120
+ def initialize(*)
121
+ super
122
+ @header = meta.fetch(:header)
123
+ end
124
+
125
+ # Return tuple keys from the header
126
+ #
127
+ # @return [Array<Symbol>]
128
+ #
129
+ # @api private
130
+ def tuple_keys
131
+ header.tuple_keys
132
+ end
133
+
134
+ def pop_keys
135
+ header.pop_keys
136
+ end
137
+ end
138
+
139
+ # Array is an embedded attribute type
140
+ Array = Class.new(Embedded)
141
+
142
+ # Hash is an embedded attribute type
143
+ Hash = Class.new(Embedded)
144
+
145
+ # Combined is an embedded attribute type describing combination of multiple
146
+ # relations
147
+ Combined = Class.new(Embedded)
148
+
149
+ # Wrap is a special type of Hash attribute that requires wrapping
150
+ # transformation
151
+ Wrap = Class.new(Hash)
152
+
153
+ # Unwrap is a special type of Hash attribute that requires unwrapping
154
+ # transformation
155
+ Unwrap = Class.new(Hash)
156
+
157
+ # Group is a special type of Array attribute that requires grouping
158
+ # transformation
159
+ Group = Class.new(Array)
160
+
161
+ # Ungroup is a special type of Array attribute that requires ungrouping
162
+ # transformation
163
+ Ungroup = Class.new(Array)
164
+
165
+ # Fold is a special type of Array attribute that requires folding
166
+ # transformation
167
+ Fold = Class.new(Array)
168
+
169
+ # Unfold is a special type of Array attribute that requires unfolding
170
+ # transformation
171
+ Unfold = Class.new(Array)
172
+
173
+ # Exclude is a special type of Attribute to be removed
174
+ Exclude = Class.new(Attribute)
175
+
176
+ # TYPE_MAP is a (hash) map of ROM::Header identifiers to ROM::Header types
177
+ #
178
+ # @private
179
+ TYPE_MAP = {
180
+ combine: Combined,
181
+ wrap: Wrap,
182
+ unwrap: Unwrap,
183
+ group: Group,
184
+ ungroup: Ungroup,
185
+ fold: Fold,
186
+ unfold: Unfold,
187
+ hash: Hash,
188
+ array: Array,
189
+ exclude: Exclude
190
+ }
191
+ end
192
+ end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'dry-initializer'
2
4
 
3
5
  module ROM
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'rom/lint/linter'
2
4
 
3
5
  module ROM
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'rom/lint/linter'
2
4
 
3
5
  module ROM
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module ROM
2
4
  module Lint
3
5
  # Base class for building linters that check source code
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'rom/lint/gateway'
2
4
  require 'rom/lint/enumerable_dataset'
3
5
 
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'rom/lint/gateway'
2
4
  require 'rom/lint/enumerable_dataset'
3
5
 
@@ -0,0 +1,100 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rom/constants'
4
+ require 'rom/mapper/dsl'
5
+ require 'rom/mapper/configuration_plugin'
6
+
7
+ module ROM
8
+ # Mapper is a simple object that uses transformers to load relations
9
+ #
10
+ # @private
11
+ class Mapper
12
+ include DSL
13
+ include Dry::Equalizer(:transformers, :header)
14
+
15
+ defines :relation, :register_as, :symbolize_keys, :copy_keys,
16
+ :prefix, :prefix_separator, :inherit_header, :reject_keys
17
+
18
+ inherit_header true
19
+ reject_keys false
20
+ prefix_separator '_'.freeze
21
+
22
+ # @return [Object] transformers object built by a processor
23
+ #
24
+ # @api private
25
+ attr_reader :transformers
26
+
27
+ # @return [Header] header that was used to build the transformers
28
+ #
29
+ # @api private
30
+ attr_reader :header
31
+
32
+ # @return [Hash] registered processors
33
+ #
34
+ # @api private
35
+ def self.processors
36
+ @_processors ||= {}
37
+ end
38
+
39
+ # Register a processor class
40
+ #
41
+ # @return [Hash]
42
+ #
43
+ # @api private
44
+ def self.register_processor(processor)
45
+ name = processor.name.split('::').last.downcase.to_sym
46
+ processors.update(name => processor)
47
+ end
48
+
49
+ # Prepares an array of headers for a potentially multistep mapper
50
+ #
51
+ # @return [Array<Header>]
52
+ #
53
+ # @api private
54
+ def self.headers(header)
55
+ return [header] if steps.empty?
56
+ return steps.map(&:header) if attributes.empty?
57
+ raise(MapperMisconfiguredError, "cannot mix outer attributes and steps")
58
+ end
59
+
60
+ # Build a mapper using provided processor type
61
+ #
62
+ # @return [Mapper]
63
+ #
64
+ # @api private
65
+ def self.build(header = self.header, processor = :transproc)
66
+ new(header, processor)
67
+ end
68
+
69
+ # @api private
70
+ def self.registry(descendants)
71
+ descendants.each_with_object({}) do |klass, h|
72
+ name = klass.register_as || klass.relation
73
+ (h[klass.base_relation] ||= {})[name] = klass.build
74
+ end
75
+ end
76
+
77
+ # @api private
78
+ def initialize(header, processor = :transproc)
79
+ processor = Mapper.processors.fetch(processor)
80
+ @transformers = self.class.headers(header).map do |hdr|
81
+ processor.build(self, hdr)
82
+ end
83
+ @header = header
84
+ end
85
+
86
+ # @return [Class] optional model that is instantiated by a mapper
87
+ #
88
+ # @api private
89
+ def model
90
+ header.model
91
+ end
92
+
93
+ # Process a relation using the transformers
94
+ #
95
+ # @api private
96
+ def call(relation)
97
+ transformers.reduce(relation.to_a) { |a, e| e.call(a) }
98
+ end
99
+ end
100
+ end
@@ -0,0 +1,480 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rom/header'
4
+ require 'rom/mapper/model_dsl'
5
+
6
+ module ROM
7
+ class Mapper
8
+ # Mapper attribute DSL exposed by mapper subclasses
9
+ #
10
+ # This class is private even though its methods are exposed by mappers.
11
+ # Typically it's not meant to be used directly.
12
+ #
13
+ # TODO: break this madness down into smaller pieces
14
+ #
15
+ # @api private
16
+ class AttributeDSL
17
+ include ModelDSL
18
+
19
+ attr_reader :attributes, :options, :copy_keys, :symbolize_keys, :reject_keys, :steps
20
+
21
+ # @param [Array] attributes accumulator array
22
+ # @param [Hash] options
23
+ #
24
+ # @api private
25
+ def initialize(attributes, options)
26
+ @attributes = attributes
27
+ @options = options
28
+ @copy_keys = options.fetch(:copy_keys)
29
+ @symbolize_keys = options.fetch(:symbolize_keys)
30
+ @prefix = options.fetch(:prefix)
31
+ @prefix_separator = options.fetch(:prefix_separator)
32
+ @reject_keys = options.fetch(:reject_keys)
33
+ @steps = []
34
+ end
35
+
36
+ # Redefine the prefix for the following attributes
37
+ #
38
+ # @example
39
+ #
40
+ # dsl = AttributeDSL.new([])
41
+ # dsl.attribute(:prefix, 'user')
42
+ #
43
+ # @api public
44
+ def prefix(value = Undefined)
45
+ if value.equal?(Undefined)
46
+ @prefix
47
+ else
48
+ @prefix = value
49
+ end
50
+ end
51
+
52
+ # Redefine the prefix separator for the following attributes
53
+ #
54
+ # @example
55
+ #
56
+ # dsl = AttributeDSL.new([])
57
+ # dsl.attribute(:prefix_separator, '.')
58
+ #
59
+ # @api public
60
+ def prefix_separator(value = Undefined)
61
+ if value.equal?(Undefined)
62
+ @prefix_separator
63
+ else
64
+ @prefix_separator = value
65
+ end
66
+ end
67
+
68
+ # Define a mapping attribute with its options and/or block
69
+ #
70
+ # @example
71
+ # dsl = AttributeDSL.new([])
72
+ #
73
+ # dsl.attribute(:name)
74
+ # dsl.attribute(:email, from: 'user_email')
75
+ # dsl.attribute(:name) { 'John' }
76
+ # dsl.attribute(:name) { |t| t.upcase }
77
+ #
78
+ # @api public
79
+ def attribute(name, options = EMPTY_HASH, &block)
80
+ with_attr_options(name, options) do |attr_options|
81
+ raise ArgumentError,
82
+ "can't specify type and block at the same time" if options[:type] && block
83
+ attr_options[:coercer] = block if block
84
+ add_attribute(name, attr_options)
85
+ end
86
+ end
87
+
88
+ def exclude(name)
89
+ attributes << [name, { exclude: true }]
90
+ end
91
+
92
+ # Perform transformations sequentially
93
+ #
94
+ # @example
95
+ # dsl = AttributeDSL.new()
96
+ #
97
+ # dsl.step do
98
+ # attribute :name
99
+ # end
100
+ #
101
+ # @api public
102
+ def step(options = EMPTY_HASH, &block)
103
+ steps << new(options, &block)
104
+ end
105
+
106
+ # Define an embedded attribute
107
+ #
108
+ # Block exposes the attribute dsl too
109
+ #
110
+ # @example
111
+ # dsl = AttributeDSL.new([])
112
+ #
113
+ # dsl.embedded :tags, type: :array do
114
+ # attribute :name
115
+ # end
116
+ #
117
+ # dsl.embedded :address, type: :hash do
118
+ # model Address
119
+ # attribute :name
120
+ # end
121
+ #
122
+ # @param [Symbol] name attribute
123
+ #
124
+ # @param [Hash] options
125
+ # @option options [Symbol] :type Embedded type can be :hash or :array
126
+ # @option options [Symbol] :prefix Prefix that should be used for
127
+ # its attributes
128
+ #
129
+ # @api public
130
+ def embedded(name, options, &block)
131
+ with_attr_options(name) do |attr_options|
132
+ mapper = options[:mapper]
133
+
134
+ if mapper
135
+ embedded_options = { type: :array }.update(options)
136
+ attributes_from_mapper(
137
+ mapper, name, embedded_options.update(attr_options)
138
+ )
139
+ else
140
+ dsl = new(options, &block)
141
+ attr_options.update(options)
142
+ add_attribute(
143
+ name, { header: dsl.header, type: :array }.update(attr_options)
144
+ )
145
+ end
146
+ end
147
+ end
148
+
149
+ # Define an embedded hash attribute that requires "wrapping" transformation
150
+ #
151
+ # Typically this is used in sql context when relation is a join.
152
+ #
153
+ # @example
154
+ # dsl = AttributeDSL.new([])
155
+ #
156
+ # dsl.wrap(address: [:street, :zipcode, :city])
157
+ #
158
+ # dsl.wrap(:address) do
159
+ # model Address
160
+ # attribute :street
161
+ # attribute :zipcode
162
+ # attribute :city
163
+ # end
164
+ #
165
+ # @see AttributeDSL#embedded
166
+ #
167
+ # @api public
168
+ def wrap(*args, &block)
169
+ ensure_mapper_configuration('wrap', args, block_given?)
170
+
171
+ with_name_or_options(*args) do |name, options, mapper|
172
+ wrap_options = { type: :hash, wrap: true }.update(options)
173
+
174
+ if mapper
175
+ attributes_from_mapper(mapper, name, wrap_options)
176
+ else
177
+ dsl(name, wrap_options, &block)
178
+ end
179
+ end
180
+ end
181
+
182
+ # Define an embedded hash attribute that requires "unwrapping" transformation
183
+ #
184
+ # Typically this is used in no-sql context to normalize data before
185
+ # inserting to sql gateway.
186
+ #
187
+ # @example
188
+ # dsl = AttributeDSL.new([])
189
+ #
190
+ # dsl.unwrap(address: [:street, :zipcode, :city])
191
+ #
192
+ # dsl.unwrap(:address) do
193
+ # attribute :street
194
+ # attribute :zipcode
195
+ # attribute :city
196
+ # end
197
+ #
198
+ # @see AttributeDSL#embedded
199
+ #
200
+ # @api public
201
+ def unwrap(*args, &block)
202
+ with_name_or_options(*args) do |name, options, mapper|
203
+ unwrap_options = { type: :hash, unwrap: true }.update(options)
204
+
205
+ if mapper
206
+ attributes_from_mapper(mapper, name, unwrap_options)
207
+ else
208
+ dsl(name, unwrap_options, &block)
209
+ end
210
+ end
211
+ end
212
+
213
+ # Define an embedded hash attribute that requires "grouping" transformation
214
+ #
215
+ # Typically this is used in sql context when relation is a join.
216
+ #
217
+ # @example
218
+ # dsl = AttributeDSL.new([])
219
+ #
220
+ # dsl.group(tags: [:name])
221
+ #
222
+ # dsl.group(:tags) do
223
+ # model Tag
224
+ # attribute :name
225
+ # end
226
+ #
227
+ # @see AttributeDSL#embedded
228
+ #
229
+ # @api public
230
+ def group(*args, &block)
231
+ ensure_mapper_configuration('group', args, block_given?)
232
+
233
+ with_name_or_options(*args) do |name, options, mapper|
234
+ group_options = { type: :array, group: true }.update(options)
235
+
236
+ if mapper
237
+ attributes_from_mapper(mapper, name, group_options)
238
+ else
239
+ dsl(name, group_options, &block)
240
+ end
241
+ end
242
+ end
243
+
244
+ # Define an embedded array attribute that requires "ungrouping" transformation
245
+ #
246
+ # Typically this is used in non-sql context being prepared for import to sql.
247
+ #
248
+ # @example
249
+ # dsl = AttributeDSL.new([])
250
+ # dsl.ungroup(tags: [:name])
251
+ #
252
+ # @see AttributeDSL#embedded
253
+ #
254
+ # @api public
255
+ def ungroup(*args, &block)
256
+ with_name_or_options(*args) do |name, options, *|
257
+ ungroup_options = { type: :array, ungroup: true }.update(options)
258
+ dsl(name, ungroup_options, &block)
259
+ end
260
+ end
261
+
262
+ # Define an embedded hash attribute that requires "fold" transformation
263
+ #
264
+ # Typically this is used in sql context to fold single joined field
265
+ # to the array of values.
266
+ #
267
+ # @example
268
+ # dsl = AttributeDSL.new([])
269
+ #
270
+ # dsl.fold(tags: [:name])
271
+ #
272
+ # @see AttributeDSL#embedded
273
+ #
274
+ # @api public
275
+ def fold(*args, &block)
276
+ with_name_or_options(*args) do |name, *|
277
+ fold_options = { type: :array, fold: true }
278
+ dsl(name, fold_options, &block)
279
+ end
280
+ end
281
+
282
+ # Define an embedded hash attribute that requires "unfold" transformation
283
+ #
284
+ # Typically this is used in non-sql context to convert array of
285
+ # values (like in Cassandra 'SET' or 'LIST' types) to array of tuples.
286
+ #
287
+ # Source values are assigned to the first key, the other keys being left blank.
288
+ #
289
+ # @example
290
+ # dsl = AttributeDSL.new([])
291
+ #
292
+ # dsl.unfold(tags: [:name, :type], from: :tags_list)
293
+ #
294
+ # dsl.unfold :tags, from: :tags_list do
295
+ # attribute :name, from: :tag_name
296
+ # attribute :type, from: :tag_type
297
+ # end
298
+ #
299
+ # @see AttributeDSL#embedded
300
+ #
301
+ # @api public
302
+ def unfold(name, options = EMPTY_HASH)
303
+ with_attr_options(name, options) do |attr_options|
304
+ old_name = attr_options.fetch(:from, name)
305
+ dsl(old_name, type: :array, unfold: true) do
306
+ attribute name, attr_options
307
+ yield if block_given?
308
+ end
309
+ end
310
+ end
311
+
312
+ # Define an embedded combined attribute that requires "combine" transformation
313
+ #
314
+ # Typically this can be used to process results of eager-loading
315
+ #
316
+ # @example
317
+ # dsl = AttributeDSL.new([])
318
+ #
319
+ # dsl.combine(:tags, user_id: :id) do
320
+ # model Tag
321
+ #
322
+ # attribute :name
323
+ # end
324
+ #
325
+ # @param [Symbol] name
326
+ # @param [Hash] options
327
+ # @option options [Hash] :on The "join keys"
328
+ # @option options [Symbol] :type The type, either :array (default) or :hash
329
+ #
330
+ # @api public
331
+ def combine(name, options, &block)
332
+ dsl = new(options, &block)
333
+
334
+ attr_opts = {
335
+ type: options.fetch(:type, :array),
336
+ keys: options.fetch(:on),
337
+ combine: true,
338
+ header: dsl.header
339
+ }
340
+
341
+ add_attribute(name, attr_opts)
342
+ end
343
+
344
+ # Generate a header from attribute definitions
345
+ #
346
+ # @return [Header]
347
+ #
348
+ # @api private
349
+ def header
350
+ Header.coerce(attributes, copy_keys: copy_keys, model: model, reject_keys: reject_keys)
351
+ end
352
+
353
+ private
354
+
355
+ # Remove the attribute used somewhere else (in wrap, group, model etc.)
356
+ #
357
+ # @api private
358
+ def remove(*names)
359
+ attributes.delete_if { |attr| names.include?(attr.first) }
360
+ end
361
+
362
+ # Handle attribute options common for all definitions
363
+ #
364
+ # @api private
365
+ def with_attr_options(name, options = EMPTY_HASH)
366
+ attr_options = options.dup
367
+
368
+ if @prefix
369
+ attr_options[:from] ||= "#{@prefix}#{@prefix_separator}#{name}"
370
+ attr_options[:from] = attr_options[:from].to_sym if name.is_a? Symbol
371
+ end
372
+
373
+ if symbolize_keys
374
+ attr_options.update(from: attr_options.fetch(:from) { name }.to_s)
375
+ end
376
+
377
+ yield(attr_options)
378
+ end
379
+
380
+ # Handle "name or options" syntax used by `wrap` and `group`
381
+ #
382
+ # @api private
383
+ def with_name_or_options(*args)
384
+ name, options =
385
+ if args.size > 1
386
+ args
387
+ else
388
+ [args.first, {}]
389
+ end
390
+
391
+ yield(name, options, options[:mapper])
392
+ end
393
+
394
+ # Create another instance of the dsl for nested definitions
395
+ #
396
+ # This is used by embedded, wrap and group
397
+ #
398
+ # @api private
399
+ def dsl(name_or_attrs, options, &block)
400
+ if block
401
+ attributes_from_block(name_or_attrs, options, &block)
402
+ else
403
+ attributes_from_hash(name_or_attrs, options)
404
+ end
405
+ end
406
+
407
+ # Define attributes from a nested block
408
+ #
409
+ # Used by embedded, wrap and group
410
+ #
411
+ # @api private
412
+ def attributes_from_block(name, options, &block)
413
+ dsl = new(options, &block)
414
+ header = dsl.header
415
+ add_attribute(name, options.update(header: header))
416
+ header.each { |attr| remove(attr.key) unless name == attr.key }
417
+ end
418
+
419
+ # Define attributes from the `name => attributes` hash syntax
420
+ #
421
+ # Used by wrap and group
422
+ #
423
+ # @api private
424
+ def attributes_from_hash(hash, options)
425
+ hash.each do |name, header|
426
+ with_attr_options(name, options) do |attr_options|
427
+ add_attribute(name, attr_options.update(header: header.zip))
428
+ header.each { |attr| remove(attr) unless name == attr }
429
+ end
430
+ end
431
+ end
432
+
433
+ # Infer mapper header for an embedded attribute
434
+ #
435
+ # @api private
436
+ def attributes_from_mapper(mapper, name, options)
437
+ if mapper.is_a?(Class)
438
+ add_attribute(name, { header: mapper.header }.update(options))
439
+ else
440
+ raise(
441
+ ArgumentError, ":mapper must be a class #{mapper.inspect}"
442
+ )
443
+ end
444
+ end
445
+
446
+ # Add a new attribute and make sure it overrides previous definition
447
+ #
448
+ # @api private
449
+ def add_attribute(name, options)
450
+ remove(name, name.to_s)
451
+ attributes << [name, options]
452
+ end
453
+
454
+ # Create a new dsl instance of potentially overidden options
455
+ #
456
+ # Embedded, wrap and group can override top-level options like `prefix`
457
+ #
458
+ # @api private
459
+ def new(options, &block)
460
+ dsl = self.class.new([], @options.merge(options))
461
+ dsl.instance_exec(&block) unless block.nil?
462
+ dsl
463
+ end
464
+
465
+ # Ensure the mapping configuration isn't ambiguous
466
+ #
467
+ # @api private
468
+ def ensure_mapper_configuration(method_name, args, block_present)
469
+ if args.first.is_a?(Hash) && block_present
470
+ raise MapperMisconfiguredError,
471
+ "Cannot configure `#{method_name}#` using both options and a block"
472
+ end
473
+ if args.first.is_a?(Hash) && args.first[:mapper]
474
+ raise MapperMisconfiguredError,
475
+ "Cannot configure `#{method_name}#` using both options and a mapper"
476
+ end
477
+ end
478
+ end
479
+ end
480
+ end