nyara 0.0.1.pre

Sign up to get free protection for your applications and to get access to all the features.
Files changed (67) hide show
  1. checksums.yaml +7 -0
  2. data/example/design.rb +62 -0
  3. data/example/fib.rb +15 -0
  4. data/example/hello.rb +5 -0
  5. data/example/stream.rb +10 -0
  6. data/ext/accept.c +133 -0
  7. data/ext/event.c +89 -0
  8. data/ext/extconf.rb +34 -0
  9. data/ext/hashes.c +130 -0
  10. data/ext/http-parser/AUTHORS +41 -0
  11. data/ext/http-parser/CONTRIBUTIONS +4 -0
  12. data/ext/http-parser/LICENSE-MIT +23 -0
  13. data/ext/http-parser/contrib/parsertrace.c +156 -0
  14. data/ext/http-parser/contrib/url_parser.c +44 -0
  15. data/ext/http-parser/http_parser.c +2175 -0
  16. data/ext/http-parser/http_parser.h +304 -0
  17. data/ext/http-parser/test.c +3425 -0
  18. data/ext/http_parser.c +1 -0
  19. data/ext/inc/epoll.h +60 -0
  20. data/ext/inc/kqueue.h +77 -0
  21. data/ext/inc/status_codes.inc +64 -0
  22. data/ext/inc/str_intern.h +66 -0
  23. data/ext/inc/version.inc +1 -0
  24. data/ext/mime.c +107 -0
  25. data/ext/multipart-parser-c/README.md +18 -0
  26. data/ext/multipart-parser-c/multipart_parser.c +309 -0
  27. data/ext/multipart-parser-c/multipart_parser.h +48 -0
  28. data/ext/multipart_parser.c +1 -0
  29. data/ext/nyara.c +56 -0
  30. data/ext/nyara.h +59 -0
  31. data/ext/request.c +474 -0
  32. data/ext/route.cc +325 -0
  33. data/ext/url_encoded.c +304 -0
  34. data/hello.rb +5 -0
  35. data/lib/nyara/config.rb +64 -0
  36. data/lib/nyara/config_hash.rb +51 -0
  37. data/lib/nyara/controller.rb +336 -0
  38. data/lib/nyara/cookie.rb +31 -0
  39. data/lib/nyara/cpu_counter.rb +65 -0
  40. data/lib/nyara/header_hash.rb +18 -0
  41. data/lib/nyara/mime_types.rb +612 -0
  42. data/lib/nyara/nyara.rb +82 -0
  43. data/lib/nyara/param_hash.rb +5 -0
  44. data/lib/nyara/request.rb +144 -0
  45. data/lib/nyara/route.rb +138 -0
  46. data/lib/nyara/route_entry.rb +43 -0
  47. data/lib/nyara/session.rb +104 -0
  48. data/lib/nyara/view.rb +317 -0
  49. data/lib/nyara.rb +25 -0
  50. data/nyara.gemspec +20 -0
  51. data/rakefile +91 -0
  52. data/readme.md +35 -0
  53. data/spec/ext_mime_match_spec.rb +27 -0
  54. data/spec/ext_parse_accept_value_spec.rb +29 -0
  55. data/spec/ext_parse_spec.rb +138 -0
  56. data/spec/ext_route_spec.rb +70 -0
  57. data/spec/hashes_spec.rb +71 -0
  58. data/spec/path_helper_spec.rb +77 -0
  59. data/spec/request_delegate_spec.rb +67 -0
  60. data/spec/request_spec.rb +56 -0
  61. data/spec/route_entry_spec.rb +12 -0
  62. data/spec/route_spec.rb +84 -0
  63. data/spec/session_spec.rb +66 -0
  64. data/spec/spec_helper.rb +52 -0
  65. data/spec/view_spec.rb +87 -0
  66. data/tools/bench-cookie.rb +22 -0
  67. 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
@@ -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