tnetstrings 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
data/ext/tnetstrings.c ADDED
@@ -0,0 +1,202 @@
1
+ #include "tnetstrings.h"
2
+
3
+ static TNETS_VALUE
4
+ eTnetsParserError;
5
+
6
+ #define TNETS_PARSER_ERROR(msg) rb_raise(eTnetsParserError, msg)
7
+
8
+ TNETS_VALUE tnets_render_number(char* payload, size_t len);
9
+ TNETS_VALUE tnets_render_string(char* payload, size_t len);
10
+ TNETS_VALUE tnets_render_bool (char* payload, size_t len);
11
+ TNETS_VALUE tnets_render_null (char* payload, size_t len);
12
+ TNETS_VALUE tnets_render_list (char* payload, size_t len);
13
+ TNETS_VALUE tnets_render_dict (char* payload, size_t len);
14
+
15
+ size_t tnets_size_spec(char* data, size_t len, char** payload);
16
+
17
+ TNETS_VALUE tnets_parse_dict_key(char* data, size_t len, char** remain);
18
+ TNETS_VALUE tnets_parse(char* data, size_t len, char** remain);
19
+
20
+ TNETS_VALUE rb_tnets_parse(TNETS_VALUE self, TNETS_VALUE rbstr);
21
+
22
+ // since no checking is done on strings, just pass it right along
23
+ // to the wrapper.
24
+ #define tnets_render_string TNETS_WRAP_STRING
25
+
26
+ TNETS_VALUE
27
+ tnets_render_number(char* payload, size_t len) {
28
+ size_t i;
29
+ int number = 0; // TODO: use a type that can handle
30
+ // larger numbers without wrapping.
31
+ unsigned short int negative = 0;
32
+
33
+ if (payload[0] == '-') {
34
+ negative = 1;
35
+ payload++;
36
+ len--;
37
+ }
38
+
39
+ for(i = 0; i < len; i++) {
40
+ if (IS_NUMERIC(payload[i])) {
41
+ number *= 10;
42
+ number += CTOI(payload[i]);
43
+ }
44
+ else {
45
+ TNETS_PARSER_ERROR("Non-numeric character in a tnets number");
46
+ }
47
+ }
48
+
49
+ if (negative) {
50
+ number *= -1;
51
+ }
52
+
53
+ return TNETS_WRAP_NUMBER(number);
54
+ }
55
+
56
+ TNETS_VALUE
57
+ tnets_render_bool(char* payload, size_t len) {
58
+ // TODO: do this better.
59
+ if (len != 4) return TNETS_WRAP_FALSE;
60
+ size_t i;
61
+ for (i = 0; i < len; i++) {
62
+ if (payload[i] != "true"[i]) {
63
+ return TNETS_WRAP_FALSE;
64
+ }
65
+ }
66
+
67
+ return TNETS_WRAP_TRUE;
68
+ }
69
+
70
+ TNETS_VALUE
71
+ tnets_render_null(char* payload, size_t len) {
72
+ if (len != 0) TNETS_PARSER_ERROR("null must have an empty payload.");
73
+
74
+ return TNETS_WRAP_NULL;
75
+ }
76
+
77
+ TNETS_VALUE
78
+ tnets_render_list(char* payload, size_t len) {
79
+ char *tmp_payload = payload;
80
+ size_t sub_len;
81
+ TNETS_VALUE list = TNETS_NEW_LIST();
82
+
83
+ while (len > 0) {
84
+ TNETS_LIST_PUSH(
85
+ list,
86
+ tnets_parse(payload, len, &tmp_payload)
87
+ );
88
+
89
+ len -= (tmp_payload - payload);
90
+ payload = tmp_payload;
91
+ }
92
+
93
+ return list;
94
+ }
95
+
96
+ #define tnets_render_dict_key TNETS_WRAP_DICT_KEY
97
+
98
+ TNETS_VALUE
99
+ tnets_render_dict(char* payload, size_t len) {
100
+ char *tmp_payload = payload;
101
+ TNETS_VALUE dict = TNETS_NEW_DICT();
102
+ while(len > 0) {
103
+ TNETS_VALUE key = tnets_parse_dict_key(payload, len, &tmp_payload);
104
+ len -= (tmp_payload - payload);
105
+ payload = tmp_payload;
106
+
107
+ TNETS_DICT_SET(dict, key,
108
+ tnets_parse(payload, len, &tmp_payload)
109
+ );
110
+
111
+ len -= (tmp_payload - payload);
112
+ payload = tmp_payload;
113
+ }
114
+
115
+ return dict;
116
+ }
117
+
118
+ size_t
119
+ tnets_size_spec(char *data, size_t len, char **payload) {
120
+ size_t i;
121
+ size_t size = 0;
122
+
123
+ // only scan 11 characters (10 for length spec + 1 for colon)
124
+ size_t scan_limit = (len < 11) ? len : 11;
125
+
126
+ for (i = 0; i < scan_limit; i++) {
127
+ if (IS_NUMERIC(data[i])) {
128
+ size *= 10;
129
+ size += CTOI(data[i]);
130
+ }
131
+ else if (data[i] == ':') {
132
+ if (i + size > len + 1) {
133
+ TNETS_PARSER_ERROR("length spec longer than given string");
134
+ }
135
+
136
+ if (payload != NULL) {
137
+ *payload = data + i + 1; // add one to skip over the colon
138
+ }
139
+ return size;
140
+ }
141
+ else {
142
+ TNETS_PARSER_ERROR("non-numeric character in length spec");
143
+ }
144
+ }
145
+
146
+ TNETS_PARSER_ERROR("length spec more than 10 characters");
147
+
148
+ return 0;
149
+ }
150
+
151
+ TNETS_VALUE
152
+ tnets_parse_dict_key(char* data, size_t len, char** remain) {
153
+ char* payload;
154
+ size_t payload_len = tnets_size_spec(data, len, &payload);
155
+ if (remain != NULL) *remain = payload + payload_len + 1;
156
+
157
+ if (payload[payload_len] != tnets_tag_string) {
158
+ TNETS_PARSER_ERROR("non-string dict keys are not supported");
159
+ }
160
+
161
+ return tnets_render_dict_key(payload, payload_len);
162
+ }
163
+
164
+ TNETS_VALUE
165
+ tnets_parse(char *data, size_t len, char** remain) {
166
+ char *payload = NULL;
167
+ size_t payload_len;
168
+ tnets_type_tag type = tnets_tag_invalid;
169
+
170
+ payload_len = tnets_size_spec(data, len, &payload);
171
+
172
+ if (remain != NULL) *remain = payload + payload_len + 1;
173
+
174
+ type = payload[payload_len];
175
+
176
+ switch(type) {
177
+ case tnets_tag_string: return tnets_render_string(payload, payload_len);
178
+ case tnets_tag_number: return tnets_render_number(payload, payload_len);
179
+ case tnets_tag_null: return tnets_render_null(payload, payload_len);
180
+ case tnets_tag_bool: return tnets_render_bool(payload, payload_len);
181
+ case tnets_tag_list: return tnets_render_list(payload, payload_len);
182
+ case tnets_tag_dict: return tnets_render_dict(payload, payload_len);
183
+ default:
184
+ TNETS_PARSER_ERROR("Invalid type tag");
185
+ }
186
+ }
187
+
188
+ VALUE
189
+ rb_tnets_parse(VALUE self, VALUE rbstr) {
190
+ char* data = RSTRING_PTR(rbstr);
191
+ size_t len = RSTRING_LEN(rbstr);
192
+ return tnets_parse(data, len, NULL);
193
+ }
194
+
195
+ void
196
+ Init_tnetstrings() {
197
+ VALUE mTnetstrings = rb_define_module("TNETS");
198
+
199
+ rb_define_method(mTnetstrings, "c_parse", rb_tnets_parse, 1);
200
+ eTnetsParserError =
201
+ rb_define_class_under(mTnetstrings, "ParserError", rb_eIOError);
202
+ }
data/ext/tnetstrings.h ADDED
@@ -0,0 +1,44 @@
1
+ #include <ruby/ruby.h>
2
+ // #include <stdlib.h>
3
+ // #include <string.h>
4
+ #include <stdio.h>
5
+ #include <ruby/ruby.h>
6
+
7
+ typedef enum tnets_type_tag_e {
8
+ tnets_tag_string = ',',
9
+ tnets_tag_number = '#',
10
+ // tnets_tag_float = '^', // coming soon
11
+ tnets_tag_bool = '!',
12
+ tnets_tag_null = '~',
13
+ tnets_tag_dict = '}',
14
+ tnets_tag_list = ']',
15
+ tnets_tag_invalid = 'Z',
16
+ } tnets_type_tag;
17
+
18
+ #define IS_NUMERIC(ch) ('0' <= ch && ch <= '9')
19
+ #define CTOI(ch) (ch - '0');
20
+
21
+ #define TNETS_VALUE VALUE
22
+
23
+ #define TNETS_WRAP_STRING(s,l) (rb_str_new(s,l))
24
+ #define TNETS_WRAP_NUMBER INT2FIX
25
+ #define TNETS_WRAP_NULL Qnil
26
+ #define TNETS_WRAP_TRUE Qtrue
27
+ #define TNETS_WRAP_FALSE Qfalse
28
+ // TODO: find a better way to symbolize length-specified
29
+ // strings.
30
+ #define TNETS_WRAP_DICT_KEY(k,size) (ID2SYM(rb_to_id(TNETS_WRAP_STRING(k,size))))
31
+
32
+ #define TNETS_NEW_LIST rb_ary_new
33
+ #define TNETS_LIST_PUSH(a,v) rb_ary_push(a,v)
34
+ #define TNETS_NEW_DICT rb_hash_new
35
+ #define TNETS_DICT_SET(dict,key,val) (rb_hash_aset(dict,key,val))
36
+
37
+ /**
38
+ * Parse an object off the front of a tnetstring.
39
+ * Returns a pointer to the parsed object, or NULL if an error occurs.
40
+ * The third argument is an output parameter; if non-NULL it will
41
+ * receive the unparsed remainder of the string.
42
+ */
43
+ TNETS_VALUE
44
+ tnets_parse(char *data, size_t len, char** remain);
Binary file
@@ -0,0 +1,108 @@
1
+ require File.expand_path('../ext/tnetstrings', File.dirname(__FILE__))
2
+
3
+ module TNETS
4
+ extend self
5
+
6
+ def load(str)
7
+ c_parse(str)
8
+ end
9
+
10
+ def dump(obj, *a)
11
+ obj.to_tnets(*a)
12
+ end
13
+
14
+ class StreamParser
15
+ attr_reader :io
16
+ def initialize(io)
17
+ @io = io
18
+ end
19
+
20
+ include Enumerable
21
+ def each(&blk)
22
+ c_tnets_stream(io, &blk)
23
+ end
24
+ end
25
+ end
26
+
27
+ class TrueClass
28
+ TO_TNETS = '4:true!'
29
+
30
+ def to_tnets(*)
31
+ TO_TNETS
32
+ end
33
+ end
34
+
35
+ class FalseClass
36
+ TO_TNETS = '5:false!'
37
+ def to_tnets(*)
38
+ TO_TNETS
39
+ end
40
+ end
41
+
42
+ class NilClass
43
+ TO_TNETS = '0:~'
44
+ def to_tnets(*)
45
+ TO_TNETS
46
+ end
47
+ end
48
+
49
+ class String
50
+ def to_tnets(*)
51
+ "#{size}:#{self},"
52
+ end
53
+ end
54
+
55
+ class Fixnum
56
+ def to_tnets(*)
57
+ "#{to_s.size}:#{self}#"
58
+ end
59
+ end
60
+
61
+ module Enumerable
62
+ def to_tnets(*a, &b)
63
+ return each_tnets(&b) if block_given?
64
+
65
+ out = ''
66
+ each { |x| out << x.to_tnets(*a) }
67
+ "#{out.size}:#{out}]"
68
+ end
69
+
70
+ def stream_tnets(io=$stdout)
71
+ each_tnets { |x| io << x }
72
+ end
73
+
74
+ def each_tnets
75
+ each { |x| yield x.to_tnets }
76
+ end
77
+ end
78
+
79
+ class Hash
80
+ def to_tnets(*)
81
+ out = ''
82
+ each do |k, v|
83
+ # symbols don't respond to :to_str for some reason
84
+ k = k.to_s if k.is_a? Symbol
85
+
86
+ unless k.respond_to? :to_str
87
+ raise TNETS::DumpError, "only string-like objects are allowed as keys"
88
+ end
89
+
90
+ out << k.to_str.to_tnets
91
+ out << v.to_tnets
92
+ end
93
+
94
+ "#{out.size}:#{out}}"
95
+ end
96
+ end
97
+
98
+ class Object
99
+ def to_tnets(*a, &b)
100
+ if respond_to? :as_tnets
101
+ as_tnets(*a).to_tnets
102
+ elsif respond_to? :as_json
103
+ as_json(*a).to_tnets
104
+ else
105
+ method_missing(:to_tnets, *a, &b)
106
+ end
107
+ end
108
+ end
@@ -0,0 +1,7 @@
1
+ module TNETS
2
+ VERSION = '0.0.1'
3
+
4
+ def self.version
5
+ VERSION
6
+ end
7
+ end
@@ -0,0 +1,182 @@
1
+ require './lib/tnetstrings'
2
+
3
+ describe TNETS do
4
+ context "parsing" do
5
+ context "successful examples" do
6
+ it "parses a string" do
7
+ TNETS.load("3:abc,").should == "abc"
8
+ end
9
+
10
+ it "parses null" do
11
+ TNETS.load("0:~").should == nil
12
+ end
13
+
14
+ it "parses true" do
15
+ TNETS.load("4:true!").should == true
16
+ end
17
+
18
+ it "parses false" do
19
+ TNETS.load("5:false!").should == false
20
+ end
21
+
22
+ it "parses a number" do
23
+ TNETS.load("2:32#").should == 32
24
+ end
25
+
26
+ it "parses a negative number" do
27
+ TNETS.load("3:-32#").should == -32
28
+ end
29
+
30
+ it "parses an array" do
31
+ TNETS.load("17:2:32#2:84#4:true!]").should == [32, 84, true]
32
+ end
33
+
34
+ it "parses a dict" do
35
+ TNETS.load(%[16:1:a,1:1#1:b,1:2#}]).should == {:a => 1, :b => 2}
36
+ end
37
+ end
38
+
39
+ context "degenerate cases" do
40
+ it "parses an empty dict" do
41
+ TNETS.load(%|0:}|).should == {}
42
+ end
43
+
44
+ it "parses an empty array" do
45
+ TNETS.load(%|0:]|).should == []
46
+ end
47
+
48
+ it "parses an empty string" do
49
+ TNETS.load(%|0:,|).should == ''
50
+ end
51
+
52
+ it "parses an empty bool" do
53
+ TNETS.load(%|0:!|).should == false
54
+ end
55
+ end
56
+
57
+ context "error cases" do
58
+ def expect_error(str, *args)
59
+ expect { TNETS.load(str) }
60
+ .to raise_error(TNETS::ParserError, *args)
61
+ end
62
+
63
+ context "length spec" do
64
+ it "requires digits at the front" do
65
+ expect_error("abc:true!")
66
+ end
67
+
68
+ it "requires a colon before the 10th character" do
69
+ expect_error("12345678901:true!")
70
+ end
71
+
72
+ it "requires a valid symbol at the end" do
73
+ expect_error("1:f")
74
+ end
75
+
76
+ it "requires the data to be at least as long as the length spec" do
77
+ expect_error("100:true!")
78
+ end
79
+ end
80
+
81
+ context "null" do
82
+ it "requires the length to be zero" do
83
+ expect_error("1:x~")
84
+ end
85
+ end
86
+
87
+ context "bool" do
88
+ it "returns false given anything but true" do
89
+ TNETS.load("4:trub!").should == false
90
+ TNETS.load("6:sdflkj!").should == false
91
+ TNETS.load("4:true!").should == true
92
+ end
93
+ end
94
+
95
+ context "number" do
96
+ it "requires digits only" do
97
+ expect_error("3:1bc#")
98
+ end
99
+ end
100
+
101
+ context "array" do
102
+ it "requires sane lengths" do
103
+ expect_error("5:10:abcde,]")
104
+ end
105
+ end
106
+
107
+ context "dict" do
108
+ it "requires sane lengths" do
109
+ expect_error("13:1:a,10:abcde,}")
110
+ end
111
+
112
+ it "requires keys to be strings" do
113
+ # { 1 => 2 }
114
+ expect_error("8:1:1#1:2#}")
115
+ # { nil => 2 }
116
+ expect_error("7:0:~1:2#}")
117
+ # { false => 2 }
118
+ expect_error("7:0:!1:2#}")
119
+ end
120
+
121
+ it "requires a balanced hash" do
122
+ # one item only
123
+ # { :a => }
124
+ expect_error("4:1:,a}")
125
+
126
+ # three items
127
+ # { :a => 1, :b => }
128
+ expect_error("12:1:a,1:1#1:b,}")
129
+ end
130
+ end
131
+ end
132
+ end
133
+
134
+ context "dumping" do
135
+ it "dumps a string" do
136
+ "abc".to_tnets.should == "3:abc,"
137
+ TNETS.load(TNETS.dump("abc")).should == "abc"
138
+ end
139
+
140
+ it "dumps a number" do
141
+ 34.to_tnets.should == "2:34#"
142
+ TNETS.load(TNETS.dump(34)).should == 34
143
+ end
144
+
145
+ it "dumps nil" do
146
+ nil.to_tnets.should == '0:~'
147
+ TNETS.load(TNETS.dump(nil)).should == nil
148
+ end
149
+
150
+ it "dumps true and false" do
151
+ true.to_tnets.should == '4:true!'
152
+ false.to_tnets.should == '5:false!'
153
+ TNETS.load(TNETS.dump(true)).should == true
154
+ TNETS.load(TNETS.dump(false)).should == false
155
+ end
156
+
157
+ it "dumps an array" do
158
+ arr = [1, true, "three", nil]
159
+ arr.to_tnets.should == '22:1:1#4:true!5:three,0:~]'
160
+ TNETS.load(TNETS.dump(arr)).should == arr
161
+ end
162
+
163
+ it "streams an enumerable" do
164
+ sio = StringIO.new
165
+ enum = Class.new do
166
+ include Enumerable
167
+ def each(&b)
168
+ [1, true, "three", nil].each(&b)
169
+ end
170
+ end.new
171
+
172
+ enum.stream_tnets(sio)
173
+ sio.string.should == enum.map(&:to_tnets).join
174
+ end
175
+
176
+ it "dumps a hash" do
177
+ hsh = { :a => 1, :b => true, :c => "three", :d => nil }
178
+ hsh.to_tnets.should == '38:1:a,1:1#1:b,4:true!1:c,5:three,1:d,0:~}'
179
+ TNETS.load(TNETS.dump(hsh)).should == hsh
180
+ end
181
+ end
182
+ end
metadata ADDED
@@ -0,0 +1,82 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: tnetstrings
3
+ version: !ruby/object:Gem::Version
4
+ prerelease:
5
+ version: 0.0.1
6
+ platform: ruby
7
+ authors:
8
+ - Jay Adkisson
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+
13
+ date: 2011-07-27 00:00:00 -07:00
14
+ default_executable:
15
+ dependencies:
16
+ - !ruby/object:Gem::Dependency
17
+ name: rspec
18
+ prerelease: false
19
+ requirement: &id001 !ruby/object:Gem::Requirement
20
+ none: false
21
+ requirements:
22
+ - - ~>
23
+ - !ruby/object:Gem::Version
24
+ version: "2.0"
25
+ type: :development
26
+ version_requirements: *id001
27
+ - !ruby/object:Gem::Dependency
28
+ name: rake
29
+ prerelease: false
30
+ requirement: &id002 !ruby/object:Gem::Requirement
31
+ none: false
32
+ requirements:
33
+ - - ">="
34
+ - !ruby/object:Gem::Version
35
+ version: "0"
36
+ type: :development
37
+ version_requirements: *id002
38
+ description: A general-purpose tnetstrings library for Ruby
39
+ email: jjmadkisson@gmail.com
40
+ executables: []
41
+
42
+ extensions: []
43
+
44
+ extra_rdoc_files: []
45
+
46
+ files:
47
+ - ext/tnetstrings.c
48
+ - ext/tnetstrings.h
49
+ - ext/tnetstrings.so
50
+ - lib/tnetstrings.rb
51
+ - lib/tnetstrings/version.rb
52
+ - spec/tnetstrings_spec.rb
53
+ has_rdoc: true
54
+ homepage: http://github.com/causes/iplogic
55
+ licenses: []
56
+
57
+ post_install_message:
58
+ rdoc_options: []
59
+
60
+ require_paths:
61
+ - lib
62
+ required_ruby_version: !ruby/object:Gem::Requirement
63
+ none: false
64
+ requirements:
65
+ - - ">="
66
+ - !ruby/object:Gem::Version
67
+ version: "0"
68
+ required_rubygems_version: !ruby/object:Gem::Requirement
69
+ none: false
70
+ requirements:
71
+ - - ">="
72
+ - !ruby/object:Gem::Version
73
+ version: "0"
74
+ requirements: []
75
+
76
+ rubyforge_project:
77
+ rubygems_version: 1.5.2
78
+ signing_key:
79
+ specification_version: 3
80
+ summary: A general-purpose tnetstrings library for Ruby
81
+ test_files:
82
+ - spec/tnetstrings_spec.rb