nyara 0.0.1.pre
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/example/design.rb +62 -0
- data/example/fib.rb +15 -0
- data/example/hello.rb +5 -0
- data/example/stream.rb +10 -0
- data/ext/accept.c +133 -0
- data/ext/event.c +89 -0
- data/ext/extconf.rb +34 -0
- data/ext/hashes.c +130 -0
- data/ext/http-parser/AUTHORS +41 -0
- data/ext/http-parser/CONTRIBUTIONS +4 -0
- data/ext/http-parser/LICENSE-MIT +23 -0
- data/ext/http-parser/contrib/parsertrace.c +156 -0
- data/ext/http-parser/contrib/url_parser.c +44 -0
- data/ext/http-parser/http_parser.c +2175 -0
- data/ext/http-parser/http_parser.h +304 -0
- data/ext/http-parser/test.c +3425 -0
- data/ext/http_parser.c +1 -0
- data/ext/inc/epoll.h +60 -0
- data/ext/inc/kqueue.h +77 -0
- data/ext/inc/status_codes.inc +64 -0
- data/ext/inc/str_intern.h +66 -0
- data/ext/inc/version.inc +1 -0
- data/ext/mime.c +107 -0
- data/ext/multipart-parser-c/README.md +18 -0
- data/ext/multipart-parser-c/multipart_parser.c +309 -0
- data/ext/multipart-parser-c/multipart_parser.h +48 -0
- data/ext/multipart_parser.c +1 -0
- data/ext/nyara.c +56 -0
- data/ext/nyara.h +59 -0
- data/ext/request.c +474 -0
- data/ext/route.cc +325 -0
- data/ext/url_encoded.c +304 -0
- data/hello.rb +5 -0
- data/lib/nyara/config.rb +64 -0
- data/lib/nyara/config_hash.rb +51 -0
- data/lib/nyara/controller.rb +336 -0
- data/lib/nyara/cookie.rb +31 -0
- data/lib/nyara/cpu_counter.rb +65 -0
- data/lib/nyara/header_hash.rb +18 -0
- data/lib/nyara/mime_types.rb +612 -0
- data/lib/nyara/nyara.rb +82 -0
- data/lib/nyara/param_hash.rb +5 -0
- data/lib/nyara/request.rb +144 -0
- data/lib/nyara/route.rb +138 -0
- data/lib/nyara/route_entry.rb +43 -0
- data/lib/nyara/session.rb +104 -0
- data/lib/nyara/view.rb +317 -0
- data/lib/nyara.rb +25 -0
- data/nyara.gemspec +20 -0
- data/rakefile +91 -0
- data/readme.md +35 -0
- data/spec/ext_mime_match_spec.rb +27 -0
- data/spec/ext_parse_accept_value_spec.rb +29 -0
- data/spec/ext_parse_spec.rb +138 -0
- data/spec/ext_route_spec.rb +70 -0
- data/spec/hashes_spec.rb +71 -0
- data/spec/path_helper_spec.rb +77 -0
- data/spec/request_delegate_spec.rb +67 -0
- data/spec/request_spec.rb +56 -0
- data/spec/route_entry_spec.rb +12 -0
- data/spec/route_spec.rb +84 -0
- data/spec/session_spec.rb +66 -0
- data/spec/spec_helper.rb +52 -0
- data/spec/view_spec.rb +87 -0
- data/tools/bench-cookie.rb +22 -0
- metadata +111 -0
@@ -0,0 +1,51 @@
|
|
1
|
+
module Nyara
|
2
|
+
class ConfigHash
|
3
|
+
alias _aref []
|
4
|
+
alias _aset []=
|
5
|
+
|
6
|
+
# so you can find with chained keys
|
7
|
+
def [] *keys
|
8
|
+
h = self
|
9
|
+
keys.each do |key|
|
10
|
+
if h.has_key?(key)
|
11
|
+
if h.is_a?(ConfigHash)
|
12
|
+
h = h._aref key
|
13
|
+
else
|
14
|
+
h = h[key]
|
15
|
+
end
|
16
|
+
else
|
17
|
+
return nil # todo default value?
|
18
|
+
end
|
19
|
+
end
|
20
|
+
h
|
21
|
+
end
|
22
|
+
|
23
|
+
# so you can write:
|
24
|
+
# config['a', 'very', 'deep', 'key'] = 'value
|
25
|
+
def []= *keys, last_key, value
|
26
|
+
h = self
|
27
|
+
keys.each do |key|
|
28
|
+
if h.has_key?(key)
|
29
|
+
if h.is_a?(ConfigHash)
|
30
|
+
h = h._aref key
|
31
|
+
else
|
32
|
+
h = h[key]
|
33
|
+
end
|
34
|
+
else
|
35
|
+
new_h = ConfigHash.new
|
36
|
+
if h.is_a?(ConfigHash)
|
37
|
+
h._aset key, new_h
|
38
|
+
else
|
39
|
+
h[key] = new_h
|
40
|
+
end
|
41
|
+
h = new_h
|
42
|
+
end
|
43
|
+
end
|
44
|
+
if h.is_a?(ConfigHash)
|
45
|
+
h._aset last_key, value
|
46
|
+
else
|
47
|
+
h[last_key] = value
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,336 @@
|
|
1
|
+
module Nyara
|
2
|
+
# Contain render methods
|
3
|
+
module Renderable
|
4
|
+
end
|
5
|
+
|
6
|
+
Controller = Struct.new :request
|
7
|
+
class Controller
|
8
|
+
module ClassMethods
|
9
|
+
# Connect HTTP +method+, +path+ with +blk+ action
|
10
|
+
def http method, path, &blk
|
11
|
+
@route_entries ||= []
|
12
|
+
@used_ids = {}
|
13
|
+
|
14
|
+
action = RouteEntry.new
|
15
|
+
action.http_method = HTTP_METHODS[method]
|
16
|
+
action.path = path
|
17
|
+
action.set_accept_exts @accept
|
18
|
+
action.id = @curr_id.to_sym if @curr_id
|
19
|
+
action.blk = blk
|
20
|
+
@route_entries << action
|
21
|
+
|
22
|
+
if @curr_id
|
23
|
+
raise ArgumentError, "action id #{@curr_id} already in use" if @used_ids[@curr_id]
|
24
|
+
@used_ids[@curr_id] = true
|
25
|
+
@curr_id = nil
|
26
|
+
@meta_exist = nil
|
27
|
+
end
|
28
|
+
@accept = nil
|
29
|
+
end
|
30
|
+
|
31
|
+
# Set meta data for next action
|
32
|
+
def meta tag=nil, opts=nil
|
33
|
+
if @meta_exist
|
34
|
+
raise 'contiguous meta data descriptors, should follow by an action'
|
35
|
+
end
|
36
|
+
if tag.nil? and opts.nil?
|
37
|
+
raise ArgumentError, 'expect tag or options'
|
38
|
+
end
|
39
|
+
|
40
|
+
if opts.nil? and tag.is_a?(Hash)
|
41
|
+
opts = tag
|
42
|
+
tag = nil
|
43
|
+
end
|
44
|
+
|
45
|
+
if tag
|
46
|
+
# todo scan class
|
47
|
+
id = tag[/\#\w++(\-\w++)*/]
|
48
|
+
@curr_id = id
|
49
|
+
end
|
50
|
+
|
51
|
+
if opts
|
52
|
+
# todo add opts: strong param, etag, cache-control
|
53
|
+
@accept = opts[:accept]
|
54
|
+
end
|
55
|
+
|
56
|
+
@meta_exist = true
|
57
|
+
end
|
58
|
+
|
59
|
+
# HTTP GET
|
60
|
+
def get path, &blk
|
61
|
+
http 'GET', path, &blk
|
62
|
+
end
|
63
|
+
|
64
|
+
# HTTP POST
|
65
|
+
def post path, &blk
|
66
|
+
http 'POST', path, &blk
|
67
|
+
end
|
68
|
+
|
69
|
+
# HTTP PUT
|
70
|
+
def put path, &blk
|
71
|
+
http 'PUT', path, &blk
|
72
|
+
end
|
73
|
+
|
74
|
+
# HTTP DELETE
|
75
|
+
def delete path, &blk
|
76
|
+
http 'DELETE', path, &blk
|
77
|
+
end
|
78
|
+
|
79
|
+
# HTTP PATCH
|
80
|
+
def patch path, &blk
|
81
|
+
http 'PATCH', path, &blk
|
82
|
+
end
|
83
|
+
|
84
|
+
# HTTP OPTIONS
|
85
|
+
# todo generate options response for a url
|
86
|
+
# see http://tools.ietf.org/html/rfc5789
|
87
|
+
def options path, &blk
|
88
|
+
http 'OPTIONS', path, &blk
|
89
|
+
end
|
90
|
+
|
91
|
+
# ---
|
92
|
+
# todo http method: trace ?
|
93
|
+
# +++
|
94
|
+
|
95
|
+
# Set default layout
|
96
|
+
def layout l
|
97
|
+
@default_layout = l
|
98
|
+
end
|
99
|
+
attr_reader :default_layout
|
100
|
+
|
101
|
+
# Set controller name, so you can use a shorter name to reference the controller in path helper
|
102
|
+
def set_name n
|
103
|
+
@controller_name = n
|
104
|
+
end
|
105
|
+
attr_reader :controller_name
|
106
|
+
|
107
|
+
# :nodoc:
|
108
|
+
def preprocess_actions
|
109
|
+
raise "#{self}: no action defined" unless @route_entries
|
110
|
+
|
111
|
+
curr_id = :'#0'
|
112
|
+
next_id = proc{
|
113
|
+
while @used_ids[curr_id]
|
114
|
+
curr_id = curr_id.succ
|
115
|
+
end
|
116
|
+
@used_ids[curr_id] = true
|
117
|
+
curr_id
|
118
|
+
}
|
119
|
+
next_id[]
|
120
|
+
|
121
|
+
@route_entries.each do |e|
|
122
|
+
e.id ||= next_id[]
|
123
|
+
define_method e.id, &e.blk
|
124
|
+
end
|
125
|
+
@route_entries
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
include Renderable
|
130
|
+
|
131
|
+
# :nodoc:
|
132
|
+
def self.inherited klass
|
133
|
+
# klass will also have this inherited method
|
134
|
+
# todo check class name
|
135
|
+
klass.extend ClassMethods
|
136
|
+
[:@route_entries, :@usred_ids, :@default_layout].each do |iv|
|
137
|
+
klass.instance_variable_set iv, klass.superclass.instance_variable_get(iv)
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
# Path helper
|
142
|
+
def path_for id, *args
|
143
|
+
if args.last.is_a?(Hash)
|
144
|
+
opts = args.pop
|
145
|
+
end
|
146
|
+
|
147
|
+
r = Route.path_template(self.class, id) % args
|
148
|
+
|
149
|
+
if opts
|
150
|
+
r << ".#{opts[:format]}" if opts[:format]
|
151
|
+
query = opts.map do |k, v|
|
152
|
+
next if k == :format
|
153
|
+
"#{CGI.escape k.to_s}=#{CGI.escape v}"
|
154
|
+
end
|
155
|
+
query.compact!
|
156
|
+
r << '?' << query.join('&') unless query.empty?
|
157
|
+
end
|
158
|
+
r
|
159
|
+
end
|
160
|
+
|
161
|
+
# Url helper
|
162
|
+
# NOTE: host can include port
|
163
|
+
def url_for id, *args, scheme: nil, host: Config['host'], **opts
|
164
|
+
scheme = scheme ? scheme.sub(/\:?$/, '://') : '//'
|
165
|
+
host ||= 'localhost'
|
166
|
+
path = path_for id, *args, opts
|
167
|
+
scheme << host << path
|
168
|
+
end
|
169
|
+
|
170
|
+
def matched_accept
|
171
|
+
request.matched_accept
|
172
|
+
end
|
173
|
+
|
174
|
+
def header
|
175
|
+
request.header
|
176
|
+
end
|
177
|
+
alias headers header
|
178
|
+
|
179
|
+
def set_header k, v
|
180
|
+
request.response_header[k] = v
|
181
|
+
end
|
182
|
+
|
183
|
+
def add_header_line h
|
184
|
+
raise 'can not modify sent header' if request.response_header.frozen?
|
185
|
+
h = h.sub /(?<![\r\n])\z/, "\r\n"
|
186
|
+
request.response_header_extra_lines << s
|
187
|
+
end
|
188
|
+
|
189
|
+
# todo args helper
|
190
|
+
|
191
|
+
def param
|
192
|
+
request.param
|
193
|
+
end
|
194
|
+
alias params param
|
195
|
+
|
196
|
+
def cookie
|
197
|
+
request.cookie
|
198
|
+
end
|
199
|
+
alias cookies cookie
|
200
|
+
|
201
|
+
def set_cookie k, v=nil, opts
|
202
|
+
# todo default domain ?
|
203
|
+
opts = Hash[opts.map{|k,v| [k.to_sym,v]}]
|
204
|
+
Cookie.output_set_cookie response.response_header_extra_lines, k, v, opts
|
205
|
+
end
|
206
|
+
|
207
|
+
def delete_cookie k
|
208
|
+
# todo domain ? path ?
|
209
|
+
set_cookie k, expires: Time.now, max_age: 0
|
210
|
+
end
|
211
|
+
|
212
|
+
def clear_cookie
|
213
|
+
cookie.each do |k, _|
|
214
|
+
delete_cookie k
|
215
|
+
end
|
216
|
+
end
|
217
|
+
alias clear_cookies clear_cookie
|
218
|
+
|
219
|
+
def session
|
220
|
+
request.session
|
221
|
+
end
|
222
|
+
|
223
|
+
# Set response status
|
224
|
+
def status n
|
225
|
+
raise ArgumentError, "unsupported status: #{n}" unless HTTP_STATUS_FIRST_LINES[n]
|
226
|
+
Ext.request_set_status request, n
|
227
|
+
end
|
228
|
+
|
229
|
+
# Set response Content-Type, if there's no +charset+ in +ty+, and +ty+ is not text, adds default charset
|
230
|
+
def content_type ty
|
231
|
+
mime_ty = MIME_TYPES[ty.to_s]
|
232
|
+
raise ArgumentError, "bad content type: #{ty.inspect}" unless mime_ty
|
233
|
+
request.response_content_type = mime_ty
|
234
|
+
end
|
235
|
+
|
236
|
+
# Send respones first line and header data, and freeze +header+ to forbid further changes
|
237
|
+
def send_header template_deduced_content_type=nil
|
238
|
+
r = request
|
239
|
+
header = r.response_header
|
240
|
+
|
241
|
+
Ext.send_data r, HTTP_STATUS_FIRST_LINES[r.status]
|
242
|
+
|
243
|
+
header.aset_content_type \
|
244
|
+
r.response_content_type ||
|
245
|
+
header.aref_content_type ||
|
246
|
+
(r.accept and MIME_TYPES[r.accept]) ||
|
247
|
+
template_deduced_content_type
|
248
|
+
|
249
|
+
header.reverse_merge! OK_RESP_HEADER
|
250
|
+
|
251
|
+
data = header.map do |k, v|
|
252
|
+
"#{k}: #{v}\r\n"
|
253
|
+
end
|
254
|
+
data.concat r.response_header_extra_lines
|
255
|
+
data << "\r\n"
|
256
|
+
Ext.send_data r, data.join
|
257
|
+
|
258
|
+
# forbid further modification
|
259
|
+
header.freeze
|
260
|
+
end
|
261
|
+
|
262
|
+
# Send raw data, that is, not wrapped in chunked encoding<br>
|
263
|
+
# NOTE: often you should call send_header before doing this.
|
264
|
+
def send_data data
|
265
|
+
Ext.send_data request, data.to_s
|
266
|
+
end
|
267
|
+
|
268
|
+
# Send a data chunk, it can send_header first if header is not sent.
|
269
|
+
#
|
270
|
+
# :call-seq:
|
271
|
+
#
|
272
|
+
# send_chunk 'hello world!'
|
273
|
+
def send_chunk data
|
274
|
+
send_header unless request.response_header.frozen?
|
275
|
+
Ext.send_chunk request, data.to_s
|
276
|
+
end
|
277
|
+
alias send_string send_chunk
|
278
|
+
|
279
|
+
# Send file
|
280
|
+
def send_file file
|
281
|
+
if behind_proxy? # todo
|
282
|
+
header['X-Sendfile'] = file # todo escape name?
|
283
|
+
# todo content type and disposition
|
284
|
+
header['Content-Type'] = determine_ct_by_file_name
|
285
|
+
send_header unless request.response_header.frozen?
|
286
|
+
else
|
287
|
+
data = File.binread file
|
288
|
+
header['Content-Type'] = determine_ct_by_file_name
|
289
|
+
send_header unless request.response_header.frozen?
|
290
|
+
send_data data
|
291
|
+
end
|
292
|
+
Fiber.yield :term_close # is it right? content type changed
|
293
|
+
end
|
294
|
+
|
295
|
+
# Resume action after +seconds+
|
296
|
+
def sleep seconds
|
297
|
+
Fiber.yield seconds.to_f # todo
|
298
|
+
end
|
299
|
+
|
300
|
+
# One shot render, and terminate the action.
|
301
|
+
#
|
302
|
+
# :call-seq:
|
303
|
+
#
|
304
|
+
# # render a template, engine determined by extension
|
305
|
+
# render 'user/index', locals: {}
|
306
|
+
#
|
307
|
+
# # with template source, set content type to +text/html+ if not given
|
308
|
+
# render erb: "<%= 1 + 1 %>"
|
309
|
+
#
|
310
|
+
# For steam rendering, see #stream
|
311
|
+
def render view_path=nil, layout: self.class.default_layout, locals: nil, **opts
|
312
|
+
view = View.new self, view_path, layout, locals, opts
|
313
|
+
unless request.response_header.frozen?
|
314
|
+
send_header view.deduced_content_type
|
315
|
+
end
|
316
|
+
view.render
|
317
|
+
end
|
318
|
+
|
319
|
+
# Stream rendering
|
320
|
+
#
|
321
|
+
# :call-seq:
|
322
|
+
#
|
323
|
+
# view = stream erb: "<% 5.times do |i| %>i<% Fiber.yield %><% end %>"
|
324
|
+
# view.resume # sends "0"
|
325
|
+
# view.resume # sends "1"
|
326
|
+
# view.resume # sends "2"
|
327
|
+
# view.end # sends "34" and closes connection
|
328
|
+
def stream view_path=nil, layout: self.class.default_layout, locals: nil, **opts
|
329
|
+
view = View.new self, view_path, layout, locals, opts
|
330
|
+
unless request.response_header.frozen?
|
331
|
+
send_header view.deduced_content_type
|
332
|
+
end
|
333
|
+
view.stream
|
334
|
+
end
|
335
|
+
end
|
336
|
+
end
|
data/lib/nyara/cookie.rb
ADDED
@@ -0,0 +1,31 @@
|
|
1
|
+
module Nyara
|
2
|
+
# http://www.ietf.org/rfc/rfc6265.txt (don't look at rfc2109)
|
3
|
+
module Cookie
|
4
|
+
extend self
|
5
|
+
|
6
|
+
def decode header
|
7
|
+
res = ParamHash.new
|
8
|
+
if data = header['Cookie']
|
9
|
+
Ext.parse_cookie res, data
|
10
|
+
end
|
11
|
+
res
|
12
|
+
end
|
13
|
+
|
14
|
+
def add_set_cookie r, k, v, expires: nil, max_age: nil, domain: nil, path: nil, secure: nil, httponly: true
|
15
|
+
r << "Set-Cookie: "
|
16
|
+
if v.nil? or v == true
|
17
|
+
r << "#{CGI.escape k.to_s}; "
|
18
|
+
else
|
19
|
+
r << "#{CGI.escape k.to_s}=#{CGI.escape v.to_s}; "
|
20
|
+
end
|
21
|
+
r << "Expires=#{expires.to_time.gmtime.rfc2822}; " if expires
|
22
|
+
r << "Max-Age=#{max_age.to_i}; " if max_age
|
23
|
+
# todo lint rfc1123 §2.1, rfc1034 §3.5
|
24
|
+
r << "Domain=#{domain}; " if domain
|
25
|
+
r << "Path=#{path}; " if path
|
26
|
+
r << "Secure; " if secure
|
27
|
+
r << "HttpOnly; " if httponly
|
28
|
+
r << "\r\n"
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,65 @@
|
|
1
|
+
# https://gist.github.com/jimweirich/5813834
|
2
|
+
require 'rbconfig'
|
3
|
+
|
4
|
+
module Nyara
|
5
|
+
# Based on a script at:
|
6
|
+
# http://stackoverflow.com/questions/891537/ruby-detect-number-of-cpus-installed
|
7
|
+
class CpuCounter
|
8
|
+
def self.count
|
9
|
+
new.count
|
10
|
+
end
|
11
|
+
|
12
|
+
def count
|
13
|
+
case RbConfig::CONFIG['host_os']
|
14
|
+
when /darwin9/
|
15
|
+
`hwprefs cpu_count`.to_i
|
16
|
+
when /darwin/
|
17
|
+
darwin_count
|
18
|
+
when /linux/
|
19
|
+
linux_count
|
20
|
+
when /freebsd/
|
21
|
+
freebsd_count
|
22
|
+
when /mswin|mingw/
|
23
|
+
win32_count
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def darwin_count
|
28
|
+
if cmd = resolve_command('hwprefs')
|
29
|
+
`#{cmd} thread_count`.to_i
|
30
|
+
elsif cmd = resolve_command('sysctl')
|
31
|
+
`#{cmd} -n hw.ncpu`.to_i
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def linux_count
|
36
|
+
open('/proc/cpuinfo') { |f| f.readlines }.grep(/processor/).size
|
37
|
+
end
|
38
|
+
|
39
|
+
def freebsd_count
|
40
|
+
if cmd = resolve_command('sysctl')
|
41
|
+
`#{cmd} -n hw.ncpu`.to_i
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def win32_count
|
46
|
+
require 'win32ole'
|
47
|
+
wmi = WIN32OLE.connect("winmgmts://")
|
48
|
+
cpu = wmi.ExecQuery("select NumberOfCores from Win32_Processor") # TODO count hyper-threaded in this
|
49
|
+
cpu.to_enum.first.NumberOfCores
|
50
|
+
end
|
51
|
+
|
52
|
+
def resolve_command(command)
|
53
|
+
try_command("/sbin/", command) || try_command("/usr/sbin/", command) || in_path_command(command)
|
54
|
+
end
|
55
|
+
|
56
|
+
def in_path_command(command)
|
57
|
+
`which #{command}` != '' ? command : nil
|
58
|
+
end
|
59
|
+
|
60
|
+
def try_command(dir, command)
|
61
|
+
path = dir + command
|
62
|
+
File.exist?(path) ? path : nil
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
module Nyara
|
2
|
+
class HeaderHash
|
3
|
+
alias has_key? key?
|
4
|
+
|
5
|
+
CONTENT_TYPE = 'Content-Type'.freeze
|
6
|
+
|
7
|
+
def aref_content_type
|
8
|
+
self._aref CONTENT_TYPE
|
9
|
+
end
|
10
|
+
|
11
|
+
def aset_content_type value
|
12
|
+
unless value.index 'charset'
|
13
|
+
value = "#{value}; charset=UTF-8"
|
14
|
+
end
|
15
|
+
self._aset CONTENT_TYPE, value
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|