Module: Ionian::Extension::IO

Defined in:
lib/ionian/extension/io.rb

Overview

A mixin for IO objects that allows regular expression matching and convenient notification of received data.

This module was designed to be extended by instantiated objects that implement the standard library IO class. my_socket.extend Ionian::IO

Instance Attribute Summary (collapse)

Class Method Summary (collapse)

Instance Method Summary (collapse)

Instance Attribute Details

- (Object) ionian_timeout

Number of seconds to attempt an IO operation before timing out. See standard library IO::select.



14
15
16
# File 'lib/ionian/extension/io.rb', line 14

def ionian_timeout
  @ionian_timeout
end

Class Method Details

+ (Object) extended(obj)

Called automaticallly when the object is extended with #extend.



17
18
19
# File 'lib/ionian/extension/io.rb', line 17

def self.extended obj
  obj.initialize_ionian
end

Instance Method Details

- (Regexp) expression

Returns Regular expression used for #read_match.

Returns:

  • (Regexp)

    Regular expression used for #read_match.



43
44
45
# File 'lib/ionian/extension/io.rb', line 43

def expression
  @ionian_expression
end

- (Object) expression=(exp)

Set the expression to match against the read buffer. Can be a regular expression specifying capture groups, or a string specifying the separator or line terminator sequence. It is possible to use named captures in a regex, which allows for convienient accessors like match.



53
54
55
56
# File 'lib/ionian/extension/io.rb', line 53

def expression= exp
  @ionian_expression = exp
  @ionian_expression = Regexp.new "(.*?)#{expression}" if exp.is_a? String
end

- (Boolean) has_data?(timeout: 0)

Returns True if there is data in the receive buffer.

Parameters:

  • timeout (Numeric)

    Number of seconds to wait for data until giving up. Set to nil for blocking.

Returns:

  • (Boolean)

    True if there is data in the receive buffer.



38
39
40
# File 'lib/ionian/extension/io.rb', line 38

def has_data? timeout: 0
  ::IO.select([self], nil, nil, timeout) ? true : false
end

- (Object) initialize_ionian

Initialize the Ionian instance variables. This is called automatically if #extend is called on an object.



23
24
25
26
27
28
29
30
31
# File 'lib/ionian/extension/io.rb', line 23

def initialize_ionian
  @ionian_match_handlers = []
  @ionian_error_handlers = []
  @ionian_buf            = ''
  @ionian_expression     = /(.*?)[\r\n]+/
  @ionian_timeout        = nil
  @ionian_skip_select    = false
  @ionian_build_methods  = true
end

- (Object) purge

Erase the data in the IO and Ionian buffers. This is typically handled automatically.



189
190
191
192
193
# File 'lib/ionian/extension/io.rb', line 189

def purge
  # Erase IO buffer.
  read_all
  @ionian_buf = ''
end

- (Object) read_all(nonblocking: false)

Read all data in the buffer. An alternative to using #readpartial with a large length. Blocks until data is available unless nonblocking: true. If nonblocking, returns nil if no data available.



62
63
64
65
66
67
68
69
70
# File 'lib/ionian/extension/io.rb', line 62

def read_all nonblocking: false
  return nil if nonblocking and not has_data?
  
  # Block until data has arrived.
  data =  readpartial 0xFFFF
  # If there is more data in the buffer, retrieve it nonblocking.
  data += readpartial 0xFFFF while has_data?
  data
end

- (Array<MatchData>?) read_match(**kwargs) {|match| ... }

Read matched data from the buffer. This method SHOULD NOT be used if #run_match is used.

Junk data that could exist before a match in the buffer can be accessed with match.pre_match.

Data at the end of the buffer that is not matched can be accessed in the last match with match.post_match. This data remains in the buffer for the next #read_match cycle. This is helpful for protocols like RS232 that do not have packet boundries.

Parameters:

  • kwargs (Hash)

    a customizable set of options

Options Hash (**kwargs):

  • :timeout (Numeric) — default: nil

    Timeout in seconds IO::select will block. Blocks indefinitely by default. Set to 0 for nonblocking.

  • :expression (Regexp, String)

    Override the expression match for this single method call.

  • :notify (Boolean) — default: true

    Set to false to skip notifying match handler procs.

  • :skip_select (Boolean) — default: false

    Skip over the IO::select statement. Use if you are calling IO::select ahead of this method.

  • :build_methods (Boolean) — default: true

    Build accessor methods from named capture groups.

Yield Parameters:

  • match (MatchData)

    If there are multiple matches, the block is called multiple times.

Returns:

  • (Array<MatchData>, nil)

    matches. Nil if no data was received within the timeout period.



107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
# File 'lib/ionian/extension/io.rb', line 107

def read_match **kwargs, &block
  timeout       = kwargs.fetch :timeout,        @ionian_timeout
  notify        = kwargs.fetch :notify,         true
  skip_select   = kwargs.fetch :skip_select,    @ionian_skip_select
  build_methods = kwargs.fetch :build_methods,  @ionian_build_methods
  
  exp           = kwargs.fetch :expression,     @ionian_expression
  exp           = Regexp.new "(.*?)#{exp}" if exp.is_a? String
  
  unless skip_select
    return nil unless self.has_data? timeout: timeout
  end
  
  @matches = []
  
  # TODO: Implement an option for number of bytes or timeout to throw away
  #       data if no match is found.
  Timeout.timeout(timeout) do
    loop do
      # Read data from the IO buffer until it's empty.
      @ionian_buf << read_all
      break if @ionian_buf =~ exp
    end
  end
  
  while @ionian_buf =~ exp
    @matches << $~   # Match data.
    @ionian_buf = $' # Leave post match data in the buffer.
  end
  
  # Convert named captures to methods.
  if build_methods
    @matches.each do |match|
      match.names
        .map { |name| name.to_sym }
        .each { |symbol|
          match.singleton_class
            .send(:define_method, symbol) { match[symbol] } \
              unless match.respond_to? symbol
        }
    end
  end
  
  # Pass each match to block.
  @matches.each { |match| yield match } if block_given?
  
  # Notify on_match handlers unless the #run_match thread is active.
  @matches.each { |match| notify_match_handlers match } \
    if notify and not @run_match_thread
  
  @matches
end

- (Block) register_error_handler {|Exception, self| ... } Also known as: on_error

Register a block to be called when #run_match raises an error. Method callbacks can be registered with &object.method(:method).

Yields:

  • (Exception, self)

Returns:

  • (Block)

    a reference to the given block.



234
235
236
237
# File 'lib/ionian/extension/io.rb', line 234

def register_error_handler &block
  @ionian_error_handlers << block unless @ionian_error_handlers.include? block
  block
end

- (Block) register_match_handler {|MatchData, self| ... } Also known as: on_match

Register a block to be called when #run_match receives matched data. Method callbacks can be registered with &object.method(:method).

Examples:

registered_block = ionian_socket.register_match_handler { |match| ... }
registered_block = ionian_socket.register_match_handler &my_object.method(:foo)

Yields:

  • (MatchData, self)

    Regex match.

Returns:

  • (Block)

    a reference to the given block.



205
206
207
208
# File 'lib/ionian/extension/io.rb', line 205

def register_match_handler &block
  @ionian_match_handlers << block unless @ionian_match_handlers.include? block
  block
end

- (Object) register_observer(&block)

Deprecated.


213
214
215
216
# File 'lib/ionian/extension/io.rb', line 213

def register_observer &block
  STDOUT.puts "WARN: Call to deprecated method #{__method__}"
  register_match_handler &block
end

- (Object) run_match(**kwargs)

Start a thread that checks for data and notifies match and error handlers. Passes kwargs to #read_match. This method SHOULD NOT be used if #read_match is used.



163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
# File 'lib/ionian/extension/io.rb', line 163

def run_match **kwargs
  @run_match_thread ||= Thread.new do
    Thread.current.thread_variable_set :match_thread_running, true
    begin
      while not closed? do
        matches = read_match **kwargs
        matches.each { |match| notify_match_handlers match } if matches
      end
    rescue Exception => e
      notify_error_handlers e
    ensure
      @run_match_thread = nil
    end
  end
end

- (Boolean) run_match_is_running?

Returns:

  • (Boolean)


179
180
181
182
183
184
185
# File 'lib/ionian/extension/io.rb', line 179

def run_match_is_running?
  return true if \
    @run_match_thread and
    @run_match_thread.thread_variable_get(:match_thread_running)
  
  false
end

- (Object) unregister_error_handler(&block)

Unregister a block from being called when a #run_match error is raised.



242
243
244
245
# File 'lib/ionian/extension/io.rb', line 242

def unregister_error_handler &block
  @ionian_error_handlers.delete_if { |o| o == block }
  block
end

- (Object) unregister_match_handler(&block)

Unregister a block from being called when matched data is received.



219
220
221
222
# File 'lib/ionian/extension/io.rb', line 219

def unregister_match_handler &block
  @ionian_match_handlers.delete_if { |o| o == block }
  block
end

- (Object) unregister_observer(&block)

Deprecated.


225
226
227
228
# File 'lib/ionian/extension/io.rb', line 225

def unregister_observer &block
  STDOUT.puts "WARN: Call to deprecated method #{__method__}"
  unregister_match_handler &block
end