rom-core 4.2.1 → 5.0.0

Sign up to get free protection for your applications and to get access to all the features.
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