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

#ionian_timeoutObject

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

.extended(obj) ⇒ Object

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

#expressionObject

Returns the regular expression used for #read_match.



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

def expression
  @ionian_expression
end

#expression=(exp) ⇒ Object

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.



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

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

#has_data?(timeout: 0) ⇒ Boolean

Returns True if there is data in the receive buffer.

Parameters:

  • timeout (Numeric) (defaults to: 0)

    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.



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

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

#initialize_ionianObject

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



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

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

#purgeObject

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



172
173
174
175
176
# File 'lib/ionian/extension/io.rb', line 172

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

#read_all(nonblocking: false) ⇒ Object

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.



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

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

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

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 listener 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)

    Returns an array of matches. Returns nil if no data was received within the timeout period.



106
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
# File 'lib/ionian/extension/io.rb', line 106

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 ::IO.select [self], nil, nil, timeout
  end
  
  # Read data from the IO buffer until it's empty.
  @ionian_buf << read_all
  
  @matches = []
  
  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 listeners unless the #run_match thread is active.
  @matches.each { |match| notify_listeners match } \
    if notify and not @match_listener
  
  @matches
end

#register_observer(&block) ⇒ Object 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). Returns a reference to the given block.

Examples:

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


187
188
189
190
# File 'lib/ionian/extension/io.rb', line 187

def register_observer &block
  @ionian_listeners << block unless @ionian_listeners.include? block
  block
end

#run_match(**kwargs) ⇒ Object

Start a thread that checks for data and notifies listeners (do |match, socket|). Passes kwargs to #read_match. This method SHOULD NOT be used if #read_match is used.



155
156
157
158
159
160
161
162
163
164
165
166
167
168
# File 'lib/ionian/extension/io.rb', line 155

def run_match **kwargs
  @match_listener ||= Thread.new do
    begin
      while not closed? do
        matches = read_match **kwargs
        matches.each { |match| notify_listeners match } if matches
      end
    rescue EOFError
    rescue IOError
    ensure
      @match_listener = nil
    end
  end
end

#unregister_observer(&block) ⇒ Object

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



195
196
197
198
# File 'lib/ionian/extension/io.rb', line 195

def unregister_observer &block
  @ionian_listeners.delete_if { |o| o == block }
  block
end