webmate 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (62) hide show
  1. data/.gitignore +19 -0
  2. data/GUIDE.md +115 -0
  3. data/Gemfile +10 -0
  4. data/LICENSE.txt +22 -0
  5. data/README.md +47 -0
  6. data/Rakefile +1 -0
  7. data/bin/webmate +70 -0
  8. data/lib/webmate/application.rb +138 -0
  9. data/lib/webmate/config.rb +23 -0
  10. data/lib/webmate/decorators/base.rb +38 -0
  11. data/lib/webmate/env.rb +17 -0
  12. data/lib/webmate/logger.rb +20 -0
  13. data/lib/webmate/observers/base.rb +24 -0
  14. data/lib/webmate/presenters/base.rb +69 -0
  15. data/lib/webmate/presenters/base_presenter.rb +115 -0
  16. data/lib/webmate/presenters/scoped.rb +30 -0
  17. data/lib/webmate/responders/abstract.rb +127 -0
  18. data/lib/webmate/responders/base.rb +21 -0
  19. data/lib/webmate/responders/callbacks.rb +79 -0
  20. data/lib/webmate/responders/exceptions.rb +4 -0
  21. data/lib/webmate/responders/response.rb +36 -0
  22. data/lib/webmate/responders/templates.rb +65 -0
  23. data/lib/webmate/route_helpers/route.rb +91 -0
  24. data/lib/webmate/route_helpers/routes_collection.rb +273 -0
  25. data/lib/webmate/socket.io/actions/connection.rb +14 -0
  26. data/lib/webmate/socket.io/actions/handshake.rb +34 -0
  27. data/lib/webmate/socket.io/packets/ack.rb +5 -0
  28. data/lib/webmate/socket.io/packets/base.rb +156 -0
  29. data/lib/webmate/socket.io/packets/connect.rb +5 -0
  30. data/lib/webmate/socket.io/packets/disconnect.rb +5 -0
  31. data/lib/webmate/socket.io/packets/error.rb +5 -0
  32. data/lib/webmate/socket.io/packets/event.rb +5 -0
  33. data/lib/webmate/socket.io/packets/heartbeat.rb +5 -0
  34. data/lib/webmate/socket.io/packets/json.rb +5 -0
  35. data/lib/webmate/socket.io/packets/message.rb +5 -0
  36. data/lib/webmate/socket.io/packets/noop.rb +5 -0
  37. data/lib/webmate/support/em_mongoid.rb +53 -0
  38. data/lib/webmate/version.rb +3 -0
  39. data/lib/webmate/views/scope.rb +25 -0
  40. data/lib/webmate/websockets.rb +50 -0
  41. data/lib/webmate.rb +129 -0
  42. data/spec/lib/route_helpers/route_spec.rb +41 -0
  43. data/spec/spec_helper.rb +18 -0
  44. data/vendor/.DS_Store +0 -0
  45. data/vendor/assets/.DS_Store +0 -0
  46. data/vendor/assets/javascripts/.DS_Store +0 -0
  47. data/vendor/assets/javascripts/webmate/.DS_Store +0 -0
  48. data/vendor/assets/javascripts/webmate/auth.coffee +63 -0
  49. data/vendor/assets/javascripts/webmate/backbone_ext/.DS_Store +0 -0
  50. data/vendor/assets/javascripts/webmate/backbone_ext/resources.coffee +60 -0
  51. data/vendor/assets/javascripts/webmate/backbone_ext/sync.coffee +131 -0
  52. data/vendor/assets/javascripts/webmate/client.coffee +133 -0
  53. data/vendor/assets/javascripts/webmate/init.coffee +7 -0
  54. data/vendor/assets/javascripts/webmate/libs/.DS_Store +0 -0
  55. data/vendor/assets/javascripts/webmate/libs/backbone.js +1572 -0
  56. data/vendor/assets/javascripts/webmate/libs/benchmark.coffee +27 -0
  57. data/vendor/assets/javascripts/webmate/libs/icanhaz.js +542 -0
  58. data/vendor/assets/javascripts/webmate/libs/socket.io.js +3871 -0
  59. data/vendor/assets/javascripts/webmate/libs/underscore.js +1 -0
  60. data/vendor/assets/javascripts/webmate.js +10 -0
  61. data/webmate.gemspec +31 -0
  62. metadata +290 -0
@@ -0,0 +1,115 @@
1
+ module Webmate
2
+ class BasePresenter
3
+ include Webmate::Presenters::Scoped
4
+
5
+ attr_accessor :accessor, :resources
6
+
7
+ def initialize(resources)
8
+ raise ArgumentError, "Resources should not be blank" if resources.blank?
9
+ @resources = resources
10
+ @errors = []
11
+ end
12
+
13
+ def to_serializable
14
+ build_serialized default_resource do |object|
15
+ attributes object.attributes.keys
16
+ end
17
+ end
18
+
19
+ def errors
20
+ @errors
21
+ end
22
+
23
+ def to_json(options = {})
24
+ serialize_resource.to_json
25
+ end
26
+
27
+ private
28
+
29
+ def serialize_resource
30
+ if errors.present?
31
+ serialize_errors
32
+ else
33
+ to_serializable
34
+ end
35
+ end
36
+
37
+ def serialize_errors
38
+ build_serialized do
39
+ namespace 'errors' do
40
+ errors.each do |error|
41
+ attribute error.key do
42
+ error.value
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
48
+
49
+ def resource_by_name(name)
50
+ @resources.resource(name.to_sym)
51
+ end
52
+
53
+ def default_resource
54
+ @resources
55
+ end
56
+ end
57
+ end
58
+ =begin
59
+ class BasePresenter
60
+ include Serializers::Scoped
61
+
62
+ attr_accessor :accessor, :resources
63
+
64
+ def initialize(resources)
65
+ raise ArgumentError, "Resources should not be blank" if resources.blank?
66
+ @resources = resources
67
+ end
68
+
69
+ def to_serializable
70
+ build_serialized default_resource do |object|
71
+ attributes object.attributes.keys
72
+ end
73
+ end
74
+
75
+ def errors
76
+ resource_by_name(:errors) || []
77
+ end
78
+
79
+ def to_json(options = {})
80
+ serialize_resource.to_json
81
+ end
82
+
83
+ private
84
+
85
+ def serialize_resource
86
+ if errors.present?
87
+ serialize_errors
88
+ else
89
+ to_serializable
90
+ end
91
+ end
92
+
93
+ def serialize_errors
94
+ errors = resource_by_name(:errors)
95
+ build_serialized do
96
+ namespace 'errors' do
97
+ errors.each do |error|
98
+ attribute error.key do
99
+ error.value
100
+ end
101
+ end
102
+ end
103
+ end
104
+ end
105
+
106
+ def resource_by_name(name)
107
+ @resources.resource(name.to_sym)
108
+ end
109
+
110
+ def default_resource
111
+ @resources.resource(:default)
112
+ end
113
+
114
+ end
115
+ =end
@@ -0,0 +1,30 @@
1
+ module Webmate::Presenters
2
+ module Scoped
3
+ extend ActiveSupport::Concern
4
+
5
+ def build_serialized(*args, &block)
6
+ object = args.first
7
+ if object.is_a?(Array)
8
+ object.map do |obj|
9
+ get_serializable_attributes(obj, &block)
10
+ end
11
+ else
12
+ get_serializable_attributes(object, &block)
13
+ end
14
+ end
15
+
16
+ private
17
+
18
+ def get_serializable_attributes(object, &block)
19
+ serializer = serializable_class.new(object)
20
+ instance_exec do
21
+ serializer.instance_exec(object, &block)
22
+ end
23
+ serializer.attrs
24
+ end
25
+
26
+ def serializable_class
27
+ Webmate::Presenters::Base
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,127 @@
1
+ require 'em-hiredis'
2
+ require 'webmate/responders/callbacks'
3
+
4
+ module Webmate::Responders
5
+ class Abstract
6
+ attr_accessor :action, :path, :metadata, :params
7
+ attr_reader :request
8
+
9
+ # request info - current request params
10
+ def initialize(request_info)
11
+ @action = request_info[:action]
12
+ @path = request_info[:path]
13
+ @metadata = request_info[:metadata]
14
+ @params = request_info[:params]
15
+ @request = request_info[:request]
16
+
17
+ # publishing actions
18
+ @users_to_notify = []
19
+
20
+ @response = nil
21
+ end
22
+
23
+ def action_method
24
+ action.to_s
25
+ end
26
+
27
+ def params
28
+ @params.with_indifferent_access
29
+ end
30
+
31
+ def respond
32
+ process_action
33
+ rescue Exception => e
34
+ rescue_with_handler(e)
35
+ end
36
+
37
+ def respond_with(response, options = {})
38
+ if @response.is_a?(Response)
39
+ @response = response
40
+ else
41
+ default_options = {
42
+ status: 200,
43
+ path: path,
44
+ metadata: metadata,
45
+ params: params,
46
+ action: action
47
+ }
48
+ options = default_options.merge(options)
49
+ @response = Response.new(response, options)
50
+ end
51
+ # publish changes to users actions
52
+ async { publish(@response) }
53
+
54
+ @response
55
+ end
56
+
57
+ def rescue_with_handler(exception)
58
+ if handler = handler_for_rescue(exception)
59
+ handler.arity != 0 ? handler.call(exception) : handler.call
60
+ else
61
+ raise(exception)
62
+ end
63
+ end
64
+
65
+ def process_action
66
+ raise ActionNotFound unless respond_to?(action_method)
67
+ respond_with send(action_method)
68
+ end
69
+
70
+ def render_not_found
71
+ @response = Response.new("Action not Found", status: 200)
72
+ end
73
+
74
+ def async(&block)
75
+ block.call
76
+ end
77
+
78
+ include ActiveSupport::Rescuable
79
+ include Webmate::Responders::Callbacks
80
+
81
+ # subscriptions
82
+ def publisher
83
+ @publisher ||= build_connection
84
+ end
85
+
86
+ def build_connection
87
+ EM::Hiredis.connect
88
+ rescue
89
+ warn("problem with connections to redis")
90
+ nil
91
+ end
92
+
93
+ # publish current response to private channels
94
+ # for users in user_ids
95
+ #
96
+ # publish_to(123, 456)
97
+ def publish_to(*user_ids)
98
+ @users_to_notify += user_ids
99
+ end
100
+
101
+ # take @users_to_notify
102
+ # and pass back channels names with listeners
103
+ #
104
+ def channels_to_publish
105
+ @users_to_notify.flatten.each_with_object([]) do |user_id, channels|
106
+ channel_name = Webmate::Application.get_channel_name_for(user_id)
107
+ channels << channel_name if channel_active?(channel_name)
108
+ end
109
+ end
110
+
111
+ def channel_active?(channel_name)
112
+ return true # in development
113
+ end
114
+
115
+ def publish(response)
116
+ return if publisher.nil?
117
+
118
+ # prepare args for socket.io message packet
119
+ # this should be prepared data to create socket.io message
120
+ # without any additional actions
121
+ packet_data = Webmate::SocketIO::Packets::Message.prepare_packet_data(response)
122
+ data = Webmate::Application.dump(packet_data)
123
+
124
+ channels_to_publish.each {|channel_name| publisher.publish(channel_name, data) }
125
+ end
126
+ end
127
+ end
@@ -0,0 +1,21 @@
1
+ require 'webmate/responders/abstract'
2
+ module Webmate::Responders
3
+ class Base < Abstract
4
+ after_filter :_run_observer_callbacks
5
+ after_filter :_send_websocket_events
6
+
7
+ def _send_websocket_events
8
+ packet = Webmate::SocketIO::Packets::Message.new(@response.packed)
9
+
10
+ #async do
11
+ # #Webmate::Websockets.publish(params[:channel], packet.to_packet)
12
+ #end
13
+ end
14
+
15
+ def _run_observer_callbacks
16
+ async do
17
+ Webmate::Observers::Base.execute_all(action, @response)
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,79 @@
1
+ module Webmate::Responders
2
+ module Callbacks
3
+ extend ActiveSupport::Concern
4
+ include ActiveSupport::Callbacks
5
+
6
+ included do
7
+ define_callbacks :process_action
8
+ class_eval <<-RUBY_EVAL, __FILE__, __LINE__ + 1
9
+ alias_method :process_action!, :process_action
10
+ def process_action
11
+ run_callbacks(:process_action, action_method) do
12
+ process_action!
13
+ end
14
+ end
15
+ RUBY_EVAL
16
+ end
17
+
18
+ module ClassMethods
19
+ def _normalize_callback_options(options)
20
+ if only = options[:only]
21
+ only = Array(only).map {|o| "action_method == '#{o}'"}.join(" || ")
22
+ options[:per_key] = {:if => only}
23
+ end
24
+ if except = options[:except]
25
+ except = Array(except).map {|e| "action_method == '#{e}'"}.join(" || ")
26
+ options[:per_key] = {:unless => except}
27
+ end
28
+ end
29
+
30
+ def _insert_callbacks(callbacks, block)
31
+ options = callbacks.last.is_a?(Hash) ? callbacks.pop : {}
32
+ _normalize_callback_options(options)
33
+ callbacks.push(block) if block
34
+ callbacks.each do |callback|
35
+ yield callback, options
36
+ end
37
+ end
38
+
39
+ def skip_filter(*names, &blk)
40
+ skip_before_filter(*names)
41
+ skip_after_filter(*names)
42
+ skip_around_filter(*names)
43
+ end
44
+
45
+ [:before, :after, :around].each do |filter|
46
+ class_eval <<-RUBY_EVAL, __FILE__, __LINE__ + 1
47
+ # Append a before, after or around filter. See _insert_callbacks
48
+ # for details on the allowed parameters.
49
+ def #{filter}_filter(*names, &blk)
50
+ _insert_callbacks(names, blk) do |name, options|
51
+ options[:if] = (Array.wrap(options[:if]) << "!halted") if #{filter == :after}
52
+ set_callback(:process_action, :#{filter}, name, options)
53
+ end
54
+ end
55
+
56
+ # Prepend a before, after or around filter. See _insert_callbacks
57
+ # for details on the allowed parameters.
58
+ def prepend_#{filter}_filter(*names, &blk)
59
+ _insert_callbacks(names, blk) do |name, options|
60
+ options[:if] = (Array.wrap(options[:if]) << "!halted") if #{filter == :after}
61
+ set_callback(:process_action, :#{filter}, name, options.merge(:prepend => true))
62
+ end
63
+ end
64
+
65
+ # Skip a before, after or around filter. See _insert_callbacks
66
+ # for details on the allowed parameters.
67
+ def skip_#{filter}_filter(*names, &blk)
68
+ _insert_callbacks(names, blk) do |name, options|
69
+ skip_callback(:process_action, :#{filter}, name, options)
70
+ end
71
+ end
72
+
73
+ # *_filter is the same as append_*_filter
74
+ alias_method :append_#{filter}_filter, :#{filter}_filter
75
+ RUBY_EVAL
76
+ end
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,4 @@
1
+ module Webmate::Responders
2
+ class ActionNotFound < Exception; end
3
+ class TemplateNotFound < Exception; end
4
+ end
@@ -0,0 +1,36 @@
1
+ require 'webmate/responders/abstract'
2
+ module Webmate::Responders
3
+ class Response
4
+ attr_accessor :data, :status, :params, :action, :path, :metadata
5
+ def initialize(data, options = {})
6
+ @data = data
7
+ @status = options[:status] || 200
8
+ @params = options[:params] || {}
9
+ @action = options[:action] || @params[:action] || ''
10
+ @metadata = options[:metadata] || {}
11
+ @path = options[:path] || "/"
12
+ end
13
+
14
+ def json
15
+ Yajl::Encoder.new.encode(self.packed)
16
+ end
17
+
18
+ def packed
19
+ { action: @action, resource: @resource, response: @data, params: safe_params }
20
+ end
21
+
22
+ def safe_params
23
+ safe_params = {}
24
+ params.each do |key, value|
25
+ if value.is_a?(String) || value.is_a?(Integer)
26
+ safe_params[key] = value
27
+ end
28
+ end
29
+ safe_params
30
+ end
31
+
32
+ def rack_format
33
+ [@status, {}, @data]
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,65 @@
1
+ module Webmate::Responders
2
+ module Templates
3
+
4
+ def slim(template, options = {}, locals = {}, &block)
5
+ render(:slim, template, options, locals, &block)
6
+ end
7
+
8
+ private
9
+
10
+ def template_cache
11
+ @cache ||= Webmate::Application.template_cache
12
+ end
13
+
14
+ def scope
15
+ @scope ||= Webmate::Views::Scope.new(self)
16
+ end
17
+
18
+ def render(engine, data, options = {}, locals = {}, &block)
19
+ views = Webmate::Application.views
20
+ layouts = Webmate::Application.layouts
21
+
22
+ layout = options.delete(:layout) || false
23
+
24
+ # compile and render template
25
+ template = compile_template(engine, data, options, views)
26
+ output = template.render(scope, locals, &block)
27
+
28
+ if layout
29
+ layout_template = compile_template(engine, layout, options, layouts)
30
+ output = layout_template.render(scope, locals) { output }
31
+ end
32
+
33
+ output
34
+ end
35
+
36
+ def compile_template(engine, data, options, views)
37
+ template_cache.fetch engine, data, options, views do
38
+ template = Tilt[engine]
39
+
40
+ # find template ./views /name /action_name engine_name
41
+ file = find_template(views, data, engine)
42
+
43
+ template.new(file)
44
+ end
45
+ end
46
+
47
+ # search through all available paths
48
+ # [./views/]name.[extension].[engine]/
49
+ # this will not search for responder's custom folder
50
+ # responder
51
+ def find_template(views, name, engine)
52
+ responder_folder = self.class.name.underscore.sub(/_responder$/, '') # => namespace/responder_name
53
+
54
+ # NOTE: we can add shared, and other paths from settings
55
+ folders_to_search = [File.join(views, responder_folder, "*.#{engine.to_s}")]
56
+ folders_to_search << File.join(views, "*.#{engine.to_s}")
57
+
58
+ search_regexp = /\/#{name}[\w|\.]*.#{engine}$/
59
+ Dir[*folders_to_search].each do |file_path|
60
+ return file_path if search_regexp.match(file_path)
61
+ end
62
+ raise TemplateNotFound.new
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,91 @@
1
+ module Webmate
2
+ class Route
3
+ FIELDS = [:method, :path, :action, :transport, :responder, :route_regexp, :static_params]
4
+ attr_reader *FIELDS
5
+
6
+ # method: GET/POST/PUT/DELETE
7
+ # path : /user/123/posts/123/comments
8
+ # transport: HTTP/WS/
9
+ # responder: class, responsible to 'respond' action
10
+ # action: method in webmate responders, called to fetch data
11
+ # static params: additional params hash, which will be passed to responder
12
+ # for example, { :scope => :user }
13
+ #
14
+ def initialize(args)
15
+ values = args.with_indifferent_access
16
+ FIELDS.each do |field_name|
17
+ instance_variable_set("@#{field_name.to_s}", values[field_name])
18
+ end
19
+
20
+ normalize_data_if_needed
21
+ @route_regexp ||= construct_match_regexp
22
+ end
23
+
24
+ # method should check coincidence of path pattern and
25
+ # given path
26
+ # '/projects/qwerty123/tasks/asdf13/comments/zxcv123'
27
+ # will be parsed with route
28
+ # /projects/:project_id/tasks/:task_id/comments/:comment_id
29
+ # and return
30
+ # result = {
31
+ # action: 'read',
32
+ # responder: CommentsResponder,
33
+ # params: {
34
+ # project_id: 'qwerty123',
35
+ # task_id: :asdf13,
36
+ # comment_id: :zxcv123
37
+ # }
38
+ # }
39
+ def match(request_path)
40
+ if match_data = @route_regexp.match(request_path)
41
+ route_data = {
42
+ action: @action,
43
+ responder: @responder,
44
+ params: HashWithIndifferentAccess.new(static_params || {})
45
+ }
46
+ @substitution_attrs.each_with_index do |key, index|
47
+ if key == :splat
48
+ route_data[:params][key] ||= []
49
+ route_data[:params][key] += match_data[index.next].split('/')
50
+ else
51
+ route_data[:params][key] = match_data[index.next]
52
+ end
53
+ end
54
+ route_data
55
+ else
56
+ nil # not matched.
57
+ end
58
+ end
59
+
60
+ private
61
+
62
+ # /projects/:project_id/tasks/:task_id/comments/:comment_id
63
+ # result should be
64
+ # substitution_attrs = [:project_id, :task_id, :comment_id]
65
+ # route_regexp =
66
+ # (?-mix:^\/projects\/([\w\d]*)\/tasks\/([\w\d]*)\/comments\/([\w\d]*)\/?$)
67
+ #
68
+ # substitute :resource_id elements with regexp group in order
69
+ # to easy extract
70
+ def construct_match_regexp
71
+ substitutions = path.scan(/\/:(\w*)|\/(\*)/)
72
+ @substitution_attrs = substitutions.each_with_object([]) do |scan, attrs|
73
+ if scan[0]
74
+ attrs << scan[0].to_sym
75
+ elsif scan[1]
76
+ attrs << :splat
77
+ end
78
+ end
79
+ regexp_string = path.gsub(/\/:(\w*_id)/) {|t| "/([\\w\\d]*)" }
80
+ regexp_string = regexp_string.gsub(/\/\*/) {|t| "\/(.*)"}
81
+ Regexp.new("^#{regexp_string}\/?$")
82
+ end
83
+
84
+ # update attributes by following rules
85
+ # - responder should be a Class, not String
86
+ # - ..
87
+ def normalize_data_if_needed
88
+ @responder = @responder.to_s.classify.constantize unless @responder.is_a?(Class)
89
+ end
90
+ end
91
+ end