plezi 0.7.0

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