lowdown 0.2.0 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,84 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "celluloid/current"
4
+
5
+ module Lowdown
6
+ class Connection
7
+ module Monitor
8
+ # The normal Celluloid::Future implementation expects an object that responds to `value`, when assigning the value
9
+ # via `#signal`:
10
+ #
11
+ # 1. https://github.com/celluloid/celluloid/blob/bb282f826c275c0d60d9591c1bb5b08798799cbe/lib/celluloid/future.rb#L106
12
+ # 2. https://github.com/celluloid/celluloid/blob/bb282f826c275c0d60d9591c1bb5b08798799cbe/lib/celluloid/future.rb#L96
13
+ #
14
+ # Besides that, this class provides a few more conveniences related to how we use this future.
15
+ #
16
+ class Condition < Celluloid::Future
17
+ Result = Struct.new(:value)
18
+
19
+ # Only signal once.
20
+ #
21
+ def signal(value = nil)
22
+ super(Result.new(value)) unless ready?
23
+ end
24
+
25
+ alias_method :wait, :value
26
+ end
27
+
28
+ # @!group Overrides
29
+
30
+ def initialize(*)
31
+ super
32
+ @lowdown_crash_conditions_mutex = Mutex.new
33
+ @lowdown_crash_conditions = []
34
+ end
35
+
36
+ # Send the exception to each of our conditions, to signal that an exception occurred on one of the actors in the
37
+ # pool.
38
+ #
39
+ # @param [Actor] actor
40
+ # @param [Exception] reason
41
+ # @return [void]
42
+ #
43
+ def __crash_handler__(actor, reason)
44
+ if reason # is nil if the actor exits normally
45
+ @lowdown_crash_conditions_mutex.synchronize do
46
+ @lowdown_crash_conditions.each do |condition|
47
+ condition.signal(reason)
48
+ end
49
+ end
50
+ end
51
+ super
52
+ end
53
+
54
+ # @!group Crash condition registration
55
+
56
+ # Adds a condition to the list of conditions to be notified when an actors dies because of an unhandled exception.
57
+ #
58
+ # @param [Condition] condition
59
+ # @return [void]
60
+ #
61
+ def __register_lowdown_crash_condition__(condition)
62
+ @lowdown_crash_conditions_mutex.synchronize do
63
+ @lowdown_crash_conditions << condition
64
+ end
65
+ end
66
+
67
+ # Removes a condition from the list of conditions that get notified when an actor dies because of an unhandled
68
+ # exception.
69
+ #
70
+ # @param [Condition] condition
71
+ # @return [void]
72
+ #
73
+ def __deregister_lowdown_crash_condition__(condition)
74
+ @lowdown_crash_conditions_mutex.synchronize do
75
+ @lowdown_crash_conditions.delete(condition)
76
+ end
77
+ end
78
+ end
79
+
80
+ # Prepend to ensure our overrides are called first.
81
+ Celluloid::Supervision::Container::Pool.send(:prepend, Monitor)
82
+ end
83
+ end
84
+
@@ -1,7 +1,8 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "lowdown/certificate"
2
4
  require "lowdown/client"
3
5
  require "lowdown/response"
4
- require "lowdown/threading"
5
6
 
6
7
  module Lowdown
7
8
  # Provides a collection of test helpers.
@@ -56,10 +57,11 @@ module Lowdown
56
57
  # @return [Client]
57
58
  # a Client configured with the `uri` and a self-signed certificate that has the `app_bundle_id` encoded.
58
59
  #
59
- def self.client(uri: nil, app_bundle_id: "com.example.MockApp")
60
+ def self.client(uri: nil, app_bundle_id: "com.example.MockApp", keep_alive: false)
60
61
  certificate = certificate(app_bundle_id)
61
- connection = Connection.new(uri: uri, ssl_context: certificate.ssl_context)
62
- Client.client_with_connection(connection, certificate)
62
+ connection = Connection.new(uri, certificate.ssl_context, keep_alive)
63
+ connection.connect if keep_alive
64
+ Client.client_with_connection(connection, certificate: certificate)
63
65
  end
64
66
 
65
67
  # A mock object that can be used instead of a real Connection object.
@@ -67,7 +69,7 @@ module Lowdown
67
69
  class Connection
68
70
  # Represents a recorded request.
69
71
  #
70
- Request = Struct.new(:path, :headers, :body, :response)
72
+ Request = Struct.new(:path, :headers, :body, :response, :delegate, :context)
71
73
 
72
74
  # @!group Mock API: Instance Attribute Summary
73
75
 
@@ -81,29 +83,38 @@ module Lowdown
81
83
  #
82
84
  attr_reader :responses
83
85
 
86
+ # @return [Boolean]
87
+ # whether or not the connection should be opened on initialization. In a pool this basically equals the
88
+ # `keep_alive` Client option.
89
+ #
90
+ attr_reader :pool_keep_alive
91
+
92
+ # @return [Fixnum]
93
+ # the number of workers in a pool.
94
+ #
95
+ attr_accessor :pool_size
96
+
84
97
  # @!group Mock API: Instance Method Summary
85
98
 
86
99
  # @param (see Lowdown::Connection#initialize)
87
100
  #
88
- def initialize(uri: nil, ssl_context: nil)
89
- @uri, @ssl_context = uri, ssl_context
101
+ def initialize(uri = nil, ssl_context = nil, connect = true)
102
+ @uri, @ssl_context, @pool_keep_alive = uri, ssl_context, connect
90
103
  @responses = []
91
104
  @requests = []
92
105
  end
93
106
 
94
- # @param (see Response#unformatted_id)
95
- #
96
107
  # @return [Array<Notification>]
97
108
  # returns the recorded requests as Notification objects.
98
109
  #
99
- def requests_as_notifications(unformatted_id_length = nil)
110
+ def requests_as_notifications
100
111
  @requests.map do |request|
101
112
  headers = request.headers
102
113
  hash = {
103
114
  :token => File.basename(request.path),
104
- :id => request.response.unformatted_id(unformatted_id_length),
115
+ :id => request.response.id,
105
116
  :payload => JSON.parse(request.body),
106
- :topic => headers["apns-topic"]
117
+ :topic => headers["apns-topic"],
107
118
  }
108
119
  hash[:expiration] = Time.at(headers["apns-expiration"].to_i) if headers["apns-expiration"]
109
120
  hash[:priority] = headers["apns-priority"].to_i if headers["apns-priority"]
@@ -121,68 +132,65 @@ module Lowdown
121
132
  #
122
133
  attr_reader :ssl_context
123
134
 
135
+ # @!group Celluloid API
136
+
137
+ def self.pool(size:, args:)
138
+ connection = new(*args)
139
+ connection.pool_size = size
140
+ connection
141
+ end
142
+
143
+ def async
144
+ self
145
+ end
146
+
124
147
  # @!group Real API: Instance Method Summary
125
148
 
126
149
  # Yields stubbed {#responses} or if none are available defaults to success responses. It does this on a different
127
150
  # thread, just like the real API does.
128
151
  #
152
+ # To make the connection simulate being closed from the other end, specify the `test-close-connection` header.
153
+ #
129
154
  # @param (see Lowdown::Connection#post)
130
155
  # @yield (see Lowdown::Connection#post)
131
156
  # @yieldparam (see Lowdown::Connection#post)
132
157
  # @return (see Lowdown::Connection#post)
133
158
  #
134
- def post(path, headers, body, &callback)
135
- response = @responses.shift || Response.new(":status" => "200", "apns-id" => (headers["apns-id"] || generate_id))
136
- @requests << Request.new(path, headers, body, response)
137
- @callbacks.enqueue { callback.call(response) }
138
- end
159
+ def post(path:, headers:, body:, delegate:, context: nil)
160
+ raise "First open the connection." unless @connected
139
161
 
140
- # Changes {#open?} to return `true`.
141
- #
142
- # @return [void]
143
- #
144
- def open
145
- @callbacks = Threading::Consumer.new
146
- @open = true
162
+ unless headers["test-close-connection"]
163
+ response = @responses.shift || Response.new(":status" => "200", "apns-id" => headers["apns-id"])
164
+ end
165
+ @requests << Request.new(path, headers, body, response, delegate, context)
166
+
167
+ raise EOFError, "Stubbed EOF" if headers["test-close-connection"]
168
+
169
+ delegate.handle_apns_response(response, context: context)
147
170
  end
148
171
 
149
- # Changes {#open?} to return `false`.
172
+ # Changes {#connected?} to return `true`.
150
173
  #
151
174
  # @return [void]
152
175
  #
153
- def close
154
- flush
155
- @callbacks.kill
156
- @callbacks = nil
157
- @open = false
176
+ def connect
177
+ @connected = true
158
178
  end
159
179
 
160
- # no-op
180
+ # Changes {#connected?} to return `false`.
161
181
  #
162
182
  # @return [void]
163
183
  #
164
- def flush
165
- caller_thread = Thread.current
166
- @callbacks.enqueue do
167
- sleep 0.1
168
- caller_thread.run
169
- end
170
- Thread.stop
184
+ def disconnect
185
+ @connected = false
171
186
  end
172
187
 
173
- # @return (see Lowdown::Connection#open?)
188
+ # @return (see Lowdown::Connection#connected?)
174
189
  #
175
- def open?
176
- !!@open
177
- end
178
-
179
- private
180
-
181
- def generate_id
182
- @counter ||= 0
183
- @counter += 1
184
- Notification.new(:id => @counter).formatted_id
190
+ def connected?
191
+ !!@connected
185
192
  end
186
193
  end
187
194
  end
188
195
  end
196
+
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Lowdown
2
4
  # A Notification holds the data and metadata about a Remote Notification.
3
5
  #
@@ -6,7 +8,21 @@ module Lowdown
6
8
  #
7
9
  class Notification
8
10
  # @!visibility private
9
- APS_KEYS = %w{ alert badge sound content-available category }.freeze
11
+ APS_KEYS = %w( alert badge sound content-available category ).freeze
12
+
13
+ @id_mutex = Mutex.new
14
+ @id_counter = 0
15
+
16
+ def self.generate_id
17
+ @id_mutex.synchronize do
18
+ @id_counter += 1
19
+ end
20
+ end
21
+
22
+ def self.format_id(id)
23
+ padded = id.to_s.rjust(32, "0")
24
+ [padded[0, 8], padded[8, 4], padded[12, 4], padded[16, 4], padded[20, 12]].join("-")
25
+ end
10
26
 
11
27
  # @return [String]
12
28
  # a device token.
@@ -53,6 +69,10 @@ module Lowdown
53
69
  !!(@token && @payload)
54
70
  end
55
71
 
72
+ def id
73
+ @id ||= self.class.generate_id
74
+ end
75
+
56
76
  # Formats the {#id} in the format required by the APN service, which is in groups of 8-4-4-12. It is padded with
57
77
  # leading zeroes.
58
78
  #
@@ -60,10 +80,7 @@ module Lowdown
60
80
  # the formatted ID.
61
81
  #
62
82
  def formatted_id
63
- if @id
64
- padded = @id.to_s.rjust(32, "0")
65
- [padded[0,8], padded[8,4], padded[12,4], padded[16,4], padded[20,12]].join("-")
66
- end
83
+ @formatted_id ||= self.class.format_id(id)
67
84
  end
68
85
 
69
86
  # Unless the payload contains an `aps` entry, the payload is assumed to be a mix of APN defined attributes and
@@ -75,7 +92,7 @@ module Lowdown
75
92
  # the payload organized according to the APN specification.
76
93
  #
77
94
  def formatted_payload
78
- if @payload.has_key?("aps")
95
+ if @payload.key?("aps")
79
96
  @payload
80
97
  else
81
98
  payload = {}
@@ -94,3 +111,4 @@ module Lowdown
94
111
  end
95
112
  end
96
113
  end
114
+
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Lowdown
2
4
  # An object that represents a response from the Apple Push Notification service for a single notification delivery.
3
5
  #
@@ -22,12 +24,12 @@ module Lowdown
22
24
  413 => "The notification payload was too large",
23
25
  429 => "The server received too many requests for the same device token",
24
26
  500 => "Internal server error",
25
- 503 => "The server is shutting down and unavailable"
26
- }
27
+ 503 => "The server is shutting down and unavailable",
28
+ }.freeze
27
29
 
28
30
  # The reasons that indicate a device token not being valid besides just being unregistered.
29
31
  #
30
- INVALID_TOKEN_REASONS = %{ Unregistered BadDeviceToken DeviceTokenNotForTopic }.freeze
32
+ INVALID_TOKEN_REASONS = %( Unregistered BadDeviceToken DeviceTokenNotForTopic ).freeze
31
33
 
32
34
  # @return [String]
33
35
  # either the {Notification#id} or, if none was provided, an ID generated by the service.
@@ -36,19 +38,6 @@ module Lowdown
36
38
  headers["apns-id"]
37
39
  end
38
40
 
39
- # Tries to convert the ID back to the Notification {Notification#id} by removing leading zeroes.
40
- #
41
- # @param [Integer] unformatted_id_length
42
- # the expected length of an ID, which ensures that **required** leading zeroes are not removed.
43
- #
44
- # @return [String]
45
- # the ID that was assigned to the Notification.
46
- #
47
- def unformatted_id(unformatted_id_length = nil)
48
- id = self.id.tr('-', '')
49
- unformatted_id_length ? id[32-unformatted_id_length,unformatted_id_length] : id.gsub(/\A0*/, '')
50
- end
51
-
52
41
  # @return [Integer]
53
42
  # the HTTP status returned by the service.
54
43
  #
@@ -113,10 +102,9 @@ module Lowdown
113
102
  # a formatted description of the response.
114
103
  #
115
104
  def to_s
116
- s = "#{status} (#{message})"
117
- s << ": #{failure_reason}" unless success?
118
- s << " last checked at #{activity_last_checked_at}" if inactive_token?
119
- s
105
+ reason = ": #{failure_reason}" unless success?
106
+ last_check = " last checked at #{activity_last_checked_at}" if inactive_token?
107
+ "#{status} (#{message})#{reason}#{last_check}"
120
108
  end
121
109
 
122
110
  # @return [String]
@@ -127,3 +115,4 @@ module Lowdown
127
115
  end
128
116
  end
129
117
  end
118
+
@@ -1,3 +1,6 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Lowdown
2
- VERSION = "0.2.0"
4
+ VERSION = "0.3.0".freeze
3
5
  end
6
+
@@ -1,7 +1,7 @@
1
1
  # coding: utf-8
2
- lib = File.expand_path('../lib', __FILE__)
2
+ lib = File.expand_path("../lib", __FILE__)
3
3
  $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
- require 'lowdown/version'
4
+ require "lowdown/version"
5
5
 
6
6
  Gem::Specification.new do |spec|
7
7
  spec.name = "lowdown"
@@ -19,11 +19,13 @@ Gem::Specification.new do |spec|
19
19
  spec.require_paths = ["lib"]
20
20
 
21
21
  # This is currently set to >= 2.0.0 in the http-2 gemspec, which is incorrect, as it uses required keyword arguments.
22
- spec.required_ruby_version = '>= 2.1.1'
22
+ spec.required_ruby_version = ">= 2.1.1"
23
23
 
24
24
  spec.add_runtime_dependency "http-2", ">= 0.8"
25
+ spec.add_runtime_dependency "celluloid-io", ">= 0.17.3" # has Ruby 2.3.0 support
25
26
 
26
27
  spec.add_development_dependency "bundler", "~> 1.11"
27
28
  spec.add_development_dependency "rake", "~> 10.0"
28
29
  spec.add_development_dependency "minitest", "~> 5.0"
29
30
  end
31
+
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: lowdown
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Eloy Durán
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2016-01-19 00:00:00.000000000 Z
11
+ date: 2016-01-29 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: http-2
@@ -24,6 +24,20 @@ dependencies:
24
24
  - - ">="
25
25
  - !ruby/object:Gem::Version
26
26
  version: '0.8'
27
+ - !ruby/object:Gem::Dependency
28
+ name: celluloid-io
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: 0.17.3
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: 0.17.3
27
41
  - !ruby/object:Gem::Dependency
28
42
  name: bundler
29
43
  requirement: !ruby/object:Gem::Requirement
@@ -75,6 +89,7 @@ extensions: []
75
89
  extra_rdoc_files: []
76
90
  files:
77
91
  - ".gitignore"
92
+ - ".rubocop.yml"
78
93
  - ".ruby-version"
79
94
  - ".travis.yml"
80
95
  - ".yardopts"
@@ -84,14 +99,17 @@ files:
84
99
  - Rakefile
85
100
  - bin/lowdown
86
101
  - doc/lowdown.png
102
+ - examples/long-running.rb
103
+ - examples/simple.rb
87
104
  - lib/lowdown.rb
88
105
  - lib/lowdown/certificate.rb
89
106
  - lib/lowdown/client.rb
107
+ - lib/lowdown/client/request_group.rb
90
108
  - lib/lowdown/connection.rb
109
+ - lib/lowdown/connection/monitor.rb
91
110
  - lib/lowdown/mock.rb
92
111
  - lib/lowdown/notification.rb
93
112
  - lib/lowdown/response.rb
94
- - lib/lowdown/threading.rb
95
113
  - lib/lowdown/version.rb
96
114
  - lowdown.gemspec
97
115
  homepage: https://github.com/alloy/lowdown
@@ -114,7 +132,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
114
132
  version: '0'
115
133
  requirements: []
116
134
  rubyforge_project:
117
- rubygems_version: 2.4.5.1
135
+ rubygems_version: 2.5.1
118
136
  signing_key:
119
137
  specification_version: 4
120
138
  summary: A Ruby client for the HTTP/2 version of the Apple Push Notification Service.