plezi 0.7.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (68) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +22 -0
  3. data/CHANGELOG.md +450 -0
  4. data/Gemfile +4 -0
  5. data/KNOWN_ISSUES.md +13 -0
  6. data/LICENSE.txt +22 -0
  7. data/README.md +341 -0
  8. data/Rakefile +2 -0
  9. data/TODO.md +19 -0
  10. data/bin/plezi +301 -0
  11. data/lib/plezi.rb +125 -0
  12. data/lib/plezi/base/cache.rb +77 -0
  13. data/lib/plezi/base/connections.rb +33 -0
  14. data/lib/plezi/base/dsl.rb +177 -0
  15. data/lib/plezi/base/engine.rb +85 -0
  16. data/lib/plezi/base/events.rb +84 -0
  17. data/lib/plezi/base/io_reactor.rb +41 -0
  18. data/lib/plezi/base/logging.rb +62 -0
  19. data/lib/plezi/base/rack_app.rb +89 -0
  20. data/lib/plezi/base/services.rb +57 -0
  21. data/lib/plezi/base/timers.rb +71 -0
  22. data/lib/plezi/handlers/controller_magic.rb +383 -0
  23. data/lib/plezi/handlers/http_echo.rb +27 -0
  24. data/lib/plezi/handlers/http_host.rb +215 -0
  25. data/lib/plezi/handlers/http_router.rb +69 -0
  26. data/lib/plezi/handlers/magic_helpers.rb +43 -0
  27. data/lib/plezi/handlers/route.rb +272 -0
  28. data/lib/plezi/handlers/stubs.rb +143 -0
  29. data/lib/plezi/server/README.md +33 -0
  30. data/lib/plezi/server/helpers/http.rb +169 -0
  31. data/lib/plezi/server/helpers/mime_types.rb +999 -0
  32. data/lib/plezi/server/protocols/http_protocol.rb +318 -0
  33. data/lib/plezi/server/protocols/http_request.rb +133 -0
  34. data/lib/plezi/server/protocols/http_response.rb +294 -0
  35. data/lib/plezi/server/protocols/websocket.rb +208 -0
  36. data/lib/plezi/server/protocols/ws_response.rb +92 -0
  37. data/lib/plezi/server/services/basic_service.rb +224 -0
  38. data/lib/plezi/server/services/no_service.rb +196 -0
  39. data/lib/plezi/server/services/ssl_service.rb +193 -0
  40. data/lib/plezi/version.rb +3 -0
  41. data/plezi.gemspec +26 -0
  42. data/resources/404.erb +68 -0
  43. data/resources/404.haml +64 -0
  44. data/resources/404.html +67 -0
  45. data/resources/404.slim +63 -0
  46. data/resources/500.erb +68 -0
  47. data/resources/500.haml +63 -0
  48. data/resources/500.html +67 -0
  49. data/resources/500.slim +63 -0
  50. data/resources/Gemfile +85 -0
  51. data/resources/anorexic_gray.png +0 -0
  52. data/resources/anorexic_websockets.html +47 -0
  53. data/resources/code.rb +8 -0
  54. data/resources/config.ru +39 -0
  55. data/resources/controller.rb +139 -0
  56. data/resources/db_ac_config.rb +58 -0
  57. data/resources/db_dm_config.rb +51 -0
  58. data/resources/db_sequel_config.rb +42 -0
  59. data/resources/en.yml +204 -0
  60. data/resources/environment.rb +41 -0
  61. data/resources/haml_config.rb +6 -0
  62. data/resources/i18n_config.rb +14 -0
  63. data/resources/rakefile.rb +22 -0
  64. data/resources/redis_config.rb +35 -0
  65. data/resources/routes.rb +26 -0
  66. data/resources/welcome_page.html +72 -0
  67. data/websocket chatroom.md +639 -0
  68. metadata +141 -0
@@ -0,0 +1,84 @@
1
+
2
+ module Plezi
3
+
4
+ module_function
5
+
6
+ #######################
7
+ ## Events (Callbacks) / Multi-tasking Platform
8
+
9
+ # DANGER ZONE - Plezi Engine. an Array containing all the shutdown callbacks that need to be called.
10
+ SHUTDOWN_CALLBACKS = []
11
+ # DANGER ZONE - Plezi Engine. an Array containing all the current events.
12
+ EVENTS = []
13
+ # DANGER ZONE - Plezi Engine. the Mutex locker for the event machine.
14
+ LOCKER = Mutex.new
15
+
16
+ # returns true if there are any unhandled events
17
+ def events?
18
+ LOCKER.synchronize {!EVENTS.empty?}
19
+ end
20
+
21
+ # Public API. pushes an event to the event's stack
22
+ #
23
+ # accepts:
24
+ # handler:: an object that answers to `call`, usually a Proc or a method.
25
+ # *arg:: any arguments that will be passed to the handler's `call` method.
26
+ #
27
+ # if a block is passed along, it will be used as a callback: the block will be called with the values returned by the handler's `call` method.
28
+ def push_event handler, *args, &block
29
+ if block
30
+ LOCKER.synchronize {EVENTS << [(Proc.new {|a| Plezi.push_event block, handler.call(*a)} ), args]}
31
+ else
32
+ LOCKER.synchronize {EVENTS << [handler, args]}
33
+ end
34
+ end
35
+
36
+ # Public API. creates an asynchronous call to a method, with an optional callback:
37
+ # demo use:
38
+ # `callback( Kernel, :sleep, 1 ) { puts "this is a demo" }`
39
+ # callback sets an asynchronous method call with a callback.
40
+ #
41
+ # paramaters:
42
+ # object:: the object holding the method to be called (use `Kernel` for global methods).
43
+ # method:: the method's name (Symbol). this is the method that will be called.
44
+ # *arguments:: any additional arguments that should be sent to the method (the main method, not the callback).
45
+ #
46
+ # this method also accepts an optional block (the callback) that will be called once the main method has completed.
47
+ # the block will recieve the method's returned value.
48
+ #
49
+ # i.e.
50
+ # puts 'while we wait for my work to complete, can you tell me your name?'
51
+ # Plezi.callback(STDIO, :gets) do |name|
52
+ # puts "thank you, #{name}. I'm working on your request as we speak."
53
+ # end
54
+ # # do more work without waiting for the chat to start nor complete.
55
+ def callback object, method, *args, &block
56
+ push_event object.method(method), *args, &block
57
+ end
58
+
59
+ # Public API. adds a callback to be called once the services were shut down. see: callback for more info.
60
+ def on_shutdown object=nil, method=nil, *args, &block
61
+ if block && !object && !method
62
+ LOCKER.synchronize {SHUTDOWN_CALLBACKS << [block, args]}
63
+ elsif block
64
+ LOCKER.synchronize {SHUTDOWN_CALLBACKS << [(Proc.new {|*a| block.call(object.method(method).call(*a))} ), args]}
65
+ elsif object && method
66
+ LOCKER.synchronize {SHUTDOWN_CALLBACKS << [object.method(method), args]}
67
+ end
68
+ end
69
+
70
+ # Plezi Engine, DO NOT CALL. pulls an event from the event's stack and processes it.
71
+ def fire_event
72
+ event = LOCKER.synchronize {EVENTS.shift}
73
+ return false unless event
74
+ begin
75
+ event[0].call(*event[1])
76
+ rescue OpenSSL::SSL::SSLError => e
77
+ warn "SSL Bump - SSL Certificate refused?"
78
+ rescue Exception => e
79
+ raise if e.is_a?(SignalException) || e.is_a?(SystemExit)
80
+ error e
81
+ end
82
+ true
83
+ end
84
+ end
@@ -0,0 +1,41 @@
1
+
2
+ module Plezi
3
+
4
+ module_function
5
+
6
+
7
+ # DANGER ZONE - Plezi Engine. the io reactor mutex
8
+ IO_LOCKER = Mutex.new
9
+
10
+ # Plezi Engine, DO NOT CALL. waits on IO and pushes events. all threads hang while reactor is active (unless events are already 'in the pipe'.)
11
+ def io_reactor
12
+ IO_LOCKER.synchronize do
13
+ return false unless EVENTS.empty?
14
+ united = SERVICES.keys + IO_CONNECTION_DIC.keys
15
+ return false if united.empty?
16
+ io_r = (IO.select(united, nil, united, idle_sleep) ) #rescue false)
17
+ if io_r
18
+ io_r[0].each do |io|
19
+ if SERVICES[io]
20
+ begin
21
+ connection = io.accept_nonblock
22
+ callback Plezi, :add_connection, connection, SERVICES[io]
23
+ rescue Errno::EWOULDBLOCK => e
24
+
25
+ rescue Exception => e
26
+ error e
27
+ # SERVICES.delete s if s.closed?
28
+ end
29
+ elsif IO_CONNECTION_DIC[io]
30
+ callback(IO_CONNECTION_DIC[io], :on_message)
31
+ else
32
+ IO_CONNECTION_DIC.delete(io)
33
+ SERVICES.delete(io)
34
+ end
35
+ end
36
+ io_r[2].each { |io| (IO_CONNECTION_DIC.delete(io) || SERVICES.delete(io)).close rescue true }
37
+ end
38
+ end
39
+ true
40
+ end
41
+ end
@@ -0,0 +1,62 @@
1
+
2
+ module Plezi
3
+
4
+ #######################
5
+ ## Logging
6
+ module_function
7
+
8
+ # the logger object
9
+ @logger = ::Logger.new(STDOUT)
10
+
11
+ # gets the active logger
12
+ def logger
13
+ @logger
14
+ end
15
+ # gets the active STDOUT copy, if exists
16
+ def logger_copy
17
+ @copy_to_stdout
18
+ end
19
+
20
+ # create and set the logger object. accepts:
21
+ # log_file:: a log file name to be used for logging
22
+ # copy_to_stdout:: if false, log will only log to file. defaults to true.
23
+ def create_logger log_file = STDOUT, copy_to_stdout = false
24
+ @copy_to_stdout = false
25
+ @copy_to_stdout = ::Logger.new(STDOUT) if copy_to_stdout
26
+ @logger = ::Logger.new(log_file)
27
+ @logger
28
+ end
29
+ alias :set_logger :create_logger
30
+
31
+ # writes a raw line to the log\
32
+ def log_raw line
33
+ @logger << line
34
+ @copy_to_stdout << line if @copy_to_stdout
35
+ end
36
+
37
+ # logs info
38
+ def log line
39
+ @logger.info line
40
+ @copy_to_stdout.info line if @copy_to_stdout
41
+ end
42
+ # logs info
43
+ def info line
44
+ @logger.info line
45
+ @copy_to_stdout.info line if @copy_to_stdout
46
+ end
47
+ # logs warning
48
+ def warn line
49
+ @logger.warn line
50
+ @copy_to_stdout.warn line if @copy_to_stdout
51
+ end
52
+ # logs errors
53
+ def error line
54
+ @logger.error line
55
+ @copy_to_stdout.error line if @copy_to_stdout
56
+ end
57
+ # logs a fatal error
58
+ def fatal line
59
+ @logger.fatal line
60
+ @copy_to_stdout.fatal line if @copy_to_stdout
61
+ end
62
+ end
@@ -0,0 +1,89 @@
1
+
2
+ module Plezi
3
+
4
+ # Rack application model support
5
+ module_function
6
+
7
+ # todo: move falsh into the special cookie class...?
8
+
9
+
10
+ # Plezi dresses up for Rack - this is a watered down version missing some features (such as flash and WebSockets).
11
+ # a full featured Plezi app, with WebSockets, requires the use of the Plezi server
12
+ # (the built-in server)
13
+ def call env
14
+ raise "No Plezi Services" unless Plezi::SERVICES[0]
15
+ Object.const_set('PLEZI_ON_RACK', true) unless defined? PLEZI_ON_RACK
16
+
17
+ # re-encode to utf-8, as it's all BINARY encoding at first
18
+ env["rack.input"].rewind
19
+ env['rack.input'] = StringIO.new env["rack.input"].read.encode("utf-8", "binary", invalid: :replace, undef: :replace, replace: '')
20
+ env.each do |k, v|
21
+ if k.to_s.match /^[A-Z]/
22
+ if v.is_a?(String) && !v.frozen?
23
+ v.force_encoding("binary").encode!("utf-8", "binary", invalid: :replace, undef: :replace, replace: '') unless v.force_encoding("utf-8").valid_encoding?
24
+ end
25
+ end
26
+ end
27
+ # re-key params
28
+ # new_params = {}
29
+ # env[:params].each {|k,v| HTTP.add_param_to_hash k, v, new_params}
30
+ # env[:params] = new_params
31
+
32
+ # make hashes magical
33
+ make_hash_accept_symbols(env)
34
+
35
+ # use Plezi Cookies
36
+ env["rack.request.cookie_string"] = env["HTTP_COOKIE"]
37
+ env["rack.request.cookie_hash"] = Plezi::Cookies.new.update(env["rack.request.cookie_hash"] || {})
38
+
39
+ # chomp path
40
+ env["PATH_INFO"].chomp! '/'
41
+
42
+ # get response
43
+ response = Plezi::SERVICES[0][1][:handler].call env
44
+
45
+ return response if response.is_a?(Array)
46
+
47
+ response.finish
48
+ response.fix_headers
49
+ headers = response.headers
50
+ # set cookie headers
51
+ headers.delete 'transfer-encoding'
52
+ headers.delete 'connection'
53
+ unless response.cookies.empty?
54
+ headers["Set-Cookie"] = []
55
+ response.cookies.each {|k,v| headers["Set-Cookie"] << ("#{k.to_s}=#{v.to_s}")}
56
+ end
57
+ [response.status, headers, response.body]
58
+ end
59
+ end
60
+
61
+ # # rack code to set cookie headers
62
+ # # File actionpack/lib/action_controller/vendor/rack-1.0/rack/response.rb, line 56
63
+ # def set_cookie(key, value)
64
+ # case value
65
+ # when Hash
66
+ # domain = "; domain=" + value[:domain] if value[:domain]
67
+ # path = "; path=" + value[:path] if value[:path]
68
+ # # According to RFC 2109, we need dashes here.
69
+ # # N.B.: cgi.rb uses spaces...
70
+ # expires = "; expires=" + value[:expires].clone.gmtime.
71
+ # strftime("%a, %d-%b-%Y %H:%M:%S GMT") if value[:expires]
72
+ # secure = "; secure" if value[:secure]
73
+ # httponly = "; HttpOnly" if value[:httponly]
74
+ # value = value[:value]
75
+ # end
76
+ # value = [value] unless Array === value
77
+ # cookie = Utils.escape(key) + "=" +
78
+ # value.map { |v| Utils.escape v }.join("&") +
79
+ # "#{domain}#{path}#{expires}#{secure}#{httponly}"
80
+
81
+ # case self["Set-Cookie"]
82
+ # when Array
83
+ # self["Set-Cookie"] << cookie
84
+ # when String
85
+ # self["Set-Cookie"] = [self["Set-Cookie"], cookie]
86
+ # when nil
87
+ # self["Set-Cookie"] = cookie
88
+ # end
89
+ # end
@@ -0,0 +1,57 @@
1
+
2
+ module Plezi
3
+
4
+ module_function
5
+
6
+ #######################
7
+ ## Services pooling and calling
8
+
9
+ # DANGER ZONE - Plezi Engine. the services store
10
+ SERVICES = {}
11
+ # DANGER ZONE - Plezi Engine. the services mutex
12
+ S_LOCKER = Mutex.new
13
+
14
+ # public API to add a service to the framework.
15
+ # accepts:
16
+ # port:: port number
17
+ # parameters:: a hash of paramaters that are passed on to the service for handling (and from there, service dependent, to the protocol and/or handler).
18
+ #
19
+ # parameters are any of the following:
20
+ # host:: the host name. defaults to any host not explicitly defined (a catch-all).
21
+ # alias:: a String or an Array of Strings which represent alternative host names (i.e. `alias: ["admin.google.com", "admin.gmail.com"]`).
22
+ # root:: the public root folder. if this is defined, static files will be served from the location.
23
+ # assets:: the assets root folder. defaults to nil (no assets support). if the path is defined, assets will be served from `/assets/...` (or the public_asset path defined) before any static files. assets will not be served if the file in the /public/assets folder if up to date (a rendering attempt will be made for systems that allow file writing).
24
+ # assets_public:: the assets public uri location (uri format, NOT a file path). defaults to `/assets`. assets will be saved (or rendered) to the assets public folder and served as static files.
25
+ # assets_callback:: a method that accepts one parameters: `request` and renders any custom assets. the method should return `false` unless it has created a response object (`response = Plezi::HTTPResponse.new(request)`) and sent a response to the client using `response.finish`.
26
+ # save_assets:: saves the rendered assets to the filesystem, under the public folder. defaults to false.
27
+ # templates:: the templates root folder. defaults to nil (no template support). templates can be rendered by a Controller class, using the `render` method.
28
+ # ssl:: if true, an SSL service will be attempted. if no certificate is defined, an attempt will be made to create a self signed certificate.
29
+ # ssl_key:: the public key for the SSL service.
30
+ # ssl_cert:: the certificate for the SSL service.
31
+ #
32
+ #
33
+ # assets:
34
+ #
35
+ # assets support will render `.sass`, `.scss` and `.coffee` and save them as local files (`.css`, `.css`, and `.js` respectively) before sending them as static files. if it is impossible to write the files, they will be rendered dynamically for every request (it would be better to render them before-hand).
36
+ #
37
+ # templates:
38
+ #
39
+ # templates can be either an ERB file on a Haml file.
40
+ #
41
+ def add_service port, paramaters = {}
42
+ paramaters[:port] ||= port
43
+ paramaters[:service_type] ||= ( paramaters[:ssl] ? SSLService : BasicService)
44
+ service = nil
45
+ service = paramaters[:service_type].create_service(port, paramaters) unless ( defined?(BUILDING_PLEZI_TEMPLATE) || defined?(PLEZI_ON_RACK) )
46
+ S_LOCKER.synchronize {SERVICES[service] = paramaters}
47
+ info "Started listening on port #{port}."
48
+ true
49
+ end
50
+
51
+ # Plezi Engine, DO NOT CALL. stops all services - active connection will remain open until completion.
52
+ def stop_services
53
+ info 'Stopping services'
54
+ S_LOCKER.synchronize {SERVICES.each {|s, p| s.close rescue true; info "Stoped listening on port #{p[:port]}"}; SERVICES.clear }
55
+ end
56
+
57
+ end
@@ -0,0 +1,71 @@
1
+
2
+ module Plezi
3
+
4
+ module_function
5
+
6
+ #######################
7
+ ## Timed Events / Multi-tasking
8
+
9
+ # DANGER ZONE - Plezi Engine. an Array containing all the current events.
10
+ TIMERS = []
11
+ # DANGER ZONE - Plezi Engine. the Mutex locker for the event machine.
12
+ T_LOCKER = Mutex.new
13
+
14
+ # This class is used by Plezi to hold events and push them into the events stack when the time comes.
15
+ class TimedEvent
16
+
17
+ def initialize seconds, repeat, handler, args, block
18
+ @time = Time.now + seconds
19
+ @seconds, @repeat, @handler, @args, @block = seconds, repeat, handler, args, block
20
+ end
21
+
22
+ def done?
23
+ return false unless @time <= Time.now
24
+ Plezi.push_event @handler, *@args, &@block
25
+ return true unless @repeat
26
+ @time = Time.now + @seconds
27
+ false
28
+ end
29
+
30
+ def stop_repeat
31
+ @repeat = false
32
+ end
33
+
34
+ attr_reader :timed
35
+ end
36
+
37
+ # returns true if there are any unhandled events
38
+ def timers?
39
+ T_LOCKER.synchronize {!TIMERS.empty?}
40
+ end
41
+
42
+ # pushes a timed event to the timers's stack
43
+ #
44
+ # accepts:
45
+ # seconds:: the minimal amount of seconds to wait before calling the handler's `call` method.
46
+ # handler:: an object that answers to `call`, usually a Proc or a method.
47
+ # *arg:: any arguments that will be passed to the handler's `call` method.
48
+ #
49
+ # if a block is passed along, it will be used as a callback: the block will be called with the values returned by the handler's `call` method.
50
+ def run_after seconds, handler, *args, &block
51
+ T_LOCKER.synchronize {TIMERS << TimedEvent.new(seconds, false, handler, args, block); TIMERS.last}
52
+ end
53
+
54
+ # pushes a repeated timed event to the timers's stack
55
+ #
56
+ # accepts:
57
+ # seconds:: the minimal amount of seconds to wait before calling the handler's `call` method.
58
+ # handler:: an object that answers to `call`, usually a Proc or a method.
59
+ # *arg:: any arguments that will be passed to the handler's `call` method.
60
+ #
61
+ # if a block is passed along, it will be used as a callback: the block will be called with the values returned by the handler's `call` method.
62
+ def run_every seconds, handler, *args, &block
63
+ T_LOCKER.synchronize {TIMERS << TimedEvent.new(seconds, true, handler, args, block); TIMERS.last}
64
+ end
65
+
66
+ # DANGER ZONE - Used by the Plezi engine to review timed events and push them to the event stack
67
+ def fire_timers
68
+ return false if T_LOCKER.locked?
69
+ T_LOCKER.synchronize { TIMERS.delete_if {|t| t.done? } }
70
+ end
71
+ end
@@ -0,0 +1,383 @@
1
+ module Plezi
2
+
3
+ # the methods defined in this module will be injected into the Controller class passed to
4
+ # Plezi (using the `route` or `shared_route` commands), and will be available
5
+ # for the controller to use within it's methods.
6
+ #
7
+ # for some reason, the documentation ignores the following additional attributes, which are listed here:
8
+ #
9
+ # request:: the HTTPRequest object containing all the data from the HTTP request. If a WebSocket connection was established, the `request` object will continue to contain the HTTP request establishing the connection (cookies, parameters sent and other information).
10
+ # params:: any parameters sent with the request (short-cut for `request.params`), will contain any GET or POST form data sent (including file upload and JSON format support).
11
+ # cookies:: a cookie-jar to get and set cookies (set: `cookie\[:name] = data` or get: `cookie\[:name]`). Cookies and some other data must be set BEFORE the response's headers are sent.
12
+ # flash:: a temporary cookie-jar, good for one request. this is a short-cut for the `response.flash` which handles this magical cookie style.
13
+ # response:: the HTTPResponse **OR** the WSResponse object that formats the response and sends it. use `response << data`. This object can be used to send partial data (such as headers, or partial html content) in blocking mode as well as sending data in the default non-blocking mode.
14
+ # host_params:: a copy of the parameters used to create the host and service which accepted the request and created this instance of the controller class.
15
+ #
16
+ module ControllerMagic
17
+ def self.included base
18
+ base.send :include, InstanceMethods
19
+ base.extend ClassMethods
20
+ end
21
+
22
+ module InstanceMethods
23
+ module_function
24
+ public
25
+
26
+ # the request object, class: HTTPRequest.
27
+ attr_reader :request
28
+
29
+ # the ::params variable contains all the paramaters set by the request (/path?locale=he => params["locale"] == "he").
30
+ attr_reader :params
31
+
32
+ # a cookie-jar to get and set cookies (set: `cookie\[:name] = data` or get: `cookie\[:name]`).
33
+ #
34
+ # Cookies and some other data must be set BEFORE the response's headers are sent.
35
+ attr_reader :cookies
36
+
37
+ # the HTTPResponse **OR** the WSResponse object that formats the response and sends it. use `response << data`. This object can be used to send partial data (such as headers, or partial html content) in blocking mode as well as sending data in the default non-blocking mode.
38
+ attr_accessor :response
39
+
40
+ # the ::flash is a little bit of a magic hash that sets and reads temporary cookies.
41
+ # these cookies will live for one successful request to a Controller and will then be removed.
42
+ attr_reader :flash
43
+
44
+ # the parameters used to create the host (the parameters passed to the `listen` / `add_service` call).
45
+ attr_reader :host_params
46
+
47
+ # a unique UUID to identify the object - used to make sure Radis broadcasts don't triger the
48
+ # boadcasting object's event.
49
+ attr_reader :uuid
50
+
51
+ # checks whether this instance accepts broadcasts (WebSocket instances).
52
+ def accepts_broadcast?
53
+ @_accepts_broadcast
54
+ end
55
+
56
+ # this method does two things.
57
+ #
58
+ # 1. sets redirection headers for the response.
59
+ # 2. sets the `flash` object (short-time cookies) with all the values passed except the :status value.
60
+ #
61
+ # use:
62
+ # redirect_to 'http://google.com', notice: "foo", status: 302
63
+ # # => redirects to 'http://google.com' with status 302 and adds notice: "foo" to the flash
64
+ # or simply:
65
+ # redirect_to 'http://google.com'
66
+ # # => redirects to 'http://google.com' with status 302 (default status)
67
+ #
68
+ # if the url is a symbol, the method will try to format it into a correct url, replacing any
69
+ # underscores ('_') with a backslash ('/').
70
+ #
71
+ # if the url is an empty string, the method will try to format it into a correct url
72
+ # representing the index of the application (http://server/)
73
+ #
74
+ def redirect_to url, options = {}
75
+ return super *[] if defined? super
76
+ raise 'Cannot redirect after headers were sent' if response.headers_sent?
77
+ url = "#{request.base_url}/#{url.to_s.gsub('_', '/')}" if url.is_a?(Symbol) || ( url.is_a?(String) && url.empty? ) || url.nil?
78
+ # redirect
79
+ response.status = options.delete(:status) || 302
80
+ response['Location'] = url
81
+ response['content-length'] ||= 0
82
+ flash.update options
83
+ response.finish
84
+ true
85
+ end
86
+
87
+ # this method adds data to be sent.
88
+ #
89
+ # this is usful for sending 'attachments' (data to be downloaded) rather then
90
+ # a regular response.
91
+ #
92
+ # this is also usful for offering a file name for the browser to "save as".
93
+ #
94
+ # it accepts:
95
+ # data:: the data to be sent
96
+ # options:: a hash of any of the options listed furtheron.
97
+ #
98
+ # the :symbol=>value options are:
99
+ # type:: the type of the data to be sent. defaults to empty. if :filename is supplied, an attempt to guess will be made.
100
+ # inline:: sets the data to be sent an an inline object (to be viewed rather then downloaded). defaults to false.
101
+ # filename:: sets a filename for the browser to "save as". defaults to empty.
102
+ #
103
+ def send_data data, options = {}
104
+ raise 'Cannot use "send_data" after headers were sent' if response.headers_sent?
105
+ # write data to response object
106
+ response << data
107
+
108
+ # set headers
109
+ content_disposition = "attachment"
110
+
111
+ options[:type] ||= MimeTypeHelper::MIME_DICTIONARY[::File.extname(options[:filename])] if options[:filename]
112
+
113
+ if options[:type]
114
+ response["content-type"] = options[:type]
115
+ options.delete :type
116
+ end
117
+ if options[:inline]
118
+ content_disposition = "inline"
119
+ options.delete :inline
120
+ end
121
+ if options[:attachment]
122
+ options.delete :attachment
123
+ end
124
+ if options[:filename]
125
+ content_disposition << "; filename=#{options[:filename]}"
126
+ options.delete :filename
127
+ end
128
+ response["content-length"] = data.bytesize rescue true
129
+ response["content-disposition"] = content_disposition
130
+ response.finish
131
+ true
132
+ end
133
+
134
+ # renders a template file (.erb/.haml) or an html file (.html) to text
135
+ # for example, to render the file `body.html.haml` with the layout `main_layout.html.haml`:
136
+ # render :body, layout: :main_layout
137
+ #
138
+ # or, for example, to render the file `json.js.haml`
139
+ # render :json, type: 'js'
140
+ #
141
+ # or, for example, to render the file `template.haml`
142
+ # render :template, type: ''
143
+ #
144
+ # template:: a Symbol for the template to be used.
145
+ # options:: a Hash for any options such as `:layout` or `locale`.
146
+ # block:: an optional block, in case the template has `yield`, the block will be passed on to the template and it's value will be used inplace of the yield statement.
147
+ #
148
+ # options aceept the following keys:
149
+ # type:: the types for the `:layout' and 'template'. can be any extention, such as `"json"`. defaults to `"html"`.
150
+ # layout:: a layout template that has at least one `yield` statement where the template will be rendered.
151
+ # locale:: the I18n locale for the render. (defaults to params[:locale]) - only if the I18n gem namespace is defined (`require 'i18n'`).
152
+ #
153
+ # if template is a string, it will assume the string is an
154
+ # absolute path to a template file. it will NOT search for the template but might raise exceptions.
155
+ #
156
+ # if the template is a symbol, the '_' caracters will be used to destinguish sub-folders (NOT a partial template).
157
+ #
158
+ # returns false if the template or layout files cannot be found.
159
+ def render template, options = {}, &block
160
+ # make sure templates are enabled
161
+ return false if host_params[:templates].nil?
162
+ # render layout by recursion, if exists
163
+ (return render(options.delete(:layout), options) { render template, options, &block }) if options[:layout]
164
+ # set up defaults
165
+ options[:type] ||= 'html'
166
+ options[:locale] ||= params[:locale].to_sym if params[:locale]
167
+ # options[:locals] ||= {}
168
+ I18n.locale = options[:locale] if defined?(I18n) && options[:locale]
169
+ # find template and create template object
170
+ filename = template.is_a?(String) ? File.join( host_params[:templates].to_s, template) : (File.join( host_params[:templates].to_s, *template.to_s.split('_')) + (options[:type].empty? ? '': ".#{options[:type]}") + '.slim')
171
+ return ( Plezi.cache_needs_update?(filename) ? Plezi.cache_data( filename, ( Slim::Template.new() { IO.read filename } ) ) : (Plezi.get_cached filename) ).render(self, &block) if defined?(::Slim) && Plezi.file_exists?(filename)
172
+ filename.gsub! /\.slim$/, '.haml'
173
+ return ( Plezi.cache_needs_update?(filename) ? Plezi.cache_data( filename, ( Haml::Engine.new( IO.read(filename) ) ) ) : (Plezi.get_cached filename) ).render(self, &block) if defined?(::Haml) && Plezi.file_exists?(filename)
174
+ filename.gsub! /\.haml$/, '.erb'
175
+ return ( Plezi.cache_needs_update?(filename) ? Plezi.cache_data( filename, ( ERB.new( IO.read(filename) ) ) ) : (Plezi.get_cached filename) ).result(binding, &block) if defined?(::ERB) && Plezi.file_exists?(filename)
176
+ return false
177
+ end
178
+
179
+ # returns the initial method called (or about to be called) by the router for the HTTP request.
180
+ #
181
+ # this is can be very useful within the before / after filters:
182
+ # def before
183
+ # return false unless "check credentials" && [:save, :update, :delete].include?(requested_method)
184
+ #
185
+ # if the controller responds to a WebSockets request (a controller that defines the `on_message` method),
186
+ # the value returned is invalid and will remain 'stuck' on :pre_connect
187
+ # (which is the last method called before the protocol is switched from HTTP to WebSockets).
188
+ def requested_method
189
+ # respond to websocket special case
190
+ return :pre_connect if request['upgrade'] && request['upgrade'].to_s.downcase == 'websocket' && request['connection'].to_s.downcase == 'upgrade'
191
+ # respond to save 'new' special case
192
+ return :save if request.request_method.match(/POST|PUT/) && params[:id].nil? || params[:id] == 'new'
193
+ # set DELETE method if simulated
194
+ request.request_method = 'DELETE' if params[:_method].to_s.downcase == 'delete'
195
+ # respond to special :id routing
196
+ return params[:id].to_sym if params[:id] && available_public_methods.include?(params[:id].to_sym)
197
+ #review general cases
198
+ case request.request_method
199
+ when 'GET', 'HEAD'
200
+ return :index unless params[:id]
201
+ return :show
202
+ when 'POST', 'PUT'
203
+ return :update
204
+ when 'DELETE'
205
+ return :delete
206
+ end
207
+ false
208
+ end
209
+
210
+ # lists the available methods that will be exposed to HTTP requests
211
+ def available_public_methods
212
+ # set class global to improve performance while checking for supported methods
213
+ @@___available_public_methods___ ||= available_routing_methods - [:before, :after, :save, :show, :update, :delete, :initialize, :on_message, :pre_connect, :on_connect, :on_disconnect]
214
+ end
215
+
216
+ # lists the available methods that will be exposed to the HTTP router
217
+ def available_routing_methods
218
+ # set class global to improve performance while checking for supported methods
219
+ @@___available_routing_methods___ ||= (((self.class.public_instance_methods - Object.public_instance_methods) - Plezi::ControllerMagic::InstanceMethods.instance_methods).delete_if {|m| m.to_s[0] == '_'})
220
+ end
221
+
222
+ ## WebSockets Magic
223
+
224
+ # WebSockets.
225
+ #
226
+ # Use this to brodcast an event to all 'sibling' websockets (websockets that have been created using the same Controller class).
227
+ #
228
+ # accepts:
229
+ # method_name:: a Symbol with the method's name that should respond to the broadcast.
230
+ # *args:: any arguments that should be passed to the method (IF REDIS IS USED, LIMITATIONS APPLY).
231
+ #
232
+ # the method will be called asynchrnously for each sibling instance of this Controller class.
233
+ #
234
+ def broadcast method_name, *args, &block
235
+ return false unless self.class.public_instance_methods.include?(method_name)
236
+ @uuid ||= SecureRandom.uuid
237
+ self.class.__inner_redis_broadcast(uuid, method_name, args, &block) || self.class.__inner_process_broadcast(uuid, method_name.to_sym, args, &block)
238
+ end
239
+
240
+
241
+ # WebSockets.
242
+ #
243
+ # Use this to collect data from all 'sibling' websockets (websockets that have been created using the same Controller class).
244
+ #
245
+ # This method will call the requested method on all instance siblings and return an Array of the returned values (including nil values).
246
+ #
247
+ # This method will block the excecution unless a block is passed to the method - in which case
248
+ # the block will used as a callback and recieve the Array as a parameter.
249
+ #
250
+ # i.e.
251
+ #
252
+ # this will block: `collect :_get_id`
253
+ #
254
+ # this will not block: `collect(:_get_id) {|a| puts "got #{a.length} responses."; a.each { |id| puts "#{id}"} }
255
+ #
256
+ # accepts:
257
+ # method_name:: a Symbol with the method's name that should respond to the broadcast.
258
+ # *args:: any arguments that should be passed to the method.
259
+ # &block:: an optional block to be used as a callback.
260
+ #
261
+ # the method will be called asynchrnously for each sibling instance of this Controller class.
262
+ def collect method_name, *args, &block
263
+ return Plezi.callback(self, :collect, *args, &block) if block
264
+
265
+ r = []
266
+ ObjectSpace.each_object(self.class) { |controller| r << controller.method(method_name).call(*args) if controller.accepts_broadcast? && (controller.object_id != self.object_id) }
267
+ return r
268
+ end
269
+
270
+
271
+ # # will (probably NOT), in the future, require authentication or, alternatively, return an Array [user_name, password]
272
+ # #
273
+ # #
274
+ # def request_http_auth realm = false, auth = 'Digest'
275
+ # return request.service.handler.hosts[request[:host] || :default].send_by_code request, 401, "WWW-Authenticate" => "#{auth}#{realm ? "realm=\"#{realm}\"" : ''}" unless request['authorization']
276
+ # request['authorization']
277
+ # end
278
+ end
279
+
280
+ module ClassMethods
281
+ public
282
+
283
+ # reviews the Redis connection, sets it up if it's missing and returns the Redis connection.
284
+ #
285
+ # todo: review thread status? (incase an exception killed it)
286
+ def redis_connection
287
+ return false unless defined?(Redis) && ENV['PL_REDIS_URL']
288
+ return @@redis if defined?(@@redis_sub_thread) && @@redis
289
+ @@redis_uri ||= URI.parse(ENV['PL_REDIS_URL'])
290
+ @@redis ||= Redis.new(host: @@redis_uri.host, port: @@redis_uri.port, password: @@redis_uri.password)
291
+ @@redis_sub_thread = Thread.new do
292
+ begin
293
+ Redis.new(host: @@redis_uri.host, port: @@redis_uri.port, password: @@redis_uri.password).subscribe(redis_channel_name) do |on|
294
+ on.message do |channel, msg|
295
+ args = JSON.parse(msg)
296
+ params = args.shift
297
+ __inner_process_broadcast params['_an_ignore_object'], params['_an_method_broadcasted'].to_sym, args
298
+ end
299
+ end
300
+ rescue Exception => e
301
+ Plezi.error e
302
+ retry
303
+ end
304
+ end
305
+ raise "Redis connction failed for: #{ENV['PL_REDIS_URL']}" unless @@redis
306
+ @@redis
307
+ end
308
+
309
+ # returns a Redis channel name for this controller.
310
+ def redis_channel_name
311
+ self.name.to_s
312
+ end
313
+
314
+ # broadcasts messages (methods) for this process
315
+ def __inner_process_broadcast ignore, method_name, args, &block
316
+ ObjectSpace.each_object(self) { |controller| Plezi.callback controller, method_name, *args, &block if controller.accepts_broadcast? && (!ignore || controller.uuid != ignore) }
317
+ end
318
+
319
+ # broadcasts messages (methods) between all processes (using Redis).
320
+ def __inner_redis_broadcast ignore, method_name, args, &block
321
+ return false unless redis_connection
322
+ raise "Radis broadcasts cannot accept blocks (no inter-process callbacks of memory sharing)!" if block
323
+ # raise "Radis broadcasts accept only one paramater, which is an optional Hash (no inter-process memory sharing)" if args.length > 1 || (args[0] && !args[0].is_a?(Hash))
324
+ args.unshift ({_an_method_broadcasted: method_name, _an_ignore_object: ignore})
325
+ redis_connection.publish(redis_channel_name, args.to_json )
326
+ true
327
+ end
328
+
329
+ # WebSockets.
330
+ #
331
+ # Class method.
332
+ #
333
+ # Use this to brodcast an event to all connections.
334
+ #
335
+ # accepts:
336
+ # method_name:: a Symbol with the method's name that should respond to the broadcast.
337
+ # *args:: any arguments that should be passed to the method (IF REDIS IS USED, LIMITATIONS APPLY).
338
+ #
339
+ # this method accepts and optional block (NON-REDIS ONLY) to be used as a callback for each sibling's event.
340
+ #
341
+ # the method will be called asynchrnously for each sibling instance of this Controller class.
342
+ def broadcast method_name, *args, &block
343
+ return false unless public_instance_methods.include?(method_name)
344
+ __inner_redis_broadcast(nil, method_name, args, &block) || __inner_process_broadcast(nil, method_name.to_sym, args, &block)
345
+ end
346
+
347
+ # WebSockets.
348
+ #
349
+ # Class method.
350
+ #
351
+ # Use this to collect data from all websockets for the calling class (websockets that have been created using the same Controller class).
352
+ #
353
+ # This method will call the requested method on all instance and return an Array of the returned values (including nil values).
354
+ #
355
+ # This method will block the excecution unless a block is passed to the method - in which case
356
+ # the block will used as a callback and recieve the Array as a parameter.
357
+ #
358
+ # i.e.
359
+ #
360
+ # this will block: `collect :_get_id`
361
+ #
362
+ # this will not block: `collect(:_get_id) {|a| puts "got #{a.length} responses."; a.each { |id| puts "#{id}"} }
363
+ #
364
+ # accepts:
365
+ # method_name:: a Symbol with the method's name that should respond to the broadcast.
366
+ # *args:: any arguments that should be passed to the method.
367
+ # &block:: an optional block to be used as a callback.
368
+ #
369
+ # the method will be called asynchrnously for each instance of this Controller class.
370
+ def collect method_name, *args, &block
371
+ return Plezi.push_event(self.method(:collect), *args, &block) if block
372
+
373
+ r = []
374
+ ObjectSpace.each_object(self) { |controller| r << controller.method(method_name).call(*args) if controller.accepts_broadcast? }
375
+ return r
376
+ end
377
+ end
378
+
379
+ module_function
380
+
381
+
382
+ end
383
+ end