crampy 0.15.3

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/MIT-LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2009-2011 Pratik Naik
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/bin/cramp ADDED
@@ -0,0 +1,17 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'cramp'
4
+ require 'cramp/generators/application'
5
+
6
+ if ['--version', '-v'].include?(ARGV.first)
7
+ puts "Cramp #{Cramp::VERSION}"
8
+ exit(0)
9
+ end
10
+
11
+ if ARGV.first != "new"
12
+ ARGV[0] = "--help"
13
+ else
14
+ ARGV.shift
15
+ end
16
+
17
+ Cramp::Generators::Application.start
@@ -0,0 +1,104 @@
1
+ require 'active_support/core_ext/hash/keys'
2
+
3
+ module Cramp
4
+ class Abstract
5
+ include Callbacks
6
+ include FiberPool
7
+
8
+ class_attribute :transport
9
+ self.transport = :regular
10
+
11
+ class << self
12
+ def call(env)
13
+ new(env).process
14
+ end
15
+ end
16
+
17
+ def initialize(env)
18
+ @env = env
19
+ @finished = false
20
+
21
+ @_state = :init
22
+ end
23
+
24
+ def process
25
+ EM.next_tick { before_start }
26
+ throw :async
27
+ end
28
+
29
+ protected
30
+
31
+ def continue
32
+ init_async_body
33
+ send_headers
34
+
35
+ @_state = :started
36
+ EM.next_tick { on_start }
37
+ end
38
+
39
+ def send_headers
40
+ status, headers = build_headers
41
+ send_initial_response(status, headers, @body)
42
+ rescue StandardError, LoadError, SyntaxError => exception
43
+ handle_exception(exception)
44
+ end
45
+
46
+ def build_headers
47
+ status, headers = respond_to?(:respond_with, true) ? respond_with.dup : [200, {'Content-Type' => 'text/html'}]
48
+ headers['Connection'] ||= 'keep-alive'
49
+ [status, headers]
50
+ end
51
+
52
+ def init_async_body
53
+ @body = Body.new
54
+
55
+ if self.class.on_finish_callbacks.any?
56
+ @body.callback { on_finish }
57
+ @body.errback { on_finish }
58
+ end
59
+ end
60
+
61
+ def finished?
62
+ !!@finished
63
+ end
64
+
65
+ def finish
66
+ @_state = :finishing
67
+ @body.succeed if is_finishable?
68
+ ensure
69
+ @_state = :finished
70
+ @finished = true
71
+ end
72
+
73
+ def send_initial_response(response_status, response_headers, response_body)
74
+ send_response(response_status, response_headers, response_body)
75
+ end
76
+
77
+ def halt(status, headers = {}, halt_body = '')
78
+ send_response(status, headers, halt_body)
79
+ end
80
+
81
+ def send_response(response_status, response_headers, response_body)
82
+ @env['async.callback'].call [response_status, response_headers, response_body]
83
+ end
84
+
85
+ def request
86
+ @request ||= Rack::Request.new(@env)
87
+ end
88
+
89
+ def params
90
+ @params ||= request.params.update(route_params).symbolize_keys
91
+ end
92
+
93
+ def route_params
94
+ @env['router.params'] || {}
95
+ end
96
+
97
+ private
98
+
99
+ def is_finishable?
100
+ !finished? && @body && !@body.closed?
101
+ end
102
+
103
+ end
104
+ end
@@ -0,0 +1,134 @@
1
+ module Cramp
2
+ class Action < Abstract
3
+ include PeriodicTimer
4
+ include KeepConnectionAlive
5
+
6
+ def initialize(env)
7
+ super
8
+
9
+ case
10
+ when Faye::EventSource.eventsource?(env)
11
+ # request has Accept: text/event-stream
12
+ # faye server adapter intercepts headers - need to send them in send_initial_response or use faye's implementation
13
+ @eventsource_detected = true
14
+ unless transport == :sse
15
+ err = "WARNING: Cramp got request with EventSource header on action with transport #{transport} (not sse)! Response may not contain valid http headers!"
16
+ Cramp.logger ? Cramp.logger.error(err) : $stderr.puts(err)
17
+ end
18
+ when Faye::WebSocket.websocket?(env)
19
+ @web_socket = Faye::WebSocket.new(env)
20
+ @web_socket.onmessage = lambda do |event|
21
+ message = event.data
22
+ _invoke_data_callbacks(message) if message.is_a?(String)
23
+ end
24
+ end
25
+ end
26
+
27
+ protected
28
+
29
+ def render(body, *args)
30
+ send(:"render_#{transport}", body, *args)
31
+ end
32
+
33
+ def send_initial_response(status, headers, body)
34
+ case transport
35
+ when :long_polling
36
+ # Dont send no initial response. Just cache it for later.
37
+ @_lp_status = status
38
+ @_lp_headers = headers
39
+ when :sse
40
+ super
41
+ if @eventsource_detected
42
+ # Reconstruct headers that were killed by faye server adapter:
43
+ @body.call("HTTP/1.1 200 OK\r\n#{headers.map{|(k,v)| "#{k}: #{v.is_a?(Time) ? v.httpdate : v.to_s}"}.join("\r\n")}\r\n\r\n")
44
+ end
45
+ # send retry? @body.call("retry: #{ (@retry * 1000).floor }\r\n\r\n")
46
+ else
47
+ super
48
+ end
49
+ end
50
+
51
+ class_attribute :default_sse_headers
52
+ self.default_sse_headers = {'Content-Type' => 'text/event-stream', 'Cache-Control' => 'no-cache, no-store', 'Connection' => 'keep-alive'}
53
+
54
+ class_attribute :default_chunked_headers
55
+ self.default_chunked_headers = {'Transfer-Encoding' => 'chunked', 'Connection' => 'keep-alive'}
56
+
57
+ def build_headers
58
+ case transport
59
+ when :sse
60
+ status, headers = respond_to?(:respond_with, true) ? respond_with : [200, {'Content-Type' => 'text/html'}]
61
+ [status, headers.merge(self.default_sse_headers)]
62
+ when :chunked
63
+ status, headers = respond_to?(:respond_with, true) ? respond_with : [200, {}]
64
+
65
+ headers = headers.merge(self.default_chunked_headers)
66
+ headers['Content-Type'] ||= 'text/html'
67
+ headers['Cache-Control'] ||= 'no-cache'
68
+
69
+ [status, headers]
70
+ else
71
+ super
72
+ end
73
+ end
74
+
75
+ def render_regular(body, *)
76
+ @body.call(body)
77
+ end
78
+
79
+ def render_long_polling(data, *)
80
+ @_lp_headers['Content-Length'] = data.size.to_s
81
+
82
+ send_response(@_lp_status, @_lp_headers, @body)
83
+ @body.call(data)
84
+
85
+ finish
86
+ end
87
+
88
+ def render_sse(data, options = {})
89
+ #TODO: Faye uses \r\n for newlines, some compatibility?
90
+ result = "id: #{sse_event_id}\n"
91
+ result << "event: #{options[:event]}\n" if options[:event]
92
+ result << "retry: #{options[:retry]}\n" if options[:retry]
93
+
94
+ data.split(/\n/).each {|d| result << "data: #{d}\n" }
95
+ result << "\n"
96
+
97
+ @body.call(result)
98
+ end
99
+
100
+ def render_websocket(body, *)
101
+ @web_socket.send(body)
102
+ end
103
+
104
+ CHUNKED_TERM = "\r\n"
105
+ CHUNKED_TAIL = "0#{CHUNKED_TERM}#{CHUNKED_TERM}"
106
+
107
+ def render_chunked(body, *)
108
+ data = [Rack::Utils.bytesize(body).to_s(16), CHUNKED_TERM, body, CHUNKED_TERM].join
109
+
110
+ @body.call(data)
111
+ end
112
+
113
+ # Used by SSE
114
+ def sse_event_id
115
+ @sse_event_id ||= Time.now.to_i
116
+ end
117
+
118
+ def encode(string, encoding = 'UTF-8')
119
+ string.respond_to?(:force_encoding) ? string.force_encoding(encoding) : string
120
+ end
121
+
122
+ protected
123
+
124
+ def finish
125
+ case transport
126
+ when :chunked
127
+ @body.call(CHUNKED_TAIL) if is_finishable?
128
+ end
129
+
130
+ super
131
+ end
132
+
133
+ end
134
+ end
data/lib/cramp/body.rb ADDED
@@ -0,0 +1,48 @@
1
+ # Copyright 2008 James Tucker <raggi@rubyforge.org>.
2
+
3
+ module Cramp
4
+ class Body
5
+ include EventMachine::Deferrable
6
+
7
+ def initialize
8
+ @queue = []
9
+
10
+ # Make sure to flush out the queue before closing the connection
11
+ callback { flush }
12
+ end
13
+
14
+ def call(body)
15
+ @queue << body
16
+ schedule_dequeue
17
+ end
18
+
19
+ def each &blk
20
+ @body_callback = blk
21
+ schedule_dequeue
22
+ end
23
+
24
+ def closed?
25
+ @deferred_status != :unknown
26
+ end
27
+
28
+ def flush
29
+ return unless @body_callback
30
+
31
+ until @queue.empty?
32
+ Array(@queue.shift).each {|chunk| @body_callback.call(chunk) }
33
+ end
34
+ end
35
+
36
+ def schedule_dequeue
37
+ return unless @body_callback
38
+
39
+ EventMachine.next_tick do
40
+ next unless body = @queue.shift
41
+
42
+ Array(body).each {|chunk| @body_callback.call(chunk) }
43
+ schedule_dequeue unless @queue.empty?
44
+ end
45
+ end
46
+
47
+ end
48
+ end
@@ -0,0 +1,92 @@
1
+ module Cramp
2
+ module Callbacks
3
+
4
+ extend ActiveSupport::Concern
5
+
6
+ included do
7
+ #was class_inheritable_accessor
8
+ class_attribute :before_start_callbacks, :on_finish_callbacks, :on_start_callback, :on_data_callbacks, :instance_reader => false
9
+
10
+ self.before_start_callbacks = []
11
+ self.on_finish_callbacks = []
12
+ self.on_start_callback = []
13
+ self.on_data_callbacks = []
14
+ end
15
+
16
+ module ClassMethods
17
+ def before_start(*methods)
18
+ self.before_start_callbacks += methods
19
+ end
20
+
21
+ def on_finish(*methods)
22
+ self.on_finish_callbacks += methods
23
+ end
24
+
25
+ def on_start(*methods)
26
+ self.on_start_callback += methods
27
+ end
28
+
29
+ def on_data(*methods)
30
+ self.on_data_callbacks += methods
31
+ end
32
+ end
33
+
34
+ def before_start(n = 0)
35
+ if callback = self.class.before_start_callbacks[n]
36
+ callback_wrapper { send(callback) { before_start(n+1) } }
37
+ else
38
+ continue
39
+ end
40
+ end
41
+
42
+ def on_start
43
+ callback_wrapper { start } if respond_to?(:start)
44
+
45
+ self.class.on_start_callback.each do |callback|
46
+ callback_wrapper { send(callback) unless @finished }
47
+ end
48
+ end
49
+
50
+ def on_finish
51
+ self.class.on_finish_callbacks.each do |callback|
52
+ callback_wrapper { send(callback) }
53
+ end
54
+ end
55
+
56
+ def callback_wrapper
57
+ EM.next_tick do
58
+ begin
59
+ yield
60
+ rescue StandardError, LoadError, SyntaxError => exception
61
+ handle_exception(exception)
62
+ end
63
+ end
64
+ end
65
+
66
+ protected
67
+
68
+ def _invoke_data_callbacks(message)
69
+ self.class.on_data_callbacks.each do |callback|
70
+ callback_wrapper { send(callback, message) }
71
+ end
72
+ end
73
+
74
+ def handle_exception(exception)
75
+ handler = ExceptionHandler.new(@env, exception)
76
+
77
+ # Log the exception
78
+ unless ENV['RACK_ENV'] == 'test'
79
+ exception_body = handler.dump_exception
80
+ Cramp.logger ? Cramp.logger.error(exception_body) : $stderr.puts(exception_body)
81
+ end
82
+
83
+ case @_state
84
+ when :init
85
+ halt 500, {"Content-Type" => 'text/html'}, ENV['RACK_ENV'] == 'development' ? handler.pretty : 'Something went wrong'
86
+ else
87
+ finish
88
+ end
89
+ end
90
+
91
+ end
92
+ end