ably 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (41) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +4 -0
  3. data/Gemfile +4 -0
  4. data/LICENSE.txt +22 -0
  5. data/README.md +103 -0
  6. data/Rakefile +4 -0
  7. data/ably.gemspec +32 -0
  8. data/lib/ably.rb +11 -0
  9. data/lib/ably/auth.rb +381 -0
  10. data/lib/ably/exceptions.rb +16 -0
  11. data/lib/ably/realtime.rb +38 -0
  12. data/lib/ably/realtime/callbacks.rb +15 -0
  13. data/lib/ably/realtime/channel.rb +51 -0
  14. data/lib/ably/realtime/client.rb +82 -0
  15. data/lib/ably/realtime/connection.rb +61 -0
  16. data/lib/ably/rest.rb +15 -0
  17. data/lib/ably/rest/channel.rb +58 -0
  18. data/lib/ably/rest/client.rb +194 -0
  19. data/lib/ably/rest/middleware/exceptions.rb +42 -0
  20. data/lib/ably/rest/middleware/external_exceptions.rb +26 -0
  21. data/lib/ably/rest/middleware/parse_json.rb +15 -0
  22. data/lib/ably/rest/paged_resource.rb +107 -0
  23. data/lib/ably/rest/presence.rb +44 -0
  24. data/lib/ably/support.rb +14 -0
  25. data/lib/ably/token.rb +55 -0
  26. data/lib/ably/version.rb +3 -0
  27. data/spec/acceptance/realtime_client_spec.rb +12 -0
  28. data/spec/acceptance/rest/auth_spec.rb +441 -0
  29. data/spec/acceptance/rest/base_spec.rb +113 -0
  30. data/spec/acceptance/rest/channel_spec.rb +68 -0
  31. data/spec/acceptance/rest/presence_spec.rb +22 -0
  32. data/spec/acceptance/rest/stats_spec.rb +57 -0
  33. data/spec/acceptance/rest/time_spec.rb +14 -0
  34. data/spec/spec_helper.rb +31 -0
  35. data/spec/support/api_helper.rb +41 -0
  36. data/spec/support/test_app.rb +77 -0
  37. data/spec/unit/auth.rb +9 -0
  38. data/spec/unit/realtime_spec.rb +9 -0
  39. data/spec/unit/rest_spec.rb +99 -0
  40. data/spec/unit/token_spec.rb +90 -0
  41. metadata +240 -0
@@ -0,0 +1,16 @@
1
+ module Ably
2
+ class InvalidRequest < StandardError
3
+ attr_reader :status, :code
4
+ def initialize(message, status: nil, code: nil)
5
+ super message
6
+ @status = status
7
+ @code = code
8
+ end
9
+ end
10
+
11
+ class ServerError < StandardError; end
12
+ class InvalidPageError < StandardError; end
13
+ class InvalidResponseBody < StandardError; end
14
+ class InsecureRequestError < StandardError; end
15
+ class TokenRequestError < StandardError; end
16
+ end
@@ -0,0 +1,38 @@
1
+ require "eventmachine"
2
+ require "websocket/driver"
3
+
4
+ require "ably/realtime/callbacks"
5
+ require "ably/realtime/channel"
6
+ require "ably/realtime/client"
7
+ require "ably/realtime/connection"
8
+
9
+ module Ably
10
+ module Realtime
11
+ # Actions which are sent by the Ably Realtime API
12
+ #
13
+ # The values correspond to the ints which the API
14
+ # understands.
15
+ ACTIONS = {
16
+ heartbeat: 0,
17
+ ack: 1,
18
+ nack: 2,
19
+ connect: 3,
20
+ connected: 4,
21
+ disconnect: 5,
22
+ disconnected: 6,
23
+ close: 7,
24
+ closed: 8,
25
+ error: 9,
26
+ attach: 10,
27
+ attached: 11,
28
+ detach: 12,
29
+ detached: 13,
30
+ presence: 14,
31
+ message: 15
32
+ }
33
+
34
+ def self.new(*args)
35
+ Ably::Realtime::Client.new(*args)
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,15 @@
1
+ module Ably
2
+ module Realtime
3
+ module Callbacks
4
+ def on(event, &block)
5
+ @callbacks ||= Hash.new { |hash, key| hash[key] = [] }
6
+ @callbacks[event] << block
7
+ end
8
+
9
+ def trigger(event, *args)
10
+ @callbacks ||= Hash.new { |hash, key| hash[key] = [] }
11
+ @callbacks[event].each { |cb| cb.call(*args) }
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,51 @@
1
+ module Ably
2
+ module Realtime
3
+ class Channel
4
+ include Callbacks
5
+
6
+ attr_reader :client, :name
7
+
8
+ def initialize(client, name)
9
+ @state = :initialised
10
+ @client = client
11
+ @name = name
12
+ @subscriptions = Hash.new { |hash, key| hash[key] = [] }
13
+
14
+ on(:message) do |message|
15
+ event = message[:name]
16
+
17
+ @subscriptions[:all].each { |cb| cb.call(message) }
18
+ @subscriptions[event].each { |cb| cb.call(message) }
19
+ end
20
+ end
21
+
22
+ def publish(event, data)
23
+ message = { name: event, data: data }
24
+
25
+ if attached?
26
+ client.send_message(name, message)
27
+ else
28
+ on(:attached) { client.send_message(name, message) }
29
+ attach
30
+ end
31
+ end
32
+
33
+ def subscribe(event = :all, &blk)
34
+ @subscriptions[event] << blk
35
+ end
36
+
37
+ private
38
+ def attached?
39
+ @state == :attached
40
+ end
41
+
42
+ def attach
43
+ unless @state == :attaching
44
+ @state = :attaching
45
+ client.attach_to_channel(name)
46
+ on(:attached) { @state = :attached }
47
+ end
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,82 @@
1
+ module Ably
2
+ module Realtime
3
+ # A client for the Ably Realtime API
4
+ class Client
5
+ include Callbacks
6
+
7
+ DOMAIN = "staging-realtime.ably.io"
8
+
9
+ def initialize(options)
10
+ @rest_client = Ably::Rest::Client.new(options)
11
+
12
+ on(:attached) do |data|
13
+ channel = channel(data[:channel])
14
+
15
+ channel.trigger(:attached)
16
+ end
17
+
18
+ on(:message) do |data|
19
+ channel = channel(data[:channel])
20
+
21
+ data[:messages].each do |message|
22
+ channel.trigger(:message, message)
23
+ end
24
+ end
25
+ end
26
+
27
+ def token
28
+ @token ||= @rest_client.request_token
29
+ end
30
+
31
+ # Return a Realtime Channel for the given name
32
+ #
33
+ # @param name [String] The name of the channel
34
+ # @return [Ably::Realtime::Channel]
35
+ def channel(name)
36
+ @channels ||= {}
37
+ @channels[name] ||= Ably::Realtime::Channel.new(self, name)
38
+ end
39
+
40
+ def send_message(channel_name, message)
41
+ payload = {
42
+ action: ACTIONS[:message],
43
+ channel: channel_name,
44
+ messages: [message]
45
+ }.to_json
46
+
47
+ connection.send(payload)
48
+ end
49
+
50
+ def attach_to_channel(channel_name)
51
+ payload = {
52
+ action: ACTIONS[:attach],
53
+ channel: channel_name
54
+ }.to_json
55
+
56
+ connection.send(payload)
57
+ end
58
+
59
+ def use_tls?
60
+ @rest_client.use_tls?
61
+ end
62
+
63
+ def endpoint
64
+ @endpoint ||= URI::Generic.build(
65
+ scheme: use_tls? ? "wss" : "ws",
66
+ host: DOMAIN,
67
+ query: "access_token=#{token.id}&binary=false&timestamp=#{Time.now.to_i}"
68
+ )
69
+ end
70
+
71
+ def connection
72
+ @connection ||= begin
73
+ host = endpoint.host
74
+ port = use_tls? ? 443 : 80
75
+
76
+ EventMachine.connect(host, port, Connection, self)
77
+ end
78
+ end
79
+ end
80
+ end
81
+ end
82
+
@@ -0,0 +1,61 @@
1
+ module Ably
2
+ module Realtime
3
+ class Connection < EventMachine::Connection
4
+ include Callbacks
5
+
6
+ def initialize(client)
7
+ @client = client
8
+ end
9
+
10
+ # Ably::Realtime interface
11
+ def send(data)
12
+ @driver.text(data)
13
+ end
14
+
15
+ # EventMachine::Connection interface
16
+ def post_init
17
+ trigger :initalised
18
+
19
+ setup_driver
20
+ end
21
+
22
+ def connection_completed
23
+ trigger :connecting
24
+
25
+ start_tls if @client.use_tls?
26
+ @driver.start
27
+ end
28
+
29
+ def receive_data(data)
30
+ @driver.parse(data)
31
+ end
32
+
33
+ def unbind
34
+ trigger :disconnected
35
+ end
36
+
37
+ # WebSocket::Driver interface
38
+ def url
39
+ @client.endpoint.to_s
40
+ end
41
+
42
+ def write(data)
43
+ send_data(data)
44
+ end
45
+
46
+ private
47
+ def setup_driver
48
+ @driver = WebSocket::Driver.client(self)
49
+
50
+ @driver.on("open") { trigger :connected }
51
+
52
+ @driver.on("message") do |event|
53
+ message = JSON.parse(event.data, symbolize_names: true)
54
+ action = ACTIONS.detect { |k,v| v == message[:action] }.first
55
+
56
+ @client.trigger action, message
57
+ end
58
+ end
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,15 @@
1
+ require "ably/rest/channel"
2
+ require "ably/rest/client"
3
+ require "ably/rest/paged_resource"
4
+ require "ably/rest/presence"
5
+
6
+ module Ably
7
+ module Rest
8
+ # Convenience method providing an alias to {Ably::Rest::Client} constructor.
9
+ #
10
+ # @return [Ably::Rest::Client]
11
+ def self.new(*args)
12
+ Ably::Rest::Client.new(*args)
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,58 @@
1
+ module Ably
2
+ module Rest
3
+ class Channel
4
+ attr_reader :client, :name
5
+
6
+ # Initialize a new Channel object
7
+ #
8
+ # @param client [Ably::Rest::Client]
9
+ # @param name [String] The name of the channel
10
+ def initialize(client, name)
11
+ @client = client
12
+ @name = name
13
+ end
14
+
15
+ # Publish a message to the channel
16
+ #
17
+ # @param message [Hash] The message to publish (must contain :name and :data keys)
18
+ # @return [Boolean] true if the message was published, otherwise false
19
+ def publish(event, message)
20
+ payload = {
21
+ name: event,
22
+ data: message
23
+ }
24
+
25
+ response = client.post("#{base_path}/publish", payload)
26
+
27
+ response.status == 201
28
+ end
29
+
30
+ # Return the message history of the channel
31
+ #
32
+ # Options:
33
+ # - start: Time or millisecond since epoch
34
+ # - end: Time or millisecond since epoch
35
+ # - direction: :forwards or :backwards
36
+ # - limit: Maximum number of messages to retrieve up to 10,000
37
+ # - by: :message, :bundle or :hour. Defaults to :message
38
+ #
39
+ # @return [PagedResource] An Array of hashes representing the message history that supports paging (next, first)
40
+ def history(options = {})
41
+ url = "#{base_path}/messages"
42
+ # TODO: Remove live param as all history should be live
43
+ response = client.get(url, options.merge(live: true))
44
+
45
+ PagedResource.new(response, url, client)
46
+ end
47
+
48
+ def presence
49
+ @presence ||= Presence.new(client, self)
50
+ end
51
+
52
+ private
53
+ def base_path
54
+ "/channels/#{CGI.escape(name)}"
55
+ end
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,194 @@
1
+ require "json"
2
+ require "faraday"
3
+
4
+ require "ably/rest/middleware/exceptions"
5
+ require "ably/rest/middleware/parse_json"
6
+
7
+ module Ably
8
+ module Rest
9
+ # Wrapper for the Ably REST API
10
+ #
11
+ # @!attribute [r] auth
12
+ # @return {Ably::Auth} authentication object configured for this connection
13
+ # @!attribute [r] client_id
14
+ # @return [String] A client ID, used for identifying this client for presence purposes
15
+ # @!attribute [r] auth_options
16
+ # @return [Hash] {Ably::Auth} options configured for this client
17
+ # @!attribute [r] tls
18
+ # @return [Boolean] True if client is configured to use TLS for all Ably communication
19
+ # @!attribute [r] environment
20
+ # @return [String] May contain 'sandbox' when testing the client library against an alternate Ably environment
21
+ class Client
22
+ include Ably::Support
23
+ extend Forwardable
24
+
25
+ DOMAIN = "rest.ably.io"
26
+
27
+ attr_reader :tls, :environment, :auth
28
+ def_delegator :auth, :client_id, :auth_options
29
+
30
+ # Creates a {Ably::Rest::Client Rest Client} and configures the {Ably::Auth} object for the connection.
31
+ #
32
+ # @param [Hash,String] options an options Hash used to configure the client and the authentication, or String with an API key
33
+ # @option options [Boolean] :tls TLS is used by default, providing a value of false disbles TLS. Please note Basic Auth is disallowed without TLS as secrets cannot be transmitted over unsecured connections.
34
+ # @option options [String] :api_key API key comprising the key ID and key secret in a single string
35
+ # @option options [String] :key_id key ID for the designated application (defaults to client key_id)
36
+ # @option options [String] :key_secret key secret for the designated application used to sign token requests (defaults to client key_secret)
37
+ # @option options [String] :client_id client ID identifying this connection to other clients (defaults to client client_id if configured)
38
+ # @option options [String] :auth_url a URL to be used to GET or POST a set of token request params, to obtain a signed token request.
39
+ # @option options [Hash] :auth_headers a set of application-specific headers to be added to any request made to the authUrl
40
+ # @option options [Hash] :auth_params a set of application-specific query params to be added to any request made to the authUrl
41
+ # @option options [Symbol] :auth_method HTTP method to use with auth_url, must be either `:get` or `:post` (defaults to :get)
42
+ # @option options [Integer] :ttl validity time in seconds for the requested {Ably::Token}. Limits may apply, see {http://docs.ably.io/other/authentication/}
43
+ # @option options [Hash] :capability canonicalised representation of the resource paths and associated operations
44
+ # @option options [Boolean] :query_time when true will query the {https://ably.io Ably} system for the current time instead of using the local time
45
+ # @option options [Integer] :timestamp the time of the of the request in seconds since the epoch
46
+ # @option options [String] :nonce an unquoted, unescaped random string of at least 16 characters
47
+ # @option options [String] :environment Specify 'sandbox' when testing the client library against an alternate Ably environment
48
+ # @option options [Boolean] :debug_http Send HTTP debugging information from Faraday for all HTTP requests to STDOUT
49
+ #
50
+ # @yield [options] (optional) if an auth block is passed to this method, then this block will be called to create a new token request object
51
+ # @yieldparam [Hash] options options passed to request_token will be in turn sent to the block in this argument
52
+ # @yieldreturn [Hash] valid token request object, see {#create_token_request}
53
+ #
54
+ # @return [Ably::Rest::Client]
55
+ #
56
+ # @example
57
+ # # create a new client authenticating with basic auth
58
+ # client = Ably::Rest::Client.new('key.id:secret')
59
+ #
60
+ # # create a new client and configure a client ID used for presence
61
+ # client = Ably::Rest::Client.new(api_key: 'key.id:secret', client_id: 'john')
62
+ #
63
+ def initialize(options, &auth_block)
64
+ if options.kind_of?(String)
65
+ options = { api_key: options }
66
+ end
67
+
68
+ @tls = options.delete(:tls) == false ? false : true
69
+ @environment = options.delete(:environment) # nil is production
70
+ @debug_http = options.delete(:debug_http)
71
+
72
+ @auth = Auth.new(self, options, &auth_block)
73
+ end
74
+
75
+ # Return a REST {Ably::Rest::Channel} for the given name
76
+ #
77
+ # @param name [String] The name of the channel
78
+ # @return [Ably::Rest::Channel]
79
+ def channel(name)
80
+ @channels ||= {}
81
+ @channels[name] ||= Ably::Rest::Channel.new(self, name)
82
+ end
83
+
84
+ # Return the stats for the application
85
+ #
86
+ # @return [Array] An Array of hashes representing the stats
87
+ def stats(params = {})
88
+ default_params = {
89
+ :direction => :forwards,
90
+ :by => :minute
91
+ }
92
+
93
+ response = get("/stats", default_params.merge(params))
94
+
95
+ response.body
96
+ end
97
+
98
+ # Return the Ably service time
99
+ #
100
+ # @return [Time] The time as reported by the Ably service
101
+ def time
102
+ response = get('/time', {}, send_auth_header: false)
103
+
104
+ Time.at(response.body.first / 1000.0)
105
+ end
106
+
107
+ # True if client is configured to use TLS for all Ably communication
108
+ #
109
+ # @return [Boolean]
110
+ def use_tls?
111
+ @tls == true
112
+ end
113
+
114
+ # Perform an HTTP GET request to the API using configured authentication
115
+ #
116
+ # @return [Faraday::Response]
117
+ def get(path, params = {}, options = {})
118
+ request(:get, path, params, options)
119
+ end
120
+
121
+ # Perform an HTTP POST request to the API using configured authentication
122
+ #
123
+ # @return [Faraday::Response]
124
+ def post(path, params, options = {})
125
+ request(:post, path, params, options)
126
+ end
127
+
128
+ # Default Ably REST endpoint used for all requests
129
+ #
130
+ # @return [URI::Generic]
131
+ def endpoint
132
+ URI::Generic.build(
133
+ scheme: use_tls? ? "https" : "http",
134
+ host: [@environment, DOMAIN].compact.join('-')
135
+ )
136
+ end
137
+
138
+ private
139
+ def request(method, path, params = {}, options = {})
140
+ connection.send(method, path, params) do |request|
141
+ unless options[:send_auth_header] == false
142
+ request.headers[:authorization] = auth.auth_header
143
+ end
144
+ end
145
+ end
146
+
147
+ # Return a Faraday::Connection to use to make HTTP requests
148
+ #
149
+ # @return [Faraday::Connection]
150
+ def connection
151
+ @connection ||= Faraday.new(endpoint.to_s, connection_options)
152
+ end
153
+
154
+ # Return a Hash of connection options to initiate the Faraday::Connection with
155
+ #
156
+ # @return [Hash]
157
+ def connection_options
158
+ @connection_options ||= {
159
+ builder: middleware,
160
+ headers: {
161
+ accept: "application/json",
162
+ user_agent: user_agent
163
+ },
164
+ request: {
165
+ open_timeout: 5,
166
+ timeout: 10
167
+ }
168
+ }
169
+ end
170
+
171
+ # Return a Faraday middleware stack to initiate the Faraday::Connection with
172
+ #
173
+ # @see http://mislav.uniqpath.com/2011/07/faraday-advanced-http/
174
+ def middleware
175
+ @middleware ||= Faraday::RackBuilder.new do |builder|
176
+ # Convert request params to "www-form-urlencoded"
177
+ builder.use Faraday::Request::UrlEncoded
178
+
179
+ # Parse JSON response bodies
180
+ builder.use Ably::Rest::Middleware::ParseJson
181
+
182
+ # Log HTTP requests if debug_http option set
183
+ builder.response :logger if @debug_http
184
+
185
+ # Raise exceptions if response code is invalid
186
+ builder.use Ably::Rest::Middleware::Exceptions
187
+
188
+ # Set Faraday's HTTP adapter
189
+ builder.adapter Faraday.default_adapter
190
+ end
191
+ end
192
+ end
193
+ end
194
+ end