nyara 0.0.1.pre
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.
- 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
|