piperator 0.3.0 → 1.2.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: 5ce14dd4a45b8ce9bc5453cb30df67c1357c8903
4
- data.tar.gz: b3df089403c086b52d8f0a48d2292d9f06495c51
2
+ SHA256:
3
+ metadata.gz: 1475b2b74289ce3c32f638a67156b943101bff0b548283e2e1ea635d5048a6e5
4
+ data.tar.gz: 3d06228a633975fde9409387471bb505eb192971c8e50e7ce0df34bbc74e2ae1
5
5
  SHA512:
6
- metadata.gz: e93f487947ca4c0bacc904b3b04d1e90a245a3bc2efa68bb2b461f3df0427b6e34182cfc01c8cb50be9ae79906de91848ee70102687b55b31d769ed4938f40d9
7
- data.tar.gz: 0a2e61d19a4533ac76bb302f6fbc505f7a8e4cf5e97f0652ab9bd6b8f34aff1e79e22d271c12656a7e7a0b0cb460be2f6f3e681bbdf6e31d4c55c06f2296499c
6
+ metadata.gz: 9a11be79d16a0a54c21cb62518ce6426a184c9c9d6609dc6424533413d6cee875f57d74411d92b39dc57fbf9d25d5e712b112149d7bc16d3f42a6f972e52284a
7
+ data.tar.gz: e2cec664b6832086b53cdaa200b03852f6dcd62cb4f3c5b18532ea636054b0c599471d84a181857e36919cea400a68f1e6fdd93bdeb03e9f612107acdcd4768f
data/.travis.yml CHANGED
@@ -1,5 +1,11 @@
1
1
  sudo: false
2
+ cache: bundler
2
3
  language: ruby
3
4
  rvm:
4
- - 2.4.0
5
- before_install: gem install bundler -v 1.14.6
5
+ - 2.3
6
+ - 2.4
7
+ - 2.5
8
+ - 2.6
9
+ - 2.7
10
+ - jruby
11
+ - truffleruby
data/CHANGELOG.md CHANGED
@@ -1,3 +1,11 @@
1
+ ## 1.1.0 (6 December 2019)
2
+
3
+ - add `#pos` to Piperator::IO to get current position
4
+
5
+ ## 1.0.0. (13 October 2019)
6
+
7
+ - add `Piperator.build` to build pipelines with DSL
8
+
1
9
  ## 0.3.0 (13 July 2017)
2
10
 
3
11
  - remove implicit wrapping to callable from `Pipeline.pipe`
data/README.md CHANGED
@@ -6,6 +6,20 @@ The library is heavily inspired by [Elixir pipe operator](https://elixirschool.c
6
6
 
7
7
  [![Build Status](https://travis-ci.org/lautis/piperator.svg?branch=master)](https://travis-ci.org/lautis/piperator)
8
8
 
9
+ <!-- START doctoc generated TOC please keep comment here to allow auto update -->
10
+ <!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
11
+ **Table of Contents**
12
+
13
+ - [Installation](#installation)
14
+ - [Usage](#usage)
15
+ - [Pipelines](#pipelines)
16
+ - [Enumerators as IO objects](#enumerators-as-io-objects)
17
+ - [Development](#development)
18
+ - [Related Projects](#related-projects)
19
+ - [Contributing](#contributing)
20
+ - [License](#license)
21
+
22
+ <!-- END doctoc generated TOC please keep comment here to allow auto update -->
9
23
 
10
24
  ## Installation
11
25
 
@@ -35,7 +49,16 @@ Piperator.
35
49
  # => 18
36
50
  ```
37
51
 
38
- If desired, the input enumerable can also be given as the first elmenent of the pipeline using `Piperator.wrap`.
52
+ The same could also be achieved using DSL instead of method chaining:
53
+
54
+ ```ruby
55
+ Piperator.build do
56
+ pipe(->(values) { values.lazy.map { |i| i * 3 } })
57
+ pipe(->(values) { values.sum })
58
+ end.call([1, 2, 3])
59
+ ```
60
+
61
+ If desired, the input enumerable can also be given as the first element of the pipeline using `Piperator.wrap`.
39
62
 
40
63
  ```ruby
41
64
  Piperator.
@@ -46,6 +69,18 @@ Piperator.
46
69
  # => 18
47
70
  ```
48
71
 
72
+ Have reasons to defer constructing a pipe? Evaluate it lazily:
73
+
74
+ ```ruby
75
+ summing = ->(values) { values.sum }
76
+ Piperator.build
77
+ pipe(->(values) { values.lazy.map { |i| i * 3 } })
78
+ lazy do
79
+ summing
80
+ end
81
+ end.call([1, 2, 3])
82
+ ```
83
+
49
84
  There is, of course, a much more idiomatic alternative in Ruby:
50
85
 
51
86
  ```ruby
@@ -156,6 +191,12 @@ After checking out the repo, run `bin/setup` to install dependencies. Then, run
156
191
 
157
192
  To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
158
193
 
194
+ ## Related Projects
195
+
196
+ * [D★Stream](https://github.com/ddfreyne/d-stream) - Set of extensions for writing stream-processing code.
197
+ * [ddbuffer](https://github.com/ddfreyne/ddbuffer) - Buffer enumerables/enumerators.
198
+ * [Down::ChunkedIO](https://github.com/janko-m/down/blob/master/lib/down/chunked_io.rb) - A similar IO class as Piperator::IO.
199
+
159
200
  ## Contributing
160
201
 
161
202
  Bug reports and pull requests are welcome on GitHub at https://github.com/lautis/piperator.
@@ -0,0 +1,54 @@
1
+ module Piperator
2
+ # Builder is used to provide DSL-based Pipeline building. Using Builder,
3
+ # Pipelines can be built without pipe chaining, which might be easier if
4
+ # some steps need to be included only on specific conditions.
5
+ #
6
+ # @see Piperator.build
7
+ class Builder
8
+ # Expose a chained method in Pipeline in DSL
9
+ #
10
+ # @param method_name Name of method in Pipeline
11
+ # @see Pipeline
12
+ #
13
+ # @!macro [attach] dsl_method
14
+ # @method $1
15
+ # Call Pipeline#$1 given arguments and use the return value as builder state.
16
+ #
17
+ # @see Pipeline.$1
18
+ def self.dsl_method(method_name)
19
+ define_method(method_name) do |*arguments, &block|
20
+ @pipeline = @pipeline.send(method_name, *arguments, &block)
21
+ end
22
+ end
23
+
24
+ dsl_method :lazy
25
+ dsl_method :pipe
26
+ dsl_method :wrap
27
+
28
+ def initialize(saved_binding, pipeline = Pipeline.new)
29
+ @pipeline = pipeline
30
+ @saved_binding = saved_binding
31
+ end
32
+
33
+ # Return build pipeline
34
+ #
35
+ # @return [Pipeline]
36
+ def to_pipeline
37
+ @pipeline
38
+ end
39
+
40
+ private
41
+
42
+ def method_missing(method_name, *arguments, &block)
43
+ if @saved_binding.receiver.respond_to?(method_name, true)
44
+ @saved_binding.receiver.send(method_name, *arguments, &block)
45
+ else
46
+ super
47
+ end
48
+ end
49
+
50
+ def respond_to_missing?(method_name, include_private = false)
51
+ @saved_binding.receiver.respond_to?(method_name, include_private) || super
52
+ end
53
+ end
54
+ end
data/lib/piperator/io.rb CHANGED
@@ -6,22 +6,24 @@ module Piperator
6
6
  FLUSH_THRESHOLD = 128 * 1028 # 128KiB
7
7
 
8
8
  attr_reader :eof
9
+ attr_reader :pos
9
10
 
10
11
  def initialize(enumerator, flush_threshold: FLUSH_THRESHOLD)
11
12
  @enumerator = enumerator
12
13
  @flush_threshold = flush_threshold
13
- @io = StringIO.new
14
- @buffer_start_pos = 0
15
- @io_read_pos = 0
14
+ @buffer = StringIO.new
15
+ @pos = 0
16
+ @buffer_read_pos = 0
16
17
  @eof = false
17
18
  end
18
19
 
19
20
  alias eof? eof
21
+ alias tell pos
20
22
 
21
23
  # Return the first bytes of the buffer without marking the buffer as read.
22
24
  def peek(bytes)
23
25
  while @eof == false && readable_bytes < (bytes || 1)
24
- @io.write(@enumerator.next)
26
+ @buffer.write(@enumerator.next)
25
27
  end
26
28
  peek_buffer(bytes)
27
29
  rescue StopIteration
@@ -38,22 +40,22 @@ module Piperator
38
40
  def gets(separator = $INPUT_RECORD_SEPARATOR, _limit = nil)
39
41
  while !@eof && !contains_line?(separator)
40
42
  begin
41
- @io.write(@enumerator.next)
43
+ @buffer.write(@enumerator.next)
42
44
  rescue StopIteration
43
45
  @eof = true
44
46
  nil
45
47
  end
46
48
  end
47
- read_with { @io.gets(separator) }
49
+ read_with { @buffer.gets(separator) }
48
50
  end
49
51
 
50
52
  # Flush internal buffer until the last unread byte
51
53
  def flush
52
- if @io.pos == @io_read_pos
54
+ if @buffer.pos == @buffer_read_pos
53
55
  initialize_buffer
54
56
  else
55
- @io.pos = @io_read_pos
56
- initialize_buffer(@io.read)
57
+ @buffer.pos = @buffer_read_pos
58
+ initialize_buffer(@buffer.read)
57
59
  end
58
60
  end
59
61
 
@@ -62,56 +64,56 @@ module Piperator
62
64
  # @param length [Integer] number of bytes to read
63
65
  # @return String
64
66
  def read(length = nil)
65
- return @enumerator.next if length.nil? && readable_bytes.zero?
66
- @io.write(@enumerator.next) while !@eof && readable_bytes < (length || 1)
67
- read_with { @io.read(length) }
67
+ return @enumerator.next.tap { |e| @pos += e.bytesize } if length.nil? && readable_bytes.zero?
68
+ @buffer.write(@enumerator.next) while !@eof && readable_bytes < (length || 1)
69
+ read_with { @buffer.read(length) }
68
70
  rescue StopIteration
69
71
  @eof = true
70
- read_with { @io.read(length) } if readable_bytes > 0
72
+ read_with { @buffer.read(length) } if readable_bytes > 0
71
73
  end
72
74
 
73
75
  # Current buffer size - including non-freed read content
74
76
  #
75
77
  # @return [Integer] number of bytes stored in buffer
76
78
  def used
77
- @io.size
79
+ @buffer.size
78
80
  end
79
81
 
80
82
  private
81
83
 
82
84
  def readable_bytes
83
- @io.pos - @io_read_pos
85
+ @buffer.pos - @buffer_read_pos
84
86
  end
85
87
 
86
88
  def read_with
87
- pos = @io.pos
88
- @io.pos = @io_read_pos
89
+ pos = @buffer.pos
90
+ @buffer.pos = @buffer_read_pos
89
91
 
90
92
  yield.tap do |data|
91
- @io_read_pos += data.bytesize if data
92
- @io.pos = pos
93
+ @buffer_read_pos += data.bytesize if data
94
+ @buffer.pos = pos
93
95
  flush if flush?
94
96
  end
95
97
  end
96
98
 
97
99
  def peek_buffer(bytes)
98
- @io.string.byteslice(@io_read_pos...@io_read_pos + bytes)
100
+ @buffer.string.byteslice(@buffer_read_pos...@buffer_read_pos + bytes)
99
101
  end
100
102
 
101
103
  def flush?
102
- @io.pos == @io_read_pos || @io.pos > @flush_threshold
104
+ @buffer.pos == @buffer_read_pos || @buffer.pos > @flush_threshold
103
105
  end
104
106
 
105
107
  def initialize_buffer(data = nil)
106
- @io_read_pos = 0
107
- @buffer_start_pos += @io.pos if @io
108
- @io = StringIO.new
109
- @io.write(data) if data
108
+ @pos += @buffer_read_pos
109
+ @buffer_read_pos = 0
110
+ @buffer = StringIO.new
111
+ @buffer.write(data) if data
110
112
  end
111
113
 
112
114
  def contains_line?(separator = $INPUT_RECORD_SEPARATOR)
113
115
  return true if @eof
114
- @io.string.byteslice(@io_read_pos..-1).include?(separator)
116
+ @buffer.string.byteslice(@buffer_read_pos..-1).include?(separator)
115
117
  rescue ArgumentError # Invalid UTF-8
116
118
  false
117
119
  end
@@ -6,6 +6,20 @@ module Piperator
6
6
  # For streaming purposes, it usually is desirable to have pipes that takes
7
7
  # a lazy Enumerator as an argument a return a (modified) lazy Enumerator.
8
8
  class Pipeline
9
+ # Build a new pipeline from a lazily evaluated callable or an enumerable
10
+ # object.
11
+ #
12
+ # @param block A block returning a callable(enumerable)
13
+ # @return [Pipeline] A pipeline containing only the lazily evaluated
14
+ # callable.
15
+ def self.lazy(&block)
16
+ callable = nil
17
+ Pipeline.new([lambda do |e|
18
+ callable ||= block.call
19
+ callable.call(e)
20
+ end])
21
+ end
22
+
9
23
  # Build a new pipeline from a callable or an enumerable object
10
24
  #
11
25
  # @param callable An object responding to call(enumerable)
@@ -59,6 +73,19 @@ module Piperator
59
73
  call(enumerable).to_a
60
74
  end
61
75
 
76
+ # Add a new lazily evaluated part to the pipeline.
77
+ #
78
+ # @param block A block returning a callable(enumerable) to append in
79
+ # pipeline.
80
+ # @return [Pipeline] A new pipeline instance
81
+ def lazy(&block)
82
+ callable = nil
83
+ Pipeline.new(@pipes + [lambda do |e|
84
+ callable ||= block.call
85
+ callable.call(e)
86
+ end])
87
+ end
88
+
62
89
  # Add a new part to the pipeline
63
90
  #
64
91
  # @param other A pipe to append in pipeline. Responds to #call.
@@ -1,3 +1,4 @@
1
1
  module Piperator
2
- VERSION = '0.3.0'.freeze
2
+ # Piperator version
3
+ VERSION = '1.2.0'.freeze
3
4
  end
data/lib/piperator.rb CHANGED
@@ -1,13 +1,43 @@
1
1
  require 'piperator/version'
2
2
  require 'piperator/pipeline'
3
3
  require 'piperator/io'
4
+ require 'piperator/builder'
4
5
 
5
6
  # Top-level shortcuts
6
7
  module Piperator
8
+ # Build a new pipeline using DSL. This enables easy control of the pipeline
9
+ # stucture.
10
+ #
11
+ # Piperator.build do
12
+ # wrap [1, 2, 3]
13
+ # pipe(-> (enumerable) { enumerable.map { |i| i + 1 } })
14
+ # end
15
+ # # => Pipeline that returns [2, 3, 4] called
16
+ #
17
+ # # Alternatively, the Builder is also given as argument to the block
18
+ # Piperator.build do |p|
19
+ # p.wrap [1, 2, 3]
20
+ # p.pipe(-> (enumerable) { enumerable.map { |i| i + 1 } })
21
+ # end
22
+ # # This is similar, but allows access to instance variables.
23
+ #
24
+ # @return [Pipeline] Pipeline containing defined steps
25
+ def self.build(&block)
26
+ return Pipeline.new unless block_given?
27
+
28
+ Builder.new(block&.binding).tap do |builder|
29
+ if block.arity.positive?
30
+ yield builder
31
+ else
32
+ builder.instance_eval(&block)
33
+ end
34
+ end.to_pipeline
35
+ end
36
+
7
37
  # Build a new pipeline from a callable or an enumerable object
8
38
  #
9
39
  # @see Piperator::Pipeline.pipe
10
- # @param callable An object responding to call(enumerable)
40
+ # @param enumerable An object responding to call(enumerable)
11
41
  # @return [Pipeline] A pipeline containing only the callable
12
42
  def self.pipe(enumerable)
13
43
  Pipeline.pipe(enumerable)
data/piperator.gemspec CHANGED
@@ -23,7 +23,7 @@ Gem::Specification.new do |spec|
23
23
  spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
24
24
  spec.require_paths = ['lib']
25
25
 
26
- spec.add_development_dependency 'bundler', '~> 1.14'
26
+ spec.add_development_dependency 'bundler', '>= 1.14'
27
27
  spec.add_development_dependency 'rake', '~> 12.0'
28
28
  spec.add_development_dependency 'rspec', '~> 3.0'
29
29
  end
metadata CHANGED
@@ -1,27 +1,27 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: piperator
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.0
4
+ version: 1.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ville Lautanala
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2017-07-13 00:00:00.000000000 Z
11
+ date: 2020-06-02 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
15
15
  requirement: !ruby/object:Gem::Requirement
16
16
  requirements:
17
- - - "~>"
17
+ - - ">="
18
18
  - !ruby/object:Gem::Version
19
19
  version: '1.14'
20
20
  type: :development
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
- - - "~>"
24
+ - - ">="
25
25
  - !ruby/object:Gem::Version
26
26
  version: '1.14'
27
27
  - !ruby/object:Gem::Dependency
@@ -71,6 +71,7 @@ files:
71
71
  - bin/console
72
72
  - bin/setup
73
73
  - lib/piperator.rb
74
+ - lib/piperator/builder.rb
74
75
  - lib/piperator/io.rb
75
76
  - lib/piperator/pipeline.rb
76
77
  - lib/piperator/version.rb
@@ -94,8 +95,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
94
95
  - !ruby/object:Gem::Version
95
96
  version: '0'
96
97
  requirements: []
97
- rubyforge_project:
98
- rubygems_version: 2.6.11
98
+ rubygems_version: 3.1.2
99
99
  signing_key:
100
100
  specification_version: 4
101
101
  summary: Composable pipelines for streaming large collections