push_builder 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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: