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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 74b301dd024aaf5cf6933d95639ff6b0f016e26f
4
- data.tar.gz: 13981473dbf0c6a8c117fadd09152e23a93c9073
3
+ metadata.gz: ab366da937c62aeebaaa7070b47f3f3a108e1460
4
+ data.tar.gz: 568554722d19bf9899cfdfda1fa96de1698365ce
5
5
  SHA512:
6
- metadata.gz: dc3bef31593849519775831cc608a34321c5883a12f72b7ad8baabbd9796d70bf5d184dd092b0beaa7b14e85a171f683a58c0a3f6a66623ca5d71fa196298c10
7
- data.tar.gz: 191a7e45a3e6af3e9ac3c8af8ad04733195754d7822c5463ef26939d0171e0bbe02479dd34100255b20443ddfea8b618f429e810b7549cfc69424fdbe776ae3f
6
+ metadata.gz: 90a597f2d9faae4afe0a4640b3618303b8740de0132fbfd5d9b291526ef3a240b0bbbe35208279197811a021305cb0edff7408c4d96e356a14a7a3a686b4db01
7
+ data.tar.gz: 81fe7bf93ff1c4fa94090720c32293155a08b59fe2b6e6e0b5ca9dce7feac5d322d0f6c260a9ba68a8476443656710882dd33610b824a21601131cc9db96abba
@@ -0,0 +1,121 @@
1
+ AllCops:
2
+ TargetRubyVersion: 2.1
3
+
4
+ Documentation:
5
+ Enabled: false
6
+
7
+ # They are idiomatic
8
+ Lint/AssignmentInCondition:
9
+ Enabled: false
10
+
11
+ Lint/RescueException:
12
+ Enabled: false
13
+
14
+ Lint/StringConversionInInterpolation:
15
+ Enabled: false
16
+
17
+ Lint/UselessAssignment:
18
+ Enabled: false
19
+
20
+ Lint/UnusedMethodArgument:
21
+ Enabled: false
22
+
23
+ Lint/HandleExceptions:
24
+ Enabled: false
25
+
26
+ Metrics/LineLength:
27
+ Max: 120
28
+ AllowURI: true
29
+ URISchemes:
30
+ - http
31
+ - https
32
+
33
+ # Arbitrary max lengths for classes simply do not work and enabling this will
34
+ # lead to a never ending stream of annoyance and changes.
35
+ Metrics/ClassLength:
36
+ Enabled: false
37
+
38
+ # Arbitrary max lengths for modules simply do not work and enabling this will
39
+ # lead to a never ending stream of annoyance and changes.
40
+ Metrics/ModuleLength:
41
+ Enabled: false
42
+
43
+ # Arbitrary max lengths for methods simply do not work and enabling this will
44
+ # lead to a never ending stream of annoyance and changes.
45
+ Metrics/MethodLength:
46
+ Enabled: false
47
+
48
+ # No enforced convention here.
49
+ Metrics/BlockNesting:
50
+ Enabled: false
51
+
52
+ # It will be obvious which code is complex, Rubocop should only lint simple
53
+ # rules for us.
54
+ Metrics/AbcSize:
55
+ Enabled: false
56
+
57
+ # It will be obvious which code is complex, Rubocop should only lint simple
58
+ # rules for us.
59
+ Metrics/CyclomaticComplexity:
60
+ Enabled: false
61
+
62
+ # It will be obvious which code is complex, Rubocop should only lint simple
63
+ # rules for us.
64
+ Metrics/PerceivedComplexity:
65
+ Enabled: false
66
+
67
+ Metrics/ParameterLists:
68
+ Enabled: false
69
+
70
+ Style/GuardClause:
71
+ Enabled: false
72
+
73
+ Style/Alias:
74
+ EnforcedStyle: prefer_alias_method
75
+
76
+ Style/AsciiComments:
77
+ Enabled: false
78
+
79
+ Style/SignalException:
80
+ EnforcedStyle: only_raise
81
+
82
+ Style/DoubleNegation:
83
+ Enabled: false
84
+
85
+ Style/EmptyLines:
86
+ Exclude:
87
+ - 'test/test_helper.rb'
88
+
89
+ Style/FileName:
90
+ Exclude:
91
+ - 'examples/*.rb'
92
+
93
+ Style/GlobalVars:
94
+ Enabled: false
95
+
96
+ # Should have `EnforcedStyle: hash_rockets`, because that’s the only consistent style,
97
+ # but that incorrectly flags keyword arguments as well.
98
+ Style/HashSyntax:
99
+ Enabled: false
100
+
101
+ Style/IfInsideElse:
102
+ Enabled: false
103
+
104
+ Style/IndentHash:
105
+ Enabled: false
106
+
107
+ Style/ParallelAssignment:
108
+ Enabled: false
109
+
110
+ Style/StringLiterals:
111
+ EnforcedStyle: double_quotes
112
+
113
+ Style/StructInheritance:
114
+ Enabled: false
115
+
116
+ Style/TrailingBlankLines:
117
+ EnforcedStyle: final_blank_line
118
+
119
+ Style/TrailingCommaInLiteral:
120
+ EnforcedStyleForMultiline: comma
121
+
data/Gemfile CHANGED
@@ -1,9 +1,17 @@
1
- source 'https://rubygems.org'
1
+ source "https://rubygems.org"
2
2
 
3
- #gem 'http-2', :git => 'https://github.com/alloy/http-2.git', :branch => "apns"
3
+ # gem "http-2", :git => "https://github.com/alloy/http-2.git", :branch => "apns"
4
+
5
+ # Test on minimum required dependency for now.
6
+ # gem "celluloid-io", "0.17.0"
4
7
 
5
8
  gemspec
6
9
 
10
+ group :development do
11
+ gem "rubocop"
12
+ end
13
+
7
14
  group :doc do
8
- gem 'yard'
15
+ gem "yard"
9
16
  end
17
+
data/README.md CHANGED
@@ -4,18 +4,29 @@
4
4
 
5
5
  [![Build Status](https://travis-ci.org/alloy/lowdown.svg?branch=master)](https://travis-ci.org/alloy/lowdown)
6
6
 
7
- Lowdown is a Ruby client for the HTTP/2 version of the Apple Push Notification Service.
7
+ NOTE: _This is not battle-tested yet. This will follow over the next few weeks._
8
8
 
9
- Multiple notifications are multiplexed for efficiency.
9
+ Lowdown is a Ruby client for the HTTP/2 version of the Apple Push Notification Service.
10
10
 
11
- If you need to cotinuously send notifications, it’s a good idea to keep an open connection. Managing that, in for
12
- instance a daemon, is beyond the scope of this library. We might release an extra daemon/server tool in the future that
13
- provides this functionality, but for now you should simply use the Client provided in this library without the block
14
- form (which automatically closes the connection) and build your own daemon/server setup, as required.
11
+ For efficiency, multiple notification requests are multiplexed and a single client can manage a pool of connections.
15
12
 
16
- Also checkout [this library](https://github.com/alloy/time_zone_scheduler) for scheduling across time zones.
13
+ ```
14
+ $ bundle exec ruby examples/simple.rb path/to/certificate.pem development <device-token>
15
+ Sent notification with ID: 13
16
+ Sent notification with ID: 1
17
+ Sent notification with ID: 10
18
+ Sent notification with ID: 7
19
+ Sent notification with ID: 25
20
+ ...
21
+ Sent notification with ID: 10000
22
+ Sent notification with ID: 9984
23
+ Sent notification with ID: 9979
24
+ Sent notification with ID: 9992
25
+ Sent notification with ID: 9999
26
+ Finished in 14.98157 seconds
27
+ ```
17
28
 
18
- NOTE: _It is not yet battle-tested. This will all follow over the next few weeks._
29
+ _This example was run with a pool of 10 connections._
19
30
 
20
31
  ## Installation
21
32
 
@@ -25,19 +36,135 @@ Add this line to your application's Gemfile:
25
36
  gem 'lowdown'
26
37
  ```
27
38
 
28
- And then execute:
29
-
30
- $ bundle install
31
-
32
- Or install it yourself as:
39
+ Or install it yourself, for instance for the command-line usage, as:
33
40
 
34
- $ gem install lowdown
41
+ ```
42
+ $ gem install lowdown
43
+ ```
35
44
 
36
45
  ## Usage
37
46
 
38
47
  You can use the `lowdown` bin that comes with this gem or for code usage see
39
48
  [the documentation](http://www.rubydoc.info/gems/lowdown).
40
49
 
50
+ There are mainly two different modes in which you’ll typically use [this client][client]. Either you deliver a batch of
51
+ notifications every now and then, in which case you only want to open a connection to the remote service when needed, or
52
+ you need to be able to continuously deliver transactional notifications, in which case you’ll want to maintain
53
+ persistent connections. You can find examples of both these modes in the `examples` directory.
54
+
55
+ But first things first, this is how you create [a notification object][notification]:
56
+
57
+ ```ruby
58
+ notification = Lowdown::Notification.new(:token => "device-token", :payload => { :alert => "Hello World!" })
59
+ ```
60
+
61
+ There’s plenty more options for a notification, please refer to [the Notification documentation][notification].
62
+
63
+ ### Short-lived connection
64
+
65
+ After obtaining a client, the simplest way to open a connection for a short period is by passing a block to `connect`.
66
+ This will open the connection, yield the block, and close the connection by the end of the block:
67
+
68
+ ```ruby
69
+ client = Lowdown::Client.production(true, File.read("path/to/certificate.pem")
70
+ client.connect do |group|
71
+ # ...
72
+ end
73
+ ```
74
+
75
+ ### Persistent connection
76
+
77
+ The trick to creating a persistent connection is to specify the `keep_alive: true` option when creating the client:
78
+
79
+ ```ruby
80
+ client = Lowdown::Client.production(true, File.read("path/to/certificate.pem"), keep_alive: true)
81
+
82
+ # Send a batch of notifications
83
+ client.group do |group|
84
+ # ...
85
+ end
86
+
87
+ # Send another batch of notifications
88
+ client.group do |group|
89
+ # ...
90
+ end
91
+ ```
92
+
93
+ One big difference you’ll notice with the short-lived connection example, is that you no longer use the `Client#connect`
94
+ method, nor do you close the connection (at least not until your process ends). Instead you use the `group` method to
95
+ group a set of deliveries.
96
+
97
+ ### Grouping requests
98
+
99
+ Because Lowdown uses background threads to deliver notifications, the thread you’re delivering them _from_ would
100
+ normally chug along, which is often not what you’d want. To solve this, the `group` method provides you with [a group
101
+ object][group] which allows you to handle responses for the requests made in that group and halts the caller thread
102
+ until all responses have been handled.
103
+
104
+ All responses in a group will be handled in a single background thread, without halting the connection threads.
105
+
106
+ In typical Ruby fashion, a group provides a way to specify callbacks as blocks:
107
+
108
+ ```ruby
109
+ group.send_notification(notification) do |response|
110
+ # ...
111
+ end
112
+ ```
113
+
114
+ But there’s another possiblity, which is to provide [a delegate object][delegate] which gets a message sent for each
115
+ response:
116
+
117
+ ```ruby
118
+ class Delegate
119
+ def handle_apns_response(response, context:)
120
+ # ...
121
+ end
122
+ end
123
+
124
+ delegate = Delegate.new
125
+
126
+ client.group do |group|
127
+ group.send_notification(notification, delegate: delegate)
128
+ end
129
+ ```
130
+
131
+ Keep in mind that, like with the block version, this message is sent on the group’s background thread.
132
+
133
+ ### Threading
134
+
135
+ While we’re on the topic of threading anyways, here’s an important thing to keep in mind; each set of `group` callbacks
136
+ is performed on its own thread. It is thus _your_ responsibility to take this into account. E.g. if you are planning to
137
+ update a DB model with the status of a notification delivery, be sure to respect the treading rules of your DB client,
138
+ which usually means to not re-use models that were loaded on a different thread.
139
+
140
+ A simple approach to this is by passing the data you need to be able to update the DB as a `context`, which can be any
141
+ type of object or an array objects:
142
+
143
+ ```ruby
144
+ group.send_notification(notification, context: model.id) do |response, model_id|
145
+ reloaded_model = Model.find(model_id)
146
+ if response.success?
147
+ reloaded_model.touch(:sent_at)
148
+ else
149
+ reloaded_model.update_attribute(:last_response, response.status)
150
+ end
151
+ end
152
+ ```
153
+
154
+ ### Connection pool
155
+
156
+ When you need to be able to deliver many notifications in a short amount of time, it can be beneficial to open multiple
157
+ connections to the remote service. By default Lowdown will initialize clients with a single connection, but you may
158
+ increase this with the `pool_size` option:
159
+
160
+ ```ruby
161
+ Lowdown::Client.production(true, File.read("path/to/certificate.pem"), pool_size: 3)
162
+ ```
163
+
164
+ ## Related tool ☞
165
+
166
+ Also checkout [this library](https://github.com/alloy/time_zone_scheduler) for scheduling across time zones.
167
+
41
168
  ## Contributing
42
169
 
43
170
  Bug reports and pull requests are welcome on GitHub at https://github.com/alloy/lowdown.
@@ -46,3 +173,7 @@ Bug reports and pull requests are welcome on GitHub at https://github.com/alloy/
46
173
 
47
174
  The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
48
175
 
176
+ [client]: http://www.rubydoc.info/gems/lowdown/Lowdown/Client
177
+ [notification]: http://www.rubydoc.info/gems/lowdown/Lowdown/Notification
178
+ [group]: http://www.rubydoc.info/gems/lowdown/Lowdown/RequestGroup
179
+ [delegate]: http://www.rubydoc.info/gems/lowdown/Lowdown/Connection/DelegateProtocol
data/Rakefile CHANGED
@@ -1,8 +1,8 @@
1
1
  desc "Install all dependencies"
2
2
  task :bootstrap do
3
- if system('which bundle')
3
+ if system("which bundle")
4
4
  sh "bundle install"
5
- #sh "git submodule update --init"
5
+ # sh "git submodule update --init"
6
6
  else
7
7
  $stderr.puts "\033[0;31m[!] Please install the bundler gem manually: $ [sudo] gem install bundler\e[0m"
8
8
  exit 1
@@ -10,23 +10,30 @@ task :bootstrap do
10
10
  end
11
11
 
12
12
  begin
13
- require 'bundler/gem_tasks'
13
+ require "bundler/gem_tasks"
14
14
 
15
15
  desc "Generate documentation"
16
16
  task :doc do
17
17
  sh "yard doc"
18
18
  end
19
19
 
20
+ desc "Run rubocop"
21
+ task :rubocop do
22
+ sh "rubocop"
23
+ end
24
+
20
25
  require "rake/testtask"
21
26
  Rake::TestTask.new(:test) do |t|
22
27
  t.options = "--verbose"
23
28
  t.libs << "test"
24
29
  t.libs << "lib"
25
- t.test_files = FileList['test/**/*_test.rb']
30
+ t.test_files = FileList["test/**/*_test.rb"]
26
31
  end
27
32
 
28
- task :default => :test
33
+ task :default => [:test, :rubocop]
29
34
 
30
35
  rescue LoadError
31
- $stderr.puts "\033[0;33m[!] Disabling rake tasks because the environment couldn’t be loaded. Be sure to run `rake bootstrap` first.\e[0m"
36
+ $stderr.puts "\033[0;33m[!] Disabling rake tasks because the environment couldn’t be loaded. Be sure to run `rake " \
37
+ "bootstrap` first.\e[0m"
32
38
  end
39
+
@@ -6,17 +6,20 @@ include Lowdown
6
6
  require "json"
7
7
  require "optparse"
8
8
 
9
- options = { :payload => {}, :custom_data => {} }
9
+ Celluloid.logger.level = Logger::WARN
10
+
11
+ options = { :payload => {}, :custom_data => {}, :pool_size => 1 }
10
12
 
11
13
  OPTION_PARSER = OptionParser.new do |opts|
12
14
  opts.banner = "Usage: lowdown [options] <tokens …>"
13
15
 
14
- opts.on("-v", "--version", "Print version") do |v|
16
+ opts.on("-v", "--version", "Print version") do
15
17
  puts VERSION
16
18
  exit
17
19
  end
18
20
 
19
- opts.on("-m", "--alert ALERT", "Body of the alert to send in the push notification") do |alert|
21
+ opts.on("-m", "--alert ALERT", "Body of the alert to send in the push notification (the %d format code will be " \
22
+ "replaced by the notification ID)") do |alert|
20
23
  options[:alert] = alert
21
24
  end
22
25
 
@@ -37,7 +40,8 @@ OPTION_PARSER = OptionParser.new do |opts|
37
40
  options[:custom_data][key] = value
38
41
  end
39
42
 
40
- opts.on("-P", "--payload PAYLOAD", "JSON payload for notifications, merged with --alert, --badge, --sound, and --data") do |payload|
43
+ opts.on("-P", "--payload PAYLOAD", "JSON payload for notifications, merged with --alert, --badge, --sound, and " \
44
+ "--data") do |payload|
41
45
  options[:payload] = JSON.parse(payload)
42
46
  end
43
47
 
@@ -45,7 +49,8 @@ OPTION_PARSER = OptionParser.new do |opts|
45
49
  options[:topic] = topic
46
50
  end
47
51
 
48
- opts.on("-e", "--environment ENV", "Environment to send push notification (production or development), defaults to production if the certificate supports that or otherwise development") do |env|
52
+ opts.on("-e", "--environment ENV", "Environment to send push notification (production or development), defaults to " \
53
+ "production if the certificate supports that or otherwise development") do |env|
49
54
  options[:env] = env
50
55
  end
51
56
 
@@ -56,6 +61,19 @@ OPTION_PARSER = OptionParser.new do |opts|
56
61
  opts.on("-p", "--passphrase PASSPHRASE", "Certificate passphrase") do |passphrase|
57
62
  options[:certificate_passphrase] = passphrase
58
63
  end
64
+
65
+ opts.on("-n", "--connections NUMBER", "Number of simultaneous connections to make") do |pool_size|
66
+ options[:pool_size] = pool_size.to_i
67
+ end
68
+
69
+ opts.on("--debug", "Debug logging") do
70
+ Celluloid.logger.level = Logger::INFO
71
+ end
72
+
73
+ opts.on("--verbose", "Verbose logging") do
74
+ $CELLULOID_DEBUG = true
75
+ Celluloid.logger.level = Logger::DEBUG
76
+ end
59
77
  end
60
78
 
61
79
  OPTION_PARSER.parse!
@@ -70,13 +88,15 @@ end
70
88
 
71
89
  certificate = nil
72
90
  file, passphrase = options.values_at(:certificate_file, :certificate_passphrase)
91
+ # rubocop:disable Style/RescueModifier
73
92
  unless file && File.exist?(file) && certificate = (Certificate.from_pem_data(File.read(file), passphrase) rescue nil)
93
+ # rubocop:enable Style/RescueModifier
74
94
  help! "A valid certificate path is required."
75
95
  end
76
96
 
77
97
  production = false
78
98
  if options[:env]
79
- unless %w{ production development }.include?(options[:env])
99
+ unless %w( production development ).include?(options[:env])
80
100
  help! "Invalid environment specified."
81
101
  end
82
102
  production = options[:env] == "production"
@@ -89,7 +109,7 @@ else
89
109
  end
90
110
 
91
111
  begin
92
- client = Client.production(production, certificate)
112
+ client = Client.production(production, certificate: certificate, pool_size: options[:pool_size])
93
113
  rescue ArgumentError => e
94
114
  help! e.message
95
115
  end
@@ -99,23 +119,22 @@ payload.merge!(options[:custom_data])
99
119
  payload["alert"] = options[:alert] if options[:alert]
100
120
  payload["badge"] = options[:badge] if options[:badge]
101
121
  payload["sound"] = options[:sound] if options[:sound]
102
- payload["content-available"] = options[:content_available] ? 1 : 0 if options.has_key?(:content_available)
103
- if payload.empty?
104
- help! "No payload data specified."
105
- end
122
+ payload["content-available"] = options[:content_available] ? 1 : 0 if options.key?(:content_available)
106
123
 
107
- if tokens.empty?
108
- help! "No device tokens specified."
109
- end
124
+ help! "No payload data specified." if payload.empty?
125
+ help! "No device tokens specified." if tokens.empty?
110
126
 
111
- notifications = tokens.map.with_index do |token, index|
112
- Notification.new(:token => token, :id => index+1, :payload => payload, :topic => options[:topic])
127
+ notifications = tokens.map do |token|
128
+ Notification.new(:token => token, :payload => payload.dup, :topic => options[:topic]).tap do |notification|
129
+ notification.payload["alert"] = notification.payload["alert"] % notification.id
130
+ end
113
131
  end
114
132
 
115
- client.connect do
133
+ client.connect do |group|
116
134
  notifications.each do |notification|
117
- client.send_notification(notification) do |response|
118
- puts "[#{notification.token} ##{notification.id}] #{response}"
135
+ group.send_notification(notification) do |response|
136
+ Celluloid.logger.unknown "[#{notification.token} ##{notification.id}] #{response}"
119
137
  end
120
138
  end
121
139
  end
140
+