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:



169
170
171
172
173
174
175
176
177
# File 'lib/remailer/interpreter.rb', line 169

def initialize(options = nil)
  @delegate = (options and options[:delegate])
  @state = nil
  @error = nil
  
  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

.configObject



31
32
33
34
35
36
37
38
# File 'lib/remailer/interpreter.rb', line 31

def self.config
  {
    states: @states,
    default_interpreter: @default,
    default_parser: @parser,
    on_error_handler: @on_error
  }
end

.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 Integer, or a Regexp. A Integer 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.



89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
# File 'lib/remailer/interpreter.rb', line 89

def self.create_parser_for_spec(spec, &block)
  case (spec)
  when nil
    block
  when Integer
    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.



120
121
122
# File 'lib/remailer/interpreter.rb', line 120

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

.default_interpreterObject

Returns the current default_interpreter.



141
142
143
144
145
146
147
148
# File 'lib/remailer/interpreter.rb', line 141

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

.default_parserObject

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



131
132
133
134
135
136
137
138
# File 'lib/remailer/interpreter.rb', line 131

def self.default_parser
  @parser ||=
    if (superclass.respond_to?(:default_parser))
      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.



126
127
128
# File 'lib/remailer/interpreter.rb', line 126

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

.on_error_handlerObject

Returns the defined error handler



151
152
153
154
155
156
157
158
# File 'lib/remailer/interpreter.rb', line 151

def self.on_error_handler
  @on_error ||=
    if (superclass.respond_to?(:on_error_handler))
      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.



115
116
117
# File 'lib/remailer/interpreter.rb', line 115

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.



78
79
80
81
82
# File 'lib/remailer/interpreter.rb', line 78

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)


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

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.



49
50
51
52
53
54
55
56
# File 'lib/remailer/interpreter.rb', line 49

def self.states
  @states ||=
    if (superclass.respond_to?(:states))
      superclass.states.dup
    else
      self.states_default
    end
end

.states_defaultObject



40
41
42
43
44
45
# File 'lib/remailer/interpreter.rb', line 40

def self.states_default
  {
      :initialized => { },
      :terminated => { }
    }
end

.states_definedObject

Returns a list of the defined states.



68
69
70
# File 'lib/remailer/interpreter.rb', line 68

def self.states_defined
  self.states.keys
end

.states_empty?Boolean

Returns:

  • (Boolean)


58
59
60
# File 'lib/remailer/interpreter.rb', line 58

def self.states_empty?
  self.states == self.states_default
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.



184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
# File 'lib/remailer/interpreter.rb', line 184

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)


317
318
319
# File 'lib/remailer/interpreter.rb', line 317

def error?
  !!@error
end

#finished?Boolean

Should return true if this interpreter no longer wants any data, false otherwise. Subclasses should implement their own behavior here.

Returns:

  • (Boolean)


311
312
313
# File 'lib/remailer/interpreter.rb', line 311

def finished?
  false
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.



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
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
# File 'lib/remailer/interpreter.rb', line 238

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_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.



206
207
208
# File 'lib/remailer/interpreter.rb', line 206

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.



213
214
215
216
217
# File 'lib/remailer/interpreter.rb', line 213

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.



223
224
225
226
227
228
229
230
231
232
233
# File 'lib/remailer/interpreter.rb', line 223

def process(s)
  _parser = parser

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

    interpret(*parsed)

    break if (s.empty? or self.finished?)
  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)


305
306
307
# File 'lib/remailer/interpreter.rb', line 305

def will_interpret?(proc, args)
  true
end