webmate 0.1.4 → 0.1.5

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore CHANGED
@@ -17,4 +17,5 @@ spec/reports
17
17
  test/tmp
18
18
  test/version_tmp
19
19
  tmp
20
- .DS_Store
20
+ .DS_Store
21
+ log
data/README.md CHANGED
@@ -1,6 +1,19 @@
1
1
  # Webmate
2
2
 
3
- Real-time web applications framework in Ruby, based on WebSockets and EventMachine.
3
+ Webmate is a fully asynchronous real-time web application framework in Ruby. It is built using EventMachine and WebSockets. Webmate primarily designed for providing full-duplex bi-directional communication.
4
+
5
+ ## Why Webmate?
6
+
7
+ Webmate provides high-level api to create applications based on Websocket. Instead of separating code to http/websocket, you write one code, which may work using http or websocket (or both).
8
+
9
+ Sample:
10
+
11
+ Webmate::Application.define_routes do
12
+ resources :tasks, transport: [:http, :WS]
13
+ end
14
+
15
+ This simple route will allow you create, update, delete tasks using ONE websocket connection.
16
+
4
17
 
5
18
  ## Quick start
6
19
 
data/lib/webmate.rb CHANGED
@@ -15,6 +15,7 @@ require 'webmate/application'
15
15
  require 'webmate/config'
16
16
  require 'webmate/websockets'
17
17
  require 'webmate/logger'
18
+ require 'webmate/support/json'
18
19
 
19
20
  require 'bundler'
20
21
  Bundler.setup
@@ -30,8 +31,9 @@ require 'webmate/responders/response'
30
31
  require 'webmate/responders/templates'
31
32
  require 'webmate/observers/base'
32
33
  require 'webmate/decorators/base'
33
- require 'webmate/route_helpers/routes_collection'
34
- require 'webmate/route_helpers/route'
34
+ require 'webmate/routes/collection'
35
+ require 'webmate/routes/base'
36
+ require 'webmate/routes/handler'
35
37
 
36
38
  Bundler.require(:default, Webmate.env.to_sym)
37
39
 
@@ -52,10 +54,13 @@ require 'webmate/presenters/base'
52
54
  require 'webmate/presenters/scoped'
53
55
  require 'webmate/presenters/base_presenter'
54
56
 
55
- # it's not correct. app config file should be required by app
56
- file = "#{Webmate.root}/config/config.rb"
57
- require file if FileTest.exists?(file)
57
+ # require priority initialization files
58
+ configatron.app.priotity_initialize_files.each do |path|
59
+ file = "#{Webmate.root}/#{path}"
60
+ require file if FileTest.exists?(file)
61
+ end
58
62
 
63
+ # auto-load files
59
64
  configatron.app.load_paths.each do |path|
60
65
  Dir[ File.join( Webmate.root, path, '**', '*.rb') ].each do |file|
61
66
  class_name = File.basename(file, '.rb')
@@ -65,17 +70,17 @@ configatron.app.load_paths.each do |path|
65
70
  end
66
71
  end
67
72
 
68
- # run observers
69
- Dir[ File.join( Webmate.root, 'app', 'observers', '**', '*.rb')].each do |file|
70
- require file
73
+ # require initialization files
74
+ configatron.app.initialize_paths.each do |path|
75
+ Dir[ File.join( Webmate.root, path, '**', '*.rb')].each do |file|
76
+ require file
77
+ end
71
78
  end
72
79
 
73
80
  class Webmate::Application
74
- #register Webmate::RouteHelpers::Channels
75
81
  register Sinatra::Reloader
76
82
  register SinatraMore::MarkupPlugin
77
83
 
78
- #helpers Webmate::Views::Helpers
79
84
  helpers Sinatra::Cookies
80
85
  helpers Webmate::Sprockets::Helpers
81
86
 
@@ -1,102 +1,25 @@
1
1
  module Webmate
2
2
  class Application < Sinatra::Base
3
3
  # override sinatra's method
4
- def route!(base = settings, pass_block = nil)
5
- transport = @request.websocket? ? 'WS' : 'HTTP'
6
-
7
- route_info = base.routes.match(@request.request_method, transport, @request.path)
4
+ def route!(base = settings, pass_block = nil)
5
+ route_info = find_route(base.routes, @request)
8
6
 
9
7
  # no route case - use default sinatra's processors
10
- if !route_info
8
+ if route_info
9
+ handler = Webmate::Routes::Handler.new(base, @request)
10
+ handler.handle(route_info)
11
+ else
11
12
  route_eval(&pass_block) if pass_block
12
13
  route_missing
13
14
  end
14
-
15
- if @request.websocket?
16
- unless authorized_to_open_connection?(route_info[:params][:scope])
17
- return [401, {}, []]
18
- end
19
-
20
- session_id = route_info[:params][:session_id].inspect
21
- Webmate::Websockets.subscribe(session_id, @request) do |message|
22
- if route_info = base.routes.match(message['method'], 'WS', message.path)
23
- request_info = {
24
- path: message.path,
25
- metadata: message.metadata || {},
26
- action: route_info[:action],
27
- params: message.params.merge(route_info[:params]),
28
- request: @request
29
- }
30
- # here we should create subscriber who can live
31
- # between messages.. but not between requests.
32
- response = route_info[:responder].new(request_info).respond
33
-
34
- # result of block will be sent back to user
35
- response
36
- end
37
- end
38
-
39
- # this response not pass to user - so we keep connection alive.
40
- # passing other response will close connection and socket
41
- non_pass_response = [-1, {}, []]
42
- return non_pass_response
43
-
44
- else # HTTP
45
- # this should return correct Rack response..
46
- request_info = params_for_responder(route_info)
47
- response = route_info[:responder].new(request_info).respond
48
-
49
- return response.rack_format
50
- end
51
- end
52
-
53
- # this method prepare data for responder
54
- # {
55
- # path: '/',
56
- # metadata: {},
57
- # action: 'index',
58
- # params: { test: true }
59
- # }
60
- def params_for_responder(route_info)
61
- # create unified request info
62
- # request_info = { path: '/', metadata: {}, action: 'index', params: { test: true } }
63
- request_params = parsed_request_params
64
- metadata = request_params.delete(:metadata)
65
- {
66
- path: @request.path,
67
- metadata: metadata || {},
68
- action: route_info[:action],
69
- params: request_params.merge(route_info[:params]),
70
- request: @request
71
- }
72
- end
73
-
74
- # @request.params working only for get params
75
- # and params in url line ?key=value
76
- def parsed_request_params
77
- request_params = HashWithIndifferentAccess.new
78
- request_params.merge!(@request.params || {})
79
-
80
- # read post or put params. this will erase params
81
- # {code: 123, mode: 123}
82
- # "code=123&mode=123"
83
- request_body = @request.body.read
84
- if request_body.present?
85
- body_params = begin
86
- JSON.parse(request_body) # {code: 123, mode: 123}
87
- rescue JSON::ParserError
88
- Rack::Utils.parse_nested_query(request_body) # "code=123&mode=123"
89
- end
90
- else
91
- body_params = {}
92
- end
93
-
94
- request_params.merge(body_params)
95
15
  end
96
16
 
97
- # update this method to create auth restrictions
98
- def authorized_to_open_connection?(scope = :user)
99
- return true
17
+ # Find matched route by routes collection and request
18
+ # @param Webmate::Routes::Collection routes
19
+ # @param Sinatra::Request request
20
+ def find_route(routes, request)
21
+ transport = request.websocket? ? 'WS' : 'HTTP'
22
+ routes.match(request.request_method, transport, request.path)
100
23
  end
101
24
 
102
25
  class << self
@@ -108,11 +31,11 @@ module Webmate
108
31
 
109
32
  def define_routes(&block)
110
33
  settings = Webmate::Application
111
- unless settings.routes.is_a?(RoutesCollection)
112
- routes = RoutesCollection.new()
34
+ unless settings.routes.is_a?(Routes::Collection)
35
+ routes = Routes::Collection.new()
113
36
  settings.set(:routes, routes)
114
37
  end
115
- settings.routes.define_routes(&block)
38
+ settings.routes.define(&block)
116
39
 
117
40
  routes
118
41
  end
@@ -121,14 +44,6 @@ module Webmate
121
44
  channel_name = "some-unique-key-for-app-#{user_id}"
122
45
  end
123
46
 
124
- def dump(obj)
125
- Yajl::Encoder.encode(obj)
126
- end
127
-
128
- def restore(str)
129
- Yajl::Parser.parse(str)
130
- end
131
-
132
47
  def load_tasks
133
48
  file_path = Pathname.new(__FILE__)
134
49
  load File.join(file_path.dirname, "../../tasks/routes.rake")
@@ -1,8 +1,10 @@
1
1
  Webmate::Application.configure do |config|
2
- config.app.load_paths = [
3
- "app/responders", "app/models", "app/services",
4
- "app/observers", "app/decorators", "app/routes"
5
- ]
2
+ # these files will be required with high priority
3
+ config.app.priotity_initialize_files = ["config/config.rb"]
4
+ # files from these paths will be required on prod env and auto-loaded on dev env
5
+ config.app.load_paths = ["app/responders", "app/models", "app/services", "app/decorators"]
6
+ # files from these paths will be required on on any env
7
+ config.app.initialize_paths = ["app/observers", "app/routes"]
6
8
  config.app.cache_classes = false
7
9
 
8
10
  config.app.name = 'webmate'
@@ -23,4 +25,5 @@ Webmate::Application.configure do |config|
23
25
 
24
26
  config.websockets.enabled = true
25
27
  config.websockets.port = 80
28
+ config.websockets.namespace = 'http_over_websocket'
26
29
  end
@@ -55,61 +55,3 @@ module Webmate
55
55
  end
56
56
  end
57
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
@@ -85,7 +85,7 @@ module Webmate::Responders
85
85
 
86
86
  def build_connection
87
87
  EM::Hiredis.connect
88
- rescue
88
+ rescue
89
89
  warn("problem with connections to redis")
90
90
  nil
91
91
  end
@@ -119,7 +119,7 @@ module Webmate::Responders
119
119
  # this should be prepared data to create socket.io message
120
120
  # without any additional actions
121
121
  packet_data = Webmate::SocketIO::Packets::Message.prepare_packet_data(response)
122
- data = Webmate::Application.dump(packet_data)
122
+ data = Webmate::JSON.dump(packet_data)
123
123
 
124
124
  channels_to_publish.each {|channel_name| publisher.publish(channel_name, data) }
125
125
  end
@@ -2,15 +2,6 @@ require 'webmate/responders/abstract'
2
2
  module Webmate::Responders
3
3
  class Base < Abstract
4
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
5
 
15
6
  def _run_observer_callbacks
16
7
  async do
@@ -11,25 +11,7 @@ module Webmate::Responders
11
11
  @path = options[:path] || "/"
12
12
  end
13
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
14
+ def to_rack
33
15
  [@status, {}, @data]
34
16
  end
35
17
  end
@@ -0,0 +1,99 @@
1
+ module Webmate::Routes
2
+ class Base
3
+ FIELDS = [:method, :path, :action, :transport, :responder, :route_regexp, :static_params]
4
+ attr_reader *FIELDS
5
+ attr_reader :regexp, :substitution_attrs
6
+
7
+ # method: GET/POST/PUT/DELETE
8
+ # path : /user/123/posts/123/comments
9
+ # transport: HTTP/WS/
10
+ # responder: class, responsible to 'respond' action
11
+ # action: method in webmate responders, called to fetch data
12
+ # static params: additional params hash, which will be passed to responder
13
+ # for example, { :scope => :user }
14
+ #
15
+ def initialize(args)
16
+ values = args.with_indifferent_access
17
+ FIELDS.each do |field_name|
18
+ instance_variable_set("@#{field_name.to_s}", values[field_name])
19
+ end
20
+
21
+ normalize_data
22
+ create_matching_regexp
23
+ create_substitution_attrs
24
+ end
25
+
26
+ # method should check coincidence of path pattern and
27
+ # given path
28
+ # '/projects/qwerty123/tasks/asdf13/comments/zxcv123'
29
+ # will be parsed with route
30
+ # /projects/:project_id/tasks/:task_id/comments/:comment_id
31
+ # and return
32
+ # result = {
33
+ # action: 'read',
34
+ # responder: CommentsResponder,
35
+ # params: {
36
+ # project_id: 'qwerty123',
37
+ # task_id: :asdf13,
38
+ # comment_id: :zxcv123
39
+ # }
40
+ # }
41
+ def match(request_path)
42
+ if regexp.match(request_path)
43
+ {
44
+ action: action,
45
+ responder: responder,
46
+ params: (static_params || {}).merge(params_from_path(request_path))
47
+ }
48
+ end
49
+ end
50
+
51
+ def params_from_path(path)
52
+ match_data = regexp.match(path)
53
+ params = {}
54
+ substitution_attrs.each_with_index do |key, index|
55
+ if key == :splat
56
+ params[key] ||= []
57
+ params[key] += match_data[index.next].split('/')
58
+ else
59
+ params[key] = match_data[index.next]
60
+ end
61
+ end
62
+ params
63
+ end
64
+
65
+ private
66
+
67
+ # /projects/:project_id/tasks/:task_id/comments/:comment_id
68
+ # result should be
69
+ # substitution_attrs = [:project_id, :task_id, :comment_id]
70
+ # route_regexp =
71
+ # (?-mix:^\/projects\/([\w\d]*)\/tasks\/([\w\d]*)\/comments\/([\w\d]*)\/?$)
72
+ #
73
+ # substitute :resource_id elements with regexp group in order
74
+ # to easy extract
75
+ def create_substitution_attrs
76
+ substitutions = path.scan(/\/:(\w*)|\/(\*)/)
77
+ @substitution_attrs = substitutions.each_with_object([]) do |scan, attrs|
78
+ if scan[0]
79
+ attrs << scan[0].to_sym
80
+ elsif scan[1]
81
+ attrs << :splat
82
+ end
83
+ end
84
+ end
85
+
86
+ def create_matching_regexp
87
+ regexp_string = path.gsub(/\/:(\w*_id)/) {|t| "/([\\w\\d]*)" }
88
+ regexp_string = regexp_string.gsub(/\/\*/) {|t| "\/(.*)"}
89
+ @regexp = Regexp.new("^#{regexp_string}\/?$")
90
+ end
91
+
92
+ # update attributes by following rules
93
+ # - responder should be a Class, not String
94
+ # - ..
95
+ def normalize_data
96
+ @responder = @responder.to_s.classify.constantize unless @responder.is_a?(Class)
97
+ end
98
+ end
99
+ end
@@ -1,5 +1,5 @@
1
- module Webmate
2
- class RoutesCollection
1
+ module Webmate::Routes
2
+ class Collection
3
3
  TRANSPORTS = [:ws, :http]
4
4
 
5
5
  attr_reader :routes
@@ -7,22 +7,28 @@ module Webmate
7
7
  def initialize
8
8
  @routes = {}
9
9
  @resource_scope = []
10
-
11
- enable_websockets_support
10
+
11
+ enable_websockets if configatron.websockets.enabled
12
12
  end
13
13
 
14
- def define_routes(&block)
14
+ # Call this method to define routes in application
15
+ # Examples:
16
+ # routes = Webmate::Routes::Collection.new
17
+ # routes.define do
18
+ # resources :projects
19
+ # end
20
+ def define(&block)
15
21
  instance_eval(&block)
16
22
  end
17
23
 
18
- # get info about matched route
19
- # method - GET/POST/PUT/PATCH/DELETE
20
- # transport - HTTP / WS [ HTTPS / WSS ]
21
- # path - /projects/123/tasks
24
+ # Get list of matched routes
22
25
  #
26
+ # @param String method - GET/POST/PUT/PATCH/DELETE
27
+ # @param String transport - HTTP / WS [ HTTPS / WSS ]
28
+ # @param String path - /projects/123/tasks
29
+ # @return [Hash, nil]
23
30
  def match(method, transport, path)
24
- routes = get_routes(method, transport)
25
- routes.each do |route|
31
+ get_routes(method, transport.upcase).each do |route|
26
32
  if info = route.match(path)
27
33
  return info
28
34
  end
@@ -30,6 +36,11 @@ module Webmate
30
36
  nil
31
37
  end
32
38
 
39
+ # Get routes by method and transport
40
+ #
41
+ # @param String method - GET/POST/PUT/PATCH/DELETE
42
+ # @param String transport - HTTP / WS [ HTTPS / WSS ]
43
+ # @return Array list of routes
33
44
  def get_routes(method, transport)
34
45
  @routes[method] ||= {}
35
46
  @routes[method][transport] || []
@@ -40,32 +51,36 @@ module Webmate
40
51
  # if websockets enabled, we should add specific http routes
41
52
  # - for handshake [ get session id ]
42
53
  # - for connection opening [ switch protocol from http to ws ]
43
- def enable_websockets_support
54
+ def enable_websockets
44
55
  namespace = configatron.websockets.namespace
45
- namespace = 'api' if namespace.blank? # || not working with configatron
56
+ namespace = 'http_over_websocket' if namespace.blank?
46
57
 
47
- route_options = { method: 'GET', transport: ['HTTP'], action: 'websocket' }
58
+ route_options = { method: 'GET', transport: ['HTTP'] }
48
59
 
49
60
  # handshake
50
- add_route(Webmate::Route.new(route_options.merge(
61
+ add_route(route_options.merge(
51
62
  path: "/#{namespace}/:version_id",
52
63
  responder: Webmate::SocketIO::Actions::Handshake,
53
- )))
64
+ action: 'websocket'
65
+ ))
54
66
 
55
67
  # transport connection
56
- add_route(Webmate::Route.new(route_options.merge(
68
+ add_route(route_options.merge(
57
69
  transport: ["WS"],
58
70
  path: "/#{namespace}/:version_id/websocket/:session_id",
59
71
  responder: Webmate::SocketIO::Actions::Connection,
60
72
  action: 'open'
61
- )))
73
+ ))
62
74
  end
63
75
 
64
- # we store routes in following structure
65
- # { method:
66
- # transport: [ routes ]
67
- # route - valid object of Webmate::Route class
76
+ # Add router object to routes
77
+ #
78
+ # @param [Webmate::Routes::Base, Hash] route
68
79
  def add_route(route)
80
+ unless route.is_a?(Webmate::Routes::Base)
81
+ route = Webmate::Routes::Base.new(route)
82
+ end
83
+
69
84
  # add route to specific node of routes hash
70
85
  @routes[route.method.to_s.upcase] ||= {}
71
86
  route.transport.each do |transport|
@@ -74,8 +89,9 @@ module Webmate
74
89
  end
75
90
 
76
91
  # define methods for separate routes
77
- # get '/path', to: , transport: ,
78
- # or
92
+ #
93
+ # Examples:
94
+ # get '/path', to: 'tasks#list', transport: [:http]
79
95
  # resources :projects
80
96
  # member do
81
97
  # get 'read_formatted'
@@ -93,7 +109,7 @@ module Webmate
93
109
  end
94
110
  route_options[:method] = method_name.to_sym
95
111
 
96
- add_route(Webmate::Route.new(route_options))
112
+ add_route(route_options)
97
113
  end
98
114
  end
99
115
 
@@ -195,7 +211,7 @@ module Webmate
195
211
  #
196
212
  # member do
197
213
  # get "do_on_member"
198
- # end
214
+ # end
199
215
  # prefix /resource_name/resource_id
200
216
  def member(&block)
201
217
  return if @resource_scope.blank?
@@ -230,7 +246,7 @@ module Webmate
230
246
 
231
247
 
232
248
  # helper methods
233
- # normalize_transport_option
249
+ # normalize_transport_option
234
250
  # returns array of requested transports, but available ones only
235
251
  def normalized_transport_option(transport = nil)
236
252
  return TRANSPORTS.dup if transport.blank?
@@ -250,7 +266,7 @@ module Webmate
250
266
  methods.map{|m| m.to_s.downcase.to_sym} & default_methods
251
267
  end
252
268
 
253
- def define_resource_read_all_method(resource_name, route_args)
269
+ def define_resource_read_all_method(resource_name, route_args)
254
270
  get "#{path_prefix}/#{resource_name}", route_args.merge(action: :read_all)
255
271
  end
256
272
 
@@ -0,0 +1,90 @@
1
+ module Webmate::Routes
2
+ class Handler
3
+ attr_accessor :application, :request
4
+
5
+ def initialize(application, request)
6
+ @application = application
7
+ @request = request
8
+ end
9
+
10
+ def handle(route_info)
11
+ if request.websocket?
12
+ unless websocket_connection_authorized?(request)
13
+ return [401, {}, []]
14
+ end
15
+
16
+ session_id = route_info[:params][:session_id]
17
+ Webmate::Websockets.subscribe(session_id, request) do |message|
18
+ if route_info = application.routes.match(message['method'], 'WS', message.path)
19
+ request_info = params_from_websoket(route_info, message)
20
+ # here we should create subscriber who can live
21
+ # between messages.. but not between requests.
22
+ route_info[:responder].new(request_info).respond
23
+ end
24
+ end
25
+
26
+ # this response not pass to user - so we keep connection alive.
27
+ # passing other response will close connection and socket
28
+ [-1, {}, []]
29
+
30
+ else # HTTP
31
+ # this should return correct Rack response..
32
+ request_info = params_from_http(route_info)
33
+ response = route_info[:responder].new(request_info).respond
34
+ response.to_rack
35
+ end
36
+ end
37
+
38
+ # this method prepare data for responder from http request
39
+ # @param Hash request_info = { path: '/', metadata: {}, action: 'index', params: { test: true } }
40
+ def params_from_http(route_info)
41
+ # create unified request info
42
+ request_params = http_body_request_params
43
+ metadata = request_params.delete(:metadata)
44
+ {
45
+ path: request.path,
46
+ metadata: metadata || {},
47
+ action: route_info[:action],
48
+ params: request_params.merge(route_info[:params]),
49
+ request: request
50
+ }
51
+ end
52
+
53
+ # this method prepare data for responder from http request
54
+ def params_from_websoket(route_info, message)
55
+ {
56
+ path: message.path,
57
+ metadata: message.metadata || {},
58
+ action: route_info[:action],
59
+ params: message.params.merge(route_info[:params])
60
+ }
61
+ end
62
+
63
+ # Get and parse all request params
64
+ def http_body_request_params
65
+ request_params = HashWithIndifferentAccess.new
66
+ request_params.merge!(@request.params || {})
67
+
68
+ # read post or put params. this will erase params
69
+ # {code: 123, mode: 123}
70
+ # "code=123&mode=123"
71
+ request_body = @request.body.read
72
+ if request_body.present?
73
+ body_params = begin
74
+ JSON.parse(request_body) # {code: 123, mode: 123}
75
+ rescue JSON::ParserError
76
+ Rack::Utils.parse_nested_query(request_body) # "code=123&mode=123"
77
+ end
78
+ else
79
+ body_params = {}
80
+ end
81
+
82
+ request_params.merge(body_params)
83
+ end
84
+
85
+ # Check that client with that scope is authorized to open connection
86
+ def websocket_connection_authorized?(request)
87
+ true
88
+ end
89
+ end
90
+ end
@@ -23,8 +23,8 @@ module Webmate
23
23
  @packet_data = packet_data.with_indifferent_access
24
24
  end
25
25
 
26
- # packet should be created by socket.io spec
27
- #[message type] ':' [message id ('+')] ':' [message endpoint] (':' [message data])
26
+ # packet should be created by socket.io spec
27
+ # [message type] ':' [message id ('+')] ':' [message endpoint] (':' [message data])
28
28
  # and webmate spec
29
29
  # message_data = {
30
30
  # method: GET/POST/...
@@ -59,14 +59,13 @@ module Webmate
59
59
  end
60
60
 
61
61
  # convert response from Responders::Base to socket io message
62
- #
62
+ #
63
63
  def self.build_response_packet(response)
64
64
  new(self.prepare_packet_data(response))
65
65
  end
66
66
 
67
67
  def self.prepare_packet_data(response)
68
68
  packet_data = {
69
- action: response.action,
70
69
  body: response.data,
71
70
  path: response.path,
72
71
  params: response.params,
@@ -75,12 +74,11 @@ module Webmate
75
74
  end
76
75
 
77
76
  # socket io spec
78
- #[message type] ':' [message id ('+')] ':' [message endpoint] (':' [message data])
77
+ #[message type] ':' [message id ('+')] ':' [message endpoint] (':' [message data])
79
78
  def to_packet
80
79
  data = {
81
- action: action,
82
- request: {
83
- path: path,
80
+ request: {
81
+ path: path,
84
82
  metadata: metadata
85
83
  },
86
84
  response: {
@@ -88,7 +86,7 @@ module Webmate
88
86
  status: status || 200
89
87
  }
90
88
  }
91
- encoded_data = Yajl::Encoder.new.encode(data)
89
+ encoded_data = JSON.dump(data)
92
90
  [
93
91
  packet_type_id,
94
92
  packet_id,
@@ -117,10 +115,6 @@ module Webmate
117
115
  packet_data[:path]
118
116
  end
119
117
 
120
- def action
121
- packet_data[:action]
122
- end
123
-
124
118
  def params
125
119
  packet_data[:params]
126
120
  end
@@ -137,14 +131,8 @@ module Webmate
137
131
  @id ||= generate_packet_id
138
132
  end
139
133
 
140
- # update counter
141
- def packet_id=(new_packet_id)
142
- self.class.current_id = new_packet_id
143
- @id ||= generate_packet_id
144
- end
145
-
146
134
  # unique packet id
147
- # didn't find any influence for now,
135
+ # didn't find any influence for now,
148
136
  # uniqueness not matter
149
137
  def generate_packet_id
150
138
  self.class.current_id ||= 0
@@ -0,0 +1,15 @@
1
+ module Webmate
2
+ class JSON
3
+ class << self
4
+ def dump(obj)
5
+ Yajl::Encoder.encode(obj)
6
+ end
7
+
8
+ def parse(str)
9
+ Yajl::Parser.parse(str)
10
+ rescue
11
+ raise JSON::ParserError
12
+ end
13
+ end
14
+ end
15
+ end
@@ -1,3 +1,3 @@
1
1
  module Webmate
2
- VERSION = '0.1.4'
2
+ VERSION = '0.1.5'
3
3
  end
@@ -1,7 +1,6 @@
1
1
  module Webmate::Views
2
2
  class Scope
3
3
  include Sinatra::Cookies
4
- include Webmate::Sprockets::Helpers
5
4
 
6
5
  def initialize(responder)
7
6
  @responder = responder
@@ -2,7 +2,7 @@ module Webmate
2
2
  class Websockets
3
3
  class << self
4
4
  def subscribe(session_id, request, &block)
5
- user_id = request.env['warden'].user.id
5
+ user_id = request.env['warden'].try(:user).try(:id)
6
6
 
7
7
  request.websocket do |websocket|
8
8
  # subscribe user to redis channel
@@ -14,12 +14,14 @@ module Webmate
14
14
  end
15
15
 
16
16
  websocket.onmessage do |message|
17
- response = block.call(Webmate::SocketIO::Packets::Base.parse(message))
17
+ request = Webmate::SocketIO::Packets::Base.parse(message)
18
+ response = block.call(request)
18
19
  if response
19
20
  packet = Webmate::SocketIO::Packets::Message.build_response_packet(response)
20
21
  websocket.send(packet.to_packet)
21
22
  else
22
- warn("empty response for #{message.inspect}")
23
+ packet = Webmate::SocketIO::Packets::Error.build_response_packet("{error: 'empty response for #{message.inspect}'}")
24
+ websocket.send(packet.to_packet)
23
25
  end
24
26
  end
25
27
 
@@ -37,7 +39,7 @@ module Webmate
37
39
  warn("user has been subscribed to channel '#{channel_name}'")
38
40
 
39
41
  subscriber.on(:message) do |channel, message_data|
40
- response_data = Webmate::Application.restore(message_data)
42
+ response_data = Webmate::JSON.parse(message_data)
41
43
  packet = Webmate::SocketIO::Packets::Message.new(response_data)
42
44
 
43
45
  websocket.send(packet.to_packet)
@@ -0,0 +1,20 @@
1
+ require 'spec_helper'
2
+
3
+ class FooResponder; end
4
+
5
+ describe Webmate::Application do
6
+
7
+ let(:subject) { Webmate::Application }
8
+
9
+ describe "#define_routes" do
10
+ context "responder and action from params" do
11
+ it "should define applicatio routes" do
12
+ subject.define_routes do
13
+ get '/projects', responder: FooResponder, action: 'bar'
14
+ end
15
+ route = subject.routes.match('GET', 'HTTP', '/projects')
16
+ route[:responder].should == FooResponder
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,46 @@
1
+ require 'spec_helper'
2
+
3
+ class TestResponder; end
4
+
5
+ def build_route_for(path, action = "any", responder = "test_responder")
6
+ route_args = {
7
+ method: "any",
8
+ transport: "transport",
9
+ }.merge(
10
+ path: path,
11
+ action: action,
12
+ responder: responder
13
+ )
14
+
15
+ Webmate::Routes::Base.new(route_args)
16
+ end
17
+
18
+ describe Webmate::Routes::Base do
19
+ describe "#match" do
20
+ it "should match simple routes" do
21
+ result = build_route_for('/projects').match("/projects")
22
+ result.should_not be_nil
23
+ end
24
+
25
+ it "should match empty routes" do
26
+ result = build_route_for('/').match("/")
27
+ result.should_not be_nil
28
+ end
29
+
30
+ it "should match routes with placements" do
31
+ result = build_route_for('/projects/:project_id').match("/projects/qwerty")
32
+ result.should_not be_nil
33
+ result[:params][:project_id].should == 'qwerty'
34
+ end
35
+
36
+ it "should match routes with wildcards" do
37
+ route = build_route_for('/projects/*')
38
+ result = build_route_for('/projects/*').match("/projects/qwerty/code")
39
+ end
40
+
41
+ it "should return nil for unmatched route" do
42
+ route = build_route_for('/foo').match('/bar')
43
+ route.should be_nil
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,75 @@
1
+ require 'spec_helper'
2
+
3
+ class FooResponder; end
4
+ class ProjectsResponder; end
5
+
6
+ describe Webmate::Routes::Collection do
7
+
8
+ let(:subject) { Webmate::Routes::Collection.new }
9
+
10
+ describe "#define" do
11
+ context "responder and action from params" do
12
+ it "should allow setting responder and action using params" do
13
+ subject.define do
14
+ get '/projects', responder: FooResponder, action: 'bar'
15
+ end
16
+ route = subject.match('GET', 'HTTP', '/projects')
17
+ route[:responder].should == FooResponder
18
+ route[:action].should == 'bar'
19
+ end
20
+
21
+ it "should allow setting responder and action using :to option" do
22
+ subject.define do
23
+ get '/projects', to: 'foo#bar'
24
+ end
25
+ route = subject.match('GET', 'HTTP', '/projects')
26
+ route[:responder].should == FooResponder
27
+ route[:action].to_s.should == 'bar'
28
+ end
29
+ end
30
+
31
+ context "responder and action by resource scope" do
32
+ before do
33
+ subject.define do
34
+ resources :projects
35
+ end
36
+ end
37
+
38
+ it "index action" do
39
+ route = subject.match('GET', 'HTTP', '/projects')
40
+ route[:responder].should == ProjectsResponder
41
+ route[:action].to_s.should == 'read_all'
42
+ end
43
+
44
+ it "show action" do
45
+ route = subject.match('GET', 'HTTP', '/projects/1')
46
+ route[:responder].should == ProjectsResponder
47
+ route[:action].to_s.should == 'read'
48
+ end
49
+ end
50
+ end
51
+
52
+ describe "#match" do
53
+ before do
54
+ subject.define do
55
+ get '/foo', responder: FooResponder, action: 'bar'
56
+ resources :projects, transport: [:http]
57
+ end
58
+ end
59
+
60
+ it "should return matched route" do
61
+ subject.match('GET', 'HTTP', '/foo').should_not be_nil
62
+ subject.match('GET', 'WS', '/foo').should_not be_nil
63
+ end
64
+
65
+ it "should return route only for matched transport" do
66
+ subject.match('GET', 'HTTP', '/projects/1').should_not be_nil
67
+ subject.match('GET', 'WS', '/projects/1').should be_nil
68
+ end
69
+
70
+ it "should return nil if no route found" do
71
+ subject.match('GET', 'HTTP', '/bar').should be_nil
72
+ subject.match('GET', 'WS', '/bar').should be_nil
73
+ end
74
+ end
75
+ end
data/spec/spec_helper.rb CHANGED
@@ -2,7 +2,7 @@ dir = File.expand_path(File.dirname(__FILE__))
2
2
 
3
3
  WEBMATE_ROOT = File.join(dir, '..')
4
4
 
5
- SPECDIR = dir
5
+ SPECDIR = dir
6
6
  $LOAD_PATH.unshift("#{dir}/../lib")
7
7
 
8
8
  require 'rubygems'
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: webmate
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.4
4
+ version: 0.1.5
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2013-07-09 00:00:00.000000000 Z
12
+ date: 2013-07-26 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: thin
@@ -217,8 +217,9 @@ files:
217
217
  - lib/webmate/responders/exceptions.rb
218
218
  - lib/webmate/responders/response.rb
219
219
  - lib/webmate/responders/templates.rb
220
- - lib/webmate/route_helpers/route.rb
221
- - lib/webmate/route_helpers/routes_collection.rb
220
+ - lib/webmate/routes/base.rb
221
+ - lib/webmate/routes/collection.rb
222
+ - lib/webmate/routes/handler.rb
222
223
  - lib/webmate/socket.io/actions/connection.rb
223
224
  - lib/webmate/socket.io/actions/handshake.rb
224
225
  - lib/webmate/socket.io/packets/ack.rb
@@ -231,11 +232,13 @@ files:
231
232
  - lib/webmate/socket.io/packets/json.rb
232
233
  - lib/webmate/socket.io/packets/message.rb
233
234
  - lib/webmate/socket.io/packets/noop.rb
234
- - lib/webmate/support/em_mongoid.rb
235
+ - lib/webmate/support/json.rb
235
236
  - lib/webmate/version.rb
236
237
  - lib/webmate/views/scope.rb
237
238
  - lib/webmate/websockets.rb
238
- - spec/lib/route_helpers/route_spec.rb
239
+ - spec/lib/application_spec.rb
240
+ - spec/lib/routes/base_spec.rb
241
+ - spec/lib/routes/collection_spec.rb
239
242
  - spec/spec_helper.rb
240
243
  - webmate.gemspec
241
244
  homepage: https://github.com/webmate/webmate
@@ -253,7 +256,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
253
256
  version: '0'
254
257
  segments:
255
258
  - 0
256
- hash: 2410033587434335146
259
+ hash: 1691783446799875159
257
260
  required_rubygems_version: !ruby/object:Gem::Requirement
258
261
  none: false
259
262
  requirements:
@@ -262,7 +265,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
262
265
  version: '0'
263
266
  segments:
264
267
  - 0
265
- hash: 2410033587434335146
268
+ hash: 1691783446799875159
266
269
  requirements: []
267
270
  rubyforge_project:
268
271
  rubygems_version: 1.8.25
@@ -1,91 +0,0 @@
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
@@ -1,53 +0,0 @@
1
- # TODO: this is needed to use latest mongoid with moped, but it doesn't work at this moment
2
-
3
- begin
4
- require "moped"
5
- rescue LoadError => error
6
- raise "Missing EM-Synchrony dependency: gem install moped"
7
- end
8
-
9
- module Moped
10
- class TimeoutHandler
11
- def self.timeout(op_timeout, &block)
12
- f = Fiber.current
13
- timer = EM::Timer.new(op_timeout) { f.resume(nil) }
14
- res = block.call
15
- timer.cancel
16
- res
17
- end
18
- end
19
- module Sockets
20
- module Connectable
21
- module ClassMethods
22
- def connect(host, port, timeout)
23
- TimeoutHandler.timeout(timeout) do
24
- sock = new(host, port)
25
- #sock.set_encoding('binary')
26
- #sock.setsockopt(Socket::IPPROTO_TCP, Socket::TCP_NODELAY, 1)
27
- sock
28
- end
29
- end
30
- end
31
- end
32
-
33
- class EM_TCP < ::EventMachine::Synchrony::TCPSocket
34
- include Connectable
35
-
36
- def connection_completed
37
- @opening = false
38
- @in_req.succeed(self) if @in_req
39
- end
40
- end
41
- Mutex = ::EventMachine::Synchrony::Thread::Mutex
42
- ConditionVariable = ::EventMachine::Synchrony::Thread::ConditionVariable
43
- end
44
- class Connection
45
- def connect
46
- @sock = if !!options[:ssl]
47
- Sockets::SSL.connect(host, port, timeout)
48
- else
49
- Sockets::EM_TCP.connect(host, port, timeout)
50
- end
51
- end
52
- end
53
- end
@@ -1,41 +0,0 @@
1
- require 'spec_helper'
2
-
3
- # responder to use as param for route creation.
4
- # should not be used for another
5
- class TestResponder; end
6
-
7
- def build_route_for(path, action = "any", responder = "test_responder")
8
- route_args = {
9
- method: "any",
10
- transport: "transport",
11
- }.merge(
12
- path: path,
13
- action: action,
14
- responder: responder
15
- )
16
-
17
- Webmate::Route.new(route_args)
18
- end
19
-
20
- describe Webmate::Route do
21
- it "should match simple routes" do
22
- result = build_route_for('/projects').match("/projects")
23
- result.should_not be_nil
24
- end
25
-
26
- it "should match empty routes" do
27
- result = build_route_for('/').match("/")
28
- result.should_not be_nil
29
- end
30
-
31
- it "should match routes with placements" do
32
- result = build_route_for('/projects/:project_id').match("/projects/qwerty")
33
- result.should_not be_nil
34
- result[:params][:project_id].should == 'qwerty'
35
- end
36
-
37
- it "should match routes with wildcards" do
38
- route = build_route_for('/projects/*')
39
- result = build_route_for('/projects/*').match("/projects/qwerty/code")
40
- end
41
- end