cancer 0.1.0.a1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/.autotest +3 -0
- data/.gitignore +5 -0
- data/LICENSE.txt +22 -0
- data/Rakefile +46 -0
- data/VERSION +1 -0
- data/cancer.gemspec +156 -0
- data/documentation/STREAM_INITIATION.markdown +18 -0
- data/examples/example.rb +80 -0
- data/examples/example_2.rb +20 -0
- data/examples/example_em.rb +11 -0
- data/examples/example_em_helper.rb +23 -0
- data/examples/example_im_roster.rb +26 -0
- data/examples/example_xep_0004.rb +124 -0
- data/examples/example_xep_0047.rb +35 -0
- data/examples/example_xep_0050.rb +78 -0
- data/examples/example_xep_0065.rb +66 -0
- data/examples/example_xep_0096.dup.rb +40 -0
- data/examples/example_xep_0096_xep_0047.rb +42 -0
- data/examples/example_xep_0096_xep_0065.rb +40 -0
- data/lib/cancer.rb +122 -0
- data/lib/cancer/adapter.rb +33 -0
- data/lib/cancer/adapters/em.rb +88 -0
- data/lib/cancer/adapters/socket.rb +122 -0
- data/lib/cancer/builder.rb +28 -0
- data/lib/cancer/controller.rb +32 -0
- data/lib/cancer/dependencies.rb +187 -0
- data/lib/cancer/events/abstract_event.rb +10 -0
- data/lib/cancer/events/base_matchers.rb +63 -0
- data/lib/cancer/events/binary_matchers.rb +30 -0
- data/lib/cancer/events/eventable.rb +92 -0
- data/lib/cancer/events/exception_events.rb +28 -0
- data/lib/cancer/events/handler.rb +33 -0
- data/lib/cancer/events/manager.rb +130 -0
- data/lib/cancer/events/named_events.rb +25 -0
- data/lib/cancer/events/proc_matcher.rb +16 -0
- data/lib/cancer/events/xml_events.rb +44 -0
- data/lib/cancer/exceptions.rb +53 -0
- data/lib/cancer/jid.rb +215 -0
- data/lib/cancer/lock.rb +32 -0
- data/lib/cancer/spec.rb +35 -0
- data/lib/cancer/spec/error.rb +18 -0
- data/lib/cancer/spec/extentions.rb +79 -0
- data/lib/cancer/spec/matcher.rb +30 -0
- data/lib/cancer/spec/mock_adapter.rb +139 -0
- data/lib/cancer/spec/mock_stream.rb +15 -0
- data/lib/cancer/spec/transcript.rb +107 -0
- data/lib/cancer/stream.rb +182 -0
- data/lib/cancer/stream/builder.rb +20 -0
- data/lib/cancer/stream/controller.rb +36 -0
- data/lib/cancer/stream/event_helper.rb +12 -0
- data/lib/cancer/stream/xep.rb +52 -0
- data/lib/cancer/stream_parser.rb +144 -0
- data/lib/cancer/support.rb +27 -0
- data/lib/cancer/support/hash.rb +32 -0
- data/lib/cancer/support/string.rb +22 -0
- data/lib/cancer/synchronized_stanza.rb +79 -0
- data/lib/cancer/thread_pool.rb +118 -0
- data/lib/cancer/xep.rb +43 -0
- data/lib/cancer/xeps/core.rb +109 -0
- data/lib/cancer/xeps/core/bind.rb +33 -0
- data/lib/cancer/xeps/core/sasl.rb +113 -0
- data/lib/cancer/xeps/core/session.rb +22 -0
- data/lib/cancer/xeps/core/stream.rb +18 -0
- data/lib/cancer/xeps/core/terminator.rb +21 -0
- data/lib/cancer/xeps/core/tls.rb +34 -0
- data/lib/cancer/xeps/im.rb +323 -0
- data/lib/cancer/xeps/xep_0004_x_data.rb +692 -0
- data/lib/cancer/xeps/xep_0020_feature_neg.rb +35 -0
- data/lib/cancer/xeps/xep_0030_disco.rb +167 -0
- data/lib/cancer/xeps/xep_0047_ibb.rb +322 -0
- data/lib/cancer/xeps/xep_0050_commands.rb +256 -0
- data/lib/cancer/xeps/xep_0065_bytestreams.rb +306 -0
- data/lib/cancer/xeps/xep_0066_oob.rb +69 -0
- data/lib/cancer/xeps/xep_0095_si.rb +211 -0
- data/lib/cancer/xeps/xep_0096_si_filetransfer.rb +173 -0
- data/lib/cancer/xeps/xep_0114_component.rb +73 -0
- data/lib/cancer/xeps/xep_0115_caps.rb +180 -0
- data/lib/cancer/xeps/xep_0138_compress.rb +134 -0
- data/lib/cancer/xeps/xep_0144_rosterx.rb +250 -0
- data/lib/cancer/xeps/xep_0184_receipts.rb +40 -0
- data/lib/cancer/xeps/xep_0199_ping.rb +41 -0
- data/spec/spec.opts +2 -0
- data/spec/spec_helper.rb +14 -0
- data/spec/stream/stanza_errors_spec.rb +47 -0
- data/spec/stream/stream_errors_spec.rb +38 -0
- data/spec/stream/stream_initialization_spec.rb +160 -0
- data/spec/xep_0050/local_spec.rb +165 -0
- data/spec/xep_0050/remote_spec.rb +44 -0
- metadata +200 -0
@@ -0,0 +1,139 @@
|
|
1
|
+
module Cancer
|
2
|
+
module Spec
|
3
|
+
|
4
|
+
class MockAdapter < Cancer::Adapter
|
5
|
+
class << self
|
6
|
+
attr_accessor :transcript, :instance, :dialog
|
7
|
+
end
|
8
|
+
|
9
|
+
def self.mock(&dialog)
|
10
|
+
mock = Class.new(self)
|
11
|
+
mock.dialog = dialog
|
12
|
+
mock
|
13
|
+
end
|
14
|
+
|
15
|
+
def self.should_produce_transcript(&proc)
|
16
|
+
should Cancer::Spec::Matcher.new(Cancer::Spec::Transcript.new(&proc))
|
17
|
+
rescue ::Spec::Expectations::ExpectationNotMetError => e
|
18
|
+
if $cancer_error
|
19
|
+
if $cancer_error.backtrace and $cancer_error != e
|
20
|
+
e.backtrace.clear
|
21
|
+
e.backtrace.concat($cancer_error.backtrace)
|
22
|
+
end
|
23
|
+
$cancer_error = nil
|
24
|
+
end
|
25
|
+
if !Cancer::Spec.debug
|
26
|
+
backtrace = e.backtrace.dup
|
27
|
+
e.backtrace.clear
|
28
|
+
e.backtrace.concat backtrace.select { |f| !f.include?('spec_helper.rb') }
|
29
|
+
end
|
30
|
+
raise e
|
31
|
+
end
|
32
|
+
|
33
|
+
def initialize(stream, options={})
|
34
|
+
@stream, @options = stream, options
|
35
|
+
self.class.instance = self
|
36
|
+
@transcript = self.class.transcript.dup
|
37
|
+
@queue = Queue.new
|
38
|
+
@responder_tread = Thread.new do
|
39
|
+
Thread.current.abort_on_exception = true
|
40
|
+
begin
|
41
|
+
run_server_emulation_and_state_verification
|
42
|
+
loop do
|
43
|
+
data = @queue.pop
|
44
|
+
break unless data
|
45
|
+
verify_client_data(data)
|
46
|
+
run_server_emulation_and_state_verification
|
47
|
+
end
|
48
|
+
rescue ::Spec::Expectations::ExpectationNotMetError => e
|
49
|
+
if lock = @stream.instance_variable_get('@lock')
|
50
|
+
$cancer_error ||= e
|
51
|
+
lock.continue!(e)
|
52
|
+
elsif locks = @stream.instance_variable_get('@receive_locks') and lock = locks.first
|
53
|
+
$cancer_error ||= e
|
54
|
+
lock.continue!(e)
|
55
|
+
else
|
56
|
+
$cancer_error ||= e
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
def connect
|
63
|
+
end
|
64
|
+
|
65
|
+
def start_tls?
|
66
|
+
end
|
67
|
+
|
68
|
+
def start_tls
|
69
|
+
end
|
70
|
+
|
71
|
+
def disconnect
|
72
|
+
@queue.push(nil)
|
73
|
+
@responder_tread.join
|
74
|
+
|
75
|
+
if $cancer_error
|
76
|
+
raise $cancer_error
|
77
|
+
elsif @transcript.first
|
78
|
+
messages = ""
|
79
|
+
backtrace = nil
|
80
|
+
while message = @transcript.pop
|
81
|
+
case message.first
|
82
|
+
when :in then messages += " in: #{message[1]}\n"
|
83
|
+
when :out then messages += " out: #{message[1]}\n"
|
84
|
+
when :verify then messages += " verify: #{(message[2] || message[3]).first}\n"
|
85
|
+
end
|
86
|
+
backtrace ||= message.last
|
87
|
+
end
|
88
|
+
raise Cancer::Spec::Error.new("Expected a longer dialog:\n#{messages}", backtrace)
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
def send(data)
|
93
|
+
@queue.push(data)
|
94
|
+
end
|
95
|
+
|
96
|
+
def verify_client_data(xml)
|
97
|
+
type, data, backtrace = @transcript.pop
|
98
|
+
|
99
|
+
if type == nil
|
100
|
+
raise Cancer::Spec::Error.new("Unexpected data send by client:\n #{xml}")
|
101
|
+
end
|
102
|
+
|
103
|
+
if type != :out
|
104
|
+
raise Cancer::Spec::Error.new("should be write instead of #{type}", backtrace)
|
105
|
+
end
|
106
|
+
|
107
|
+
a = (Nokogiri::XML.parse(data).root rescue data) || data
|
108
|
+
b = (Nokogiri::XML.parse(xml).root rescue xml) || xml
|
109
|
+
|
110
|
+
a.strip_empty_text! if Nokogiri::XML::Node === a
|
111
|
+
|
112
|
+
if Nokogiri::XML::Node === b and Nokogiri::XML::Node === a
|
113
|
+
if !b.matching?(a)
|
114
|
+
raise Cancer::Spec::Error.new("Expected xml-data doesn't match the actual xml-data!\n expected: #{data}\n actual: #{xml}", backtrace)
|
115
|
+
end
|
116
|
+
elsif a != b
|
117
|
+
raise Cancer::Spec::Error.new("Expected xml-data doesn't match the actual xml-data!\n expected: #{data}\n actual: #{xml}", backtrace)
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
def run_server_emulation_and_state_verification
|
122
|
+
while @transcript.first and @transcript.first[0] != :out
|
123
|
+
type, data, backtrace = @transcript.pop
|
124
|
+
return if type == nil
|
125
|
+
case type
|
126
|
+
when :in
|
127
|
+
if Proc === data
|
128
|
+
@stream.received_data(stream.build(&data))
|
129
|
+
else
|
130
|
+
@stream.received_data(data)
|
131
|
+
end
|
132
|
+
when :verify then data.call(@stream)
|
133
|
+
end
|
134
|
+
end
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
end
|
139
|
+
end
|
@@ -0,0 +1,107 @@
|
|
1
|
+
module Cancer
|
2
|
+
module Spec
|
3
|
+
|
4
|
+
class Transcript
|
5
|
+
|
6
|
+
def initialize(entries=nil)
|
7
|
+
@entries = entries || []
|
8
|
+
yield(self) if block_given?
|
9
|
+
end
|
10
|
+
|
11
|
+
def dup
|
12
|
+
self.class.new(@entries.dup)
|
13
|
+
end
|
14
|
+
|
15
|
+
def in(xml, backtrace=nil)
|
16
|
+
@entries.push [:in, xml, backtrace||caller]
|
17
|
+
end
|
18
|
+
|
19
|
+
def out(xml, backtrace=nil)
|
20
|
+
@entries.push [:out, xml, backtrace||caller]
|
21
|
+
end
|
22
|
+
|
23
|
+
def verify(&proc)
|
24
|
+
@entries.push [:verify, proc, nil, caller]
|
25
|
+
end
|
26
|
+
|
27
|
+
def should(matcher)
|
28
|
+
@entries.push [:verify, lambda { |stream| stream.should matcher }, caller]
|
29
|
+
end
|
30
|
+
|
31
|
+
def should_not(matcher)
|
32
|
+
@entries.push [:verify, lambda { |stream| stream.should_not matcher }, caller]
|
33
|
+
end
|
34
|
+
|
35
|
+
def start_stream(from, to=nil, id=nil, backtrace=nil)
|
36
|
+
backtrace = backtrace || caller
|
37
|
+
from = from.to_jid
|
38
|
+
to ||= from.server_jid
|
39
|
+
id ||= rand(6000).to_s
|
40
|
+
self.out %{<stream:stream xmlns="jabber:client" xmlns:stream="http://etherx.jabber.org/streams" to="#{to}" from="#{from.bare_jid}" version="1.0">}, backtrace
|
41
|
+
self.in %{<stream:stream xmlns="jabber:client" xmlns:stream="http://etherx.jabber.org/streams" from="to" to="#{from.bare_jid}" id="#{id}" version="1.0">}, backtrace
|
42
|
+
end
|
43
|
+
|
44
|
+
def close_stream(backtrace=nil)
|
45
|
+
backtrace = backtrace || caller
|
46
|
+
self.out %{</stream:stream>}, backtrace
|
47
|
+
self.in %{</stream:stream>}, backtrace
|
48
|
+
end
|
49
|
+
|
50
|
+
def initiate_stream(from, to=nil, id=nil, backtrace=nil)
|
51
|
+
backtrace = backtrace || caller
|
52
|
+
from = from.to_jid
|
53
|
+
|
54
|
+
self.start_stream(from, to, id, backtrace)
|
55
|
+
self.in %{<stream:features>
|
56
|
+
<starttls xmlns="urn:ietf:params:xml:ns:xmpp-tls">
|
57
|
+
<optional/>
|
58
|
+
</starttls>
|
59
|
+
<mechanisms xmlns="urn:ietf:params:xml:ns:xmpp-sasl">
|
60
|
+
<mechanism>PLAIN</mechanism>
|
61
|
+
<required/>
|
62
|
+
</mechanisms>
|
63
|
+
</stream:features>}, backtrace
|
64
|
+
|
65
|
+
self.out %{<starttls xmlns="urn:ietf:params:xml:ns:xmpp-tls"/>}, backtrace
|
66
|
+
self.in %{<proceed xmlns="urn:ietf:params:xml:ns:xmpp-tls"/>}, backtrace
|
67
|
+
|
68
|
+
self.start_stream(from, to, id, backtrace)
|
69
|
+
self.in %{<stream:features>
|
70
|
+
<mechanisms xmlns="urn:ietf:params:xml:ns:xmpp-sasl">
|
71
|
+
<mechanism>PLAIN</mechanism>
|
72
|
+
<required/>
|
73
|
+
</mechanisms>
|
74
|
+
</stream:features>}, backtrace
|
75
|
+
|
76
|
+
self.out %{<auth xmlns="urn:ietf:params:xml:ns:xmpp-sasl" mechanism="PLAIN">AGFsaWNlQHdvbmRlcmxhbmQubGl0AGloYXRldGhlcXVlZW4=</auth>}, backtrace
|
77
|
+
self.in %{<success xmlns="urn:ietf:params:xml:ns:xmpp-sasl" />}, backtrace
|
78
|
+
|
79
|
+
self.start_stream(from, to, id, backtrace)
|
80
|
+
self.in %{<stream:features>
|
81
|
+
<bind xmlns="urn:ietf:params:xml:ns:xmpp-bind">
|
82
|
+
<required/>
|
83
|
+
</bind>
|
84
|
+
<session xmlns="urn:ietf:params:xml:ns:xmpp-session">
|
85
|
+
<optional/>
|
86
|
+
</session>
|
87
|
+
</stream:features>}, backtrace
|
88
|
+
|
89
|
+
self.out %{<iq type="set" id="cancer-1"><bind xmlns="urn:ietf:params:xml:ns:xmpp-bind"/></iq>}, backtrace
|
90
|
+
self.in %{<iq type="result" id="cancer-1"><bind xmlns="urn:ietf:params:xml:ns:xmpp-bind"><jid>#{from.bare_jid}/#{from.resource || 'rabbithole'}</jid></bind></iq>}, backtrace
|
91
|
+
|
92
|
+
self.out %{<iq type="set" id="cancer-2"><session xmlns="urn:ietf:params:xml:ns:xmpp-session"/></iq>}, backtrace
|
93
|
+
self.in %{<iq type="result" id="cancer-2" />}, backtrace
|
94
|
+
|
95
|
+
end
|
96
|
+
|
97
|
+
def first
|
98
|
+
@entries.first
|
99
|
+
end
|
100
|
+
|
101
|
+
def pop
|
102
|
+
@entries.shift
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
end
|
107
|
+
end
|
@@ -0,0 +1,182 @@
|
|
1
|
+
|
2
|
+
module Cancer
|
3
|
+
class Stream
|
4
|
+
|
5
|
+
autoload :XEP, 'cancer/stream/xep'
|
6
|
+
autoload :Builder, 'cancer/stream/builder'
|
7
|
+
autoload :Controller, 'cancer/stream/controller'
|
8
|
+
autoload :EventHelper, 'cancer/stream/event_helper'
|
9
|
+
|
10
|
+
include Cancer::SynchronizedStanza
|
11
|
+
include Cancer::Stream::Controller
|
12
|
+
include Cancer::Stream::Builder
|
13
|
+
include Cancer::Stream::XEP
|
14
|
+
include Cancer::Stream::EventHelper
|
15
|
+
extend Cancer::Support
|
16
|
+
include Cancer::Events::Eventable
|
17
|
+
|
18
|
+
attr_accessor :options
|
19
|
+
|
20
|
+
def self.open(options={},&proc)
|
21
|
+
new(options).open(&proc)
|
22
|
+
end
|
23
|
+
|
24
|
+
def initialize(options={})
|
25
|
+
|
26
|
+
options[:jid] = options[:jid].to_jid if options[:jid]
|
27
|
+
options[:adapter] ||= Cancer::Adapters::Socket
|
28
|
+
|
29
|
+
@options = options
|
30
|
+
@lock = Cancer::Lock.new
|
31
|
+
@receive_locks = []
|
32
|
+
@mutex = Mutex.new
|
33
|
+
|
34
|
+
register_with Cancer::Events::Manager.new
|
35
|
+
@low_priority_queue = Cancer::ThreadPool.start!(4) do |job|
|
36
|
+
args, proc = *job
|
37
|
+
proc.call(*args)
|
38
|
+
end
|
39
|
+
|
40
|
+
@stream_parser = Cancer::StreamParser.new(self)
|
41
|
+
@xml_parser = Nokogiri::XML::SAX::PushParser.new(@stream_parser)
|
42
|
+
|
43
|
+
['core', options[:xeps]].flatten.compact.each do |xep_name|
|
44
|
+
install_xep xep_name
|
45
|
+
end
|
46
|
+
|
47
|
+
enhance_stream_with_xeps!
|
48
|
+
@manager.resolve_martchers!
|
49
|
+
|
50
|
+
@initialized = true
|
51
|
+
|
52
|
+
controller do
|
53
|
+
on { |e| e.exception(Exception) }
|
54
|
+
def err(e)
|
55
|
+
Cancer.logger.error e.exception
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
@exception = nil
|
60
|
+
end
|
61
|
+
|
62
|
+
def deferred(*args, &proc)
|
63
|
+
@manager.deferred(*args, &proc)
|
64
|
+
end
|
65
|
+
|
66
|
+
def enqueue(*args, &proc)
|
67
|
+
@low_priority_queue.push([args, proc])
|
68
|
+
end
|
69
|
+
|
70
|
+
def fire!(*args)
|
71
|
+
@manager.fire!(*args)
|
72
|
+
end
|
73
|
+
|
74
|
+
def handle_interthread_exceptions
|
75
|
+
raise @exception if @exception
|
76
|
+
end
|
77
|
+
|
78
|
+
def extend_stream(&proc)
|
79
|
+
metaklass = (class << self ; self ; end)
|
80
|
+
metaklass.instance_eval(&proc)
|
81
|
+
end
|
82
|
+
|
83
|
+
def send(stanza=nil, &proc)
|
84
|
+
handle_interthread_exceptions
|
85
|
+
stanza ||= proc
|
86
|
+
if Proc === stanza
|
87
|
+
send build(&stanza)
|
88
|
+
else
|
89
|
+
send_data stanza.to_s.gsub(/>[\n\s]+/m, '>').gsub(/[\n\s]+</m, '<')
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
def received_data(data)
|
94
|
+
data = process_received_data(data)
|
95
|
+
@xml_parser << data
|
96
|
+
|
97
|
+
rescue RuntimeError => e
|
98
|
+
if data[0, 5] == "<?xml" or data[0, 14] == "<stream:stream"
|
99
|
+
@xml_parser = Nokogiri::XML::SAX::PushParser.new(@stream_parser)
|
100
|
+
@xml_parser << data
|
101
|
+
else
|
102
|
+
raise e
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
def received_xml(xml)
|
107
|
+
Cancer.logger.debug "RECEIVED: #{xml}"
|
108
|
+
handle_stream_errors(xml) or
|
109
|
+
handle_synchronized_stanza(xml) or
|
110
|
+
@manager.fire!(Cancer::Events::XMLEvent, xml)
|
111
|
+
end
|
112
|
+
|
113
|
+
def handle_stream_errors(xml)
|
114
|
+
return unless xml.named?('error', STREAM_NS)
|
115
|
+
|
116
|
+
@exception = Cancer::StreamError.new(xml)
|
117
|
+
handle_synchronized_stanza(@exception)
|
118
|
+
if @lock
|
119
|
+
@lock.continue!(@exception)
|
120
|
+
end
|
121
|
+
|
122
|
+
true
|
123
|
+
end
|
124
|
+
|
125
|
+
def open
|
126
|
+
handle_interthread_exceptions
|
127
|
+
|
128
|
+
resolve_options(options)
|
129
|
+
|
130
|
+
@manager.start!
|
131
|
+
@adapter = options[:adapter].new(self, @options)
|
132
|
+
@adapter.connect
|
133
|
+
@manager.fire!(Cancer::Events::NamedEvent, :connected)
|
134
|
+
|
135
|
+
send_stream_header
|
136
|
+
@lock.wait!
|
137
|
+
@lock = nil
|
138
|
+
if block_given?
|
139
|
+
yield(self)
|
140
|
+
close
|
141
|
+
end
|
142
|
+
self
|
143
|
+
end
|
144
|
+
|
145
|
+
def close
|
146
|
+
handle_interthread_exceptions
|
147
|
+
|
148
|
+
@low_priority_queue.stop!
|
149
|
+
|
150
|
+
@adapter.send('</stream:stream>')
|
151
|
+
@adapter.disconnect
|
152
|
+
|
153
|
+
@manager.stop!
|
154
|
+
|
155
|
+
self
|
156
|
+
end
|
157
|
+
|
158
|
+
def initialization_completed!
|
159
|
+
@lock.continue!
|
160
|
+
end
|
161
|
+
|
162
|
+
private
|
163
|
+
|
164
|
+
def resolve_options(options)
|
165
|
+
options[:username] ||= options[:jid].user
|
166
|
+
end
|
167
|
+
|
168
|
+
def process_received_data(data)
|
169
|
+
data
|
170
|
+
end
|
171
|
+
|
172
|
+
def process_send_data(data)
|
173
|
+
data
|
174
|
+
end
|
175
|
+
|
176
|
+
def send_data(data)
|
177
|
+
Cancer.logger.debug "SEND: #{data.to_s}"
|
178
|
+
@adapter.send(process_send_data(data))
|
179
|
+
end
|
180
|
+
|
181
|
+
end
|
182
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
|
2
|
+
module Cancer
|
3
|
+
class Stream
|
4
|
+
module Builder
|
5
|
+
|
6
|
+
def builder_helpers
|
7
|
+
@builder_helpers ||= Module.new
|
8
|
+
end
|
9
|
+
|
10
|
+
def extend_builder(&proc)
|
11
|
+
self.builder_helpers.module_eval(&proc)
|
12
|
+
end
|
13
|
+
|
14
|
+
def build(builder=nil, &proc)
|
15
|
+
Cancer::Builder.new(self, builder, self.builder_helpers, &proc).doc.root
|
16
|
+
end
|
17
|
+
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|