shove 0.52 → 1.0.1

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 (63) hide show
  1. data/.gitignore +5 -0
  2. data/Gemfile +17 -0
  3. data/Gemfile.lock +63 -0
  4. data/README.markdown +263 -0
  5. data/Rakefile +47 -0
  6. data/bin/shove +128 -106
  7. data/lib/shove/app.rb +78 -0
  8. data/lib/shove/app_directory.rb +104 -0
  9. data/lib/shove/client/callback.rb +24 -0
  10. data/lib/shove/client/channel.rb +81 -0
  11. data/lib/shove/client/connection.rb +167 -0
  12. data/lib/shove/http/channel_context.rb +60 -0
  13. data/lib/shove/http/client_context.rb +82 -0
  14. data/lib/shove/http/request.rb +94 -0
  15. data/lib/shove/http/response.rb +30 -0
  16. data/lib/shove/protocol.rb +53 -0
  17. data/lib/shove.rb +60 -78
  18. data/shove.gemspec +11 -20
  19. data/spec/app_directory_spec.rb +66 -0
  20. data/spec/cassettes/should_authorize_oneself.yml +24 -0
  21. data/spec/cassettes/should_be_able_to_authorize_with_the_server.yml +24 -0
  22. data/spec/cassettes/should_cancel_a_binding.yml +24 -0
  23. data/spec/cassettes/should_configure_the_default.yml +24 -0
  24. data/spec/cassettes/should_configure_the_from_the_previous_test.yml +24 -0
  25. data/spec/cassettes/should_create_a_channel_context.yml +24 -0
  26. data/spec/cassettes/should_deny_a_connection.yml +24 -0
  27. data/spec/cassettes/should_deny_a_control_to_a_client.yml +24 -0
  28. data/spec/cassettes/should_deny_a_publishing_to_a_client.yml +24 -0
  29. data/spec/cassettes/should_deny_a_subscriptions_to_a_client.yml +24 -0
  30. data/spec/cassettes/should_deny_publishing_on_a_channel_context.yml +24 -0
  31. data/spec/cassettes/should_deny_subscriptions_on_a_channel_context.yml +24 -0
  32. data/spec/cassettes/should_get_a_set_of_nodes_for_the_network.yml +24 -0
  33. data/spec/cassettes/should_get_a_subscribe_granted_event.yml +24 -0
  34. data/spec/cassettes/should_grant_a_connection.yml +24 -0
  35. data/spec/cassettes/should_grant_a_control_to_a_client.yml +24 -0
  36. data/spec/cassettes/should_grant_a_publishing_to_a_client.yml +24 -0
  37. data/spec/cassettes/should_grant_a_subscriptions_to_a_client.yml +24 -0
  38. data/spec/cassettes/should_grant_publishing_on_a_channel_context.yml +24 -0
  39. data/spec/cassettes/should_grant_subscriptions_on_a_channel_context.yml +24 -0
  40. data/spec/cassettes/should_publish.yml +24 -0
  41. data/spec/cassettes/should_publish_on_a_channel_context.yml +24 -0
  42. data/spec/cassettes/should_publish_to_a_client.yml +24 -0
  43. data/spec/cassettes/should_receive_an_unsubscribe_event.yml +24 -0
  44. data/spec/cassettes/should_receive_messages_on_a_channel.yml +24 -0
  45. data/spec/cassettes/should_send_a_connect_op.yml +24 -0
  46. data/spec/cassettes/should_send_a_connect_op_with_an_id.yml +24 -0
  47. data/spec/cassettes/should_spawn_a_client.yml +24 -0
  48. data/spec/cassettes/should_subscribe_to_a_channel.yml +24 -0
  49. data/spec/cassettes/should_trigger_a_connect_denied_event.yml +24 -0
  50. data/spec/cassettes/should_trigger_a_connect_event.yml +24 -0
  51. data/spec/cassettes/should_trigger_a_disconnect_event.yml +24 -0
  52. data/spec/cassettes/should_trigger_an_error_event.yml +24 -0
  53. data/spec/cassettes/should_unsubscribe_from_a_channel.yml +24 -0
  54. data/spec/cassettes/should_update_the_default_app.yml +24 -0
  55. data/spec/helper.rb +60 -0
  56. data/spec/shove_client_spec.rb +194 -0
  57. data/spec/shove_http_spec.rb +142 -0
  58. metadata +111 -81
  59. data/README.md +0 -71
  60. data/lib/shove/client.rb +0 -54
  61. data/lib/shove/request.rb +0 -80
  62. data/lib/shove/response.rb +0 -23
  63. data/spec/shove_spec.rb +0 -40
data/lib/shove/app.rb ADDED
@@ -0,0 +1,78 @@
1
+ module Shove
2
+ class App
3
+
4
+ attr_accessor :config
5
+
6
+ # create an API client
7
+ # +config+ optional Confstruct
8
+ # +&block+ config block
9
+ # Example:
10
+ # Shove::App.new do
11
+ # app_id "myappid"
12
+ # app_key "myappkey"
13
+ # end
14
+ def initialize config=Confstruct::Configuration.new, &block
15
+ @config = config
16
+ configure(&block)
17
+ end
18
+
19
+ def configure params={}, &block
20
+ if params
21
+ @config.configure params
22
+ end
23
+
24
+ if block
25
+ @config.configure(&block)
26
+ end
27
+
28
+ unless @config.app_id && @config.app_key
29
+ raise ShoveException.new("App ID and App Key are required")
30
+ end
31
+ end
32
+
33
+ # is the app valid?
34
+ # do the app_id and app_key work with the remote
35
+ def valid?
36
+ !request("validate").exec_sync(:get).error?
37
+ end
38
+
39
+ # get a list of websocket hosts
40
+ def hosts
41
+ request("hosts").exec_sync(:get).parse
42
+ end
43
+
44
+ # create a channel context for acting on a channel
45
+ # +name+ the name of the channel
46
+ def channel name
47
+ Http::ChannelContext.new(self, name)
48
+ end
49
+
50
+ # create a cleint context for acting on a client
51
+ # +id+ the id of the client
52
+ def client id
53
+ Http::ClientContext.new(self, id)
54
+ end
55
+
56
+ # the base URL based on the settings
57
+ def url
58
+ "#{@config.api_url}/apps/#{@config.app_id}"
59
+ end
60
+
61
+ # Create a default request object with the base URL
62
+ # +path+ extra path info
63
+ def request path
64
+ Http::Request.new("#{url}/#{path}", @config)
65
+ end
66
+
67
+ ####
68
+
69
+ # Connect to shove as a client in the current process
70
+ # +id+ optional shove id to supply
71
+ def connect id=nil
72
+ client = Client::Connection.new(self, id)
73
+ client.connect
74
+ client
75
+ end
76
+
77
+ end
78
+ end
@@ -0,0 +1,104 @@
1
+
2
+ module Shove
3
+
4
+ # Used for storing and accessing shove credentials
5
+ # from the command line utility
6
+ class AppDirectory
7
+
8
+ attr_accessor :apps, :app
9
+
10
+ def initialize io=STDIN, path=File.expand_path("~/.shove.yml")
11
+ @io = io
12
+ @path = path
13
+ @app = nil
14
+ @apps = {}
15
+ load!
16
+ end
17
+
18
+ def put app_id, app_key
19
+ @apps[app_id] = app_key
20
+ @app = app_id if @app.nil?
21
+ save
22
+ end
23
+
24
+ def key app_id
25
+ get_config(app_id)[:app_key]
26
+ end
27
+
28
+ def default= app_id
29
+ unless app_id.nil?
30
+ @app = app_id
31
+ save
32
+ end
33
+ end
34
+
35
+ def default
36
+ @app ? get_config(@app) : get_config
37
+ end
38
+
39
+ def get_config app_id=nil
40
+ config = ENV["SHOVE_ENV"] == "development" ? {
41
+ :api_url => "http://api.shove.dev:8080",
42
+ :ws_url => "ws://shove.dev:9000"
43
+ } : {}
44
+
45
+ if !@app.nil? && app_id.nil?
46
+ config[:app_id] = @app
47
+ config[:app_key] = key(@app)
48
+ elsif app_id.nil? || !@apps.key?(app_id)
49
+
50
+ puts "We need some information to continue"
51
+
52
+ if app_id.nil?
53
+ config[:app_id] = getinput "Enter App Id"
54
+ else
55
+ config[:app_id] = app_id
56
+ end
57
+
58
+ loop do
59
+ config[:app_key] = getinput "Enter App Key"
60
+
61
+ Shove.configure config
62
+ if Shove.valid?
63
+ puts "App Settings accepted. Moving on..."
64
+ break
65
+ else
66
+ puts "App Settings invalid, please try again"
67
+ end
68
+ end
69
+
70
+ put config[:app_id], config[:app_key]
71
+ else
72
+ config[:app_id] = app_id
73
+ config[:app_key] = @apps[app_id]
74
+ end
75
+
76
+ config
77
+ end
78
+
79
+ private
80
+
81
+ def save
82
+ File.open(@path, "w") do |f|
83
+ f << {
84
+ "app" => @app,
85
+ "apps" => @apps
86
+ }.to_yaml
87
+ end
88
+ end
89
+
90
+ def getinput text
91
+ print "#{text}: "
92
+ @io.gets.strip
93
+ end
94
+
95
+ def load!
96
+ if FileTest.exist?(@path)
97
+ tmp = YAML.load_file(@path)
98
+ @app = tmp["app"]
99
+ @apps = tmp["apps"]
100
+ end
101
+ end
102
+
103
+ end
104
+ end
@@ -0,0 +1,24 @@
1
+
2
+ module Shove
3
+ module Client
4
+ # Represents a callback that can be canceled
5
+ class Callback
6
+
7
+ def initialize group, block
8
+ @group = group
9
+ @block = block
10
+ end
11
+
12
+ def call *args
13
+ @block.call(*args)
14
+ end
15
+
16
+ def cancel
17
+ @group.delete self
18
+ @block = nil
19
+ end
20
+
21
+ end
22
+ end
23
+ end
24
+
@@ -0,0 +1,81 @@
1
+ module Shove
2
+ module Client
3
+ class Channel
4
+
5
+ include Protocol
6
+
7
+ attr_accessor :name
8
+
9
+ # Create a new channel
10
+ # +name+ The name of the channel
11
+ def initialize name, conn
12
+ @conn = conn
13
+ @name = name
14
+ @callbacks = {}
15
+ end
16
+
17
+ # Bind a block to an event
18
+ # +event+ the event name
19
+ # +block+ the callback
20
+ def on event, &block
21
+ unless @callbacks.has_key?(event)
22
+ @callbacks[event] = []
23
+ end
24
+ result = Callback.new(@callbacks[event], block)
25
+ @callbacks[event] << result
26
+ result
27
+ end
28
+
29
+ # Process a message for the channel
30
+ # +message+ the message in question
31
+ def process message
32
+ op = message["opcode"]
33
+ data = message["data"]
34
+
35
+ case op
36
+ when PUBLISH
37
+ emit("message", data)
38
+ when SUBSCRIBE_GRANTED
39
+ emit("subscribe")
40
+ when SUBSCRIBE_DENIED
41
+ emit("subscribe_denied")
42
+ when PUBLISH_DENIED
43
+ emit("publish_denied")
44
+ when PUBLISH_GRANTED
45
+ emit("publish_granted")
46
+ when UNSUBSCRIBE_COMPLETE
47
+ emit("unsubscribe")
48
+ @callbacks.clear
49
+ end
50
+
51
+ end
52
+
53
+ # publish a message on the channel
54
+ # +msg+ the message to publish. It must implement to_s
55
+ def publish msg
56
+ @conn.send_data :opcode => PUBLISH, :channel => @name, :data => msg.to_s
57
+ end
58
+
59
+ # subscribe to the channel, by sending to the remote
60
+ def subscribe
61
+ @conn.send_data :opcode => SUBSCRIBE, :channel => @name
62
+ end
63
+
64
+ # unsubscribe from the channel
65
+ def unsubscribe
66
+ @conn.send_data :opcode => UNSUBSCRIBE, :channel => @name
67
+ end
68
+
69
+ private
70
+
71
+ def emit event, *args
72
+ if @callbacks.key?(event)
73
+ @callbacks[event].each do |cb|
74
+ cb.call(*args)
75
+ end
76
+ end
77
+ end
78
+
79
+ end
80
+ end
81
+ end
@@ -0,0 +1,167 @@
1
+ module Shove
2
+ module Client
3
+ class Connection
4
+
5
+ include Protocol
6
+
7
+ attr_accessor :id, :socket, :url
8
+
9
+ # Create a Publisher
10
+ # +app+ the app
11
+ def initialize app, id
12
+ @id = id
13
+ @app = app
14
+ @parser = Yajl::Parser.new(:symbolize_keys => true)
15
+ @config = app.config
16
+ @hosts = app.hosts
17
+ @channels = {}
18
+ @events = {}
19
+ @queue = []
20
+ @forcedc = false
21
+ @connected = false
22
+ end
23
+
24
+ def authorize app_key=nil
25
+ send_data :opcode => AUTHORIZE, :data => (app_key || @config.app_key)
26
+ end
27
+
28
+ # Connect to the shove stream server
29
+ def connect
30
+
31
+ if @connected
32
+ return
33
+ end
34
+
35
+ @socket = EM::WebSocketClient.new(url)
36
+ @socket.onopen do
37
+ @connected = true
38
+ send_data :opcode => CONNECT, :data => @id
39
+ until @queue.empty? do
40
+ send_data @queue.shift
41
+ end
42
+ end
43
+
44
+ @socket.onmessage do |m|
45
+ process Yajl::Parser.parse(m)
46
+ end
47
+
48
+ @socket.onclose do
49
+ @connected = false
50
+ unless @forcedc
51
+ reconnect
52
+ end
53
+ end
54
+ end
55
+
56
+ # Disconnect form the server
57
+ def disconnect
58
+ @forcedc = true
59
+ @socket.disconnect
60
+ end
61
+
62
+ # Bind to events for a given channel.
63
+ # +channel+ the channel name to subscribe to
64
+ # +event+ the event name to bind to
65
+ # +block+ the block which is called when a message is received
66
+ def on event, &block
67
+ unless @events.key?(event)
68
+ @events[event] = []
69
+ end
70
+ @events[event] << block
71
+ end
72
+
73
+ # Fetch a channel
74
+ # +name+ the name of the channel
75
+ def channel name
76
+ unless @channels.key?(name)
77
+ @channels[name] = Channel.new(name, self)
78
+ if name != "direct"
79
+ @channels[name].subscribe
80
+ end
81
+ end
82
+ @channels[name]
83
+ end
84
+
85
+ def url
86
+ if @config.ws_url
87
+ @url = "#{@config.ws_url}/#{@config.app_id}"
88
+ else
89
+ if @hosts.empty?
90
+ raise "Error fetching hosts for app #{@app_id}"
91
+ end
92
+ @url = "ws://#{@hosts.first}.shove.io/#{@config.app_id}"
93
+ end
94
+ @url
95
+ end
96
+
97
+ def send_data data
98
+ if @connected
99
+ @socket.send_data(Yajl::Encoder.encode(data))
100
+ else
101
+ @queue << data
102
+ end
103
+ end
104
+
105
+ protected
106
+
107
+ def reconnect
108
+ @reconnecting = true
109
+ end
110
+
111
+ def emit event, *args
112
+ if @events.key?(event)
113
+ @events[event].each do |block|
114
+ block.call(*args)
115
+ end
116
+ end
117
+ end
118
+
119
+ def process message
120
+
121
+ op = message["opcode"]
122
+ data = message["data"]
123
+ channel = message["channel"]
124
+
125
+ case op
126
+ when CONNECT_GRANTED
127
+ @id = data
128
+ emit "connect", @id
129
+ when CONNECT_DENIED
130
+ @id = data
131
+ emit "connect_denied", @id
132
+ when DISCONNECT
133
+ @closing = true
134
+ emit "disconnect", data
135
+ when ERROR
136
+ emit "error", data
137
+ when PUBLISH
138
+ channel = channel =~ /direct/ ? "direct" : channel
139
+ if @channels.key?(channel)
140
+ @channels[channel].process(message)
141
+ end
142
+ when SUBSCRIBE_GRANTED,
143
+ SUBSCRIBE_DENIED,
144
+ PUBLISH_GRANTED,
145
+ PUBLISH_DENIED,
146
+ UNSUBSCRIBE_COMPLETE
147
+ if @channels.key?(channel)
148
+ @channels[channel].process(message)
149
+ end
150
+ when DISCONNECT_COMPLETE
151
+ @closing = true
152
+ emit "disconnecting"
153
+ when AUTHORIZE_COMPLETE
154
+ else
155
+ #TODO: logger
156
+ puts "Unknown opcode"
157
+ end
158
+
159
+ end
160
+
161
+ def host
162
+ @hosts.first
163
+ end
164
+
165
+ end
166
+ end
167
+ end
@@ -0,0 +1,60 @@
1
+ module Shove
2
+ module Http
3
+ class ChannelContext
4
+
5
+ attr_accessor :app, :channel
6
+
7
+ def initialize app, channel
8
+ @app = app
9
+ @channel = channel
10
+ end
11
+
12
+ # publish a message on the channel
13
+ # +message+ the message to publish
14
+ # +block+ called on response
15
+ def publish message, &block
16
+ if @channel == "*"
17
+ raise ShoveException.new("Cannot publish to *")
18
+ elsif message.size > 8096
19
+ raise ShoveException.new("Max message size is 8,096 bytes")
20
+ end
21
+ @app.request("publish?channel=#{@channel}").post(message, &block)
22
+ end
23
+
24
+ # grant subscription on the channel
25
+ # +client+ the client to grant
26
+ # +block+ called on response
27
+ def grant_subscribe client, &block
28
+ control_request("grant_subscribe", client, &block)
29
+ end
30
+
31
+ # grant publishing on the current channel
32
+ # +client+ the client to grant
33
+ # +block+ called on response
34
+ def grant_publish client, &block
35
+ control_request("grant_publish", client, &block)
36
+ end
37
+
38
+ # deny subscription on the channel
39
+ # +client+ the client to deny
40
+ # +block+ called on response
41
+ def deny_subscribe client, &block
42
+ control_request("deny_subscribe", client, &block)
43
+ end
44
+
45
+ # deny publishing on the channel
46
+ # +client+ the client to deny
47
+ # +block+ called on response
48
+ def deny_publish client, &block
49
+ control_request("deny_publish", client, &block)
50
+ end
51
+
52
+ private
53
+
54
+ def control_request action, client, &block
55
+ @app.request("#{action}?channel=#{@channel}&client=#{client}").post(&block)
56
+ end
57
+
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,82 @@
1
+ module Shove
2
+ module Http
3
+ class ClientContext
4
+
5
+ attr_accessor :app, :id
6
+
7
+ def initialize app, id
8
+ @app = app
9
+ @id = id
10
+ end
11
+
12
+ # publish a message directly to the client
13
+ # +message+ the message to publish
14
+ # +block+ called on response
15
+ def publish message, &block
16
+ @app.request("publish?channel=direct:#{@id}").post(message, &block)
17
+ end
18
+
19
+ # grant connection to client
20
+ # use this in cases where the app disallows any activity
21
+ # on newly established clients.
22
+ # +block+ called on response
23
+ def grant_connect &block
24
+ @app.request("grant_connect?client=#{@id}").post(&block)
25
+ end
26
+
27
+ # grant control to client
28
+ # turns the client into a full blown control client, allowing
29
+ # them to subscribe and publish to all channels, as well
30
+ # as grant and deny actions to other clients
31
+ # +block+ called on response
32
+ def grant_control &block
33
+ @app.request("grant_control?client=#{@id}").post(&block)
34
+ end
35
+
36
+ # grant connection to client
37
+ # use this to kick a client
38
+ # +block+ called on response
39
+ def deny_connect &block
40
+ @app.request("deny_connect?client=#{@id}").post(&block)
41
+ end
42
+
43
+ # grant connection to client
44
+ # use this to revoke power. Clients will never have
45
+ # control by default, so this would always be called
46
+ # after calling grant_control
47
+ # +block+ called on response
48
+ def deny_control &block
49
+ @app.request("deny_control?client=#{@id}").post(&block)
50
+ end
51
+
52
+ # grant subscription to client
53
+ # +channel+ the channel to grant subscription on
54
+ # +block+ called on response
55
+ def grant_subscribe channel, &block
56
+ @app.request("grant_subscribe?channel=#{channel}&client=#{@id}").post(&block)
57
+ end
58
+
59
+ # grant publishing to client
60
+ # +channel+ the channel to grant publishing on
61
+ # +block+ called on response
62
+ def grant_publish channel, &block
63
+ @app.request("grant_publish?channel=#{channel}&client=#{@id}").post(&block)
64
+ end
65
+
66
+ # deny subscription to client
67
+ # +channel+ the channel to deny subscription on
68
+ # +block+ called on response
69
+ def deny_subscribe channel, &block
70
+ @app.request("deny_subscribe?channel=#{channel}&client=#{@id}").post(&block)
71
+ end
72
+
73
+ # deny publishing to client
74
+ # +channel+ the channel to deny publishing on
75
+ # +block+ called on response
76
+ def deny_publish channel, &block
77
+ @app.request("deny_publish?channel=#{channel}&client=#{@id}").post(&block)
78
+ end
79
+
80
+ end
81
+ end
82
+ end
@@ -0,0 +1,94 @@
1
+ module Shove
2
+ module Http
3
+ class Request
4
+
5
+ include EventMachine::HttpEncoding
6
+
7
+ attr_accessor :url, :key, :headers
8
+
9
+ def initialize url, config
10
+ @url = url
11
+ @key = config.app_key
12
+ end
13
+
14
+ # HTTP Delete request
15
+ def delete &block
16
+ exec :delete, &block
17
+ end
18
+
19
+ # HTTP Post request for new content
20
+ def post params={}, &block
21
+ exec :post, params, &block
22
+ end
23
+
24
+ # HTTP Put request for updates
25
+ def put params={}, &block
26
+ exec :put, params, &block
27
+ end
28
+
29
+ # HTTP Get request
30
+ def get &block
31
+ exec :get, &block
32
+ end
33
+
34
+ def exec_sync method, params={}, &block
35
+ uri = URI.parse(url)
36
+
37
+ req = {
38
+ :post => Net::HTTP::Post,
39
+ :put => Net::HTTP::Put,
40
+ :get => Net::HTTP::Get,
41
+ :delete => Net::HTTP::Delete
42
+ }[method].new(uri.path + (uri.query ? "?#{uri.query}" : ""))
43
+
44
+ req.basic_auth "", key
45
+
46
+ res = Net::HTTP.start(uri.host, uri.port) { |http|
47
+ http.request(req, normalize_body(params))
48
+ }
49
+
50
+ result = Response.new(res.code, res.body, res.code.to_i >= 400)
51
+ if block_given?
52
+ block.call(result)
53
+ end
54
+
55
+ result
56
+ end
57
+
58
+ def exec_async method, params={}, &block
59
+ http = EventMachine::HttpRequest.new(url).send(method, {
60
+ :body => params,
61
+ :head => {
62
+ :authorization => ["", key]
63
+ }
64
+ })
65
+
66
+ # handle error
67
+ http.errback {
68
+ block.call(Response.new(http.response_header.status, http.response, true))
69
+ }
70
+
71
+ # handle success
72
+ http.callback {
73
+ status = http.response_header.status
74
+ block.call(Response.new(status, http.response, status >= 400))
75
+ }
76
+ end
77
+
78
+ # exec a HTTP request, and callback with
79
+ # a response via block
80
+ def exec method, params={}, &block
81
+ if EM.reactor_running?
82
+ exec_async(method, params, &block)
83
+ else
84
+ exec_sync(method, params, &block)
85
+ end
86
+ end
87
+
88
+ def normalize_body(body)
89
+ body.is_a?(Hash) ? form_encode_body(body) : body
90
+ end
91
+
92
+ end
93
+ end
94
+ end