rflow 1.0.0a1 → 1.0.0a2
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.
- checksums.yaml +4 -4
- data/.gitignore +2 -0
- data/.rspec +1 -0
- data/Gemfile +0 -1
- data/NOTES +0 -13
- data/README.md +6 -1
- data/bin/rflow +2 -9
- data/example/basic_config.rb +1 -33
- data/example/basic_extensions.rb +0 -98
- data/example/http_config.rb +2 -3
- data/example/http_extensions.rb +6 -63
- data/lib/rflow.rb +31 -39
- data/lib/rflow/child_process.rb +112 -0
- data/lib/rflow/component.rb +77 -148
- data/lib/rflow/component/port.rb +38 -41
- data/lib/rflow/components.rb +4 -8
- data/lib/rflow/components/clock.rb +49 -0
- data/lib/rflow/components/integer.rb +39 -0
- data/lib/rflow/components/raw.rb +10 -6
- data/lib/rflow/components/replicate.rb +20 -0
- data/lib/rflow/components/ruby_proc_filter.rb +27 -0
- data/lib/rflow/configuration.rb +105 -184
- data/lib/rflow/configuration/component.rb +1 -4
- data/lib/rflow/configuration/connection.rb +11 -16
- data/lib/rflow/configuration/port.rb +3 -5
- data/lib/rflow/configuration/ruby_dsl.rb +105 -119
- data/lib/rflow/configuration/setting.rb +19 -25
- data/lib/rflow/configuration/shard.rb +1 -3
- data/lib/rflow/connection.rb +47 -10
- data/lib/rflow/connections.rb +0 -1
- data/lib/rflow/connections/zmq_connection.rb +34 -38
- data/lib/rflow/daemon_process.rb +155 -0
- data/lib/rflow/logger.rb +41 -25
- data/lib/rflow/master.rb +23 -105
- data/lib/rflow/message.rb +78 -108
- data/lib/rflow/pid_file.rb +37 -37
- data/lib/rflow/shard.rb +33 -100
- data/lib/rflow/version.rb +2 -2
- data/rflow.gemspec +2 -2
- data/schema/tick.avsc +10 -0
- data/spec/fixtures/config_ints.rb +4 -40
- data/spec/fixtures/config_shards.rb +1 -2
- data/spec/fixtures/extensions_ints.rb +0 -98
- data/spec/rflow/component/port_spec.rb +61 -0
- data/spec/rflow/components/clock_spec.rb +72 -0
- data/spec/rflow/configuration/ruby_dsl_spec.rb +150 -0
- data/spec/rflow/configuration_spec.rb +54 -0
- data/spec/rflow/forward_to_input_port_spec.rb +48 -0
- data/spec/rflow/forward_to_output_port_spec.rb +40 -0
- data/spec/rflow/logger_spec.rb +48 -0
- data/spec/rflow/message/data/raw_spec.rb +29 -0
- data/spec/rflow/message/data_spec.rb +58 -0
- data/spec/rflow/message_spec.rb +154 -0
- data/spec/rflow_spec.rb +94 -124
- data/spec/spec_helper.rb +8 -12
- metadata +46 -22
- data/lib/rflow/components/raw/extensions.rb +0 -18
- data/lib/rflow/port.rb +0 -4
- data/lib/rflow/util.rb +0 -19
- data/spec/rflow_component_port_spec.rb +0 -58
- data/spec/rflow_configuration_ruby_dsl_spec.rb +0 -148
- data/spec/rflow_configuration_spec.rb +0 -73
- data/spec/rflow_message_data_raw.rb +0 -26
- data/spec/rflow_message_data_spec.rb +0 -60
- data/spec/rflow_message_spec.rb +0 -182
- data/spec/schema_spec.rb +0 -28
- data/temp.rb +0 -295
@@ -0,0 +1,72 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
class RFlow
|
4
|
+
module Components
|
5
|
+
describe Clock do
|
6
|
+
before(:each) do
|
7
|
+
ActiveRecord::Base.establish_connection adapter: "sqlite3", database: ":memory:"
|
8
|
+
Configuration.migrate_database
|
9
|
+
end
|
10
|
+
let(:config) do
|
11
|
+
RFlow::Configuration::Component.new.tap do |c|
|
12
|
+
c.output_ports << RFlow::Configuration::OutputPort.new(name: 'tick_port')
|
13
|
+
end
|
14
|
+
end
|
15
|
+
let(:message_connection) { RFlow::MessageCollectingConnection.new }
|
16
|
+
|
17
|
+
def clock(args = {})
|
18
|
+
Clock.new(config).tap do |c|
|
19
|
+
c.configure! args
|
20
|
+
c.tick_port.connect!
|
21
|
+
c.tick_port.add_connection nil, message_connection
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def messages; message_connection.messages; end
|
26
|
+
|
27
|
+
it 'defaults configuration nicely' do
|
28
|
+
clock.tap do |c|
|
29
|
+
c.clock_name.should == 'Clock'
|
30
|
+
c.tick_interval.should == 1
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
it 'supports name overrides' do
|
35
|
+
clock('name' => 'testname').tap do |c|
|
36
|
+
c.clock_name.should == 'testname'
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
it 'supports interval overrides for floats' do
|
41
|
+
clock('tick_interval' => 1.5).tap do |c|
|
42
|
+
c.tick_interval.should == 1.5
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
it 'supports interval overrides for strings' do
|
47
|
+
clock('tick_interval' => '1.5').tap do |c|
|
48
|
+
c.tick_interval.should == 1.5
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
it 'should register a timer' do
|
53
|
+
EventMachine::PeriodicTimer.should_receive(:new).with(1)
|
54
|
+
clock.run!
|
55
|
+
end
|
56
|
+
|
57
|
+
it 'should generate a tick message when asked' do
|
58
|
+
clock.tap do |c|
|
59
|
+
now = Integer(Time.now.to_f * 1000)
|
60
|
+
messages.should be_empty
|
61
|
+
c.tick
|
62
|
+
messages.should have(1).message
|
63
|
+
messages.first.tap do |m|
|
64
|
+
m.data_type_name.should == 'RFlow::Message::Clock::Tick'
|
65
|
+
m.data.name.should == 'Clock'
|
66
|
+
m.data.timestamp.should >= now
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
@@ -0,0 +1,150 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'rflow/configuration'
|
3
|
+
|
4
|
+
class RFlow
|
5
|
+
class Configuration
|
6
|
+
describe RubyDSL do
|
7
|
+
before(:each) do
|
8
|
+
ActiveRecord::Base.establish_connection adapter: "sqlite3", database: ":memory:"
|
9
|
+
Configuration.migrate_database
|
10
|
+
end
|
11
|
+
|
12
|
+
it "should correctly process an empty DSL" do
|
13
|
+
described_class.configure {}
|
14
|
+
|
15
|
+
Shard.should have(0).shards
|
16
|
+
Component.should have(0).components
|
17
|
+
Port.should have(0).ports
|
18
|
+
Connection.should have(0).connections
|
19
|
+
end
|
20
|
+
|
21
|
+
it "should correctly process a component declaration" do
|
22
|
+
described_class.configure do |c|
|
23
|
+
c.component 'boom', 'town', 'opt1' => 'OPT1', 'opt2' => 'OPT2'
|
24
|
+
end
|
25
|
+
|
26
|
+
Shard.should have(1).shard
|
27
|
+
Component.should have(1).component
|
28
|
+
Port.should have(0).ports
|
29
|
+
Connection.should have(0).connections
|
30
|
+
|
31
|
+
Component.first.tap do |c|
|
32
|
+
c.name.should == 'boom'
|
33
|
+
c.specification.should == 'town'
|
34
|
+
c.options.should == {'opt1' => 'OPT1', 'opt2' => 'OPT2'}
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
it "should correctly process a connect declaration" do
|
39
|
+
described_class.configure do |c|
|
40
|
+
c.component 'first', 'First'
|
41
|
+
c.component 'second', 'Second'
|
42
|
+
c.connect 'first#out' => 'second#in'
|
43
|
+
c.connect 'first#out' => 'second#in[inkey]'
|
44
|
+
c.connect 'first#out[outkey]' => 'second#in'
|
45
|
+
c.connect 'first#out[outkey]' => 'second#in[inkey]'
|
46
|
+
end
|
47
|
+
|
48
|
+
Shard.should have(1).shard
|
49
|
+
Component.should have(2).components
|
50
|
+
Port.should have(2).ports
|
51
|
+
Connection.should have(4).connections
|
52
|
+
|
53
|
+
first_component = Component.where(name: 'first').first.tap do |component|
|
54
|
+
component.specification.should == 'First'
|
55
|
+
component.should have(0).input_ports
|
56
|
+
component.should have(1).output_port
|
57
|
+
component.output_ports.first.name.should == 'out'
|
58
|
+
|
59
|
+
component.output_ports.first.should have(4).connections
|
60
|
+
component.output_ports.first.connections.tap do |connections|
|
61
|
+
connections[0].input_port_key.should be_nil
|
62
|
+
connections[0].output_port_key.should be_nil
|
63
|
+
connections[1].input_port_key.should == 'inkey'
|
64
|
+
connections[1].output_port_key.should be_nil
|
65
|
+
connections[2].input_port_key.should be_nil
|
66
|
+
connections[2].output_port_key.should == 'outkey'
|
67
|
+
connections[3].input_port_key.should == 'inkey'
|
68
|
+
connections[3].output_port_key.should == 'outkey'
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
Component.where(name: 'second').first.tap do |component|
|
73
|
+
component.specification.should == 'Second'
|
74
|
+
component.should have(1).input_port
|
75
|
+
component.input_ports.first.name.should == 'in'
|
76
|
+
component.should have(0).output_ports
|
77
|
+
|
78
|
+
component.input_ports.first.should have(4).connections
|
79
|
+
component.input_ports.first.connections.should == first_component.output_ports.first.connections
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
it "should correctly process shard declarations" do
|
84
|
+
described_class.configure do |c|
|
85
|
+
c.component 'first', 'First', :opt1 => 'opt1'
|
86
|
+
|
87
|
+
c.shard "s1", :process => 2 do |s|
|
88
|
+
s.component 'second', 'Second', :opt1 => 'opt1', "opt2" => "opt2"
|
89
|
+
end
|
90
|
+
|
91
|
+
c.shard "s2", :type => :process, :count => 10 do |s|
|
92
|
+
s.component 'third', 'Third'
|
93
|
+
s.component 'fourth', 'Fourth'
|
94
|
+
end
|
95
|
+
|
96
|
+
c.shard "s-ignored", :type => :process, :count => 10 do
|
97
|
+
# ignored because there are no components
|
98
|
+
end
|
99
|
+
|
100
|
+
c.component 'fifth', 'Fifth'
|
101
|
+
|
102
|
+
c.connect 'first#out' => 'second#in'
|
103
|
+
c.connect 'second#out[outkey]' => 'third#in[inkey]'
|
104
|
+
c.connect 'second#out' => 'third#in2'
|
105
|
+
c.connect 'third#out' => 'fourth#in'
|
106
|
+
c.connect 'third#out' => 'fifth#in'
|
107
|
+
end
|
108
|
+
|
109
|
+
Shard.should have(3).shards
|
110
|
+
Component.should have(5).components
|
111
|
+
Port.should have(8).ports
|
112
|
+
Connection.should have(5).connections
|
113
|
+
|
114
|
+
Shard.all.tap do |shards|
|
115
|
+
shards.map(&:name).should == ['DEFAULT', 's1', 's2']
|
116
|
+
shards.first.components.all.map(&:name).should == ['first', 'fifth']
|
117
|
+
shards.second.components.all.map(&:name).should == ['second']
|
118
|
+
shards.third.components.all.map(&:name).should == ['third', 'fourth']
|
119
|
+
end
|
120
|
+
|
121
|
+
Port.all.map(&:name).should == ['out', 'in', 'out', 'in', 'in2', 'out', 'in', 'in']
|
122
|
+
|
123
|
+
Connection.all.map(&:name).should ==
|
124
|
+
['first#out=>second#in',
|
125
|
+
'second#out[outkey]=>third#in[inkey]',
|
126
|
+
'second#out=>third#in2',
|
127
|
+
'third#out=>fourth#in',
|
128
|
+
'third#out=>fifth#in']
|
129
|
+
end
|
130
|
+
|
131
|
+
it "should not allow two components with the same name" do
|
132
|
+
expect {
|
133
|
+
described_class.configure do |c|
|
134
|
+
c.component 'first', 'First'
|
135
|
+
c.component 'first', 'First'
|
136
|
+
end
|
137
|
+
}.to raise_error(ActiveRecord::RecordInvalid)
|
138
|
+
end
|
139
|
+
|
140
|
+
it "should not allow two shards with the same name" do
|
141
|
+
expect {
|
142
|
+
described_class.configure do |c|
|
143
|
+
c.shard("s1", :process => 2) {|c| c.component 'x', 'y' }
|
144
|
+
c.shard("s1", :process => 2) {|c| c.component 'z', 'q' }
|
145
|
+
end
|
146
|
+
}.to raise_error
|
147
|
+
end
|
148
|
+
end
|
149
|
+
end
|
150
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'rflow/configuration'
|
3
|
+
|
4
|
+
class RFlow
|
5
|
+
describe Configuration do
|
6
|
+
describe '.add_available_data_type' do
|
7
|
+
context 'if passed a data_serialization that is not avro' do
|
8
|
+
it "should throw an exception" do
|
9
|
+
expect { Configuration.add_available_data_type('A', 'boom', 'schema') }.to raise_error(
|
10
|
+
ArgumentError, "Data serialization_type must be 'avro' for 'A'")
|
11
|
+
end
|
12
|
+
|
13
|
+
it "should not update the available_data_types" do
|
14
|
+
expect {
|
15
|
+
Configuration.add_available_data_type('A', 'boom', 'schema') rescue nil
|
16
|
+
}.not_to change { Configuration.available_data_types.size }
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
describe "Data Extensions" do
|
22
|
+
describe ".add_available_data_extension" do
|
23
|
+
context 'if passed a non-module data extension' do
|
24
|
+
it "should throw an exception" do
|
25
|
+
expect {
|
26
|
+
Configuration.add_available_data_extension('data_type', 'NOTAMODULE')
|
27
|
+
}.to raise_error(ArgumentError, "Invalid data extension NOTAMODULE for data_type. Only Ruby Modules allowed")
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
context "if passed a valid Module as a data extension" do
|
32
|
+
it "should update the available_data_extensions" do
|
33
|
+
expect {
|
34
|
+
Configuration.add_available_data_extension('data_type', Module.new)
|
35
|
+
}.to change { Configuration.available_data_extensions['data_type'].size }.by(1)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
it "should perform simple 'prefix'-based inheritance for extensions" do
|
41
|
+
Configuration.add_available_data_extension('A', A = Module.new)
|
42
|
+
Configuration.add_available_data_extension('A::B', B = Module.new)
|
43
|
+
Configuration.add_available_data_extension('A::B::C', C = Module.new)
|
44
|
+
Configuration.add_available_data_extension('A::B::C::D', D = Module.new)
|
45
|
+
|
46
|
+
Configuration.available_data_extensions['A'].should == [A]
|
47
|
+
Configuration.available_data_extensions['A::B'].should == [A, B]
|
48
|
+
Configuration.available_data_extensions['A::B::C'].should == [A, B, C]
|
49
|
+
Configuration.available_data_extensions['A::B::C::D'].should == [A, B, C, D]
|
50
|
+
Configuration.available_data_extensions['A::B::C::D::E'].should == [A, B, C, D]
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
class RFlow
|
4
|
+
describe ForwardToInputPort do
|
5
|
+
before(:each) do
|
6
|
+
ActiveRecord::Base.establish_connection adapter: "sqlite3", database: ":memory:"
|
7
|
+
Configuration.migrate_database
|
8
|
+
end
|
9
|
+
|
10
|
+
let(:filtered_message_connection) { RFlow::MessageCollectingConnection.new }
|
11
|
+
let(:dropped_message_connection) { RFlow::MessageCollectingConnection.new }
|
12
|
+
|
13
|
+
let(:generator) do
|
14
|
+
config = RFlow::Configuration::Component.new.tap do |c|
|
15
|
+
c.output_ports << RFlow::Configuration::OutputPort.new(name: 'out')
|
16
|
+
end
|
17
|
+
RFlow::Components::GenerateIntegerSequence.new(config).tap do |c|
|
18
|
+
c.configure! config.options
|
19
|
+
c.out.add_connection nil, ForwardToInputPort.new(ruby_proc_filter, 'in', nil)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
let(:ruby_proc_filter) do
|
24
|
+
config = RFlow::Configuration::Component.new.tap do |c|
|
25
|
+
c.input_ports << RFlow::Configuration::InputPort.new(name: 'in')
|
26
|
+
['filtered', 'dropped'].each {|p| c.output_ports << RFlow::Configuration::OutputPort.new(name: p) }
|
27
|
+
c.options = {'filter_proc_string' => 'message.data.data_object % 2 == 0'}
|
28
|
+
end
|
29
|
+
RFlow::Components::RubyProcFilter.new(config).tap do |c|
|
30
|
+
c.configure! config.options
|
31
|
+
c.filtered.add_connection nil, filtered_message_connection
|
32
|
+
c.dropped.add_connection nil, dropped_message_connection
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def filtered_messages; filtered_message_connection.messages; end
|
37
|
+
def dropped_messages; dropped_message_connection.messages; end
|
38
|
+
|
39
|
+
it 'foo' do
|
40
|
+
5.times { generator.generate }
|
41
|
+
filtered_messages.should have(3).messages
|
42
|
+
filtered_messages.map(&:data).map(&:data_object).should == [0, 2, 4]
|
43
|
+
dropped_messages.should have(2).messages
|
44
|
+
dropped_messages.map(&:data).map(&:data_object).should == [1, 3]
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
@@ -0,0 +1,40 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
class RFlow
|
4
|
+
describe ForwardToOutputPort do
|
5
|
+
before(:each) do
|
6
|
+
ActiveRecord::Base.establish_connection adapter: "sqlite3", database: ":memory:"
|
7
|
+
Configuration.migrate_database
|
8
|
+
end
|
9
|
+
|
10
|
+
let(:message_connection) { RFlow::MessageCollectingConnection.new }
|
11
|
+
|
12
|
+
let(:generator) do
|
13
|
+
config = RFlow::Configuration::Component.new.tap do |c|
|
14
|
+
c.output_ports << RFlow::Configuration::OutputPort.new(name: 'out')
|
15
|
+
end
|
16
|
+
RFlow::Components::GenerateIntegerSequence.new(config).tap do |c|
|
17
|
+
c.configure! config.options
|
18
|
+
c.out.add_connection nil, ForwardToOutputPort.new(ruby_proc_filter, 'filtered')
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
let(:ruby_proc_filter) do
|
23
|
+
config = RFlow::Configuration::Component.new.tap do |c|
|
24
|
+
c.output_ports << RFlow::Configuration::OutputPort.new(name: 'filtered')
|
25
|
+
c.options = {'filter_proc_string' => 'message % 2 == 0'}
|
26
|
+
end
|
27
|
+
RFlow::Components::RubyProcFilter.new(config).tap do |c|
|
28
|
+
c.configure! config.options
|
29
|
+
c.filtered.add_connection nil, message_connection
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def messages; message_connection.messages; end
|
34
|
+
|
35
|
+
it 'should place the messages on the output port, regardless of the filter' do
|
36
|
+
5.times { generator.generate }
|
37
|
+
messages.map(&:data).map(&:data_object).should == [0, 1, 2, 3, 4]
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'open3'
|
3
|
+
require 'rflow'
|
4
|
+
|
5
|
+
class RFlow
|
6
|
+
describe Logger do
|
7
|
+
let(:log_file_path) { File.join(@temp_directory_path, 'logfile') }
|
8
|
+
let(:logger_config) do
|
9
|
+
{'rflow.log_file_path' => log_file_path,
|
10
|
+
'rflow.log_level' => 'DEBUG'}
|
11
|
+
end
|
12
|
+
|
13
|
+
def initialize_logger
|
14
|
+
@logger = described_class.new(logger_config)
|
15
|
+
end
|
16
|
+
let(:logger) { @logger }
|
17
|
+
|
18
|
+
before(:each) { initialize_logger }
|
19
|
+
|
20
|
+
it "should initialize correctly" do
|
21
|
+
File.exist?(log_file_path).should be true
|
22
|
+
|
23
|
+
logger.error "TESTTESTTEST"
|
24
|
+
File.read(log_file_path).should match(/TESTTESTTEST/)
|
25
|
+
|
26
|
+
logger.close
|
27
|
+
end
|
28
|
+
|
29
|
+
it "should reopen correctly" do
|
30
|
+
moved_path = log_file_path + '.old'
|
31
|
+
|
32
|
+
File.exist?(log_file_path).should be true
|
33
|
+
File.exist?(moved_path).should be false
|
34
|
+
|
35
|
+
File.rename log_file_path, moved_path
|
36
|
+
|
37
|
+
logger.reopen
|
38
|
+
|
39
|
+
logger.error "TESTTESTTEST"
|
40
|
+
File.read(log_file_path).should match(/TESTTESTTEST/)
|
41
|
+
File.read(moved_path).should_not match(/TESTTESTTEST/)
|
42
|
+
|
43
|
+
logger.close
|
44
|
+
end
|
45
|
+
|
46
|
+
it "should toggle log level"
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'rflow/components/raw'
|
3
|
+
|
4
|
+
class RFlow
|
5
|
+
class Message
|
6
|
+
class Data
|
7
|
+
describe 'Raw Avro Schema' do
|
8
|
+
let(:schema) { Configuration.available_data_types['RFlow::Message::Data::Raw']['avro'] }
|
9
|
+
|
10
|
+
it "should load the schema" do
|
11
|
+
schema.should_not be_nil
|
12
|
+
end
|
13
|
+
|
14
|
+
it "should encode and decode an object" do
|
15
|
+
raw = {'raw' => Array.new(256) { rand(256) }.pack('c*')}
|
16
|
+
|
17
|
+
expect { encode_avro(schema, raw) }.to_not raise_error
|
18
|
+
encoded = encode_avro(schema, raw)
|
19
|
+
|
20
|
+
expect { decode_avro(schema, encoded) }.to_not raise_error
|
21
|
+
decoded = decode_avro(schema, encoded)
|
22
|
+
|
23
|
+
decoded.should == raw
|
24
|
+
decoded['raw'].should == raw['raw']
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|