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 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
+