jrf 0.1.2 → 0.1.4
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/Rakefile +0 -5
- data/exe/jrf +6 -0
- data/jrf.gemspec +1 -0
- data/lib/jrf/cli.rb +15 -6
- data/lib/jrf/pipeline.rb +85 -0
- data/lib/jrf/pipeline_parser.rb +1 -41
- data/lib/jrf/row_context.rb +44 -39
- data/lib/jrf/runner.rb +41 -139
- data/lib/jrf/stage.rb +184 -0
- data/lib/jrf/version.rb +1 -1
- data/lib/jrf.rb +18 -0
- data/test/jrf_test.rb +397 -18
- metadata +18 -2
data/lib/jrf/stage.rb
ADDED
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "control"
|
|
4
|
+
require_relative "reducers"
|
|
5
|
+
|
|
6
|
+
module Jrf
|
|
7
|
+
class Stage
|
|
8
|
+
ReducerToken = Struct.new(:index)
|
|
9
|
+
|
|
10
|
+
attr_reader :src
|
|
11
|
+
|
|
12
|
+
def self.resolve_template(template, reducers)
|
|
13
|
+
if template.is_a?(ReducerToken)
|
|
14
|
+
rows = reducers.fetch(template.index).finish
|
|
15
|
+
rows.length == 1 ? rows.first : rows
|
|
16
|
+
elsif template.is_a?(Array)
|
|
17
|
+
template.map { |v| resolve_template(v, reducers) }
|
|
18
|
+
elsif template.is_a?(Hash)
|
|
19
|
+
template.transform_values { |v| resolve_template(v, reducers) }
|
|
20
|
+
else
|
|
21
|
+
template
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def initialize(ctx, block, src: nil)
|
|
26
|
+
@ctx = ctx
|
|
27
|
+
@block = block
|
|
28
|
+
@src = src
|
|
29
|
+
@reducers = []
|
|
30
|
+
@cursor = 0
|
|
31
|
+
@template = nil
|
|
32
|
+
@mode = nil # nil=unknown, :reducer, :passthrough
|
|
33
|
+
@map_transforms = {}
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def call(input)
|
|
37
|
+
@ctx.reset(input)
|
|
38
|
+
@cursor = 0
|
|
39
|
+
@ctx.__jrf_current_stage = self
|
|
40
|
+
result = @ctx.instance_eval(&@block)
|
|
41
|
+
|
|
42
|
+
if @mode.nil? && @reducers.any?
|
|
43
|
+
@mode = :reducer
|
|
44
|
+
@template = result
|
|
45
|
+
elsif @mode.nil?
|
|
46
|
+
@mode = :passthrough
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
(@mode == :reducer) ? Control::DROPPED : result
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def allocate_reducer(value, initial:, finish: nil, &step_fn)
|
|
53
|
+
idx = @cursor
|
|
54
|
+
finish_rows = finish || ->(acc) { [acc] }
|
|
55
|
+
@reducers[idx] ||= Reducers.reduce(initial, finish: finish_rows, &step_fn)
|
|
56
|
+
@reducers[idx].step(value)
|
|
57
|
+
@cursor += 1
|
|
58
|
+
ReducerToken.new(idx)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def allocate_map(type, collection, &block)
|
|
62
|
+
idx = @cursor
|
|
63
|
+
@cursor += 1
|
|
64
|
+
|
|
65
|
+
# Transformation mode (detected on first call)
|
|
66
|
+
if @map_transforms[idx]
|
|
67
|
+
case type
|
|
68
|
+
when :array then return collection.map(&block)
|
|
69
|
+
when :hash then return collection.transform_values(&block)
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
map_reducer = (@reducers[idx] ||= MapReducer.new(type))
|
|
74
|
+
|
|
75
|
+
case type
|
|
76
|
+
when :array
|
|
77
|
+
raise TypeError, "map expects Array, got #{collection.class}" unless collection.is_a?(Array)
|
|
78
|
+
collection.each_with_index do |v, i|
|
|
79
|
+
slot = map_reducer.slot(i)
|
|
80
|
+
with_scoped_reducers(slot.reducers) do
|
|
81
|
+
result = block.call(v)
|
|
82
|
+
slot.template ||= result
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
when :hash
|
|
86
|
+
raise TypeError, "map_values expects Hash, got #{collection.class}" unless collection.is_a?(Hash)
|
|
87
|
+
collection.each do |k, v|
|
|
88
|
+
slot = map_reducer.slot(k)
|
|
89
|
+
with_scoped_reducers(slot.reducers) do
|
|
90
|
+
result = block.call(v)
|
|
91
|
+
slot.template ||= result
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# Detect transformation: no reducers were allocated in any slot
|
|
97
|
+
if @mode.nil? && map_reducer.slots.values.all? { |s| s.reducers.empty? }
|
|
98
|
+
@map_transforms[idx] = true
|
|
99
|
+
@reducers[idx] = nil
|
|
100
|
+
case type
|
|
101
|
+
when :array
|
|
102
|
+
return map_reducer.slots.sort_by { |k, _| k }.map { |_, s| s.template }
|
|
103
|
+
when :hash
|
|
104
|
+
return map_reducer.slots.transform_values(&:template)
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
ReducerToken.new(idx)
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def allocate_group_by(key, &block)
|
|
112
|
+
idx = @cursor
|
|
113
|
+
map_reducer = (@reducers[idx] ||= MapReducer.new(:hash))
|
|
114
|
+
|
|
115
|
+
row = @ctx._
|
|
116
|
+
slot = map_reducer.slot(key)
|
|
117
|
+
with_scoped_reducers(slot.reducers) do
|
|
118
|
+
result = block.call(row)
|
|
119
|
+
slot.template ||= result
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
@cursor += 1
|
|
123
|
+
ReducerToken.new(idx)
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def finish
|
|
127
|
+
return [] unless @mode == :reducer && @reducers.any?
|
|
128
|
+
|
|
129
|
+
if @template.is_a?(ReducerToken)
|
|
130
|
+
@reducers.fetch(@template.index).finish
|
|
131
|
+
else
|
|
132
|
+
[self.class.resolve_template(@template, @reducers)]
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
private
|
|
137
|
+
|
|
138
|
+
def with_scoped_reducers(reducer_list)
|
|
139
|
+
saved_reducers = @reducers
|
|
140
|
+
saved_cursor = @cursor
|
|
141
|
+
@reducers = reducer_list
|
|
142
|
+
@cursor = 0
|
|
143
|
+
yield
|
|
144
|
+
ensure
|
|
145
|
+
@reducers = saved_reducers
|
|
146
|
+
@cursor = saved_cursor
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
class MapReducer
|
|
150
|
+
attr_reader :slots
|
|
151
|
+
|
|
152
|
+
def initialize(type)
|
|
153
|
+
@type = type
|
|
154
|
+
@slots = {}
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
def slot(key)
|
|
158
|
+
@slots[key] ||= SlotState.new
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
def finish
|
|
162
|
+
case @type
|
|
163
|
+
when :array
|
|
164
|
+
keys = @slots.keys.sort
|
|
165
|
+
[keys.map { |k| Stage.resolve_template(@slots[k].template, @slots[k].reducers) }]
|
|
166
|
+
when :hash
|
|
167
|
+
result = {}
|
|
168
|
+
@slots.each { |k, s| result[k] = Stage.resolve_template(s.template, s.reducers) }
|
|
169
|
+
[result]
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
class SlotState
|
|
174
|
+
attr_reader :reducers
|
|
175
|
+
attr_accessor :template
|
|
176
|
+
|
|
177
|
+
def initialize
|
|
178
|
+
@reducers = []
|
|
179
|
+
@template = nil
|
|
180
|
+
end
|
|
181
|
+
end
|
|
182
|
+
end
|
|
183
|
+
end
|
|
184
|
+
end
|
data/lib/jrf/version.rb
CHANGED
data/lib/jrf.rb
CHANGED
|
@@ -2,3 +2,21 @@
|
|
|
2
2
|
|
|
3
3
|
require_relative "jrf/version"
|
|
4
4
|
require_relative "jrf/cli"
|
|
5
|
+
require_relative "jrf/pipeline"
|
|
6
|
+
|
|
7
|
+
module Jrf
|
|
8
|
+
# Create a pipeline from one or more stage blocks.
|
|
9
|
+
#
|
|
10
|
+
# Each block is evaluated in a context where +_+ is the current value.
|
|
11
|
+
# All jrf built-in functions (+select+, +sum+, +map+, +group_by+, etc.)
|
|
12
|
+
# are available inside blocks. See https://github.com/kazuho/jrf#readme for the full list.
|
|
13
|
+
#
|
|
14
|
+
# @param blocks [Array<Proc>] one or more stage procs
|
|
15
|
+
# @return [Pipeline] a callable pipeline
|
|
16
|
+
# @example
|
|
17
|
+
# j = Jrf.new(proc { select(_["x"] > 10) }, proc { sum(_["x"]) })
|
|
18
|
+
# j.call([{"x" => 20}, {"x" => 30}]) # => [50]
|
|
19
|
+
def self.new(*blocks)
|
|
20
|
+
Pipeline.new(*blocks)
|
|
21
|
+
end
|
|
22
|
+
end
|