webmate 0.1.4 → 0.1.5

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