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.
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