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.
- checksums.yaml +4 -4
- data/README.md +0 -2
- data/lib/rom-core.rb +2 -0
- data/lib/rom/array_dataset.rb +2 -0
- data/lib/rom/association_set.rb +2 -0
- data/lib/rom/associations/abstract.rb +2 -0
- data/lib/rom/associations/definitions.rb +2 -0
- data/lib/rom/associations/definitions/abstract.rb +2 -0
- data/lib/rom/associations/definitions/many_to_many.rb +2 -0
- data/lib/rom/associations/definitions/many_to_one.rb +2 -0
- data/lib/rom/associations/definitions/one_to_many.rb +2 -0
- data/lib/rom/associations/definitions/one_to_one.rb +2 -0
- data/lib/rom/associations/definitions/one_to_one_through.rb +2 -0
- data/lib/rom/associations/many_to_many.rb +2 -0
- data/lib/rom/associations/many_to_one.rb +2 -0
- data/lib/rom/associations/one_to_many.rb +2 -0
- data/lib/rom/associations/one_to_one.rb +2 -0
- data/lib/rom/associations/one_to_one_through.rb +2 -0
- data/lib/rom/associations/through_identifier.rb +2 -0
- data/lib/rom/attribute.rb +58 -72
- data/lib/rom/auto_curry.rb +2 -0
- data/lib/rom/cache.rb +2 -0
- data/lib/rom/command.rb +7 -5
- data/lib/rom/command_compiler.rb +2 -0
- data/lib/rom/command_proxy.rb +2 -0
- data/lib/rom/command_registry.rb +13 -7
- data/lib/rom/commands.rb +2 -0
- data/lib/rom/commands/class_interface.rb +4 -2
- data/lib/rom/commands/composite.rb +2 -0
- data/lib/rom/commands/create.rb +2 -0
- data/lib/rom/commands/delete.rb +2 -0
- data/lib/rom/commands/graph.rb +2 -0
- data/lib/rom/commands/graph/class_interface.rb +2 -0
- data/lib/rom/commands/graph/input_evaluator.rb +2 -0
- data/lib/rom/commands/lazy.rb +2 -0
- data/lib/rom/commands/lazy/create.rb +2 -0
- data/lib/rom/commands/lazy/delete.rb +2 -0
- data/lib/rom/commands/lazy/update.rb +2 -0
- data/lib/rom/commands/update.rb +2 -0
- data/lib/rom/configuration.rb +2 -0
- data/lib/rom/configuration_dsl.rb +2 -0
- data/lib/rom/configuration_dsl/command.rb +2 -0
- data/lib/rom/configuration_dsl/command_dsl.rb +2 -0
- data/lib/rom/configuration_dsl/relation.rb +2 -0
- data/lib/rom/configuration_plugin.rb +2 -0
- data/lib/rom/constants.rb +3 -0
- data/lib/rom/container.rb +2 -0
- data/lib/rom/core.rb +4 -1
- data/lib/rom/create_container.rb +2 -0
- data/lib/rom/data_proxy.rb +2 -0
- data/lib/rom/enumerable_dataset.rb +2 -0
- data/lib/rom/environment.rb +2 -0
- data/lib/rom/gateway.rb +2 -0
- data/lib/rom/global.rb +2 -0
- data/lib/rom/global/plugin_dsl.rb +2 -0
- data/lib/rom/header.rb +198 -0
- data/lib/rom/header/attribute.rb +192 -0
- data/lib/rom/initializer.rb +2 -0
- data/lib/rom/lint/enumerable_dataset.rb +2 -0
- data/lib/rom/lint/gateway.rb +2 -0
- data/lib/rom/lint/linter.rb +2 -0
- data/lib/rom/lint/spec.rb +2 -0
- data/lib/rom/lint/test.rb +2 -0
- data/lib/rom/mapper.rb +100 -0
- data/lib/rom/mapper/attribute_dsl.rb +480 -0
- data/lib/rom/mapper/builder.rb +39 -0
- data/lib/rom/mapper/configuration_plugin.rb +28 -0
- data/lib/rom/mapper/dsl.rb +123 -0
- data/lib/rom/mapper/mapper_dsl.rb +45 -0
- data/lib/rom/mapper/model_dsl.rb +60 -0
- data/lib/rom/mapper_compiler.rb +84 -0
- data/lib/rom/mapper_registry.rb +2 -0
- data/lib/rom/memory.rb +2 -0
- data/lib/rom/memory/associations.rb +2 -0
- data/lib/rom/memory/associations/many_to_many.rb +2 -0
- data/lib/rom/memory/associations/many_to_one.rb +2 -0
- data/lib/rom/memory/associations/one_to_many.rb +2 -0
- data/lib/rom/memory/associations/one_to_one.rb +2 -0
- data/lib/rom/memory/commands.rb +2 -0
- data/lib/rom/memory/dataset.rb +2 -0
- data/lib/rom/memory/gateway.rb +2 -0
- data/lib/rom/memory/mapper_compiler.rb +2 -0
- data/lib/rom/memory/relation.rb +2 -0
- data/lib/rom/memory/schema.rb +2 -0
- data/lib/rom/memory/storage.rb +2 -0
- data/lib/rom/memory/types.rb +2 -0
- data/lib/rom/model_builder.rb +103 -0
- data/lib/rom/open_struct.rb +37 -0
- data/lib/rom/pipeline.rb +2 -0
- data/lib/rom/plugin.rb +2 -0
- data/lib/rom/plugin_base.rb +2 -0
- data/lib/rom/plugin_registry.rb +2 -0
- data/lib/rom/plugins/command/schema.rb +2 -0
- data/lib/rom/plugins/command/timestamps.rb +2 -0
- data/lib/rom/plugins/relation/instrumentation.rb +2 -0
- data/lib/rom/plugins/relation/registry_reader.rb +2 -0
- data/lib/rom/plugins/schema/timestamps.rb +8 -1
- data/lib/rom/processor.rb +30 -0
- data/lib/rom/processor/transproc.rb +417 -0
- data/lib/rom/registry.rb +2 -0
- data/lib/rom/relation.rb +4 -2
- data/lib/rom/relation/class_interface.rb +2 -0
- data/lib/rom/relation/combined.rb +2 -0
- data/lib/rom/relation/commands.rb +2 -0
- data/lib/rom/relation/composite.rb +2 -0
- data/lib/rom/relation/curried.rb +3 -1
- data/lib/rom/relation/graph.rb +2 -0
- data/lib/rom/relation/loaded.rb +2 -0
- data/lib/rom/relation/materializable.rb +2 -0
- data/lib/rom/relation/name.rb +2 -0
- data/lib/rom/relation/view_dsl.rb +2 -0
- data/lib/rom/relation/wrap.rb +2 -0
- data/lib/rom/relation_registry.rb +2 -0
- data/lib/rom/schema.rb +39 -6
- data/lib/rom/schema/associations_dsl.rb +5 -3
- data/lib/rom/schema/dsl.rb +41 -11
- data/lib/rom/schema/inferrer.rb +21 -3
- data/lib/rom/schema_plugin.rb +2 -0
- data/lib/rom/setup.rb +2 -0
- data/lib/rom/setup/auto_registration.rb +2 -0
- data/lib/rom/setup/auto_registration_strategies/base.rb +3 -1
- data/lib/rom/setup/auto_registration_strategies/custom_namespace.rb +2 -0
- data/lib/rom/setup/auto_registration_strategies/no_namespace.rb +2 -0
- data/lib/rom/setup/auto_registration_strategies/with_namespace.rb +2 -0
- data/lib/rom/setup/finalize.rb +2 -0
- data/lib/rom/setup/finalize/finalize_commands.rb +2 -0
- data/lib/rom/setup/finalize/finalize_mappers.rb +2 -0
- data/lib/rom/setup/finalize/finalize_relations.rb +2 -0
- data/lib/rom/struct.rb +108 -0
- data/lib/rom/struct_compiler.rb +110 -0
- data/lib/rom/support/configurable.rb +2 -0
- data/lib/rom/support/inflector.rb +2 -0
- data/lib/rom/support/memoizable.rb +2 -0
- data/lib/rom/support/notifications.rb +2 -0
- data/lib/rom/transaction.rb +2 -0
- data/lib/rom/transformer.rb +34 -0
- data/lib/rom/types.rb +10 -3
- data/lib/rom/version.rb +3 -1
- 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
|
data/lib/rom/initializer.rb
CHANGED
data/lib/rom/lint/gateway.rb
CHANGED
data/lib/rom/lint/linter.rb
CHANGED
data/lib/rom/lint/spec.rb
CHANGED
data/lib/rom/lint/test.rb
CHANGED
data/lib/rom/mapper.rb
ADDED
@@ -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
|