tnetstrings 0.0.1

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/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