ably 0.7.0 → 0.7.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 (49) hide show
  1. data/.travis.yml +2 -2
  2. data/Rakefile +2 -0
  3. data/SPEC.md +230 -194
  4. data/ably.gemspec +2 -0
  5. data/lib/ably/auth.rb +7 -5
  6. data/lib/ably/models/idiomatic_ruby_wrapper.rb +5 -7
  7. data/lib/ably/models/paginated_resource.rb +14 -21
  8. data/lib/ably/models/protocol_message.rb +1 -1
  9. data/lib/ably/modules/ably.rb +4 -0
  10. data/lib/ably/modules/async_wrapper.rb +2 -2
  11. data/lib/ably/modules/channels_collection.rb +31 -8
  12. data/lib/ably/modules/conversions.rb +10 -0
  13. data/lib/ably/modules/enum.rb +2 -3
  14. data/lib/ably/modules/state_emitter.rb +8 -8
  15. data/lib/ably/modules/state_machine.rb +7 -3
  16. data/lib/ably/realtime/channel.rb +6 -5
  17. data/lib/ably/realtime/channel/channel_manager.rb +11 -10
  18. data/lib/ably/realtime/channel/channel_state_machine.rb +10 -9
  19. data/lib/ably/realtime/channels.rb +3 -0
  20. data/lib/ably/realtime/client/incoming_message_dispatcher.rb +11 -1
  21. data/lib/ably/realtime/connection.rb +55 -16
  22. data/lib/ably/realtime/connection/connection_manager.rb +25 -8
  23. data/lib/ably/realtime/connection/connection_state_machine.rb +9 -9
  24. data/lib/ably/realtime/connection/websocket_transport.rb +2 -2
  25. data/lib/ably/realtime/presence.rb +16 -17
  26. data/lib/ably/util/crypto.rb +1 -1
  27. data/lib/ably/version.rb +1 -1
  28. data/spec/acceptance/realtime/channel_history_spec.rb +6 -5
  29. data/spec/acceptance/realtime/connection_failures_spec.rb +103 -27
  30. data/spec/acceptance/realtime/connection_spec.rb +81 -17
  31. data/spec/acceptance/realtime/presence_spec.rb +82 -30
  32. data/spec/acceptance/rest/auth_spec.rb +22 -19
  33. data/spec/acceptance/rest/client_spec.rb +4 -4
  34. data/spec/acceptance/rest/presence_spec.rb +12 -6
  35. data/spec/rspec_config.rb +9 -0
  36. data/spec/shared/model_behaviour.rb +1 -1
  37. data/spec/spec_helper.rb +4 -1
  38. data/spec/support/event_machine_helper.rb +26 -37
  39. data/spec/support/markdown_spec_formatter.rb +96 -68
  40. data/spec/support/rest_testapp_before_retry.rb +15 -0
  41. data/spec/support/test_app.rb +4 -0
  42. data/spec/unit/models/idiomatic_ruby_wrapper_spec.rb +20 -2
  43. data/spec/unit/models/message_spec.rb +1 -1
  44. data/spec/unit/models/paginated_resource_spec.rb +15 -1
  45. data/spec/unit/modules/enum_spec.rb +10 -0
  46. data/spec/unit/realtime/channels_spec.rb +30 -0
  47. data/spec/unit/rest/channels_spec.rb +30 -0
  48. metadata +101 -35
  49. checksums.yaml +0 -7
@@ -5,17 +5,19 @@ describe Ably::Auth do
5
5
  include Ably::Modules::Conversions
6
6
 
7
7
  def hmac_for(token_request, secret)
8
- text = token_request.values_at(
8
+ ruby_named_token_request = Ably::Models::IdiomaticRubyWrapper.new(token_request)
9
+
10
+ text = [
9
11
  :id,
10
12
  :ttl,
11
13
  :capability,
12
14
  :client_id,
13
15
  :timestamp,
14
16
  :nonce
15
- ).map { |t| "#{t}\n" }.join("")
17
+ ].map { |key| "#{ruby_named_token_request[key]}\n" }.join("")
16
18
 
17
19
  encode64(
18
- Digest::HMAC.digest(text, key_secret, Digest::SHA256)
20
+ OpenSSL::HMAC.digest(OpenSSL::Digest::SHA256.new, secret, text)
19
21
  )
20
22
  end
21
23
 
@@ -38,7 +40,7 @@ describe Ably::Auth do
38
40
  else
39
41
  JSON.parse(request.body)
40
42
  end
41
- body[key.to_s].to_s == val.to_s
43
+ body[convert_to_mixed_case(key)].to_s == val.to_s
42
44
  end
43
45
 
44
46
  def serialize(object, protocol)
@@ -50,7 +52,7 @@ describe Ably::Auth do
50
52
  end
51
53
 
52
54
  it 'has immutable options' do
53
- expect { auth.options['key_id'] = 'new_id' }.to raise_error RuntimeError, /can't modify frozen Hash/
55
+ expect { auth.options['key_id'] = 'new_id' }.to raise_error RuntimeError, /can't modify frozen.*Hash/
54
56
  end
55
57
 
56
58
  describe '#request_token' do
@@ -70,7 +72,7 @@ describe Ably::Auth do
70
72
  end
71
73
 
72
74
  %w(client_id capability nonce timestamp ttl).each do |option|
73
- context "option :#{option}", :webmock do
75
+ context "with option :#{option}", :webmock do
74
76
  let(:random) { random_int_str }
75
77
  let(:options) { { option.to_sym => random } }
76
78
 
@@ -88,7 +90,7 @@ describe Ably::Auth do
88
90
 
89
91
  before { auth.request_token options }
90
92
 
91
- it 'overrides default' do
93
+ it 'overrides default and uses camelCase notation for all attributes' do
92
94
  expect(request_token_stub).to have_been_requested
93
95
  end
94
96
  end
@@ -134,7 +136,7 @@ describe Ably::Auth do
134
136
  context 'without :query_time option' do
135
137
  let(:options) { { query_time: false } }
136
138
 
137
- it 'queries the server for the time' do
139
+ it 'does not query the server for the time' do
138
140
  expect(client).to_not receive(:time)
139
141
  auth.request_token(options)
140
142
  end
@@ -356,25 +358,25 @@ describe Ably::Auth do
356
358
  subject { auth.create_token_request(options) }
357
359
 
358
360
  it 'uses the key ID from the client' do
359
- expect(subject[:id]).to eql(key_id)
361
+ expect(subject['id']).to eql(key_id)
360
362
  end
361
363
 
362
364
  it 'uses the default TTL' do
363
- expect(subject[:ttl]).to eql(Ably::Models::Token::DEFAULTS[:ttl])
365
+ expect(subject['ttl']).to eql(Ably::Models::Token::DEFAULTS[:ttl])
364
366
  end
365
367
 
366
368
  it 'uses the default capability' do
367
- expect(subject[:capability]).to eql(Ably::Models::Token::DEFAULTS[:capability].to_json)
369
+ expect(subject['capability']).to eql(Ably::Models::Token::DEFAULTS[:capability].to_json)
368
370
  end
369
371
 
370
372
  context 'the nonce' do
371
373
  it 'is unique for every request' do
372
- unique_nonces = 100.times.map { auth.create_token_request[:nonce] }
374
+ unique_nonces = 100.times.map { auth.create_token_request['nonce'] }
373
375
  expect(unique_nonces.uniq.length).to eql(100)
374
376
  end
375
377
 
376
378
  it 'is at least 16 characters' do
377
- expect(subject[:nonce].length).to be >= 16
379
+ expect(subject['nonce'].length).to be >= 16
378
380
  end
379
381
  end
380
382
 
@@ -385,7 +387,7 @@ describe Ably::Auth do
385
387
  options[attribute.to_sym] = option_value
386
388
  end
387
389
  it "overrides default" do
388
- expect(subject[attribute.to_sym].to_s).to eql(option_value.to_s)
390
+ expect(subject[convert_to_mixed_case(attribute)].to_s).to eql(option_value.to_s)
389
391
  end
390
392
  end
391
393
  end
@@ -394,8 +396,9 @@ describe Ably::Auth do
394
396
  let(:options) { { nonce: 'valid', is_not_used_by_token_request: 'invalid' } }
395
397
  specify 'are ignored' do
396
398
  expect(subject.keys).to_not include(:is_not_used_by_token_request)
397
- expect(subject.keys).to include(:nonce)
398
- expect(subject[:nonce]).to eql('valid')
399
+ expect(subject.keys).to_not include(convert_to_mixed_case(:is_not_used_by_token_request))
400
+ expect(subject.keys).to include('nonce')
401
+ expect(subject['nonce']).to eql('valid')
399
402
  end
400
403
  end
401
404
 
@@ -417,7 +420,7 @@ describe Ably::Auth do
417
420
 
418
421
  it 'queries the server for the timestamp' do
419
422
  expect(client).to receive(:time).and_return(time)
420
- expect(subject[:timestamp]).to eql(time.to_i)
423
+ expect(subject['timestamp']).to eql(time.to_i)
421
424
  end
422
425
  end
423
426
 
@@ -426,7 +429,7 @@ describe Ably::Auth do
426
429
  let(:options) { { timestamp: token_request_time } }
427
430
 
428
431
  it 'uses the provided timestamp in the token request' do
429
- expect(subject[:timestamp]).to eql(token_request_time.to_i)
432
+ expect(subject['timestamp']).to eql(token_request_time.to_i)
430
433
  end
431
434
  end
432
435
 
@@ -444,7 +447,7 @@ describe Ably::Auth do
444
447
 
445
448
  it 'generates a valid HMAC' do
446
449
  hmac = hmac_for(options, key_secret)
447
- expect(subject[:mac]).to eql(hmac)
450
+ expect(subject['mac']).to eql(hmac)
448
451
  end
449
452
  end
450
453
  end
@@ -54,12 +54,12 @@ describe Ably::Rest::Client do
54
54
 
55
55
  it 'creates a new token automatically when the old token expires' do
56
56
  expect { client.channel('channel_name').publish('event', 'message') }.to change { client.auth.current_token }
57
- expect(client.auth.current_token.client_id).to eql(token_request_1[:client_id])
57
+ expect(client.auth.current_token.client_id).to eql(token_request_1['clientId'])
58
58
 
59
59
  sleep 1
60
60
 
61
61
  expect { client.channel('channel_name').publish('event', 'message') }.to change { client.auth.current_token }
62
- expect(client.auth.current_token.client_id).to eql(token_request_2[:client_id])
62
+ expect(client.auth.current_token.client_id).to eql(token_request_2['clientId'])
63
63
  end
64
64
  end
65
65
 
@@ -68,12 +68,12 @@ describe Ably::Rest::Client do
68
68
 
69
69
  it 'reuses the existing token for every request' do
70
70
  expect { client.channel('channel_name').publish('event', 'message') }.to change { client.auth.current_token }
71
- expect(client.auth.current_token.client_id).to eql(token_request_1[:client_id])
71
+ expect(client.auth.current_token.client_id).to eql(token_request_1['clientId'])
72
72
 
73
73
  sleep 1
74
74
 
75
75
  expect { client.channel('channel_name').publish('event', 'message') }.to_not change { client.auth.current_token }
76
- expect(client.auth.current_token.client_id).to eql(token_request_1[:client_id])
76
+ expect(client.auth.current_token.client_id).to eql(token_request_1['clientId'])
77
77
  end
78
78
  end
79
79
  end
@@ -18,13 +18,13 @@ describe Ably::Rest::Presence do
18
18
  end
19
19
 
20
20
  context 'tested against presence fixture data set up in test app' do
21
- before(:context) do
22
- # When this test is run as a part of a test suite, the presence data injected in the test app may have expired
23
- WebMock.disable!
24
- TestApp.reload
25
- end
26
-
27
21
  describe '#get' do
22
+ before(:context) do
23
+ # When this test is run as a part of a test suite, the presence data injected in the test app may have expired
24
+ WebMock.disable!
25
+ TestApp.reload
26
+ end
27
+
28
28
  let(:channel) { client.channel('persisted:presence_fixtures') }
29
29
  let(:presence) { channel.presence.get }
30
30
 
@@ -52,6 +52,12 @@ describe Ably::Rest::Presence do
52
52
  end
53
53
 
54
54
  describe '#history' do
55
+ before(:context) do
56
+ # When this test is run as a part of a test suite, the presence data injected in the test app may have expired
57
+ WebMock.disable!
58
+ TestApp.reload
59
+ end
60
+
55
61
  let(:channel) { client.channel('persisted:presence_fixtures') }
56
62
  let(:presence_history) { channel.presence.history }
57
63
 
data/spec/rspec_config.rb CHANGED
@@ -5,6 +5,8 @@
5
5
  #
6
6
  # See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration
7
7
 
8
+ require 'rspec/retry'
9
+
8
10
  RSpec.configure do |config|
9
11
  config.run_all_when_everything_filtered = true
10
12
  config.filter_run :focus
@@ -45,4 +47,11 @@ RSpec.configure do |config|
45
47
  end
46
48
 
47
49
  config.add_formatter Ably::RSpec::PrivateApiFormatter
50
+
51
+ if ENV['RSPEC_RETRY']
52
+ puts 'Running tests using RSpec retry'
53
+ config.verbose_retry = true # show retry status in spec process
54
+ config.default_retry_count = 3
55
+ config.default_sleep_interval = 2
56
+ end
48
57
  end
@@ -74,7 +74,7 @@ shared_examples 'a model' do |shared_options = {}|
74
74
  let(:model_options) { { channel: 'name' } }
75
75
 
76
76
  it 'prevents changes' do
77
- expect { model.hash[:channel] = 'new' }.to raise_error RuntimeError, /can't modify frozen Hash/
77
+ expect { model.hash[:channel] = 'new' }.to raise_error RuntimeError, /can't modify frozen.*Hash/
78
78
  end
79
79
 
80
80
  it 'dups options' do
data/spec/spec_helper.rb CHANGED
@@ -9,9 +9,12 @@ require 'webmock/rspec'
9
9
  require 'ably'
10
10
 
11
11
  require 'support/api_helper'
12
- require 'support/event_machine_helper'
13
12
  require 'support/private_api_formatter'
14
13
  require 'support/protocol_helper'
15
14
  require 'support/random_helper'
16
15
 
17
16
  require 'rspec_config'
17
+
18
+ # EM Helper must be loaded after rspec_config to ensure around block occurs before RSpec retry
19
+ require 'support/event_machine_helper'
20
+ require 'support/rest_testapp_before_retry'
@@ -6,7 +6,7 @@ module RSpec
6
6
  module EventMachine
7
7
  extend self
8
8
 
9
- DEFAULT_TIMEOUT = 5
9
+ DEFAULT_TIMEOUT = 10
10
10
 
11
11
  def run_reactor(timeout = DEFAULT_TIMEOUT)
12
12
  Timeout::timeout(timeout + 0.5) do
@@ -24,8 +24,8 @@ module RSpec
24
24
 
25
25
  # Allows multiple Deferrables to be passed in and calls the provided block when
26
26
  # all success callbacks have completed
27
- def when_all(*deferrables, &block)
28
- raise "Block expected" unless block_given?
27
+ def when_all(*deferrables)
28
+ raise ArgumentError, 'Block required' unless block_given?
29
29
 
30
30
  options = if deferrables.last.kind_of?(Hash)
31
31
  deferrables.pop
@@ -40,9 +40,9 @@ module RSpec
40
40
  successful_deferrables[deferrable.object_id] = true
41
41
  if successful_deferrables.keys.sort == deferrables.map(&:object_id).sort
42
42
  if options[:and_wait]
43
- ::EventMachine.add_timer(options[:and_wait]) { block.call }
43
+ ::EventMachine.add_timer(options[:and_wait]) { yield }
44
44
  else
45
- block.call
45
+ yield
46
46
  end
47
47
  end
48
48
  end
@@ -52,6 +52,15 @@ module RSpec
52
52
  end
53
53
  end
54
54
  end
55
+
56
+ def wait_until(condition_block, &block)
57
+ raise ArgumentError, 'Block required' unless block_given?
58
+
59
+ yield if condition_block.call
60
+ ::EventMachine.add_timer(0.1) do
61
+ wait_until condition_block, &block
62
+ end
63
+ end
55
64
  end
56
65
  end
57
66
 
@@ -62,32 +71,11 @@ RSpec.configure do |config|
62
71
  end
63
72
  end
64
73
 
65
- # Running a reactor block and then calling the example block with #call
66
- # does not work as expected as the example completes immediately and the block
67
- # calls after hooks before it returns the EventMachine loop.
74
+ # Run the test block wrapped in an EventMachine reactor that has a configured timeout.
75
+ # As RSpec does not provide an API to wrap blocks, accessing the instance variables is required.
76
+ # Note, if you start a reactor and simply run the example with example#run then the example
77
+ # will run and not wait for the reactor to stop thus triggering after callbacks prematurely.
68
78
  #
69
- # As there is no public API to inject around blocks correctly without calling the after blocks,
70
- # we have to monkey patch the run_after_example method at https://github.com/rspec/rspec-core/blob/master/lib/rspec/core/example.rb#L376
71
- # so that it does not run until we explicitly call it once the EventMachine reactor loop is finished.
72
- #
73
- def patch_example_block_with_surrounding_eventmachine_reactor(example)
74
- example.example.class.class_eval do
75
- alias_method :run_after_example_original, :run_after_example
76
- public :run_after_example_original
77
-
78
- # prevent after hooks being run for example until EventMachine reactor has finished
79
- def run_after_example; end
80
- end
81
- end
82
-
83
- def remove_patch_example_block(example)
84
- example.example.class.class_eval do
85
- remove_method :run_after_example
86
- alias_method :run_after_example, :run_after_example_original
87
- remove_method :run_after_example_original
88
- end
89
- end
90
-
91
79
  config.around(:example, :event_machine) do |example|
92
80
  timeout = if example.metadata[:em_timeout].is_a?(Numeric)
93
81
  example.metadata[:em_timeout]
@@ -95,17 +83,18 @@ RSpec.configure do |config|
95
83
  RSpec::EventMachine::DEFAULT_TIMEOUT
96
84
  end
97
85
 
98
- patch_example_block_with_surrounding_eventmachine_reactor example
86
+ example_block = example.example.instance_variable_get('@example_block')
87
+ example_group_instance = example.example.instance_variable_get('@example_group_instance')
99
88
 
100
- begin
89
+ event_machine_block = Proc.new do
101
90
  RSpec::EventMachine.run_reactor(timeout) do
102
- example.call
103
- raise example.exception if example.exception
91
+ example_group_instance.instance_exec(example, &example_block)
104
92
  end
105
- ensure
106
- example.example.run_after_example_original
107
- remove_patch_example_block example
108
93
  end
94
+
95
+ example.example.instance_variable_set('@example_block', event_machine_block)
96
+
97
+ example.run
109
98
  end
110
99
 
111
100
  config.before(:example) do
@@ -1,89 +1,117 @@
1
- module Ably::RSpec
2
- # Generate Markdown Specification from the RSpec public API tests
3
- #
4
- class MarkdownSpecFormatter
5
- ::RSpec::Core::Formatters.register self, :start, :close,
6
- :example_group_started, :example_group_finished,
7
- :example_passed, :example_failed, :example_pending,
8
- :dump_summary
9
-
10
- def initialize(output)
11
- @output = File.open(File.expand_path('../../../SPEC.md', __FILE__), 'w')
12
- @indent = 0
13
- @passed = 0
14
- @pending = 0
15
- @failed = 0
16
- end
1
+ module Ably
2
+ module RSpec
3
+ # Generate Markdown Specification from the RSpec public API tests
4
+ #
5
+ class MarkdownSpecFormatter
6
+ ::RSpec::Core::Formatters.register self, :start, :close,
7
+ :example_group_started, :example_group_finished,
8
+ :example_passed, :example_failed, :example_pending,
9
+ :dump_summary
10
+
11
+ def initialize(output)
12
+ @output = if documenting_rest_only?
13
+ File.open(File.expand_path('../../../../../../SPEC.md', __FILE__), 'w')
14
+ else
15
+ File.open(File.expand_path('../../../SPEC.md', __FILE__), 'w')
16
+ end
17
+
18
+ @indent = 0
19
+ @passed = 0
20
+ @pending = 0
21
+ @failed = 0
22
+ end
17
23
 
18
- def start(notification)
19
- puts "\n\e[33m --> Creating SPEC.md <--\e[0m\n"
20
- output.write "# Ably Client Library #{Ably::VERSION} Specification\n"
21
- end
24
+ def start(notification)
25
+ puts "\n\e[33m --> Creating SPEC.md <--\e[0m\n"
26
+ scope = if defined?(Ably::Realtime)
27
+ 'Real-time & REST'
28
+ else
29
+ 'REST'
30
+ end
31
+ output.write "# Ably #{scope} Client Library #{Ably::VERSION} Specification\n"
32
+ end
22
33
 
23
- def close(notification)
24
- output.close
25
- end
34
+ def close(notification)
35
+ output.close
36
+ end
26
37
 
27
- def example_group_started(notification)
28
- output.write "#{indent_prefix}#{notification.group.description}\n"
29
- output.write "_(see #{heading_location_path(notification)})_\n" if indent == 0
30
- @indent += 1
31
- end
38
+ def example_group_started(notification)
39
+ output.write "#{indent_prefix}#{notification.group.description}\n"
40
+ output.write "_(see #{heading_location_path(notification)})_\n" if indent == 0
41
+ @indent += 1
42
+ end
32
43
 
33
- def example_group_finished(notification)
34
- @indent -= 1
35
- end
44
+ def example_group_finished(notification)
45
+ @indent -= 1
46
+ end
36
47
 
37
- def example_passed(notification)
38
- unless notification.example.metadata[:api_private]
39
- output.write "#{indent_prefix}#{example_name_and_link(notification)}\n"
40
- @passed += 1
48
+ def example_passed(notification)
49
+ unless notification.example.metadata[:api_private]
50
+ output.write "#{indent_prefix}#{example_name_and_link(notification)}\n"
51
+ @passed += 1
52
+ end
41
53
  end
42
- end
43
54
 
44
- def example_failed(notification)
45
- unless notification.example.metadata[:api_private]
46
- output.write "#{indent_prefix}FAILED: ~~#{example_name_and_link(notification)}~~\n"
47
- @failed += 1
55
+ def example_failed(notification)
56
+ unless notification.example.metadata[:api_private]
57
+ output.write "#{indent_prefix}FAILED: ~~#{example_name_and_link(notification)}~~\n"
58
+ @failed += 1
59
+ end
48
60
  end
49
- end
50
61
 
51
- def example_pending(notification)
52
- unless notification.example.metadata[:api_private]
53
- output.write "#{indent_prefix}PENDING: *#{example_name_and_link(notification)}*\n"
54
- @pending += 1
62
+ def example_pending(notification)
63
+ unless notification.example.metadata[:api_private]
64
+ output.write "#{indent_prefix}PENDING: *#{example_name_and_link(notification)}*\n"
65
+ @pending += 1
66
+ end
55
67
  end
56
- end
57
68
 
58
- def dump_summary(notification)
59
- output.write <<-EOF.gsub(' ', '')
69
+ def dump_summary(notification)
70
+ output.write <<-EOF.gsub(' ', '')
60
71
 
61
- -------
72
+ -------
62
73
 
63
- ## Test summary
74
+ ## Test summary
64
75
 
65
- * Passing tests: #{@passed}
66
- * Pending tests: #{@pending}
67
- * Failing tests: #{@failed}
68
- EOF
69
- end
76
+ * Passing tests: #{@passed}
77
+ * Pending tests: #{@pending}
78
+ * Failing tests: #{@failed}
79
+ EOF
80
+ end
70
81
 
71
- private
72
- attr_reader :output, :indent
82
+ private
83
+ attr_reader :output, :indent
73
84
 
74
- def example_name_and_link(notification)
75
- "[#{notification.example.metadata[:description]}](#{notification.example.location.gsub(/\:(\d+)/, '#L\1')})"
76
- end
85
+ def documenting_rest_only?
86
+ File.exists?(File.expand_path('../../../../../../ably-rest.gemspec', __FILE__))
87
+ end
77
88
 
78
- def heading_location_path(notification)
79
- "[#{notification.group.location.gsub(/\:(\d+)/, '').gsub(%r{^.\/}, '')}](#{notification.group.location.gsub(/\:(\d+)/, '')})"
80
- end
89
+ def example_name_and_link(notification)
90
+ "[#{notification.example.metadata[:description]}](#{path_for(notification.example.location).gsub(/\:(\d+)/, '#L\1')})"
91
+ end
92
+
93
+ def heading_location_path(notification)
94
+ "[#{notification.group.location.gsub(/\:(\d+)/, '').gsub(%r{^\.\/}, '')}](#{path_for(notification.group.location).gsub(/\:(\d+)/, '')})"
95
+ end
96
+
97
+ def path_for(location)
98
+ if documenting_rest_only?
99
+ "https://github.com/ably/ably-ruby/tree/#{submodule_sha}#{location.gsub(%r{^\./lib/submodules/ably-ruby}, '')}"
100
+ else
101
+ location
102
+ end
103
+ end
104
+
105
+ def submodule_sha
106
+ @sha ||= `git ls-tree HEAD:lib/submodules grep ably-ruby`[/^\w+\s+\w+\s+(\w+)/, 1]
107
+ end
81
108
 
82
- def indent_prefix
83
- if indent > 0
84
- "#{' ' * indent}* "
85
- else
86
- "\n### "
109
+ def indent_prefix
110
+ if indent > 0
111
+ "#{' ' * indent}* "
112
+ else
113
+ "\n### "
114
+ end
87
115
  end
88
116
  end
89
117
  end