webmate 0.1.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 (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