lowdown 0.2.0 → 0.3.0

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.
@@ -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.