ably 0.1.1 → 0.1.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (39) hide show
  1. checksums.yaml +4 -4
  2. data/lib/ably.rb +2 -2
  3. data/lib/ably/auth.rb +39 -7
  4. data/lib/ably/modules/conversions.rb +58 -0
  5. data/lib/ably/{support.rb → modules/http_helpers.rb} +3 -3
  6. data/lib/ably/realtime.rb +5 -23
  7. data/lib/ably/realtime/channel.rb +62 -18
  8. data/lib/ably/realtime/client.rb +76 -22
  9. data/lib/ably/realtime/connection.rb +41 -14
  10. data/lib/ably/realtime/models/error_info.rb +38 -0
  11. data/lib/ably/realtime/models/message.rb +85 -0
  12. data/lib/ably/realtime/models/protocol_message.rb +149 -0
  13. data/lib/ably/realtime/models/shared.rb +17 -0
  14. data/lib/ably/rest.rb +16 -3
  15. data/lib/ably/rest/channel.rb +2 -2
  16. data/lib/ably/rest/client.rb +17 -20
  17. data/lib/ably/rest/models/message.rb +62 -0
  18. data/lib/ably/rest/models/paged_resource.rb +117 -0
  19. data/lib/ably/rest/presence.rb +4 -4
  20. data/lib/ably/token.rb +1 -1
  21. data/lib/ably/version.rb +1 -1
  22. data/spec/acceptance/realtime/channel_spec.rb +86 -0
  23. data/spec/acceptance/rest/auth_spec.rb +14 -5
  24. data/spec/acceptance/rest/channel_spec.rb +2 -2
  25. data/spec/spec_helper.rb +1 -0
  26. data/spec/support/event_machine_helper.rb +22 -0
  27. data/spec/support/model_helper.rb +67 -0
  28. data/spec/unit/realtime/error_info_spec.rb +10 -0
  29. data/spec/unit/realtime/message_spec.rb +115 -0
  30. data/spec/unit/realtime/protocol_message_spec.rb +102 -0
  31. data/spec/unit/realtime/realtime_spec.rb +20 -0
  32. data/spec/unit/rest/message_spec.rb +74 -0
  33. data/spec/unit/{rest_spec.rb → rest/rest_spec.rb} +14 -0
  34. metadata +28 -13
  35. data/lib/ably/message.rb +0 -70
  36. data/lib/ably/rest/paged_resource.rb +0 -117
  37. data/spec/acceptance/realtime_client_spec.rb +0 -12
  38. data/spec/unit/message_spec.rb +0 -73
  39. data/spec/unit/realtime_spec.rb +0 -9
@@ -4,12 +4,16 @@ module Ably
4
4
  include Callbacks
5
5
 
6
6
  def initialize(client)
7
- @client = client
7
+ @client = client
8
+ @message_serial = 0
8
9
  end
9
10
 
10
- # Ably::Realtime interface
11
- def send(data)
12
- @driver.text(data)
11
+ def send(protocol_message)
12
+ add_message_serial_if_ack_required_to(protocol_message) do
13
+ protocol_message = Models::ProtocolMessage.new(protocol_message)
14
+ client.log_http("Prot msg sent =>: #{protocol_message.action_sym} #{protocol_message.to_json}")
15
+ driver.text(protocol_message.to_json)
16
+ end
13
17
  end
14
18
 
15
19
  # EventMachine::Connection interface
@@ -22,12 +26,12 @@ module Ably
22
26
  def connection_completed
23
27
  trigger :connecting
24
28
 
25
- start_tls if @client.use_tls?
26
- @driver.start
29
+ start_tls if client.use_tls?
30
+ driver.start
27
31
  end
28
32
 
29
33
  def receive_data(data)
30
- @driver.parse(data)
34
+ driver.parse(data)
31
35
  end
32
36
 
33
37
  def unbind
@@ -36,7 +40,9 @@ module Ably
36
40
 
37
41
  # WebSocket::Driver interface
38
42
  def url
39
- @client.endpoint.to_s
43
+ URI(client.endpoint).tap do |endpoint|
44
+ endpoint.query = URI.encode_www_form(client.auth.auth_params.merge(timestamp: Time.now.to_i, binary: false))
45
+ end.to_s
40
46
  end
41
47
 
42
48
  def write(data)
@@ -44,16 +50,37 @@ module Ably
44
50
  end
45
51
 
46
52
  private
53
+ attr_reader :client, :driver, :message_serial
54
+
55
+ def add_message_serial_if_ack_required_to(data)
56
+ if Models::ProtocolMessage.ack_required?(data[:action])
57
+ add_message_serial_to(data) { yield }
58
+ else
59
+ yield
60
+ end
61
+ end
62
+
63
+ def add_message_serial_to(data)
64
+ @message_serial += 1
65
+ data.merge!(msg_serial: @message_serial)
66
+ yield
67
+ rescue StandardError => e
68
+ @message_serial -= 1
69
+ raise e
70
+ end
71
+
47
72
  def setup_driver
48
73
  @driver = WebSocket::Driver.client(self)
49
74
 
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
75
+ driver.on("open") do
76
+ client.log_http("WebSocket connection opened to #{url}")
77
+ trigger :connected
78
+ end
55
79
 
56
- @client.trigger action, message
80
+ driver.on("message") do |event|
81
+ message = Models::ProtocolMessage.new(JSON.parse(event.data))
82
+ client.log_http("Prot msg recv <=: #{message.action_sym} #{message.to_json}")
83
+ client.trigger message.action_sym, message
57
84
  end
58
85
  end
59
86
  end
@@ -0,0 +1,38 @@
1
+ module Ably::Realtime::Models
2
+ # An exception type encapsulating error information containing
3
+ # an Ably-specific error code and generic status code.
4
+ #
5
+ # @!attribute [r] message
6
+ # @return [String] Additional reason information, where available
7
+ # @!attribute [r] code
8
+ # @return [Integer] Ably error code (see ably-common/protocol/errors.json)
9
+ # @!attribute [r] status
10
+ # @return [Integer] HTTP Status Code corresponding to this error, where applicable
11
+ # @!attribute [r] json
12
+ # @return [Hash] Access the protocol message Hash object ruby'fied to use symbolized keys
13
+ #
14
+ class ErrorInfo
15
+ include Shared
16
+ include Ably::Modules::Conversions
17
+
18
+ def initialize(json_object)
19
+ @raw_json_object = json_object
20
+ @json_object = rubify(@raw_json_object).freeze
21
+ end
22
+
23
+ %w( message code status ).each do |attribute|
24
+ define_method attribute do
25
+ json[attribute.to_sym]
26
+ end
27
+ end
28
+
29
+ def json
30
+ @json_object
31
+ end
32
+ alias_method :to_json, :json
33
+
34
+ def to_s
35
+ "Error: #{message} (code: #{code}, status: #{status})"
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,85 @@
1
+ module Ably::Realtime::Models
2
+ # A class representing an individual message to be sent or received
3
+ # via the Ably Realtime service.
4
+ #
5
+ # @!attribute [r] name
6
+ # @return [String] The event name, if available
7
+ # @!attribute [r] client_id
8
+ # @return [String] The id of the publisher of this message
9
+ # @!attribute [r] data
10
+ # @return [Object] The message payload. See the documentation for supported datatypes.
11
+ # @!attribute [r] sender_timestamp
12
+ # @return [Time] Timestamp when the message was sent according to the publisher client
13
+ # @!attribute [r] ably_timestamp
14
+ # @return [Time] Timestamp when the message was received by the Ably the service for publishing
15
+ # @!attribute [r] message_id
16
+ # @return [String] A globally unique message ID
17
+ # @!attribute [r] json
18
+ # @return [Hash] Access the protocol message Hash object ruby'fied to use symbolized keys
19
+ #
20
+ class Message
21
+ include Shared
22
+ include Ably::Modules::Conversions
23
+
24
+ def initialize(json_object, protocol_message)
25
+ @protocol_message = protocol_message
26
+ @raw_json_object = json_object
27
+ @json_object = rubify(@raw_json_object, ignore: [:data]).freeze
28
+ end
29
+
30
+ %w( name client_id ).each do |attribute|
31
+ define_method attribute do
32
+ json[attribute.to_sym]
33
+ end
34
+ end
35
+
36
+ def data
37
+ @data ||= json[:data].freeze
38
+ end
39
+
40
+ def message_id
41
+ "#{connection_id}:#{message_serial}:#{protocol_message_index}"
42
+ end
43
+
44
+ def sender_timestamp
45
+ Time.at(json[:timestamp] / 1000.0) if json[:timestamp]
46
+ end
47
+
48
+ def ably_timestamp
49
+ protocol_message.timestamp
50
+ end
51
+
52
+ def json
53
+ @json_object
54
+ end
55
+
56
+ def to_json_object
57
+ raise RuntimeError, ":name is missing, cannot generate valid JSON for Message" unless name
58
+
59
+ json_object = json.dup.tap do |json_object|
60
+ json_object[:timestamp] = Time.now.to_i * 1000 unless sender_timestamp
61
+ end
62
+
63
+ javify(json_object)
64
+ end
65
+
66
+ def to_json
67
+ to_json_object.to_json
68
+ end
69
+
70
+ private
71
+ attr_reader :protocol_message
72
+
73
+ def protocol_message_index
74
+ protocol_message.messages.index(self)
75
+ end
76
+
77
+ def connection_id
78
+ protocol_message.connection_id
79
+ end
80
+
81
+ def message_serial
82
+ protocol_message.message_serial
83
+ end
84
+ end
85
+ end
@@ -0,0 +1,149 @@
1
+ module Ably::Realtime::Models
2
+ # A message sent and received over the Realtime protocol.
3
+ # A ProtocolMessage always relates to a single channel only, but
4
+ # can contain multiple individual Messages or PresenceMessages.
5
+ # ProtocolMessages are serially numbered on a connection.
6
+ # See the {http://docs.ably.io/client-lib-development-guide/protocol/ Ably client library developer documentation}
7
+ # for further details on the members of a ProtocolMessage
8
+ #
9
+ # @!attribute [r] action
10
+ # @return [Integer] Protocol Message action from list of {ACTIONS}
11
+ # @!attribute [r] action_sym
12
+ # @return [Symbol] Protocol Message action as a symbol
13
+ # @!attribute [r] count
14
+ # @return [Integer] The count field is used for ACK and NACK actions. See {http://docs.ably.io/client-lib-development-guide/protocol/#message-acknowledgement message acknowledgement protocol}
15
+ # @!attribute [r] error_info
16
+ # @return [ErrorInfo] Contains error information
17
+ # @!attribute [r] channel
18
+ # @return [String] Channel name for messages
19
+ # @!attribute [r] channel_serial
20
+ # @return [String] Contains a serial number for amessage on the current channel
21
+ # @!attribute [r] connection_id
22
+ # @return [String] Contains a string connection ID
23
+ # @!attribute [r] connection_serial
24
+ # @return [Bignum] Contains a serial number for a message on the current connection
25
+ # @!attribute [r] message_serial
26
+ # @return [Bignum] Contains a serial number for a message sent from the client to the service.
27
+ # @!attribute [r] timestamp
28
+ # @return [Time] An optional timestamp, applied by the service in messages sent to the client, to indicate the system time at which the message was sent (milliseconds past epoch)
29
+ # @!attribute [r] messages
30
+ # @return [Message] A {ProtocolMessage} with a `:message` action contains one or more messages belonging to a channel.
31
+ # @!attribute [r] presence
32
+ # @return [PresenceMessage] A {ProtocolMessage} with a `:presence` action contains one or more presence updates belonging to a channel.
33
+ # @!attribute [r] json
34
+ # @return [Hash] Access the protocol message Hash object ruby'fied to use symbolized keys
35
+ #
36
+ class ProtocolMessage
37
+ include Shared
38
+ include Ably::Modules::Conversions
39
+
40
+ # Actions which are sent by the Ably Realtime API
41
+ #
42
+ # The values correspond to the ints which the API
43
+ # understands.
44
+ ACTIONS = {
45
+ heartbeat: 0,
46
+ ack: 1,
47
+ nack: 2,
48
+ connect: 3,
49
+ connected: 4,
50
+ disconnect: 5,
51
+ disconnected: 6,
52
+ close: 7,
53
+ closed: 8,
54
+ error: 9,
55
+ attach: 10,
56
+ attached: 11,
57
+ detach: 12,
58
+ detached: 13,
59
+ presence: 14,
60
+ message: 15
61
+ }.freeze
62
+
63
+ # Retrieve an action symbol by the integer value
64
+ def self.action_sym_for(action_int)
65
+ @actions_index_by_int ||= ACTIONS.invert.freeze
66
+ @actions_index_by_int[action_int]
67
+ end
68
+
69
+ # Retrive an action integer value from a symbol and raise an exception if invalid
70
+ def self.action!(action_sym)
71
+ ACTIONS.fetch(action_sym)
72
+ end
73
+
74
+ # Indicates this protocol message action will generate an ACK response such as :message or :presence
75
+ def self.ack_required?(for_action)
76
+ for_action = ACTIONS.fetch(for_action) if for_action.kind_of?(Symbol)
77
+ [action!(:presence), action!(:message)].include?(for_action)
78
+ end
79
+
80
+ def initialize(json_object)
81
+ @raw_json_object = json_object
82
+ @json_object = rubify(@raw_json_object).freeze
83
+ end
84
+
85
+ %w( action count
86
+ channel channel_serial
87
+ connection_id connection_serial ).each do |attribute|
88
+ define_method attribute do
89
+ json[attribute.to_sym]
90
+ end
91
+ end
92
+
93
+ def action_sym
94
+ self.class.action_sym_for(action)
95
+ end
96
+
97
+ def error
98
+ @error_info ||= ErrorInfo.new(json[:error]) if json[:error]
99
+ end
100
+
101
+ def timestamp
102
+ Time.at(json[:timestamp] / 1000.0) if json[:timestamp]
103
+ end
104
+
105
+ def message_serial
106
+ json[:msg_serial]
107
+ end
108
+
109
+ def messages
110
+ @messages ||=
111
+ Array(json[:messages]).map do |message|
112
+ Message.new(message, self)
113
+ end
114
+ end
115
+
116
+ def presence
117
+ @presence ||=
118
+ Array(json[:presence]).map do |message|
119
+ PresenceMessage.new(message, self)
120
+ end
121
+ end
122
+
123
+ def json
124
+ @json_object
125
+ end
126
+
127
+ # Indicates this protocol message will generate an ACK response when sent
128
+ # Examples of protocol messages required ACK include :message and :presence
129
+ def ack_required?
130
+ self.class.ack_required?(action)
131
+ end
132
+
133
+ def to_json_object
134
+ raise RuntimeError, ":action is missing, cannot generate valid JSON for ProtocolMessage" unless action_sym
135
+ raise RuntimeError, ":msg_serial is missing, cannot generate valid JSON for ProtocolMessage" if ack_required? && !message_serial
136
+
137
+ json_object = json.dup.tap do |json_object|
138
+ json_object[:messages] = messages.map(&:to_json_object) unless messages.empty?
139
+ json_object[:presence] = presence.map(&:to_json_object) unless presence.empty?
140
+ end
141
+
142
+ javify(json_object)
143
+ end
144
+
145
+ def to_json
146
+ to_json_object.to_json
147
+ end
148
+ end
149
+ end
@@ -0,0 +1,17 @@
1
+ module Ably::Realtime::Models
2
+ module Shared
3
+ include Ably::Modules::Conversions
4
+
5
+ # Provide a normal Hash accessor to the underlying raw message object
6
+ #
7
+ # @return [Object]
8
+ def [](key)
9
+ json[key]
10
+ end
11
+
12
+ def ==(other)
13
+ other.kind_of?(self.class) &&
14
+ json == other.json
15
+ end
16
+ end
17
+ end
@@ -1,16 +1,29 @@
1
1
  require "ably/rest/channel"
2
2
  require "ably/rest/channels"
3
3
  require "ably/rest/client"
4
- require "ably/rest/paged_resource"
4
+ require "ably/rest/models/message"
5
+ require "ably/rest/models/paged_resource"
5
6
  require "ably/rest/presence"
6
7
 
7
8
  module Ably
8
9
  module Rest
9
10
  # Convenience method providing an alias to {Ably::Rest::Client} constructor.
10
11
  #
12
+ # @param (see Ably::Rest::Client#initialize)
13
+ # @option options (see Ably::Rest::Client#initialize)
14
+ #
15
+ # @yield (see Ably::Rest::Client#initialize)
16
+ # @yieldparam (see Ably::Rest::Client#initialize)
17
+ # @yieldreturn (see Ably::Rest::Client#initialize)
18
+ #
11
19
  # @return [Ably::Rest::Client]
12
- def self.new(*args)
13
- Ably::Rest::Client.new(*args)
20
+ #
21
+ # @example
22
+ # # create a new client authenticating with basic auth
23
+ # client = Ably::Rest.new('key.id:secret')
24
+ #
25
+ def self.new(options, &auth_block)
26
+ Ably::Rest::Client.new(options, &auth_block)
14
27
  end
15
28
  end
16
29
  end
@@ -41,13 +41,13 @@ module Ably
41
41
  # @option options [Integer] :limit Maximum number of messages to retrieve up to 10,000
42
42
  # @option options [Symbol] :by `:message`, `:bundle` or `:hour`. Defaults to `:message`
43
43
  #
44
- # @return [PagedResource] An Array of hashes representing the message history that supports paging (next, first)
44
+ # @return [Models::PagedResource<Models::Message>] An Array of hashes representing the message history that supports paging (next, first)
45
45
  def history(options = {})
46
46
  url = "#{base_path}/messages"
47
47
  # TODO: Remove live param as all history should be live
48
48
  response = client.get(url, options.merge(live: true))
49
49
 
50
- PagedResource.new(response, url, client, coerce_into: 'Ably::Message')
50
+ Models::PagedResource.new(response, url, client, coerce_into: 'Ably::Rest::Models::Message')
51
51
  end
52
52
 
53
53
  def presence
@@ -6,7 +6,7 @@ require "ably/rest/middleware/parse_json"
6
6
 
7
7
  module Ably
8
8
  module Rest
9
- # Wrapper for the Ably REST API
9
+ # Client for the Ably REST API
10
10
  #
11
11
  # @!attribute [r] auth
12
12
  # @return {Ably::Auth} authentication object configured for this connection
@@ -19,37 +19,26 @@ module Ably
19
19
  # @!attribute [r] environment
20
20
  # @return [String] May contain 'sandbox' when testing the client library against an alternate Ably environment
21
21
  class Client
22
- include Ably::Support
22
+ include Ably::Modules::HttpHelpers
23
23
  extend Forwardable
24
24
 
25
25
  DOMAIN = "rest.ably.io"
26
26
 
27
27
  attr_reader :tls, :environment, :auth, :channels
28
- def_delegator :auth, :client_id, :auth_options
28
+ def_delegators :auth, :client_id, :auth_options
29
29
 
30
30
  # Creates a {Ably::Rest::Client Rest Client} and configures the {Ably::Auth} object for the connection.
31
31
  #
32
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 (see Ably::Auth#authorise)
33
34
  # @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
35
  # @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
36
  # @option options [String] :environment Specify 'sandbox' when testing the client library against an alternate Ably environment
48
37
  # @option options [Boolean] :debug_http Send HTTP debugging information from Faraday for all HTTP requests to STDOUT
49
38
  #
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}
39
+ # @yield (see Ably::Auth#authorise)
40
+ # @yieldparam (see Ably::Auth#authorise)
41
+ # @yieldreturn (see Ably::Auth#authorise)
53
42
  #
54
43
  # @return [Ably::Rest::Client]
55
44
  #
@@ -77,8 +66,9 @@ module Ably
77
66
 
78
67
  # Return a REST {Ably::Rest::Channel} for the given name
79
68
  #
80
- # @param [String] name see {Ably::Rest::Channels#get}
81
- # @param [Hash] channel_options see {Ably::Rest::Channels#get}
69
+ # @param (see Ably::Rest::Channels#get)
70
+ #
71
+ # @return (see Ably::Rest::Channels#get)
82
72
  def channel(name, channel_options = {})
83
73
  channels.get(name, channel_options)
84
74
  end
@@ -137,6 +127,13 @@ module Ably
137
127
  )
138
128
  end
139
129
 
130
+ # When true, will send HTTP debugging information from Faraday for all HTTP requests to STDOUT
131
+ #
132
+ # @return [Boolean]
133
+ def debug_http?
134
+ !!@debug_http
135
+ end
136
+
140
137
  private
141
138
  def request(method, path, params = {}, options = {})
142
139
  reauthorise_on_authorisation_failure do