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.
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Jrf
4
- VERSION = "0.1.2"
4
+ VERSION = "0.1.4"
5
5
  end
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