|
| 1 | +module TNetStrings |
| 2 | + def self.dump(data) |
| 3 | + case data |
| 4 | + when String, Symbol then "#{data.length}:#{data}," |
| 5 | + when Fixnum then "#{data.to_s.length}:#{data.to_s}#" |
| 6 | + when Float then "#{data.to_s.length}:#{data.to_s}^" |
| 7 | + when TrueClass then "4:true!" |
| 8 | + when FalseClass then "5:false!" |
| 9 | + when NilClass then "0:~" |
| 10 | + when Array then dump_array(data) |
| 11 | + when Hash then dump_hash(data) |
| 12 | + else |
| 13 | + raise "Can't serialize stuff that's '#{data.class}'." |
| 14 | + end |
| 15 | + end |
| 16 | + |
| 17 | + def self.parse(data) |
| 18 | + payload, payload_type, remain = parse_payload(data) |
| 19 | + |
| 20 | + value = case payload_type |
| 21 | + when ',' then payload |
| 22 | + when '#' then payload.to_i |
| 23 | + when '^' then payload.to_f |
| 24 | + when '!' then payload == 'true' |
| 25 | + when ']' then parse_array(payload) |
| 26 | + when '}' then parse_hash(payload) |
| 27 | + when '~' |
| 28 | + raise "Payload must be 0 length for null." unless payload.length == 0 |
| 29 | + nil |
| 30 | + else |
| 31 | + raise "Invalid payload type: #{payload_type}" |
| 32 | + end |
| 33 | + |
| 34 | + [ value, remain ] |
| 35 | + end |
| 36 | + |
| 37 | + def self.parse_payload(data) |
| 38 | + raise "Invalid payload type: #{payload_type}" if data.empty? |
| 39 | + |
| 40 | + len, extra = data.split(':', 2) |
| 41 | + len = len.to_i |
| 42 | + if len == 0 |
| 43 | + payload = '' |
| 44 | + else |
| 45 | + payload, extra = extra[0..len-1], extra[len..-1] |
| 46 | + end |
| 47 | + payload_type, remain = extra[0], extra[1..-1] |
| 48 | + |
| 49 | + [ payload, payload_type, remain ] |
| 50 | + end |
| 51 | + |
| 52 | + private |
| 53 | + |
| 54 | + def self.parse_array(data) |
| 55 | + arr = [] |
| 56 | + return arr if data.empty? |
| 57 | + |
| 58 | + begin |
| 59 | + value, data = parse(data) |
| 60 | + arr << value |
| 61 | + end while not data.empty? |
| 62 | + |
| 63 | + arr |
| 64 | + end |
| 65 | + |
| 66 | + def self.parse_pair(data) |
| 67 | + key, extra = parse(data) |
| 68 | + raise "Unbalanced hash" if extra.empty? |
| 69 | + value, extra = parse(extra) |
| 70 | + |
| 71 | + [ key, value, extra ] |
| 72 | + end |
| 73 | + |
| 74 | + def self.parse_hash(data) |
| 75 | + hsh = {} |
| 76 | + return hsh if data.empty? |
| 77 | + |
| 78 | + begin |
| 79 | + key, value, data = parse_pair(data) |
| 80 | + hsh[key.to_sym] = value |
| 81 | + end while not data.empty? |
| 82 | + |
| 83 | + hsh |
| 84 | + end |
| 85 | + |
| 86 | + def self.dump_array(data) |
| 87 | + payload = "" |
| 88 | + data.each { |v| payload << dump(v) } |
| 89 | + "#{payload.length}:#{payload}]" |
| 90 | + end |
| 91 | + |
| 92 | + def self.dump_hash(data) |
| 93 | + payload = "" |
| 94 | + data.each do |k,v| |
| 95 | + payload << dump(k.to_s) |
| 96 | + payload << dump(v) |
| 97 | + end |
| 98 | + "#{payload.length}:#{payload}}" |
| 99 | + end |
| 100 | +end |
| 101 | + |
| 102 | +if $0 == __FILE__ |
| 103 | + require 'minitest/autorun' |
| 104 | + |
| 105 | + class NmsgTestSocket < MiniTest::Unit::TestCase |
| 106 | + |
| 107 | + def test_parse_string |
| 108 | + n = "3:foo," |
| 109 | + |
| 110 | + s, r = TNetStrings::parse(n) |
| 111 | + assert_equal "foo", s |
| 112 | + assert_equal "", r |
| 113 | + end |
| 114 | + |
| 115 | + def test_parse_strings |
| 116 | + n = "3:foo,3:bar,6:foobar," |
| 117 | + |
| 118 | + s, r = TNetStrings::parse(n) |
| 119 | + assert_equal "foo", s |
| 120 | + assert_equal "3:bar,6:foobar,", r |
| 121 | + |
| 122 | + s, r = TNetStrings::parse(r) |
| 123 | + assert_equal "bar", s |
| 124 | + assert_equal "6:foobar,", r |
| 125 | + |
| 126 | + s, r = TNetStrings::parse(r) |
| 127 | + assert_equal "foobar", s |
| 128 | + assert_equal "", r |
| 129 | + end |
| 130 | + |
| 131 | + def test_parse_fixnum |
| 132 | + n = "2:42#" |
| 133 | + |
| 134 | + i, r = TNetStrings::parse(n) |
| 135 | + assert_equal 42, i |
| 136 | + assert_equal "", r |
| 137 | + end |
| 138 | + |
| 139 | + def test_parse_fixnums |
| 140 | + n = "2:42#1:7#6:123456#" |
| 141 | + |
| 142 | + i, r = TNetStrings::parse(n) |
| 143 | + assert_equal 42, i |
| 144 | + assert_equal "1:7#6:123456#", r |
| 145 | + |
| 146 | + i, r = TNetStrings::parse(r) |
| 147 | + assert_equal 7, i |
| 148 | + assert_equal "6:123456#", r |
| 149 | + |
| 150 | + i, r = TNetStrings::parse(r) |
| 151 | + assert_equal 123_456, i |
| 152 | + assert_equal "", r |
| 153 | + end |
| 154 | + |
| 155 | + def test_parse_float |
| 156 | + n = "9:3.1415926^" |
| 157 | + |
| 158 | + f, r = TNetStrings::parse(n) |
| 159 | + assert_equal 3.1415926, f |
| 160 | + assert_equal "", r |
| 161 | + end |
| 162 | + |
| 163 | + def test_parse_bool |
| 164 | + n = "4:true!" |
| 165 | + |
| 166 | + b, r = TNetStrings::parse(n) |
| 167 | + assert_equal true, b |
| 168 | + assert_equal "", r |
| 169 | + |
| 170 | + n = "5:false!" |
| 171 | + |
| 172 | + b, r = TNetStrings::parse(n) |
| 173 | + assert_equal false, b |
| 174 | + assert_equal "", r |
| 175 | + end |
| 176 | + |
| 177 | + def test_parse_nil |
| 178 | + n = "0:~" |
| 179 | + |
| 180 | + v, r = TNetStrings::parse(n) |
| 181 | + assert_equal nil, v |
| 182 | + assert_equal "", r |
| 183 | + end |
| 184 | + |
| 185 | + def test_parse_mixed |
| 186 | + n = "2:ok,4:true!3:313#" |
| 187 | + |
| 188 | + v, r = TNetStrings::parse(n) |
| 189 | + assert_equal "ok", v |
| 190 | + assert_equal "4:true!3:313#", r |
| 191 | + |
| 192 | + v, r = TNetStrings::parse(r) |
| 193 | + assert_equal true, v |
| 194 | + assert_equal "3:313#", r |
| 195 | + |
| 196 | + v, r = TNetStrings::parse(r) |
| 197 | + assert_equal 313, v |
| 198 | + assert_equal "", r |
| 199 | + end |
| 200 | + |
| 201 | + def test_parse_array |
| 202 | + s, r = TNetStrings::parse("0:]") |
| 203 | + assert_equal [], s |
| 204 | + assert_equal "", r |
| 205 | + |
| 206 | + s, r = TNetStrings::parse("18:3:foo,3:bar,3:baz,]") |
| 207 | + assert_equal [ "foo", "bar", "baz" ], s |
| 208 | + assert_equal "", r |
| 209 | + |
| 210 | + a = "0:]18:3:foo,3:bar,3:baz,]37:5:false!6:3.1415^2:42#0:~4:true!2:ok,]" |
| 211 | + s, r = TNetStrings::parse(a) |
| 212 | + assert_equal [], s |
| 213 | + assert_equal "18:3:foo,3:bar,3:baz,]37:5:false!6:3.1415^2:42#0:~4:true!2:ok,]", r |
| 214 | + |
| 215 | + s, r = TNetStrings::parse(r) |
| 216 | + assert_equal [ "foo", "bar", "baz" ], s |
| 217 | + assert_equal "37:5:false!6:3.1415^2:42#0:~4:true!2:ok,]", r |
| 218 | + |
| 219 | + s, r = TNetStrings::parse(r) |
| 220 | + assert_equal [ false, 3.1415, 42, nil, true, "ok" ], s |
| 221 | + assert_equal "", r |
| 222 | + end |
| 223 | + |
| 224 | + def test_parse_hash |
| 225 | + h, r = TNetStrings::parse("0:}") |
| 226 | + assert_equal Hash.new, h |
| 227 | + assert_equal "", r |
| 228 | + |
| 229 | + h, r = TNetStrings::parse("12:3:foo,3:bar,}") |
| 230 | + assert_equal({foo: "bar"}, h) |
| 231 | + assert_equal "", r |
| 232 | + |
| 233 | + n = "26:3:cat,4:meow,3:dog,4:bark,}12:3:cow,3:moo,}" |
| 234 | + h, r = TNetStrings::parse(n) |
| 235 | + assert_equal({ cat: "meow", dog: "bark" }, h) |
| 236 | + assert_equal "12:3:cow,3:moo,}", r |
| 237 | + |
| 238 | + h, r = TNetStrings::parse(r) |
| 239 | + assert_equal({ cow: "moo" }, h) |
| 240 | + assert_equal "", r |
| 241 | + end |
| 242 | + |
| 243 | + def test_dump_strings |
| 244 | + s = TNetStrings::dump("foobar") |
| 245 | + assert_equal "6:foobar,", s |
| 246 | + |
| 247 | + s << TNetStrings::dump("baz") |
| 248 | + assert_equal "6:foobar,3:baz,", s |
| 249 | + end |
| 250 | + |
| 251 | + def test_dump_symbol |
| 252 | + s = TNetStrings::dump(:foobarbaz) |
| 253 | + assert_equal "9:foobarbaz,", s |
| 254 | + end |
| 255 | + |
| 256 | + def test_dump_fixnums |
| 257 | + s = TNetStrings::dump(65_537) |
| 258 | + assert_equal "5:65537#", s |
| 259 | + |
| 260 | + s << TNetStrings::dump(42) |
| 261 | + assert_equal "5:65537#2:42#", s |
| 262 | + |
| 263 | + s << TNetStrings::dump(300_000_000) |
| 264 | + assert_equal "5:65537#2:42#9:300000000#", s |
| 265 | + end |
| 266 | + |
| 267 | + def test_dump_floats |
| 268 | + s = TNetStrings::dump(3.1415926) |
| 269 | + assert_equal "9:3.1415926^", s |
| 270 | + |
| 271 | + s << TNetStrings::dump(0.0000000000667) |
| 272 | + assert_equal "9:3.1415926^8:6.67e-11^", s |
| 273 | + end |
| 274 | + |
| 275 | + def test_dump_bool |
| 276 | + s = TNetStrings::dump(false) |
| 277 | + assert_equal "5:false!", s |
| 278 | + |
| 279 | + s << TNetStrings::dump(true) |
| 280 | + assert_equal "5:false!4:true!", s |
| 281 | + end |
| 282 | + |
| 283 | + def test_dump_nil |
| 284 | + s = TNetStrings::dump(nil) |
| 285 | + assert_equal "0:~", s |
| 286 | + end |
| 287 | + |
| 288 | + def test_dump_array |
| 289 | + s = TNetStrings::dump([]) |
| 290 | + assert_equal "0:]", s |
| 291 | + |
| 292 | + s << TNetStrings::dump([ "foo", "bar", "baz" ]) |
| 293 | + assert_equal "0:]18:3:foo,3:bar,3:baz,]", s |
| 294 | + |
| 295 | + s << TNetStrings::dump([ false, 3.1415, 42, nil, true, "ok" ]) |
| 296 | + assert_equal "0:]18:3:foo,3:bar,3:baz,]37:5:false!6:3.1415^2:42#0:~4:true!2:ok,]", s |
| 297 | + end |
| 298 | + |
| 299 | + def test_dump_hash |
| 300 | + s = TNetStrings::dump({}) |
| 301 | + assert_equal "0:}", s |
| 302 | + |
| 303 | + s << TNetStrings::dump({ foo: "bar"}) |
| 304 | + assert_equal "0:}12:3:foo,3:bar,}", s |
| 305 | + end |
| 306 | + end |
| 307 | +end |
0 commit comments