Class: Remailer::Interpreter

Inherits:
Object
  • Object
show all
Defined in:
lib/remailer/interpreter.rb

Defined Under Namespace

Classes: DefinitionException, StateProxy

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(options = nil) {|_self| ... } ⇒ Interpreter

Creates a new interpreter with an optional set of options. Valid options include:

  • :delegate => Which object to use as a delegate, if applicable.

  • :state => What the initial state should be. The default is :initalized

If a block is supplied, the interpreter object is supplied as an argument to give the caller an opportunity to perform any initial configuration before the first state is entered.

Yields:

  • (_self)

Yield Parameters:



156
157
158
159
160
161
162
# File 'lib/remailer/interpreter.rb', line 156

def initialize(options = nil)
  @delegate = (options and options[:delegate])
  
  yield(self) if (block_given?)
  
  enter_state(options && options[:state] || self.class.initial_state)
end

Instance Attribute Details

#delegateObject (readonly)

Properties ===========================================================



14
15
16
# File 'lib/remailer/interpreter.rb', line 14

def delegate
  @delegate
end

#errorObject (readonly)

Returns the value of attribute error.



16
17
18
# File 'lib/remailer/interpreter.rb', line 16

def error
  @error
end

#stateObject (readonly)

Returns the value of attribute state.



15
16
17
# File 'lib/remailer/interpreter.rb', line 15

def state
  @state
end

Class Method Details

.create_parser_for_spec(spec, &block) ⇒ Object

This is a method to convert a spec and a block into a proper parser method. If spec is specified, it should be a Fixnum, or a Regexp. A Fixnum defines a minimum size to process, useful for packed binary streams, while a Regexp defines a pattern that must match before the parser is engaged.



73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
# File 'lib/remailer/interpreter.rb', line 73

def self.create_parser_for_spec(spec, &block)
  case (spec)
  when nil
    block
  when Fixnum
    lambda do |s|
      if (s.length >= spec)
        part = s.slice!(0, spec)
        block.call(part)
      end
    end
  when Regexp
    lambda do |s|
      if (m = spec.match(s))
        part = m.to_s
        part = s.slice!(0, part.length)
        block.call(part)
      end
    end
  else
    raise DefinitionException, "Invalid specification for parse declaration: #{spec.inspect}"
  end
end

.default(&block) ⇒ Object

Assigns the default interpreter.



104
105
106
# File 'lib/remailer/interpreter.rb', line 104

def self.default(&block)
  @default = block if (block_given?)
end

.default_interpreterObject

Returns the current default_interpreter.



126
127
128
129
130
131
132
133
134
# File 'lib/remailer/interpreter.rb', line 126

def self.default_interpreter
  @default ||=
    case (superclass.respond_to?(:default_interpreter))
    when true
      superclass.default_interpreter
    else
      nil
    end
end

.default_parserObject

Returns the parser used when no state-specific parser has been defined.



115
116
117
118
119
120
121
122
123
# File 'lib/remailer/interpreter.rb', line 115

def self.default_parser
  @parser ||=
    case (superclass.respond_to?(:default_parser))
    when true
      superclass.default_parser
    else
      lambda { |s| _s = s.dup; s.replace(''); _s }
    end
end

.initial_stateObject

Defines the initial state for objects of this class.



21
22
23
# File 'lib/remailer/interpreter.rb', line 21

def self.initial_state
  @initial_state || :initialized
end

.initial_state=(state) ⇒ Object

Can be used to reassign the initial state for this class. May be easier than re-defining the initial_state method.



27
28
29
# File 'lib/remailer/interpreter.rb', line 27

def self.initial_state=(state)
  @initial_state = state
end

.on_error(&block) ⇒ Object

Assigns the error handler for when a specific interpretation could not be found and a default was not specified.



110
111
112
# File 'lib/remailer/interpreter.rb', line 110

def self.on_error(&block)
  @on_error = block
end

.on_error_handlerObject

Returns the defined error handler



137
138
139
140
141
142
143
144
145
# File 'lib/remailer/interpreter.rb', line 137

def self.on_error_handler
  @on_error ||=
    case (superclass.respond_to?(:on_error_handler))
    when true
      superclass.on_error_handler
    else
      nil
    end
end

.parse(spec = nil, &block) ⇒ Object

Defines a parser for this interpreter. The supplied block is executed in the context of a parser instance.



99
100
101
# File 'lib/remailer/interpreter.rb', line 99

def self.parse(spec = nil, &block)
  @parser = create_parser_for_spec(spec, &block)
end

.state(state, &block) ⇒ Object

Defines a new state for this class. A block will be executed in the context of a StateProxy that is used to provide a simple interface to the underlying options. A block can contain calls to enter and leave, or default, which do not require arguments, or interpret, which requries at least one argument that will be the class-specific object to interpret. Other paramters may be supplied by the class.



62
63
64
65
66
# File 'lib/remailer/interpreter.rb', line 62

def self.state(state, &block)
  config = self.states[state] = { }
  
  StateProxy.new(config, &block)
end

.state_defined?(state) ⇒ Boolean

Returns true if a given state is defined, false otherwise.

Returns:

  • (Boolean)


47
48
49
# File 'lib/remailer/interpreter.rb', line 47

def self.state_defined?(state)
  !!self.states[state]
end

.statesObject

Returns the states that are defined as a has with their associated options. The default keys are :initialized and :terminated.



33
34
35
36
37
38
39
40
41
42
43
44
# File 'lib/remailer/interpreter.rb', line 33

def self.states
  @states ||=
    case (superclass.respond_to?(:states))
    when true
      superclass.states.dup
    else
      {
        :initialized => { },
        :terminated => { }
      }
    end
end

.states_definedObject

Returns a list of the defined states.



52
53
54
# File 'lib/remailer/interpreter.rb', line 52

def self.states_defined
  self.states.keys
end

Instance Method Details

#enter_state(state) ⇒ Object

Enters the given state. Will call the appropriate leave_state trigger if one is defined for the previous state, and will trigger the callbacks for entry into the new state. If this state is set as a terminate state, then an immediate transition to the :terminate state will be performed after these callbacks.



169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
# File 'lib/remailer/interpreter.rb', line 169

def enter_state(state)
  if (@state)
    leave_state(@state)
  end
  
  @state = state

  delegate_call(:interpreter_entered_state, self, @state)
  
  trigger_callbacks(state, :enter)
  
  # :terminated is the state, :terminate is the trigger.
  if (@state != :terminated)
    if (trigger_callbacks(state, :terminate))
      enter_state(:terminated)
    end
  end
end

#error?Boolean

Returns true if an error has been generated, false otherwise. The error content can be retrived by calling error.

Returns:

  • (Boolean)


296
297
298
# File 'lib/remailer/interpreter.rb', line 296

def error?
  !!@error
end

#interpret(*args) ⇒ Object

Interprets a given object with an optional set of arguments. The actual interpretation should be defined by declaring a state with an interpret block defined.



223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
# File 'lib/remailer/interpreter.rb', line 223

def interpret(*args)
  object = args[0]
  config = self.class.states[@state]
  interpreters = (config and config[:interpret])

  if (interpreters)
    match_result = nil
    
    matched, proc = interpreters.find do |response, proc|
      case (response)
      when Regexp
        match_result = response.match(object)
      when Range
        response.include?(object)
      else
        response === object
      end
    end
  
    if (matched)
      case (matched)
      when Regexp
        match_result = match_result.to_a
      
        if (match_result.length > 1)
          match_string = match_result.shift
          args[0, 1] = match_result
        else
          args[0].sub!(match_result[0], '')
        end
      when String
        args[0].sub!(matched, '')
      when Range
        # Keep as-is
      else
        args.shift
      end
    
      # Specifying a block with no arguments will mean that it waits until
      # all pieces are collected before transitioning to a new state, 
      # waiting until the continue flag is false.
      will_interpret?(proc, args) and instance_exec(*args, &proc)

      return true
    end
  end
  
  if (trigger_callbacks(@state, :default, *args))
    # Handled by default
    true
  elsif (proc = self.class.default_interpreter)
    instance_exec(*args, &proc)
  else
    if (proc = self.class.on_error_handler)
      instance_exec(*args, &proc)
    end

    @error = "No handler for response #{object.inspect} in state #{@state.inspect}"
    enter_state(:terminated)

    false
  end
end

#parse(buffer) ⇒ Object

Parses a given string and returns the first interpretable token, if any, or nil otherwise. If an interpretable token is found, the supplied string will be modified to have that matching portion removed.



191
192
193
# File 'lib/remailer/interpreter.rb', line 191

def parse(buffer)
  instance_exec(buffer, &parser)
end

#parserObject

Returns the parser defined for the current state, or the default parser. The default parser simply accepts everything but this can be re-defined using the class-level parse method.



198
199
200
201
202
# File 'lib/remailer/interpreter.rb', line 198

def parser
  config = self.class.states[@state]
  
  config and config[:parser] or self.class.default_parser
end

#process(s) ⇒ Object

Processes a given input string into interpretable tokens, processes these tokens, and removes them from the input string. An optional block can be given that will be called as each interpretable token is discovered with the token provided as the argument.



208
209
210
211
212
213
214
215
216
217
218
# File 'lib/remailer/interpreter.rb', line 208

def process(s)
  _parser = parser

  while (parsed = instance_exec(s, &_parser))
    yield(parsed) if (block_given?)

    interpret(*parsed)
    
    break if (s.empty?)
  end
end

#will_interpret?(proc, args) ⇒ Boolean

This method is used by interpret to determine if the supplied block should be executed or not. The default behavior is to always execute but this can be modified in sub-classes.

Returns:

  • (Boolean)


290
291
292
# File 'lib/remailer/interpreter.rb', line 290

def will_interpret?(proc, args)
  true
end