push_builder 0.0.1

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,17 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --color
2
+ --format documentation
data/Gemfile ADDED
@@ -0,0 +1,3 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gemspec
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2013 Philipe Fatio
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,18 @@
1
+ # Anatomy of a Payload
2
+
3
+ JSON dictionary ([RFC 4627](http://www.rfc-editor.org/rfc/rfc4627.txt)) weighing in at a max of 256 bytes.
4
+
5
+ Must contain `aps` dictionary with at least one of the following keys:
6
+
7
+ - `alert` (string or dictionary): The message to display or a dictionary which let's you specify further things (see below).
8
+ - `badge` (number): The number to add to the app icon. If absent, the current badge is not changed. 0 clears the badge.
9
+ - `sound` (string): The identifier of a sound to play.
10
+
11
+ The `alert` dictionary can contain the following keys:
12
+
13
+ - `body` (string): The message to display.
14
+ - `action-loc-key` (string or null): The localization key of the string to use in place of "View" in the right button. Specify null to only show the "OK" button.
15
+ - `loc-key` (string): The localization key of the string that is used as the message.
16
+ - `loc-args` (array of strings): The arguments to appear in place of the format specifiers in `low-key`.
17
+ - `launch-image` (string): The filename of an image file in the application bundle (may omit the extension).
18
+
@@ -0,0 +1,53 @@
1
+ # PushBuilder
2
+
3
+ PushBuilder was born with one mission only: construct JSON payloads for Apple's push notification service.
4
+
5
+ ## What it Does
6
+
7
+ - Automatically crops the alert so that the payload does not exceed the allowed 256 bytes.
8
+ - Supports specifying custom data (data that the iOS app can read from the push notification).
9
+ - Supports specifying data for a third party (data that is intended for a "man in the middle" such as Urban Airship; this data is assumed to be stripped from the payload by the third party and thus does not count towards the 256 bytes limit).
10
+ - Performs some basic type checking.
11
+
12
+ ## Usage
13
+
14
+ ```ruby
15
+ PushBuilder.build(alert: 'Hello World!', badge: 3, sound: 'default').to_json
16
+ # => {"aps":{"badge":3,"alert":"Hello World!","sound":"default"}}
17
+
18
+ # Specifying custom data:
19
+ payload = PushBuilder.build(alert: 'Hello World!')
20
+ payload.custom_data[:notification_id] = 1234
21
+ payload.to_json
22
+ # => {"notification_id":1234,"aps":{"alert":"Hello World!"}}
23
+
24
+ # Specifying third party data (such as UrbanAirship aliases):
25
+ payload = PushBuilder.build(alert: 'Hello World!')
26
+ payload.third_party_data[:aliases] = %w[ 123 456 789 ]
27
+ payload.to_json
28
+ # => {"aliases":["123","456","789"],"aps":{"alert":"Hello World!"}}
29
+
30
+ # Auto crops alerts to not exceed max payload size of 256 bytes:
31
+ PushBuilder.build(alert: 'Hello World ' * 100, badge: 3, sound: 'default').to_json
32
+ # => {"aps":{"badge":3,"alert":"Hello World [...] H…","sound":"default"}}
33
+ ```
34
+
35
+ ## Limitations
36
+
37
+ The `alert` key of the `aps` dictionary only supports strings at the moment.
38
+ Technically, the alert can be customized further as described in [PAYLOAD.md](PAYLOAD.md).
39
+
40
+ ## Additional Information
41
+
42
+ ### Useful Links
43
+
44
+ - [Anatomy of a Payload](PAYLOAD.md)
45
+ - [Local and Push Notification Programming Guide](http://developer.apple.com/library/mac/#documentation/NetworkingInternet/Conceptual/RemoteNotificationsPG/ApplePushService/ApplePushService.html)
46
+
47
+ ### Maintainers
48
+
49
+ - Philipe Fatio ([@fphilipe](https://github.com/fphilipe))
50
+
51
+ ### License
52
+
53
+ MIT License. Copyright 2013 Philipe Fatio
@@ -0,0 +1,6 @@
1
+ require 'bundler/gem_tasks'
2
+ require 'rspec/core/rake_task'
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task :default => :spec
@@ -0,0 +1,15 @@
1
+ require 'push_builder/version'
2
+ require 'push_builder/payload'
3
+ require 'push_builder/aps'
4
+ require 'push_builder/compiler'
5
+ require 'push_builder/string_cropper'
6
+
7
+ module PushBuilder
8
+ TypeError = Class.new(StandardError)
9
+
10
+ def self.build(args={})
11
+ Payload.new.tap do |payload|
12
+ args.each { |k, v| payload.aps.send("#{k}=", v) }
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,25 @@
1
+ module PushBuilder
2
+ class APS
3
+ attr_reader :alert, :badge, :sound
4
+
5
+ def badge=(badge)
6
+ @badge = badge.nil? ? nil : Integer(badge)
7
+ rescue ArgumentError
8
+ fail TypeError, 'Invalid number for badge'
9
+ end
10
+
11
+ def alert=(alert)
12
+ @alert = alert.nil? ? nil : String(alert)
13
+ end
14
+
15
+ def sound=(sound)
16
+ @sound = sound.nil? ? nil : String(sound)
17
+ end
18
+
19
+ def to_hash(opts={})
20
+ { badge: badge, alert: alert, sound: sound }.reject do |_,v|
21
+ v.nil?
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,39 @@
1
+ require 'multi_json'
2
+
3
+ module PushBuilder
4
+ class Compiler
5
+ MAX_PAYLOAD_SIZE = 256
6
+
7
+ attr_reader :payload, :third_party_data
8
+
9
+ def initialize(payload, third_party_data={})
10
+ @payload = payload
11
+ @third_party_data = third_party_data
12
+ end
13
+
14
+ def compile
15
+ crop_alert_if_necessary!
16
+
17
+ to_json(third_party_data.merge(payload))
18
+ end
19
+
20
+ private
21
+
22
+ def crop_alert_if_necessary!
23
+ if (alert = payload.fetch(:aps, {})[:alert])
24
+ payload[:aps][:alert] =
25
+ StringCropper
26
+ .new(alert)
27
+ .crop_bytes(size(payload) - MAX_PAYLOAD_SIZE)
28
+ end
29
+ end
30
+
31
+ def size(data)
32
+ to_json(data).bytesize
33
+ end
34
+
35
+ def to_json(data)
36
+ MultiJson.dump(data)
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,40 @@
1
+ module PushBuilder
2
+ class Payload
3
+ attr_reader :aps, :custom_data, :third_party_data
4
+
5
+ def initialize(args={})
6
+ @aps = APS.new
7
+ @custom_data = {}
8
+ @third_party_data = {}
9
+ end
10
+
11
+ def aps=(aps)
12
+ if aps.is_a?(APS)
13
+ @aps = aps
14
+ else
15
+ fail TypeError, 'Trying to set aps to a non APS object.'
16
+ end
17
+ end
18
+
19
+ def custom_data=(data)
20
+ if data.is_a?(Hash)
21
+ @custom_data = data
22
+ else
23
+ fail TypeError, 'Trying to set custom_data to something other than Hash.'
24
+ end
25
+ end
26
+
27
+ def third_party_data=(data)
28
+ if data.is_a?(Hash)
29
+ @third_party_data = data
30
+ else
31
+ fail TypeError, 'Trying to set third_party_data to something other than Hash.'
32
+ end
33
+ end
34
+
35
+ def to_json
36
+ payload = custom_data.merge(aps: aps.to_hash)
37
+ Compiler.new(payload, third_party_data).compile
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,40 @@
1
+ # coding: UTF-8
2
+
3
+ module PushBuilder
4
+ class StringCropper
5
+ DEFAULT_CROP_INDICATOR = ?…
6
+
7
+ attr_reader :string, :indicator
8
+
9
+ def initialize(string, indicator=DEFAULT_CROP_INDICATOR)
10
+ @string = string
11
+ @indicator = indicator
12
+ end
13
+
14
+ def crop_bytes(bytes_to_crop)
15
+ return string if bytes_to_crop <= 0
16
+
17
+ bytes_to_crop += indicator.bytesize
18
+ remaining_bytes = string.bytesize - bytes_to_crop
19
+
20
+ string_with_bytes(remaining_bytes) + indicator
21
+ end
22
+
23
+ private
24
+
25
+ def string_with_bytes(bytes)
26
+ char_count = char_count_fitting_in_bytes(bytes)
27
+
28
+ string[0, char_count]
29
+ end
30
+
31
+ def char_count_fitting_in_bytes(bytes)
32
+ string.each_char.with_index.inject(0) do |byte_sum, (char, char_count)|
33
+ byte_sum += char.bytesize
34
+ break char_count if byte_sum > bytes
35
+
36
+ byte_sum
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,3 @@
1
+ module PushBuilder
2
+ VERSION = "0.0.1"
3
+ end
@@ -0,0 +1,21 @@
1
+ lib = File.expand_path('../lib', __FILE__)
2
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
3
+ require 'push_builder/version'
4
+
5
+ Gem::Specification.new do |gem|
6
+ gem.name = 'push_builder'
7
+ gem.version = PushBuilder::VERSION
8
+ gem.authors = ['Philipe Fatio']
9
+ gem.email = ['me@phili.pe']
10
+ gem.summary = %q{Easily construct JSON payloads for Apple's push notification service.}
11
+ gem.description = gem.summary
12
+ gem.homepage = 'https://github.com/fphilipe/push_builder'
13
+
14
+ gem.files = `git ls-files`.split($/)
15
+ gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
16
+ gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
17
+
18
+ gem.add_development_dependency 'rspec', '~> 2.12'
19
+
20
+ gem.add_dependency 'multi_json', '~> 1.0'
21
+ end
@@ -0,0 +1,8 @@
1
+ require 'push_builder'
2
+
3
+ RSpec.configure do |config|
4
+ config.treat_symbols_as_metadata_keys_with_true_values = true
5
+ config.run_all_when_everything_filtered = true
6
+ config.filter_run :focus
7
+ config.order = 'random'
8
+ end
@@ -0,0 +1,87 @@
1
+ require 'spec_helper'
2
+
3
+ describe PushBuilder::APS do
4
+ subject(:aps) { described_class.new }
5
+
6
+ describe '#badge=' do
7
+ it 'raises for non numeric values' do
8
+ expect { aps.badge = 'test' }.to raise_error(PushBuilder::TypeError)
9
+ end
10
+
11
+ it 'accepts numeric strings' do
12
+ aps.badge = '123'
13
+ aps.badge.should be 123
14
+ end
15
+
16
+ it 'accepts numbers' do
17
+ aps.badge = 123
18
+ aps.badge.should be 123
19
+ end
20
+
21
+ it 'accepts nil' do
22
+ aps.badge = nil
23
+ aps.badge.should be_nil
24
+ end
25
+ end
26
+
27
+ describe '#alert=' do
28
+ it 'accepts strings' do
29
+ aps.alert = 'foobar'
30
+ aps.alert.should eq 'foobar'
31
+ end
32
+
33
+ it 'accepts values convertable to strings' do
34
+ aps.alert = 123
35
+ aps.alert.should eq '123'
36
+ end
37
+
38
+ it 'accepts nil' do
39
+ aps.alert = nil
40
+ aps.alert.should be_nil
41
+ end
42
+ end
43
+
44
+ describe '#sound=' do
45
+ it 'accepts strings' do
46
+ aps.sound = 'foobar'
47
+ aps.sound.should eq 'foobar'
48
+ end
49
+
50
+ it 'accepts values convertable to strings' do
51
+ aps.sound = 123
52
+ aps.sound.should eq '123'
53
+ end
54
+
55
+ it 'accepts nil' do
56
+ aps.sound = nil
57
+ aps.sound.should be_nil
58
+ end
59
+ end
60
+
61
+ describe '#to_hash' do
62
+ before do
63
+ aps.badge = 1
64
+ aps.alert = 'foo'
65
+ aps.sound = 'bar'
66
+ end
67
+
68
+ let(:expected_hash) do
69
+ { badge: 1, alert: 'foo', sound: 'bar' }
70
+ end
71
+
72
+ it 'returns a hash' do
73
+ aps.to_hash.should eq expected_hash
74
+ end
75
+
76
+ [:badge, :alert, :sound].each do |key|
77
+ context "when #{key} is nil" do
78
+ it 'does not include it' do
79
+ aps.send("#{key}=", nil)
80
+ expected_hash.delete(key)
81
+
82
+ aps.to_hash.should eq expected_hash
83
+ end
84
+ end
85
+ end
86
+ end
87
+ end
@@ -0,0 +1,44 @@
1
+ # coding: UTF-8
2
+
3
+ require 'spec_helper'
4
+
5
+ describe PushBuilder::Compiler do
6
+ describe '#compile' do
7
+ it 'crops the alert if necessary' do
8
+ alert = 50.times.map { 'foobar' }.join
9
+ payload = { aps: { alert: alert } }
10
+
11
+ json = described_class.new(payload).compile
12
+ json.bytesize.should be 256
13
+ json.should =~ %r{"alert":"[fobar]+…"}
14
+ end
15
+
16
+ it 'does not crop the alert if not necessary' do
17
+ payload = { aps: { alert: 'foobar' } }
18
+
19
+ json = described_class.new(payload).compile
20
+ json.bytesize.should be < 256
21
+ json.should include '"alert":"foobar"'
22
+ end
23
+
24
+ context 'when third party data is large' do
25
+ it 'does not crop the alert if not necessary' do
26
+ payload = { aps: { alert: 'foobar' } }
27
+ third_party_data = { test: 50.times.map { 'foobar' }.join }
28
+
29
+ json = described_class.new(payload, third_party_data).compile
30
+ json.bytesize.should be > 256
31
+ json.should include '"alert":"foobar"'
32
+ end
33
+ end
34
+
35
+ context 'when no alert present' do
36
+ it 'does not include an alert key' do
37
+ payload = { aps: { badge: 4 } }
38
+
39
+ json = described_class.new(payload).compile
40
+ json.should_not include '"alert":'
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,59 @@
1
+ # coding: UTF-8
2
+
3
+ require 'spec_helper'
4
+
5
+ describe PushBuilder::Payload do
6
+ subject(:payload) { described_class.new }
7
+
8
+ describe '#aps' do
9
+ it 'is an APS' do
10
+ payload.aps.should be_a(PushBuilder::APS)
11
+ end
12
+ end
13
+
14
+ describe '#aps=' do
15
+ it 'accepts an APS object' do
16
+ aps = PushBuilder::APS.new
17
+ payload.aps = aps
18
+ payload.aps.should be aps
19
+ end
20
+
21
+ it 'raises when object is not an APS' do
22
+ expect { payload.aps = 'foobar' }.
23
+ to raise_error(PushBuilder::TypeError)
24
+ end
25
+ end
26
+
27
+ %w[ custom_data third_party_data ].each do |method|
28
+ describe "##{method}" do
29
+ it 'is an emtpy hash by default' do
30
+ payload.send(method).should == {}
31
+ end
32
+ end
33
+
34
+ describe "##{method}=" do
35
+ it 'accepts a Hash' do
36
+ payload.send("#{method}=", { foo: 'bar' })
37
+ payload.send(method)[:foo].should eq 'bar'
38
+ end
39
+
40
+ it 'raises when object is not a Hash' do
41
+ expect { payload.send("#{method}=", 123) }.
42
+ to raise_error(PushBuilder::TypeError)
43
+ end
44
+ end
45
+ end
46
+
47
+ describe '#to_json' do
48
+ it 'returns a JSON string' do
49
+ payload.aps.stub(to_hash: { foo: 'bar' })
50
+ payload.custom_data[:abc] = 123
51
+ payload.third_party_data[:xyz] = true
52
+
53
+ json = payload.to_json
54
+ json.should include '"abc":123'
55
+ json.should include '"aps":{"foo":"bar"}'
56
+ json.should include '"xyz":true'
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,14 @@
1
+ require 'spec_helper'
2
+
3
+ describe PushBuilder do
4
+ describe '.build' do
5
+ it 'returns a Payload' do
6
+ payload = PushBuilder.build(alert: 'Hello', sound: 'default', badge: 5)
7
+
8
+ payload.should be_a described_class::Payload
9
+ payload.aps.alert.should eq 'Hello'
10
+ payload.aps.sound.should eq 'default'
11
+ payload.aps.badge.should eq 5
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,26 @@
1
+ # coding: UTF-8
2
+
3
+ require 'spec_helper'
4
+
5
+ describe PushBuilder::StringCropper do
6
+ describe '#crop_bytes' do
7
+ def crop_bytes(string, bytes, indicator='')
8
+ PushBuilder::StringCropper.new(string, indicator).crop_bytes(bytes)
9
+ end
10
+
11
+ specify { crop_bytes('abcdefg', -1). should eq 'abcdefg' }
12
+ specify { crop_bytes('abcdefg', 0). should eq 'abcdefg' }
13
+ specify { crop_bytes('abcdefg', 1). should eq 'abcdef' }
14
+ specify { crop_bytes('abcdefg', 1, '…').should eq 'abc…' }
15
+ specify { crop_bytes('abcdefg', 4, '…').should eq '…' }
16
+
17
+ specify { crop_bytes('aäoöuüe', 2). should eq 'aäoöu' }
18
+ specify { crop_bytes('aäoöuüe', 3). should eq 'aäoöu' }
19
+ specify { crop_bytes('aäoöuüe', 4). should eq 'aäoö' }
20
+ specify { crop_bytes('aäoöuüe', 5). should eq 'aäo' }
21
+ specify { crop_bytes('aäoöuüe', 1, '…').should eq 'aäoö…' }
22
+ specify { crop_bytes('aäoöuüe', 2, '…').should eq 'aäo…' }
23
+ specify { crop_bytes('aäoöuüe', 3, '…').should eq 'aäo…' }
24
+ specify { crop_bytes('aäoöuüe', 4, '…').should eq 'aä…' }
25
+ end
26
+ end
metadata ADDED
@@ -0,0 +1,104 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: push_builder
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Philipe Fatio
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2013-02-21 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: rspec
16
+ requirement: !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ~>
20
+ - !ruby/object:Gem::Version
21
+ version: '2.12'
22
+ type: :development
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ~>
28
+ - !ruby/object:Gem::Version
29
+ version: '2.12'
30
+ - !ruby/object:Gem::Dependency
31
+ name: multi_json
32
+ requirement: !ruby/object:Gem::Requirement
33
+ none: false
34
+ requirements:
35
+ - - ~>
36
+ - !ruby/object:Gem::Version
37
+ version: '1.0'
38
+ type: :runtime
39
+ prerelease: false
40
+ version_requirements: !ruby/object:Gem::Requirement
41
+ none: false
42
+ requirements:
43
+ - - ~>
44
+ - !ruby/object:Gem::Version
45
+ version: '1.0'
46
+ description: Easily construct JSON payloads for Apple's push notification service.
47
+ email:
48
+ - me@phili.pe
49
+ executables: []
50
+ extensions: []
51
+ extra_rdoc_files: []
52
+ files:
53
+ - .gitignore
54
+ - .rspec
55
+ - Gemfile
56
+ - LICENSE.txt
57
+ - PAYLOAD.md
58
+ - README.md
59
+ - Rakefile
60
+ - lib/push_builder.rb
61
+ - lib/push_builder/aps.rb
62
+ - lib/push_builder/compiler.rb
63
+ - lib/push_builder/payload.rb
64
+ - lib/push_builder/string_cropper.rb
65
+ - lib/push_builder/version.rb
66
+ - push_builder.gemspec
67
+ - spec/spec_helper.rb
68
+ - spec/unit/aps_spec.rb
69
+ - spec/unit/compiler_spec.rb
70
+ - spec/unit/payload_spec.rb
71
+ - spec/unit/push_builder.rb
72
+ - spec/unit/string_cropper_spec.rb
73
+ homepage: https://github.com/fphilipe/push_builder
74
+ licenses: []
75
+ post_install_message:
76
+ rdoc_options: []
77
+ require_paths:
78
+ - lib
79
+ required_ruby_version: !ruby/object:Gem::Requirement
80
+ none: false
81
+ requirements:
82
+ - - ! '>='
83
+ - !ruby/object:Gem::Version
84
+ version: '0'
85
+ required_rubygems_version: !ruby/object:Gem::Requirement
86
+ none: false
87
+ requirements:
88
+ - - ! '>='
89
+ - !ruby/object:Gem::Version
90
+ version: '0'
91
+ requirements: []
92
+ rubyforge_project:
93
+ rubygems_version: 1.8.23
94
+ signing_key:
95
+ specification_version: 3
96
+ summary: Easily construct JSON payloads for Apple's push notification service.
97
+ test_files:
98
+ - spec/spec_helper.rb
99
+ - spec/unit/aps_spec.rb
100
+ - spec/unit/compiler_spec.rb
101
+ - spec/unit/payload_spec.rb
102
+ - spec/unit/push_builder.rb
103
+ - spec/unit/string_cropper_spec.rb
104
+ has_rdoc: