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.
@@ -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
@@ -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.12
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/jrf_test.rb
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: {}