groem 0.0.4
Sign up to get free protection for your applications and to get access to all the features.
- data/.autotest +24 -0
- data/.gitignore +3 -0
- data/Gemfile +3 -0
- data/HISTORY.markdown +9 -0
- data/README.markdown +185 -0
- data/TODO.markdown +7 -0
- data/groem.gemspec +24 -0
- data/lib/groem.rb +10 -0
- data/lib/groem/app.rb +197 -0
- data/lib/groem/client.rb +169 -0
- data/lib/groem/constants.rb +74 -0
- data/lib/groem/marshal.rb +349 -0
- data/lib/groem/notification.rb +140 -0
- data/lib/groem/response.rb +86 -0
- data/lib/groem/route.rb +37 -0
- data/lib/groem/version.rb +3 -0
- data/spec/functional/app_notify_adhoc_spec.rb +73 -0
- data/spec/functional/app_notify_spec.rb +390 -0
- data/spec/functional/app_register_spec.rb +113 -0
- data/spec/functional/client_spec.rb +361 -0
- data/spec/integration/notify.rb +318 -0
- data/spec/integration/register.rb +133 -0
- data/spec/shared/dummy_server.rb +198 -0
- data/spec/shared/dummy_server_helper.rb +31 -0
- data/spec/shared/marshal_helper.rb +40 -0
- data/spec/spec_helper.rb +14 -0
- data/spec/unit/app_spec.rb +77 -0
- data/spec/unit/marshal_request_spec.rb +380 -0
- data/spec/unit/marshal_response_spec.rb +162 -0
- data/spec/unit/notification_spec.rb +205 -0
- data/spec/unit/response_spec.rb +7 -0
- data/spec/unit/route_spec.rb +93 -0
- metadata +141 -0
data/lib/groem/client.rb
ADDED
@@ -0,0 +1,169 @@
|
|
1
|
+
require 'eventmachine'
|
2
|
+
|
3
|
+
module Groem
|
4
|
+
class Client < EM::Connection
|
5
|
+
include Groem::Constants
|
6
|
+
include EM::Deferrable
|
7
|
+
|
8
|
+
DEFAULT_HOST = 'localhost'
|
9
|
+
DEFAULT_PORT = 23053
|
10
|
+
|
11
|
+
class << self
|
12
|
+
def response_class
|
13
|
+
@response_class ||= anonymous_response_class
|
14
|
+
end
|
15
|
+
|
16
|
+
def response_class=(klass)
|
17
|
+
@response_class = klass
|
18
|
+
end
|
19
|
+
alias_method :load_response_as, :response_class=
|
20
|
+
|
21
|
+
def request(request, host = DEFAULT_HOST, port = DEFAULT_PORT)
|
22
|
+
connection = EM.connect host, port, self, request
|
23
|
+
connection
|
24
|
+
end
|
25
|
+
alias_method :register, :request
|
26
|
+
alias_method :notify, :request
|
27
|
+
|
28
|
+
def anonymous_response_class
|
29
|
+
@klass_resp ||= \
|
30
|
+
Class.new {
|
31
|
+
include(Groem::Marshal::Response)
|
32
|
+
require 'forwardable'
|
33
|
+
extend Forwardable
|
34
|
+
def_delegators :@raw, :[], :[]=
|
35
|
+
def raw; @raw ||= {}; end
|
36
|
+
def initialize(input = {})
|
37
|
+
@raw = input
|
38
|
+
end
|
39
|
+
}
|
40
|
+
end
|
41
|
+
|
42
|
+
def anonymous_request_class
|
43
|
+
@klass_req ||= \
|
44
|
+
Class.new {
|
45
|
+
include(Groem::Marshal::Request)
|
46
|
+
require 'forwardable'
|
47
|
+
extend Forwardable
|
48
|
+
def_delegators :@raw, :[], :[]=
|
49
|
+
def raw; @raw ||= {}; end
|
50
|
+
def initialize(input = {})
|
51
|
+
@raw = input
|
52
|
+
end
|
53
|
+
}
|
54
|
+
end
|
55
|
+
|
56
|
+
end
|
57
|
+
|
58
|
+
def response_class
|
59
|
+
self.class.response_class
|
60
|
+
end
|
61
|
+
|
62
|
+
def when_ok(&blk)
|
63
|
+
@cb_response = blk
|
64
|
+
end
|
65
|
+
|
66
|
+
def when_callback(&blk)
|
67
|
+
@cb_callback = blk
|
68
|
+
end
|
69
|
+
|
70
|
+
# deprecated
|
71
|
+
# def each_ok_response(&blk)
|
72
|
+
# @cb_each_response = blk
|
73
|
+
# end
|
74
|
+
#
|
75
|
+
# def each_callback_response(&blk)
|
76
|
+
# @cb_each_callback = blk
|
77
|
+
# end
|
78
|
+
#
|
79
|
+
# def each_error_response(&blk)
|
80
|
+
# @cb_each_errback = blk
|
81
|
+
# end
|
82
|
+
|
83
|
+
def initialize(req)
|
84
|
+
super
|
85
|
+
@req = req
|
86
|
+
@req_action = req[ENVIRONMENT_KEY][GNTP_REQUEST_METHOD_KEY]
|
87
|
+
cb_context = req[HEADERS_KEY][GNTP_NOTIFICATION_CALLBACK_CONTEXT_KEY]
|
88
|
+
cb_context_type = req[HEADERS_KEY][GNTP_NOTIFICATION_CALLBACK_CONTEXT_TYPE_KEY]
|
89
|
+
cb_target = req[HEADERS_KEY][GNTP_NOTIFICATION_CALLBACK_TARGET_KEY]
|
90
|
+
@wait_for_callback = @req_action == GNTP_NOTIFY_METHOD &&
|
91
|
+
cb_context && cb_context_type && !cb_target
|
92
|
+
end
|
93
|
+
|
94
|
+
def post_init
|
95
|
+
reset_state!
|
96
|
+
#puts @req.dump.inspect
|
97
|
+
send_data @req.dump
|
98
|
+
end
|
99
|
+
|
100
|
+
def receive_data data
|
101
|
+
@buffer.extract(data).each do |line|
|
102
|
+
#print "#{line.inspect}"
|
103
|
+
@lines << line
|
104
|
+
end
|
105
|
+
if eof?
|
106
|
+
receive_message @lines.join("\r\n") + "\r\n"
|
107
|
+
reset_message_buffer!
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
protected
|
112
|
+
|
113
|
+
def reset_state!
|
114
|
+
@state = :init
|
115
|
+
reset_message_buffer!
|
116
|
+
end
|
117
|
+
|
118
|
+
def reset_message_buffer!
|
119
|
+
@buffer = BufferedTokenizer.new("\r\n")
|
120
|
+
@lines = []
|
121
|
+
end
|
122
|
+
|
123
|
+
def eof?
|
124
|
+
@lines[-1] == ''
|
125
|
+
end
|
126
|
+
|
127
|
+
def receive_message(message)
|
128
|
+
raw = response_class.load(message, nil)
|
129
|
+
update_state_from_response!(raw)
|
130
|
+
#puts "Client received message, state = #{@state}"
|
131
|
+
resp = response_class.new(raw)
|
132
|
+
case @state
|
133
|
+
when :ok
|
134
|
+
@cb_response.call(resp) if @cb_response
|
135
|
+
self.succeed(resp) unless waiting_for_callback?
|
136
|
+
when :callback
|
137
|
+
@cb_callback.call(resp) if @cb_callback
|
138
|
+
self.succeed(resp)
|
139
|
+
when :error, :unknown
|
140
|
+
#@cb_response.call(resp) if @cb_response
|
141
|
+
self.fail(resp)
|
142
|
+
end
|
143
|
+
#puts "Waiting for callback? #{waiting_for_callback? ? 'yes' : 'no'}"
|
144
|
+
close_connection_after_writing unless waiting_for_callback?
|
145
|
+
end
|
146
|
+
|
147
|
+
def update_state_from_response!(resp)
|
148
|
+
@state = \
|
149
|
+
case resp[0].to_i
|
150
|
+
when 0
|
151
|
+
if resp[2][GNTP_NOTIFICATION_CALLBACK_RESULT_KEY]
|
152
|
+
:callback
|
153
|
+
else
|
154
|
+
:ok
|
155
|
+
end
|
156
|
+
when 100..500
|
157
|
+
:error
|
158
|
+
else
|
159
|
+
:unknown
|
160
|
+
end
|
161
|
+
end
|
162
|
+
|
163
|
+
def waiting_for_callback?
|
164
|
+
@wait_for_callback && [:ok, :init].include?(@state)
|
165
|
+
end
|
166
|
+
|
167
|
+
end
|
168
|
+
|
169
|
+
end
|
@@ -0,0 +1,74 @@
|
|
1
|
+
module Groem
|
2
|
+
|
3
|
+
module Constants
|
4
|
+
|
5
|
+
def self.included(mod)
|
6
|
+
self.constants.each do |c|
|
7
|
+
mod.const_set(c.to_s, self.const_get(c.to_s))
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
def growlify_key(str)
|
12
|
+
parts = str.to_s.tr('_','-').split('-')
|
13
|
+
parts.map {|p| p[0..0].upcase + p[1..-1]}.join('-')
|
14
|
+
end
|
15
|
+
|
16
|
+
def growlify_action(str)
|
17
|
+
act = str.to_s.upcase
|
18
|
+
act = {'CLICK' => GNTP_CLICK_CALLBACK_RESULT,
|
19
|
+
'CLICKED' => GNTP_CLICK_CALLBACK_RESULT,
|
20
|
+
'CLOSE' => GNTP_CLOSE_CALLBACK_RESULT,
|
21
|
+
'CLOSED' => GNTP_CLOSE_CALLBACK_RESULT,
|
22
|
+
'TIMEOUT' => GNTP_TIMEDOUT_CALLBACK_RESULT,
|
23
|
+
'TIMEDOUT' => GNTP_TIMEDOUT_CALLBACK_RESULT
|
24
|
+
}[act]
|
25
|
+
end
|
26
|
+
|
27
|
+
ENVIRONMENT_KEY = 'environment'
|
28
|
+
HEADERS_KEY = 'headers'
|
29
|
+
NOTIFICATIONS_KEY = 'notifications'
|
30
|
+
|
31
|
+
GNTP_PROTOCOL_KEY = 'protocol'
|
32
|
+
GNTP_VERSION_KEY = 'version'
|
33
|
+
GNTP_REQUEST_METHOD_KEY = 'request_method'
|
34
|
+
GNTP_ENCRYPTION_ID_KEY = 'encryption_id'
|
35
|
+
|
36
|
+
GNTP_REGISTER_METHOD = 'REGISTER'
|
37
|
+
GNTP_NOTIFY_METHOD = 'NOTIFY'
|
38
|
+
GNTP_SUBSCRIBE_METHOD = 'SUBSCRIBE'
|
39
|
+
|
40
|
+
GNTP_DEFAULT_ENVIRONMENT = {GNTP_PROTOCOL_KEY => 'GNTP',
|
41
|
+
GNTP_VERSION_KEY => '1.0',
|
42
|
+
GNTP_REQUEST_METHOD_KEY => 'NOTIFY',
|
43
|
+
GNTP_ENCRYPTION_ID_KEY => 'NONE'
|
44
|
+
}
|
45
|
+
|
46
|
+
GNTP_APPLICATION_NAME_KEY = 'Application-Name'
|
47
|
+
GNTP_APPLICATION_ICON_KEY = 'Application-Icon'
|
48
|
+
GNTP_NOTIFICATION_COUNT_KEY = 'Notifications-Count'
|
49
|
+
GNTP_NOTIFICATION_NAME_KEY = 'Notification-Name'
|
50
|
+
GNTP_NOTIFICATION_ICON_KEY = 'Notification-Icon'
|
51
|
+
GNTP_NOTIFICATION_ID_KEY = 'Notification-ID'
|
52
|
+
GNTP_NOTIFICATION_CALLBACK_CONTEXT_KEY = 'Notification-Callback-Context'
|
53
|
+
GNTP_NOTIFICATION_CALLBACK_CONTEXT_TYPE_KEY = 'Notification-Callback-Context-Type'
|
54
|
+
GNTP_NOTIFICATION_CALLBACK_TARGET_KEY = 'Notification-Callback-Target'
|
55
|
+
|
56
|
+
GNTP_RESPONSE_METHOD_KEY = 'response_method'
|
57
|
+
GNTP_RESPONSE_ACTION_KEY = 'Response-Action'
|
58
|
+
GNTP_ERROR_CODE_KEY = 'Error-Code'
|
59
|
+
GNTP_NOTIFICATION_CALLBACK_RESULT_KEY = 'Notification-Callback-Result'
|
60
|
+
GNTP_NOTIFICATION_CALLBACK_TIMESTAMP_KEY = 'Notification-Callback-Timestamp'
|
61
|
+
|
62
|
+
GNTP_OK_RESPONSE = '-OK'
|
63
|
+
GNTP_ERROR_RESPONSE = '-ERROR'
|
64
|
+
GNTP_CALLBACK_RESPONSE = '-CALLBACK'
|
65
|
+
|
66
|
+
GNTP_ERROR_CODE_OK = '0'
|
67
|
+
|
68
|
+
GNTP_CLICK_CALLBACK_RESULT = 'CLICK'
|
69
|
+
GNTP_CLOSE_CALLBACK_RESULT = 'CLOSE'
|
70
|
+
GNTP_TIMEDOUT_CALLBACK_RESULT = 'TIMEDOUT'
|
71
|
+
|
72
|
+
end
|
73
|
+
|
74
|
+
end
|
@@ -0,0 +1,349 @@
|
|
1
|
+
require 'strscan'
|
2
|
+
|
3
|
+
module Groem
|
4
|
+
|
5
|
+
module Marshal
|
6
|
+
module Request
|
7
|
+
include Groem::Constants
|
8
|
+
|
9
|
+
def self.included(mod)
|
10
|
+
mod.extend ClassMethods
|
11
|
+
end
|
12
|
+
|
13
|
+
# write GNTP request string, print lines as \r\n
|
14
|
+
# assumes that including class delegates :[] to raw hash
|
15
|
+
# (described below under load).
|
16
|
+
# TODO: calculate UUIDs for binary sections and output them
|
17
|
+
def dump
|
18
|
+
out = []
|
19
|
+
env = GNTP_DEFAULT_ENVIRONMENT.merge(self[ENVIRONMENT_KEY])
|
20
|
+
hdrs = self[HEADERS_KEY]
|
21
|
+
notifs = self[NOTIFICATIONS_KEY]
|
22
|
+
|
23
|
+
out << "#{env[(GNTP_PROTOCOL_KEY)]}" +
|
24
|
+
"/#{env[(GNTP_VERSION_KEY)]} "+
|
25
|
+
"#{env[(GNTP_REQUEST_METHOD_KEY)]} "+
|
26
|
+
"#{env[(GNTP_ENCRYPTION_ID_KEY)]}"
|
27
|
+
hdrs.each_pair do |k, v|
|
28
|
+
unless v.nil?
|
29
|
+
out << "#{(k)}: #{v}"
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
if env[(GNTP_REQUEST_METHOD_KEY)] == GNTP_REGISTER_METHOD
|
34
|
+
out << "#{GNTP_NOTIFICATION_COUNT_KEY}: #{notifs.keys.count}"
|
35
|
+
out << nil
|
36
|
+
notifs.each_pair do |name, pairs|
|
37
|
+
out << "#{GNTP_NOTIFICATION_NAME_KEY}: #{name}"
|
38
|
+
pairs.each do |pair|
|
39
|
+
unless pair[1].nil?
|
40
|
+
out << "#{(pair[0])}: #{pair[1]}"
|
41
|
+
end
|
42
|
+
end
|
43
|
+
out << nil
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
if out.last.nil?
|
48
|
+
1.times { out << nil }
|
49
|
+
else
|
50
|
+
2.times { out << nil }
|
51
|
+
end
|
52
|
+
|
53
|
+
out.join("\r\n")
|
54
|
+
end
|
55
|
+
|
56
|
+
|
57
|
+
module ClassMethods
|
58
|
+
include Groem::Constants
|
59
|
+
|
60
|
+
#
|
61
|
+
# Load GNTP request into hash of:
|
62
|
+
# 'environment' => hash of environment (protocol, version, request_method, encryption data)
|
63
|
+
# 'headers' => hash of headers
|
64
|
+
# 'notifications' => hash of notifications keyed by name (REGISTER requests only, otherwise empty)
|
65
|
+
#
|
66
|
+
# Note that binary identifiers are resolved in both headers and notifications.
|
67
|
+
#
|
68
|
+
# If passed an optional klass, will return klass.new(out), otherwise just the hash.
|
69
|
+
# By default it tries to use the including object's class
|
70
|
+
#
|
71
|
+
# Note entire GNTP message must be passed as input.
|
72
|
+
#
|
73
|
+
# No semantic validation of input is done,
|
74
|
+
# and all key values are stored as strings, not casted
|
75
|
+
#
|
76
|
+
# Syntactic validation may be implemented in the future.
|
77
|
+
#
|
78
|
+
def load(input, klass = self)
|
79
|
+
env, hdrs, notifs = {}, {}, {}
|
80
|
+
meth, notif_name, id, len, bin = nil
|
81
|
+
section = :init
|
82
|
+
s = StringScanner.new(input)
|
83
|
+
until s.eos?
|
84
|
+
line, section = scan_line(s, meth, section)
|
85
|
+
case section
|
86
|
+
when :first
|
87
|
+
parse_first_header(line, env)
|
88
|
+
meth = env[(GNTP_REQUEST_METHOD_KEY)]
|
89
|
+
when :headers
|
90
|
+
parse_header(line, hdrs)
|
91
|
+
when :notification_start
|
92
|
+
notif_name = parse_notification_name(line)
|
93
|
+
when :notification
|
94
|
+
parse_notification_header(line, notif_name, notifs)
|
95
|
+
when :identifier_start
|
96
|
+
id = parse_identifier(line)
|
97
|
+
when :identifier_length
|
98
|
+
len = parse_identifier_length(line)
|
99
|
+
when :binary
|
100
|
+
bin = \
|
101
|
+
(1..len).inject('') do |memo, i|
|
102
|
+
memo << s.getch; memo
|
103
|
+
end
|
104
|
+
resolve_binary_key(id, bin, hdrs)
|
105
|
+
resolve_binary_key(id, bin, notifs)
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
out = { ENVIRONMENT_KEY => env,
|
110
|
+
HEADERS_KEY => hdrs,
|
111
|
+
NOTIFICATIONS_KEY => notifs
|
112
|
+
}
|
113
|
+
|
114
|
+
klass ? klass.new(out) : out
|
115
|
+
end
|
116
|
+
|
117
|
+
protected
|
118
|
+
|
119
|
+
def scan_line(scanner, method, state)
|
120
|
+
line = nil
|
121
|
+
new_state = state
|
122
|
+
case state
|
123
|
+
when :init
|
124
|
+
line = scanner.scan(/.*\n/)
|
125
|
+
new_state = :first
|
126
|
+
when :first
|
127
|
+
line = scanner.scan(/.*\n/)
|
128
|
+
new_state = :headers
|
129
|
+
when :headers
|
130
|
+
line = scanner.scan(/.*\n/)
|
131
|
+
new_state = if line =~ /^\w*identifier\w*:/i
|
132
|
+
:identifier_start
|
133
|
+
elsif method == GNTP_REGISTER_METHOD && \
|
134
|
+
line =~ /^\s*#{GNTP_NOTIFICATION_NAME_KEY}\s*:/i
|
135
|
+
:notification_start
|
136
|
+
else
|
137
|
+
:headers
|
138
|
+
end
|
139
|
+
when :notification_start
|
140
|
+
line = scanner.scan(/.*\n/)
|
141
|
+
new_state = :notification
|
142
|
+
when :notification
|
143
|
+
line = scanner.scan(/.*\n/)
|
144
|
+
new_state = if line =~ /^\s*identifier\s*:/i
|
145
|
+
:identifier_start
|
146
|
+
elsif method == GNTP_REGISTER_METHOD && \
|
147
|
+
line =~ /^\s*#{GNTP_NOTIFICATION_NAME_KEY}\s*:/i
|
148
|
+
:notification_start
|
149
|
+
else
|
150
|
+
:notification
|
151
|
+
end
|
152
|
+
when :identifier_start
|
153
|
+
line = scanner.scan(/.*\n/)
|
154
|
+
new_state = :identifier_length if line =~ /^\s*length\s*:/i
|
155
|
+
when :identifier_length
|
156
|
+
new_state = :binary
|
157
|
+
when :binary
|
158
|
+
line = scanner.scan(/.*\n/)
|
159
|
+
new_state = if line =~ /^\s*identifier\s*:/i
|
160
|
+
:identifier_start
|
161
|
+
elsif method == GNTP_REGISTER_METHOD && \
|
162
|
+
line =~ /^\s*#{GNTP_NOTIFICATION_NAME_KEY}\s*:/i
|
163
|
+
:notification_start
|
164
|
+
else
|
165
|
+
:headers
|
166
|
+
end
|
167
|
+
end
|
168
|
+
#puts "state #{state} --> #{new_state}"
|
169
|
+
state = new_state
|
170
|
+
line = line.chomp if line
|
171
|
+
[line, state]
|
172
|
+
end
|
173
|
+
|
174
|
+
def parse_first_header(line, hash)
|
175
|
+
return hash unless line && line.size > 0
|
176
|
+
tokens = line.split(' ')
|
177
|
+
proto, vers = tokens[0].split('/')
|
178
|
+
msgtype = tokens[1]
|
179
|
+
encrypid, ivvalue = if tokens[2]; tokens[2].split(':'); end
|
180
|
+
keyhashid = if tokens[3]; tokens[3].split(':')[0]; end
|
181
|
+
keyhash, salt = if tokens[3] && tokens[3].split(':')[1]
|
182
|
+
tokens[3].split(':')[1].split('.')
|
183
|
+
end
|
184
|
+
hash[(GNTP_PROTOCOL_KEY)] = proto
|
185
|
+
hash[(GNTP_VERSION_KEY)] = vers
|
186
|
+
hash[(GNTP_REQUEST_METHOD_KEY)] = msgtype
|
187
|
+
hash[(GNTP_ENCRYPTION_ID_KEY)] = encrypid
|
188
|
+
# TODO the rest
|
189
|
+
hash
|
190
|
+
end
|
191
|
+
|
192
|
+
def parse_header(line, hash)
|
193
|
+
return hash unless line && line.size > 0
|
194
|
+
key, val = line.split(':', 2).map {|t| t.strip }
|
195
|
+
key = (key)
|
196
|
+
hash[key] = val
|
197
|
+
hash
|
198
|
+
end
|
199
|
+
|
200
|
+
def parse_notification_name(line)
|
201
|
+
return nil unless line && line.size > 0
|
202
|
+
key, val = line.split(':', 2).map {|t| t.strip }
|
203
|
+
val if key.downcase == GNTP_NOTIFICATION_NAME_KEY.downcase
|
204
|
+
end
|
205
|
+
|
206
|
+
def parse_notification_header(line, name, hash)
|
207
|
+
return hash unless line && line.size > 0
|
208
|
+
key, val = line.split(':', 2).map {|t| t.strip }
|
209
|
+
key = (key)
|
210
|
+
(hash[name] ||= {})[key] = val
|
211
|
+
hash
|
212
|
+
end
|
213
|
+
|
214
|
+
def parse_identifier(line)
|
215
|
+
return nil unless line && line.size > 0
|
216
|
+
key, val = line.split(':', 2).map {|t| t.strip }
|
217
|
+
val if key.downcase == 'identifier'
|
218
|
+
end
|
219
|
+
|
220
|
+
def parse_identifier_length(line)
|
221
|
+
return nil unless line && line.size > 0
|
222
|
+
key, val = line.split(':', 2).map {|t| t.strip }
|
223
|
+
val.to_i if key.downcase == 'length'
|
224
|
+
end
|
225
|
+
|
226
|
+
def resolve_binary_key(key, data, hash)
|
227
|
+
if key && \
|
228
|
+
pairs = hash.select do |k, v|
|
229
|
+
v =~ /x-growl-resource:\/\/#{Regexp.escape(key)}/i
|
230
|
+
end
|
231
|
+
pairs.each { |p| hash[p[0]] = data }
|
232
|
+
end
|
233
|
+
hash
|
234
|
+
end
|
235
|
+
|
236
|
+
end # GNTP::Marshal::Request::ClassMethods
|
237
|
+
|
238
|
+
end # GNTP::Marshal::Request
|
239
|
+
|
240
|
+
|
241
|
+
module Response
|
242
|
+
include Groem::Constants
|
243
|
+
|
244
|
+
def self.included(mod)
|
245
|
+
mod.extend ClassMethods
|
246
|
+
end
|
247
|
+
|
248
|
+
# write GNTP request string, print lines as \r\n
|
249
|
+
# assumes that including class delegates :[] to raw hash
|
250
|
+
# (described below under load).
|
251
|
+
def dump
|
252
|
+
#TODO
|
253
|
+
end
|
254
|
+
|
255
|
+
module ClassMethods
|
256
|
+
include Groem::Constants
|
257
|
+
|
258
|
+
# Load GNTP response into array of:
|
259
|
+
# status (error code or '0' for OK)
|
260
|
+
# hash of headers (except error code and notification-callback-*)
|
261
|
+
# hash of callback headers (for callback responses, otherwise {})
|
262
|
+
#
|
263
|
+
# Note this is explicitly modeled after Rack's interface
|
264
|
+
#
|
265
|
+
def load(input, klass = self)
|
266
|
+
env, hdrs, cb_hdrs = {}, {}, {}
|
267
|
+
status, meth = nil
|
268
|
+
section = :init
|
269
|
+
s = StringScanner.new(input)
|
270
|
+
until s.eos?
|
271
|
+
line, section = scan_line(s, meth, section)
|
272
|
+
case section
|
273
|
+
when :first
|
274
|
+
parse_first_header(line, env)
|
275
|
+
when :headers
|
276
|
+
parse_header(line, hdrs)
|
277
|
+
end
|
278
|
+
end
|
279
|
+
|
280
|
+
# pull out status from headers
|
281
|
+
status = hdrs.delete(GNTP_ERROR_CODE_KEY)
|
282
|
+
status ||= GNTP_ERROR_CODE_OK
|
283
|
+
|
284
|
+
# pull out notification-callback-* from headers
|
285
|
+
[GNTP_NOTIFICATION_CALLBACK_CONTEXT_KEY,
|
286
|
+
GNTP_NOTIFICATION_CALLBACK_CONTEXT_TYPE_KEY,
|
287
|
+
GNTP_NOTIFICATION_CALLBACK_RESULT_KEY,
|
288
|
+
GNTP_NOTIFICATION_CALLBACK_TIMESTAMP_KEY].each do |key|
|
289
|
+
if val = hdrs.delete(key)
|
290
|
+
cb_hdrs[key] = val
|
291
|
+
end
|
292
|
+
end
|
293
|
+
|
294
|
+
out = [ status, hdrs, cb_hdrs ]
|
295
|
+
|
296
|
+
klass ? klass.new(out) : out
|
297
|
+
end
|
298
|
+
|
299
|
+
protected
|
300
|
+
|
301
|
+
def scan_line(scanner, method, state)
|
302
|
+
line = nil
|
303
|
+
new_state = state
|
304
|
+
case state
|
305
|
+
when :init
|
306
|
+
line = scanner.scan(/.*\n/)
|
307
|
+
new_state = :first
|
308
|
+
when :first
|
309
|
+
line = scanner.scan(/.*\n/)
|
310
|
+
new_state = :headers
|
311
|
+
when :headers
|
312
|
+
line = scanner.scan(/.*\n/)
|
313
|
+
new_state = :headers
|
314
|
+
end
|
315
|
+
#puts "state #{state} --> #{new_state}"
|
316
|
+
state = new_state
|
317
|
+
line = line.chomp if line
|
318
|
+
[line, state]
|
319
|
+
end
|
320
|
+
|
321
|
+
def parse_first_header(line, hash)
|
322
|
+
return hash unless line && line.size > 0
|
323
|
+
tokens = line.split(' ')
|
324
|
+
proto, vers = tokens[0].split('/')
|
325
|
+
msgtype = tokens[1]
|
326
|
+
encrypid = tokens[2]
|
327
|
+
hash[(GNTP_PROTOCOL_KEY)] = proto
|
328
|
+
hash[(GNTP_VERSION_KEY)] = vers
|
329
|
+
hash[(GNTP_RESPONSE_METHOD_KEY)] = msgtype
|
330
|
+
hash[(GNTP_ENCRYPTION_ID_KEY)] = encrypid
|
331
|
+
hash
|
332
|
+
end
|
333
|
+
|
334
|
+
def parse_header(line, hash)
|
335
|
+
return hash unless line && line.size > 0
|
336
|
+
key, val = line.split(':', 2).map {|t| t.strip }
|
337
|
+
key = (key)
|
338
|
+
hash[key] = val
|
339
|
+
hash
|
340
|
+
end
|
341
|
+
|
342
|
+
end
|
343
|
+
|
344
|
+
end # GNTP::Marshal::Response
|
345
|
+
|
346
|
+
|
347
|
+
end # GNTP::Marshal
|
348
|
+
|
349
|
+
end
|