jrf 0.1.12 → 0.1.13
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/jrf.gemspec +2 -0
- data/lib/jrf/cli/runner.rb +57 -5
- data/lib/jrf/cli.rb +5 -4
- data/lib/jrf/version.rb +1 -1
- data/test/cli_runner_test.rb +951 -0
- data/test/library_api_test.rb +126 -0
- data/test/readme_examples_test.rb +16 -0
- data/test/test_helper.rb +118 -0
- metadata +33 -2
- data/test/jrf_test.rb +0 -1103
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "test_helper"
|
|
4
|
+
|
|
5
|
+
class LibraryApiTest < JrfTestCase
|
|
6
|
+
def test_basic_pipeline_api
|
|
7
|
+
j = Jrf.new(proc { _ })
|
|
8
|
+
assert_equal([{"a" => 1}, {"a" => 2}], j.call([{"a" => 1}, {"a" => 2}]), "library passthrough")
|
|
9
|
+
|
|
10
|
+
j = Jrf.new(proc { _["a"] })
|
|
11
|
+
assert_equal([1, 2], j.call([{"a" => 1}, {"a" => 2}]), "library extract")
|
|
12
|
+
|
|
13
|
+
j = Jrf.new(
|
|
14
|
+
proc { select(_["a"] > 1) },
|
|
15
|
+
proc { _["a"] }
|
|
16
|
+
)
|
|
17
|
+
assert_equal([2, 3], j.call([{"a" => 1}, {"a" => 2}, {"a" => 3}]), "library select + extract")
|
|
18
|
+
|
|
19
|
+
j = Jrf.new(proc { sum(_["a"]) })
|
|
20
|
+
assert_equal([6], j.call([{"a" => 1}, {"a" => 2}, {"a" => 3}]), "library sum")
|
|
21
|
+
|
|
22
|
+
j = Jrf.new(proc { sum(2 * _["a"]) })
|
|
23
|
+
assert_equal([12], j.call([{"a" => 1}, {"a" => 2}, {"a" => 3}]), "library sum literal on left")
|
|
24
|
+
|
|
25
|
+
j = Jrf.new(proc { {total: sum(_["a"]), n: count()} })
|
|
26
|
+
assert_equal([{total: 6, n: 3}], j.call([{"a" => 1}, {"a" => 2}, {"a" => 3}]), "library structured reducers")
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def test_map_and_map_values_api
|
|
30
|
+
j = Jrf.new(proc { map { |x| x + 1 } })
|
|
31
|
+
assert_equal([[2, 3], [4, 5]], j.call([[1, 2], [3, 4]]), "library map transform")
|
|
32
|
+
|
|
33
|
+
j = Jrf.new(proc { map { |x| sum(x) } })
|
|
34
|
+
assert_equal([[4, 6]], j.call([[1, 2], [3, 4]]), "library map reduce")
|
|
35
|
+
|
|
36
|
+
j = Jrf.new(proc { map { map { |y| [sum(y[0]), sum(y[1])] } } })
|
|
37
|
+
assert_equal([[[[4, 6]]]], j.call([[[[1, 2]]], [[[3, 4]]]]), "library nested map reduce")
|
|
38
|
+
|
|
39
|
+
j = Jrf.new(proc { map_values { |v| v * 10 } })
|
|
40
|
+
assert_equal([{"a" => 10, "b" => 20}], j.call([{"a" => 1, "b" => 2}]), "library map_values transform")
|
|
41
|
+
|
|
42
|
+
j = Jrf.new(proc { map_values { |obj| map_values { |v| sum(v) } } })
|
|
43
|
+
assert_equal([{"a" => {"x" => 4, "y" => 6}, "b" => {"x" => 40, "y" => 60}}], j.call([{"a" => {"x" => 1, "y" => 2}, "b" => {"x" => 10, "y" => 20}}, {"a" => {"x" => 3, "y" => 4}, "b" => {"x" => 30, "y" => 40}}]), "library nested map_values reduce")
|
|
44
|
+
|
|
45
|
+
j = Jrf.new(proc { map { |k, v| "#{k}=#{v}" } })
|
|
46
|
+
assert_equal([["a=1", "b=2"]], j.call([{"a" => 1, "b" => 2}]), "library map hash transform")
|
|
47
|
+
|
|
48
|
+
j = Jrf.new(proc { map { |pair| pair } })
|
|
49
|
+
assert_equal([[["a", 1], ["b", 2]]], j.call([{"a" => 1, "b" => 2}]), "library map hash single block arg")
|
|
50
|
+
|
|
51
|
+
j = Jrf.new(proc { map { |k, v| sum(v + k.length) } })
|
|
52
|
+
assert_equal([[5, 7]], j.call([{"a" => 1, "b" => 2}, {"a" => 2, "b" => 3}]), "library map hash reduce")
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def test_apply_and_group_by_api
|
|
56
|
+
j = Jrf.new(proc { [apply { |x| sum(x["foo"]) }, _.length] })
|
|
57
|
+
assert_equal([[3, 2], [10, 1]], j.call([[{"foo" => 1}, {"foo" => 2}], [{"foo" => 10}]]), "library apply reducer")
|
|
58
|
+
|
|
59
|
+
j = Jrf.new(proc { apply { |x| x["foo"] } })
|
|
60
|
+
assert_equal([[1, 2]], j.call([[{"foo" => 1}, {"foo" => 2}]]), "library apply passthrough")
|
|
61
|
+
|
|
62
|
+
j = Jrf.new(proc { apply { |x| percentile(x, 0.5) } })
|
|
63
|
+
assert_equal([20], j.call([[10, 20, 30]]), "library apply percentile")
|
|
64
|
+
|
|
65
|
+
j = Jrf.new(proc { map { |o| [apply(o["vals"]) { |x| sum(x) }, o["name"]] } })
|
|
66
|
+
assert_equal([[[3, "a"], [30, "b"]]], j.call([[{"name" => "a", "vals" => [1, 2]}, {"name" => "b", "vals" => [10, 20]}]]), "library apply explicit collection")
|
|
67
|
+
|
|
68
|
+
j = Jrf.new(proc { map(_["items"]) { |x| x * 2 } })
|
|
69
|
+
assert_equal([[2, 4, 6]], j.call([{"items" => [1, 2, 3]}]), "library map explicit collection")
|
|
70
|
+
|
|
71
|
+
j = Jrf.new(proc { map_values(_["data"]) { |v| v * 10 } })
|
|
72
|
+
assert_equal([{"a" => 10, "b" => 20}], j.call([{"data" => {"a" => 1, "b" => 2}}]), "library map_values explicit collection")
|
|
73
|
+
|
|
74
|
+
j = Jrf.new(proc { group_by(_["k"]) { count() } })
|
|
75
|
+
assert_equal([{"x" => 2, "y" => 1}], j.call([{"k" => "x"}, {"k" => "x"}, {"k" => "y"}]), "library group_by")
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def test_percentile_and_control_flow_api
|
|
79
|
+
j = Jrf.new(proc { percentile(_["a"], _["p"]) })
|
|
80
|
+
assert_equal([2], j.call([{"a" => 1, "p" => 0.5}, {"a" => 2, "p" => [0.5, 1.0]}, {"a" => 3, "p" => [0.5, 1.0]}]), "library percentile configuration fixed by first row")
|
|
81
|
+
|
|
82
|
+
counting_percentiles = Class.new do
|
|
83
|
+
include Enumerable
|
|
84
|
+
|
|
85
|
+
attr_reader :each_calls
|
|
86
|
+
|
|
87
|
+
def initialize(values)
|
|
88
|
+
@values = values
|
|
89
|
+
@each_calls = 0
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def each(&block)
|
|
93
|
+
@each_calls += 1
|
|
94
|
+
@values.each(&block)
|
|
95
|
+
end
|
|
96
|
+
end.new([0.25, 0.5, 1.0])
|
|
97
|
+
|
|
98
|
+
j = Jrf.new(proc { percentile(_["a"], counting_percentiles) })
|
|
99
|
+
assert_equal([[1, 2, 3]], j.call([{"a" => 1}, {"a" => 2}, {"a" => 3}]), "library percentile enumerable values")
|
|
100
|
+
assert_equal(1, counting_percentiles.each_calls, "library percentile materializes enumerable once")
|
|
101
|
+
|
|
102
|
+
j = Jrf.new(
|
|
103
|
+
proc { sum(_["a"]) },
|
|
104
|
+
proc { _ + 1 }
|
|
105
|
+
)
|
|
106
|
+
assert_equal([7], j.call([{"a" => 1}, {"a" => 2}, {"a" => 3}]), "library reducer then passthrough")
|
|
107
|
+
|
|
108
|
+
threshold = 2
|
|
109
|
+
j = Jrf.new(proc { select(_["a"] > threshold) })
|
|
110
|
+
assert_equal([{"a" => 3}], j.call([{"a" => 1}, {"a" => 2}, {"a" => 3}]), "library closure")
|
|
111
|
+
|
|
112
|
+
j = Jrf.new(proc { sum(_) })
|
|
113
|
+
assert_equal([], j.call([]), "library empty input")
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def test_stage_reduce_control_tokens
|
|
117
|
+
ctx = Jrf::RowContext.new
|
|
118
|
+
stage = Jrf::Stage.new(ctx, proc { })
|
|
119
|
+
first_token = stage.step_reduce(1, initial: 0) { |acc, v| acc + v }
|
|
120
|
+
assert_equal(0, first_token.index, "step_reduce returns token while classifying reducer stage")
|
|
121
|
+
stage.instance_variable_set(:@mode, :reducer)
|
|
122
|
+
stage.instance_variable_set(:@cursor, 0)
|
|
123
|
+
second_token = stage.step_reduce(2, initial: 0) { |acc, v| acc + v }
|
|
124
|
+
assert_same(Jrf::Control::DROPPED, second_token, "expected DROPPED for established reducer slot")
|
|
125
|
+
end
|
|
126
|
+
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "test_helper"
|
|
4
|
+
|
|
5
|
+
class ReadmeExamplesTest < JrfTestCase
|
|
6
|
+
def test_built_in_function_examples
|
|
7
|
+
readme_examples = extract_readme_examples("./README.md", section: "BUILT-IN FUNCTIONS")
|
|
8
|
+
refute_empty(readme_examples, "expected README built-in examples")
|
|
9
|
+
|
|
10
|
+
readme_examples.each do |example|
|
|
11
|
+
stdout, stderr, status = run_jrf(example[:expr], example[:input])
|
|
12
|
+
assert_success(status, stderr, "README example #{example[:expr]}")
|
|
13
|
+
assert_equal(example[:output], lines(stdout), "README example output #{example[:expr]}")
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
data/test/test_helper.rb
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
begin
|
|
4
|
+
require "bundler/setup"
|
|
5
|
+
rescue LoadError
|
|
6
|
+
# Allow running tests in plain Ruby environments with globally installed gems.
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
require "json"
|
|
10
|
+
require "minitest/autorun"
|
|
11
|
+
require "open3"
|
|
12
|
+
require "stringio"
|
|
13
|
+
require "tmpdir"
|
|
14
|
+
require "zlib"
|
|
15
|
+
require_relative "../lib/jrf"
|
|
16
|
+
require_relative "../lib/jrf/cli/runner"
|
|
17
|
+
|
|
18
|
+
class RecordingRunner < Jrf::CLI::Runner
|
|
19
|
+
attr_reader :writes
|
|
20
|
+
|
|
21
|
+
def initialize(**kwargs)
|
|
22
|
+
super
|
|
23
|
+
@writes = []
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
private
|
|
27
|
+
|
|
28
|
+
def write_output(str)
|
|
29
|
+
return if str.empty?
|
|
30
|
+
|
|
31
|
+
@writes << str
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
class JrfTestCase < Minitest::Test
|
|
36
|
+
def setup
|
|
37
|
+
File.chmod(0o755, "./exe/jrf")
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def run_jrf(expr, input, *opts)
|
|
41
|
+
Open3.capture3("./exe/jrf", *opts, expr, stdin_data: input)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def assert_success(status, stderr, msg = nil)
|
|
45
|
+
return if status.success?
|
|
46
|
+
|
|
47
|
+
flunk("expected success#{msg ? " (#{msg})" : ""}, got failure\nstderr: #{stderr}")
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def assert_failure(status, msg = nil)
|
|
51
|
+
return unless status.success?
|
|
52
|
+
|
|
53
|
+
flunk("expected failure#{msg ? " (#{msg})" : ""}, got success")
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def assert_float_close(expected, actual, epsilon = 1e-9, msg = nil)
|
|
57
|
+
assert_in_delta(expected, actual, epsilon, msg)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def lines(str)
|
|
61
|
+
str.lines.map(&:strip).reject(&:empty?)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def json_stream_to_ndjson(text)
|
|
65
|
+
JSON.parse("[#{text}]").map { |value| "#{JSON.generate(value)}\n" }.join
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def extract_readme_examples(path, section:)
|
|
69
|
+
content = File.read(path)
|
|
70
|
+
section_match = content.match(/^## #{Regexp.escape(section)}\n(.*?)(?=^## |\z)/m)
|
|
71
|
+
raise "section not found: #{section}" unless section_match
|
|
72
|
+
|
|
73
|
+
examples = []
|
|
74
|
+
section_text = section_match[1]
|
|
75
|
+
section_text.scan(/```sh\n(.*?)```/m) do |block_match|
|
|
76
|
+
block = block_match.first
|
|
77
|
+
lines = block.lines.map(&:chomp)
|
|
78
|
+
index = 0
|
|
79
|
+
while index < lines.length
|
|
80
|
+
line = lines[index]
|
|
81
|
+
if (command_match = line.match(/\Ajrf '(.*)'\z/))
|
|
82
|
+
comment = lines[index + 1]
|
|
83
|
+
if comment && (example_match = comment.match(/\A# (.+) → (.+)\z/))
|
|
84
|
+
examples << {
|
|
85
|
+
expr: command_match[1],
|
|
86
|
+
input: json_stream_to_ndjson(example_match[1]),
|
|
87
|
+
output: lines(json_stream_to_ndjson(example_match[2]))
|
|
88
|
+
}
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
index += 1
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
examples
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
class ChunkedSource
|
|
99
|
+
def initialize(str, chunk_size: 5)
|
|
100
|
+
@str = str
|
|
101
|
+
@chunk_size = chunk_size
|
|
102
|
+
@offset = 0
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def read(length = nil, outbuf = nil)
|
|
106
|
+
raise "expected chunked reads" if length.nil?
|
|
107
|
+
|
|
108
|
+
chunk = @str.byteslice(@offset, [length, @chunk_size].min)
|
|
109
|
+
return nil unless chunk
|
|
110
|
+
|
|
111
|
+
@offset += chunk.bytesize
|
|
112
|
+
if outbuf
|
|
113
|
+
outbuf.replace(chunk)
|
|
114
|
+
else
|
|
115
|
+
chunk
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: jrf
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.1.
|
|
4
|
+
version: 0.1.13
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- kazuho
|
|
@@ -23,6 +23,34 @@ dependencies:
|
|
|
23
23
|
- - ">="
|
|
24
24
|
- !ruby/object:Gem::Version
|
|
25
25
|
version: '3.16'
|
|
26
|
+
- !ruby/object:Gem::Dependency
|
|
27
|
+
name: minitest
|
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
|
29
|
+
requirements:
|
|
30
|
+
- - ">="
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: '5.0'
|
|
33
|
+
type: :development
|
|
34
|
+
prerelease: false
|
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - ">="
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: '5.0'
|
|
40
|
+
- !ruby/object:Gem::Dependency
|
|
41
|
+
name: rake
|
|
42
|
+
requirement: !ruby/object:Gem::Requirement
|
|
43
|
+
requirements:
|
|
44
|
+
- - ">="
|
|
45
|
+
- !ruby/object:Gem::Version
|
|
46
|
+
version: '13.0'
|
|
47
|
+
type: :development
|
|
48
|
+
prerelease: false
|
|
49
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
50
|
+
requirements:
|
|
51
|
+
- - ">="
|
|
52
|
+
- !ruby/object:Gem::Version
|
|
53
|
+
version: '13.0'
|
|
26
54
|
description: jrf is a JSON filter with the power and speed of Ruby. It lets you write
|
|
27
55
|
transforms as Ruby expressions, so you can use arbitrary Ruby logic. It supports
|
|
28
56
|
extraction, filtering, flattening, sorting, and aggregation in stage pipelines.
|
|
@@ -47,7 +75,10 @@ files:
|
|
|
47
75
|
- lib/jrf/row_context.rb
|
|
48
76
|
- lib/jrf/stage.rb
|
|
49
77
|
- lib/jrf/version.rb
|
|
50
|
-
- test/
|
|
78
|
+
- test/cli_runner_test.rb
|
|
79
|
+
- test/library_api_test.rb
|
|
80
|
+
- test/readme_examples_test.rb
|
|
81
|
+
- test/test_helper.rb
|
|
51
82
|
licenses:
|
|
52
83
|
- MIT
|
|
53
84
|
metadata: {}
|