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.
- checksums.yaml +4 -4
- data/.rubocop.yml +121 -0
- data/Gemfile +11 -3
- data/README.md +145 -14
- data/Rakefile +13 -6
- data/bin/lowdown +38 -19
- data/examples/long-running.rb +63 -0
- data/examples/simple.rb +37 -0
- data/lib/lowdown.rb +2 -21
- data/lib/lowdown/certificate.rb +21 -1
- data/lib/lowdown/client.rb +156 -60
- data/lib/lowdown/client/request_group.rb +70 -0
- data/lib/lowdown/connection.rb +257 -182
- data/lib/lowdown/connection/monitor.rb +84 -0
- data/lib/lowdown/mock.rb +57 -49
- data/lib/lowdown/notification.rb +24 -6
- data/lib/lowdown/response.rb +9 -20
- data/lib/lowdown/version.rb +4 -1
- data/lowdown.gemspec +5 -3
- metadata +22 -4
- data/lib/lowdown/threading.rb +0 -188
@@ -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
|
+
|
data/lib/lowdown/mock.rb
CHANGED
@@ -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
|
62
|
-
|
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
|
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
|
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.
|
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
|
135
|
-
|
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
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
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 {#
|
172
|
+
# Changes {#connected?} to return `true`.
|
150
173
|
#
|
151
174
|
# @return [void]
|
152
175
|
#
|
153
|
-
def
|
154
|
-
|
155
|
-
@callbacks.kill
|
156
|
-
@callbacks = nil
|
157
|
-
@open = false
|
176
|
+
def connect
|
177
|
+
@connected = true
|
158
178
|
end
|
159
179
|
|
160
|
-
#
|
180
|
+
# Changes {#connected?} to return `false`.
|
161
181
|
#
|
162
182
|
# @return [void]
|
163
183
|
#
|
164
|
-
def
|
165
|
-
|
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#
|
188
|
+
# @return (see Lowdown::Connection#connected?)
|
174
189
|
#
|
175
|
-
def
|
176
|
-
!!@
|
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
|
+
|
data/lib/lowdown/notification.rb
CHANGED
@@ -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
|
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
|
-
|
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.
|
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
|
+
|
data/lib/lowdown/response.rb
CHANGED
@@ -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 = %
|
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
|
-
|
117
|
-
|
118
|
-
|
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
|
+
|
data/lib/lowdown/version.rb
CHANGED
data/lowdown.gemspec
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
# coding: utf-8
|
2
|
-
lib = File.expand_path(
|
2
|
+
lib = File.expand_path("../lib", __FILE__)
|
3
3
|
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
-
require
|
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 =
|
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.
|
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-
|
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.
|
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.
|