rom-mapper 0.1.1 → 0.2.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 +7 -0
- data/.gitignore +2 -0
- data/.rspec +1 -1
- data/.travis.yml +19 -13
- data/{Changelog.md → CHANGELOG.md} +6 -0
- data/Gemfile +23 -10
- data/README.md +17 -12
- data/Rakefile +12 -4
- data/lib/rom-mapper.rb +6 -15
- data/lib/rom/header.rb +195 -0
- data/lib/rom/header/attribute.rb +184 -0
- data/lib/rom/mapper.rb +63 -100
- data/lib/rom/mapper/attribute_dsl.rb +477 -0
- data/lib/rom/mapper/dsl.rb +120 -0
- data/lib/rom/mapper/model_dsl.rb +55 -0
- data/lib/rom/mapper/version.rb +3 -7
- data/lib/rom/model_builder.rb +99 -0
- data/lib/rom/processor.rb +28 -0
- data/lib/rom/processor/transproc.rb +388 -0
- data/rakelib/benchmark.rake +15 -0
- data/rakelib/mutant.rake +16 -0
- data/rakelib/rubocop.rake +18 -0
- data/rom-mapper.gemspec +7 -6
- data/spec/spec_helper.rb +32 -33
- data/spec/support/constant_leak_finder.rb +14 -0
- data/spec/support/mutant.rb +10 -0
- data/spec/unit/rom/mapper/dsl_spec.rb +467 -0
- data/spec/unit/rom/mapper_spec.rb +83 -0
- data/spec/unit/rom/processor/transproc_spec.rb +448 -0
- metadata +68 -89
- data/.ruby-version +0 -1
- data/Gemfile.devtools +0 -55
- data/config/devtools.yml +0 -2
- data/config/flay.yml +0 -3
- data/config/flog.yml +0 -2
- data/config/mutant.yml +0 -3
- data/config/reek.yml +0 -103
- data/config/rubocop.yml +0 -45
- data/lib/rom/mapper/attribute.rb +0 -31
- data/lib/rom/mapper/dumper.rb +0 -27
- data/lib/rom/mapper/loader.rb +0 -22
- data/lib/rom/mapper/loader/allocator.rb +0 -32
- data/lib/rom/mapper/loader/attribute_writer.rb +0 -23
- data/lib/rom/mapper/loader/object_builder.rb +0 -28
- data/spec/shared/unit/loader_call.rb +0 -13
- data/spec/shared/unit/loader_identity.rb +0 -13
- data/spec/shared/unit/mapper_context.rb +0 -13
- data/spec/unit/rom/mapper/call_spec.rb +0 -32
- data/spec/unit/rom/mapper/class_methods/build_spec.rb +0 -64
- data/spec/unit/rom/mapper/dump_spec.rb +0 -15
- data/spec/unit/rom/mapper/dumper/call_spec.rb +0 -29
- data/spec/unit/rom/mapper/dumper/identity_spec.rb +0 -28
- data/spec/unit/rom/mapper/header/each_spec.rb +0 -28
- data/spec/unit/rom/mapper/header/element_reader_spec.rb +0 -25
- data/spec/unit/rom/mapper/header/keys_spec.rb +0 -32
- data/spec/unit/rom/mapper/identity_from_tuple_spec.rb +0 -15
- data/spec/unit/rom/mapper/identity_spec.rb +0 -15
- data/spec/unit/rom/mapper/load_spec.rb +0 -15
- data/spec/unit/rom/mapper/loader/allocator/call_spec.rb +0 -7
- data/spec/unit/rom/mapper/loader/allocator/identity_spec.rb +0 -7
- data/spec/unit/rom/mapper/loader/attribute_writer/call_spec.rb +0 -7
- data/spec/unit/rom/mapper/loader/attribute_writer/identity_spec.rb +0 -7
- data/spec/unit/rom/mapper/loader/object_builder/call_spec.rb +0 -7
- data/spec/unit/rom/mapper/loader/object_builder/identity_spec.rb +0 -7
- data/spec/unit/rom/mapper/model_spec.rb +0 -11
- data/spec/unit/rom/mapper/new_object_spec.rb +0 -14
@@ -0,0 +1,120 @@
|
|
1
|
+
require 'rom/support/class_macros'
|
2
|
+
require 'rom/mapper/attribute_dsl'
|
3
|
+
|
4
|
+
module ROM
|
5
|
+
class Mapper
|
6
|
+
# Mapper class-level DSL including Attribute DSL and Model DSL
|
7
|
+
module DSL
|
8
|
+
# Extend mapper class with macros and DSL methods
|
9
|
+
#
|
10
|
+
# @api private
|
11
|
+
def self.included(klass)
|
12
|
+
klass.extend(ClassMacros)
|
13
|
+
klass.extend(ClassMethods)
|
14
|
+
end
|
15
|
+
|
16
|
+
# Class methods for all mappers
|
17
|
+
#
|
18
|
+
# @private
|
19
|
+
module ClassMethods
|
20
|
+
# Set base ivars for the mapper class
|
21
|
+
#
|
22
|
+
# @api private
|
23
|
+
def inherited(klass)
|
24
|
+
super
|
25
|
+
|
26
|
+
klass.instance_variable_set('@attributes', nil)
|
27
|
+
klass.instance_variable_set('@header', nil)
|
28
|
+
klass.instance_variable_set('@dsl', nil)
|
29
|
+
end
|
30
|
+
|
31
|
+
# include a registered plugin in this mapper
|
32
|
+
#
|
33
|
+
# @param [Symbol] plugin
|
34
|
+
# @param [Hash] options
|
35
|
+
# @option options [Symbol] :adapter (:default) first adapter to check for plugin
|
36
|
+
#
|
37
|
+
# @api public
|
38
|
+
def use(plugin, options = {})
|
39
|
+
adapter = options.fetch(:adapter, :default)
|
40
|
+
|
41
|
+
ROM.plugin_registry.mappers.fetch(plugin, adapter).apply_to(self)
|
42
|
+
end
|
43
|
+
|
44
|
+
# Return base_relation used for creating mapper registry
|
45
|
+
#
|
46
|
+
# This is used to "gather" mappers under same root name
|
47
|
+
#
|
48
|
+
# @api private
|
49
|
+
def base_relation
|
50
|
+
if superclass.relation
|
51
|
+
superclass.relation
|
52
|
+
else
|
53
|
+
relation
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
# Return header of the mapper
|
58
|
+
#
|
59
|
+
# This is memoized so mutating mapper class won't have an effect wrt
|
60
|
+
# header after it was initialized for the first time.
|
61
|
+
#
|
62
|
+
# TODO: freezing mapper class here is probably a good idea
|
63
|
+
#
|
64
|
+
# @api private
|
65
|
+
def header
|
66
|
+
@header ||= dsl.header
|
67
|
+
end
|
68
|
+
|
69
|
+
# @api private
|
70
|
+
def respond_to_missing?(name, _include_private = false)
|
71
|
+
dsl.respond_to?(name) || super
|
72
|
+
end
|
73
|
+
|
74
|
+
private
|
75
|
+
|
76
|
+
# Return default Attribute DSL options based on settings of the mapper
|
77
|
+
# class
|
78
|
+
#
|
79
|
+
# @api private
|
80
|
+
def options
|
81
|
+
{ prefix: prefix,
|
82
|
+
prefix_separator: prefix_separator,
|
83
|
+
symbolize_keys: symbolize_keys,
|
84
|
+
reject_keys: reject_keys }
|
85
|
+
end
|
86
|
+
|
87
|
+
# Return default attributes that might have been inherited from the
|
88
|
+
# superclass
|
89
|
+
#
|
90
|
+
# @api private
|
91
|
+
def attributes
|
92
|
+
@attributes ||=
|
93
|
+
if superclass.respond_to?(:attributes, true) && inherit_header
|
94
|
+
superclass.attributes.dup
|
95
|
+
else
|
96
|
+
[]
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
# Create the attribute DSL instance used by the mapper class
|
101
|
+
#
|
102
|
+
# @api private
|
103
|
+
def dsl
|
104
|
+
@dsl ||= AttributeDSL.new(attributes, options)
|
105
|
+
end
|
106
|
+
|
107
|
+
# Delegate Attribute DSL method to the dsl instance
|
108
|
+
#
|
109
|
+
# @api private
|
110
|
+
def method_missing(name, *args, &block)
|
111
|
+
if dsl.respond_to?(name)
|
112
|
+
dsl.public_send(name, *args, &block)
|
113
|
+
else
|
114
|
+
super
|
115
|
+
end
|
116
|
+
end
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
require 'rom/model_builder'
|
2
|
+
|
3
|
+
module ROM
|
4
|
+
class Mapper
|
5
|
+
# Model DSL allows setting a model class
|
6
|
+
#
|
7
|
+
# @private
|
8
|
+
module ModelDSL
|
9
|
+
attr_reader :attributes, :builder, :klass
|
10
|
+
|
11
|
+
DEFAULT_TYPE = :poro
|
12
|
+
|
13
|
+
# Set or generate a model
|
14
|
+
#
|
15
|
+
# @example
|
16
|
+
# class MyDefinition
|
17
|
+
# include ROM::Mapper::ModelDSL
|
18
|
+
#
|
19
|
+
# def initialize
|
20
|
+
# @attributes = [[:name], [:title]]
|
21
|
+
# end
|
22
|
+
# end
|
23
|
+
#
|
24
|
+
# definition = MyDefinition.new
|
25
|
+
#
|
26
|
+
# # just set a model constant
|
27
|
+
# definition.model(User)
|
28
|
+
#
|
29
|
+
# # generate model class for the attributes
|
30
|
+
# definition.model(name: 'User')
|
31
|
+
#
|
32
|
+
# @api public
|
33
|
+
def model(options = nil)
|
34
|
+
if options.is_a?(Class)
|
35
|
+
@klass = options
|
36
|
+
elsif options
|
37
|
+
type = options.fetch(:type) { DEFAULT_TYPE }
|
38
|
+
@builder = ModelBuilder[type].new(options)
|
39
|
+
end
|
40
|
+
|
41
|
+
build_class unless options
|
42
|
+
end
|
43
|
+
|
44
|
+
private
|
45
|
+
|
46
|
+
# Build a model class using a specialized builder
|
47
|
+
#
|
48
|
+
# @api private
|
49
|
+
def build_class
|
50
|
+
return klass if klass
|
51
|
+
return builder.call(attributes.map(&:first)) if builder
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
data/lib/rom/mapper/version.rb
CHANGED
@@ -0,0 +1,99 @@
|
|
1
|
+
module ROM
|
2
|
+
# Model builders can be used to build model classes for mappers
|
3
|
+
#
|
4
|
+
# This is used when you define a mapper and setup a model using :name option.
|
5
|
+
#
|
6
|
+
# @example
|
7
|
+
# # this will define User model for you
|
8
|
+
# class UserMapper < ROM::Mapper
|
9
|
+
# model name: 'User'
|
10
|
+
# attribute :id
|
11
|
+
# attribute :name
|
12
|
+
# end
|
13
|
+
#
|
14
|
+
# @private
|
15
|
+
class ModelBuilder
|
16
|
+
attr_reader :name
|
17
|
+
|
18
|
+
attr_reader :const_name, :namespace, :klass
|
19
|
+
|
20
|
+
# Return model builder subclass based on type
|
21
|
+
#
|
22
|
+
# @param [Symbol] type
|
23
|
+
#
|
24
|
+
# @return [Class]
|
25
|
+
#
|
26
|
+
# @api private
|
27
|
+
def self.[](type)
|
28
|
+
case type
|
29
|
+
when :poro then PORO
|
30
|
+
else
|
31
|
+
raise ArgumentError, "#{type.inspect} is not a supported model type"
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
# Build a model class
|
36
|
+
#
|
37
|
+
# @return [Class]
|
38
|
+
#
|
39
|
+
# @api private
|
40
|
+
def self.call(*args)
|
41
|
+
new(*args).call
|
42
|
+
end
|
43
|
+
|
44
|
+
# @api private
|
45
|
+
def initialize(options = {})
|
46
|
+
@name = options[:name]
|
47
|
+
|
48
|
+
if name
|
49
|
+
parts = name.split('::')
|
50
|
+
|
51
|
+
@const_name = parts.pop
|
52
|
+
|
53
|
+
@namespace =
|
54
|
+
if parts.any?
|
55
|
+
Inflector.constantize(parts.join('::'))
|
56
|
+
else
|
57
|
+
Object
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
# Define a model class constant
|
63
|
+
#
|
64
|
+
# @api private
|
65
|
+
def define_const
|
66
|
+
namespace.const_set(const_name, klass)
|
67
|
+
end
|
68
|
+
|
69
|
+
# Build a model class supporting specific attributes
|
70
|
+
#
|
71
|
+
# @return [Class]
|
72
|
+
#
|
73
|
+
# @api private
|
74
|
+
def call(attrs)
|
75
|
+
define_class(attrs)
|
76
|
+
define_const if const_name
|
77
|
+
@klass
|
78
|
+
end
|
79
|
+
|
80
|
+
# PORO model class builder
|
81
|
+
#
|
82
|
+
# @private
|
83
|
+
class PORO < ModelBuilder
|
84
|
+
def define_class(attrs)
|
85
|
+
@klass = Class.new
|
86
|
+
|
87
|
+
@klass.send(:attr_reader, *attrs)
|
88
|
+
|
89
|
+
@klass.class_eval <<-RUBY, __FILE__, __LINE__ + 1
|
90
|
+
def initialize(params)
|
91
|
+
#{attrs.map { |name| "@#{name} = params[:#{name}]" }.join("\n")}
|
92
|
+
end
|
93
|
+
RUBY
|
94
|
+
|
95
|
+
self
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
require 'rom/mapper'
|
2
|
+
|
3
|
+
module ROM
|
4
|
+
# Abstract processor class
|
5
|
+
#
|
6
|
+
# Every ROM processor should inherit from this class
|
7
|
+
#
|
8
|
+
# @api public
|
9
|
+
class Processor
|
10
|
+
# Hook used to auto-register a processor class
|
11
|
+
#
|
12
|
+
# @api private
|
13
|
+
def self.inherited(processor)
|
14
|
+
Mapper.register_processor(processor)
|
15
|
+
end
|
16
|
+
|
17
|
+
# Required interface to be implemented by descendants
|
18
|
+
#
|
19
|
+
# @return [Processor]
|
20
|
+
#
|
21
|
+
# @abstract
|
22
|
+
#
|
23
|
+
# @api private
|
24
|
+
def self.build
|
25
|
+
raise NotImplementedError, "+build+ must be implemented"
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,388 @@
|
|
1
|
+
require 'transproc/all'
|
2
|
+
|
3
|
+
require 'rom/processor'
|
4
|
+
|
5
|
+
module ROM
|
6
|
+
class Processor
|
7
|
+
# Data mapping transformer builder using Transproc
|
8
|
+
#
|
9
|
+
# This builds a transproc function that is used to map a whole relation
|
10
|
+
#
|
11
|
+
# @see https://github.com/solnic/transproc too
|
12
|
+
#
|
13
|
+
# @private
|
14
|
+
class Transproc < Processor
|
15
|
+
include ::Transproc::Composer
|
16
|
+
|
17
|
+
module Functions
|
18
|
+
extend ::Transproc::Registry
|
19
|
+
|
20
|
+
import ::Transproc::Coercions
|
21
|
+
import ::Transproc::ArrayTransformations
|
22
|
+
import ::Transproc::HashTransformations
|
23
|
+
import ::Transproc::ClassTransformations
|
24
|
+
|
25
|
+
def self.identity(tuple)
|
26
|
+
tuple
|
27
|
+
end
|
28
|
+
|
29
|
+
def self.filter_empty(arr)
|
30
|
+
arr.reject { |row| row.values.all?(&:nil?) }
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
# @return [Header] header from a mapper
|
35
|
+
#
|
36
|
+
# @api private
|
37
|
+
attr_reader :header
|
38
|
+
|
39
|
+
# @return [Class] model class from a mapper
|
40
|
+
#
|
41
|
+
# @api private
|
42
|
+
attr_reader :model
|
43
|
+
|
44
|
+
# @return [Hash] header's attribute mapping
|
45
|
+
#
|
46
|
+
# @api private
|
47
|
+
attr_reader :mapping
|
48
|
+
|
49
|
+
# @return [Proc] row-processing proc
|
50
|
+
#
|
51
|
+
# @api private
|
52
|
+
attr_reader :row_proc
|
53
|
+
|
54
|
+
# Build a transproc function from the header
|
55
|
+
#
|
56
|
+
# @param [ROM::Header] header
|
57
|
+
#
|
58
|
+
# @return [Transproc::Function]
|
59
|
+
#
|
60
|
+
# @api private
|
61
|
+
def self.build(header)
|
62
|
+
new(header).to_transproc
|
63
|
+
end
|
64
|
+
|
65
|
+
# @api private
|
66
|
+
def initialize(header)
|
67
|
+
@header = header
|
68
|
+
@model = header.model
|
69
|
+
@mapping = header.mapping
|
70
|
+
initialize_row_proc
|
71
|
+
end
|
72
|
+
|
73
|
+
# Coerce mapper header to a transproc data mapping function
|
74
|
+
#
|
75
|
+
# @return [Transproc::Function]
|
76
|
+
#
|
77
|
+
# @api private
|
78
|
+
def to_transproc
|
79
|
+
compose(t(:identity)) do |ops|
|
80
|
+
combined = header.combined
|
81
|
+
ops << t(:combine, combined.map(&method(:combined_args))) if combined.any?
|
82
|
+
ops << header.preprocessed.map { |attr| visit(attr, true) }
|
83
|
+
ops << t(:map_array, row_proc) if row_proc
|
84
|
+
ops << header.postprocessed.map { |attr| visit(attr, true) }
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
private
|
89
|
+
|
90
|
+
# Visit an attribute from the header
|
91
|
+
#
|
92
|
+
# This forwards to a specialized visitor based on the attribute type
|
93
|
+
#
|
94
|
+
# @param [Header::Attribute] attribute
|
95
|
+
# @param [Array] args Allows to send `preprocess: true`
|
96
|
+
#
|
97
|
+
# @api private
|
98
|
+
def visit(attribute, *args)
|
99
|
+
type = attribute.class.name.split('::').last.downcase
|
100
|
+
send("visit_#{type}", attribute, *args)
|
101
|
+
end
|
102
|
+
|
103
|
+
# Visit plain attribute
|
104
|
+
#
|
105
|
+
# It will call block transformation if it's used
|
106
|
+
#
|
107
|
+
# If it's a typed attribute a coercion transformation is added
|
108
|
+
#
|
109
|
+
# @param [Header::Attribute] attribute
|
110
|
+
#
|
111
|
+
# @api private
|
112
|
+
def visit_attribute(attribute)
|
113
|
+
coercer = attribute.meta[:coercer]
|
114
|
+
if coercer
|
115
|
+
t(:map_value, attribute.name, coercer)
|
116
|
+
elsif attribute.typed?
|
117
|
+
t(:map_value, attribute.name, t(:"to_#{attribute.type}"))
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
# Visit hash attribute
|
122
|
+
#
|
123
|
+
# @param [Header::Attribute::Hash] attribute
|
124
|
+
#
|
125
|
+
# @api private
|
126
|
+
def visit_hash(attribute)
|
127
|
+
with_row_proc(attribute) do |row_proc|
|
128
|
+
t(:map_value, attribute.name, row_proc)
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
# Visit combined attribute
|
133
|
+
#
|
134
|
+
# @api private
|
135
|
+
def visit_combined(attribute)
|
136
|
+
op = with_row_proc(attribute) do |row_proc|
|
137
|
+
array_proc =
|
138
|
+
if attribute.type == :hash
|
139
|
+
t(:map_array, row_proc) >> -> arr { arr.first }
|
140
|
+
else
|
141
|
+
t(:map_array, row_proc)
|
142
|
+
end
|
143
|
+
|
144
|
+
t(:map_value, attribute.name, array_proc)
|
145
|
+
end
|
146
|
+
|
147
|
+
if op
|
148
|
+
op
|
149
|
+
elsif attribute.type == :hash
|
150
|
+
t(:map_value, attribute.name, -> arr { arr.first })
|
151
|
+
end
|
152
|
+
end
|
153
|
+
|
154
|
+
# Visit array attribute
|
155
|
+
#
|
156
|
+
# @param [Header::Attribute::Array] attribute
|
157
|
+
#
|
158
|
+
# @api private
|
159
|
+
def visit_array(attribute)
|
160
|
+
with_row_proc(attribute) do |row_proc|
|
161
|
+
t(:map_value, attribute.name, t(:map_array, row_proc))
|
162
|
+
end
|
163
|
+
end
|
164
|
+
|
165
|
+
# Visit wrapped hash attribute
|
166
|
+
#
|
167
|
+
# :nest transformation is added to handle wrapping
|
168
|
+
#
|
169
|
+
# @param [Header::Attribute::Wrap] attribute
|
170
|
+
#
|
171
|
+
# @api private
|
172
|
+
def visit_wrap(attribute)
|
173
|
+
name = attribute.name
|
174
|
+
keys = attribute.tuple_keys
|
175
|
+
|
176
|
+
compose do |ops|
|
177
|
+
ops << t(:nest, name, keys)
|
178
|
+
ops << visit_hash(attribute)
|
179
|
+
end
|
180
|
+
end
|
181
|
+
|
182
|
+
# Visit unwrap attribute
|
183
|
+
#
|
184
|
+
# :unwrap transformation is added to handle unwrapping
|
185
|
+
#
|
186
|
+
# @param [Header::Attributes::Unwrap]
|
187
|
+
#
|
188
|
+
# @api private
|
189
|
+
def visit_unwrap(attribute)
|
190
|
+
name = attribute.name
|
191
|
+
keys = attribute.pop_keys
|
192
|
+
|
193
|
+
compose do |ops|
|
194
|
+
ops << visit_hash(attribute)
|
195
|
+
ops << t(:unwrap, name, keys)
|
196
|
+
end
|
197
|
+
end
|
198
|
+
|
199
|
+
# Visit group hash attribute
|
200
|
+
#
|
201
|
+
# :group transformation is added to handle grouping during preprocessing.
|
202
|
+
# Otherwise we simply use array visitor for the attribute.
|
203
|
+
#
|
204
|
+
# @param [Header::Attribute::Group] attribute
|
205
|
+
# @param [Boolean] preprocess true if we are building a relation preprocessing
|
206
|
+
# function that is applied to the whole relation
|
207
|
+
#
|
208
|
+
# @api private
|
209
|
+
def visit_group(attribute, preprocess = false)
|
210
|
+
if preprocess
|
211
|
+
name = attribute.name
|
212
|
+
header = attribute.header
|
213
|
+
keys = attribute.tuple_keys
|
214
|
+
|
215
|
+
others = header.preprocessed
|
216
|
+
|
217
|
+
compose do |ops|
|
218
|
+
ops << t(:group, name, keys)
|
219
|
+
ops << t(:map_array, t(:map_value, name, t(:filter_empty)))
|
220
|
+
ops << others.map { |attr|
|
221
|
+
t(:map_array, t(:map_value, name, visit(attr, true)))
|
222
|
+
}
|
223
|
+
end
|
224
|
+
else
|
225
|
+
visit_array(attribute)
|
226
|
+
end
|
227
|
+
end
|
228
|
+
|
229
|
+
# Visit ungroup attribute
|
230
|
+
#
|
231
|
+
# :ungroup transforation is added to handle ungrouping during preprocessing.
|
232
|
+
# Otherwise we simply use array visitor for the attribute.
|
233
|
+
#
|
234
|
+
# @param [Header::Attribute::Ungroup] attribute
|
235
|
+
# @param [Boolean] preprocess true if we are building a relation preprocessing
|
236
|
+
# function that is applied to the whole relation
|
237
|
+
#
|
238
|
+
# @api private
|
239
|
+
def visit_ungroup(attribute, preprocess = false)
|
240
|
+
if preprocess
|
241
|
+
name = attribute.name
|
242
|
+
header = attribute.header
|
243
|
+
keys = attribute.pop_keys
|
244
|
+
|
245
|
+
others = header.postprocessed
|
246
|
+
|
247
|
+
compose do |ops|
|
248
|
+
ops << others.map { |attr|
|
249
|
+
t(:map_array, t(:map_value, name, visit(attr, true)))
|
250
|
+
}
|
251
|
+
ops << t(:ungroup, name, keys)
|
252
|
+
end
|
253
|
+
else
|
254
|
+
visit_array(attribute)
|
255
|
+
end
|
256
|
+
end
|
257
|
+
|
258
|
+
# Visit fold hash attribute
|
259
|
+
#
|
260
|
+
# :fold transformation is added to handle folding during preprocessing.
|
261
|
+
#
|
262
|
+
# @param [Header::Attribute::Fold] attribute
|
263
|
+
# @param [Boolean] preprocess true if we are building a relation preprocessing
|
264
|
+
# function that is applied to the whole relation
|
265
|
+
#
|
266
|
+
# @api private
|
267
|
+
def visit_fold(attribute, preprocess = false)
|
268
|
+
if preprocess
|
269
|
+
name = attribute.name
|
270
|
+
keys = attribute.tuple_keys
|
271
|
+
|
272
|
+
compose do |ops|
|
273
|
+
ops << t(:group, name, keys)
|
274
|
+
ops << t(:map_array, t(:map_value, name, t(:filter_empty)))
|
275
|
+
ops << t(:map_array, t(:fold, name, keys.first))
|
276
|
+
end
|
277
|
+
end
|
278
|
+
end
|
279
|
+
|
280
|
+
# Visit unfold hash attribute
|
281
|
+
#
|
282
|
+
# :unfold transformation is added to handle unfolding during preprocessing.
|
283
|
+
#
|
284
|
+
# @param [Header::Attribute::Unfold] attribute
|
285
|
+
# @param [Boolean] preprocess true if we are building a relation preprocessing
|
286
|
+
# function that is applied to the whole relation
|
287
|
+
#
|
288
|
+
# @api private
|
289
|
+
def visit_unfold(attribute, preprocess = false)
|
290
|
+
if preprocess
|
291
|
+
name = attribute.name
|
292
|
+
header = attribute.header
|
293
|
+
keys = attribute.pop_keys
|
294
|
+
key = keys.first
|
295
|
+
|
296
|
+
others = header.postprocessed
|
297
|
+
|
298
|
+
compose do |ops|
|
299
|
+
ops << others.map { |attr|
|
300
|
+
t(:map_array, t(:map_value, name, visit(attr, true)))
|
301
|
+
}
|
302
|
+
ops << t(:map_array, t(:map_value, name, t(:insert_key, key)))
|
303
|
+
ops << t(:map_array, t(:reject_keys, [key] - [name]))
|
304
|
+
ops << t(:ungroup, name, [key])
|
305
|
+
end
|
306
|
+
end
|
307
|
+
end
|
308
|
+
|
309
|
+
# Visit excluded attribute
|
310
|
+
#
|
311
|
+
# @param [Header::Attribute::Exclude] attribute
|
312
|
+
#
|
313
|
+
# @api private
|
314
|
+
def visit_exclude(attribute)
|
315
|
+
t(:reject_keys, [attribute.name])
|
316
|
+
end
|
317
|
+
|
318
|
+
# @api private
|
319
|
+
def combined_args(attribute)
|
320
|
+
other = attribute.header.combined
|
321
|
+
|
322
|
+
if other.any?
|
323
|
+
children = other.map(&method(:combined_args))
|
324
|
+
[attribute.name, attribute.meta[:keys], children]
|
325
|
+
else
|
326
|
+
[attribute.name, attribute.meta[:keys]]
|
327
|
+
end
|
328
|
+
end
|
329
|
+
|
330
|
+
# Build row_proc
|
331
|
+
#
|
332
|
+
# This transproc function is applied to each row in a dataset
|
333
|
+
#
|
334
|
+
# @api private
|
335
|
+
def initialize_row_proc
|
336
|
+
@row_proc = compose { |ops|
|
337
|
+
process_header_keys(ops)
|
338
|
+
|
339
|
+
ops << t(:rename_keys, mapping) if header.aliased?
|
340
|
+
ops << header.map { |attr| visit(attr) }
|
341
|
+
ops << t(:constructor_inject, model) if model
|
342
|
+
}
|
343
|
+
end
|
344
|
+
|
345
|
+
# Process row_proc header keys
|
346
|
+
#
|
347
|
+
# @api private
|
348
|
+
def process_header_keys(ops)
|
349
|
+
if header.reject_keys
|
350
|
+
all_keys = header.tuple_keys + header.non_primitives.map(&:key)
|
351
|
+
ops << t(:accept_keys, all_keys)
|
352
|
+
end
|
353
|
+
ops
|
354
|
+
end
|
355
|
+
|
356
|
+
# Yield row proc for a given attribute if any
|
357
|
+
#
|
358
|
+
# @param [Header::Attribute] attribute
|
359
|
+
#
|
360
|
+
# @api private
|
361
|
+
def with_row_proc(attribute)
|
362
|
+
row_proc = row_proc_from(attribute)
|
363
|
+
yield(row_proc) if row_proc
|
364
|
+
end
|
365
|
+
|
366
|
+
# Build a row_proc from a given attribute
|
367
|
+
#
|
368
|
+
# This is used by embedded attribute visitors
|
369
|
+
#
|
370
|
+
# @api private
|
371
|
+
def row_proc_from(attribute)
|
372
|
+
new(attribute.header).row_proc
|
373
|
+
end
|
374
|
+
|
375
|
+
# Return a new instance of the processor
|
376
|
+
#
|
377
|
+
# @api private
|
378
|
+
def new(*args)
|
379
|
+
self.class.new(*args)
|
380
|
+
end
|
381
|
+
|
382
|
+
# @api private
|
383
|
+
def t(*args)
|
384
|
+
Functions[*args]
|
385
|
+
end
|
386
|
+
end
|
387
|
+
end
|
388
|
+
end
|