groem 0.0.4
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/.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
|