lowdown 0.2.0 → 0.3.0
Sign up to get free protection for your applications and to get access to all the features.
- 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.
|