ably 0.1.1 → 0.1.2

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