rack-passbook 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.
data/Gemfile ADDED
@@ -0,0 +1,3 @@
1
+ source :rubygems
2
+
3
+ gemspec
data/Gemfile.lock ADDED
@@ -0,0 +1,30 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ rack-passbook (0.0.1)
5
+ rack (~> 1.4)
6
+ sequel (~> 3.37.0)
7
+ sinatra (~> 1.3.2)
8
+
9
+ GEM
10
+ remote: http://rubygems.org/
11
+ specs:
12
+ rack (1.4.1)
13
+ rack-protection (1.3.2)
14
+ rack
15
+ rake (0.9.6)
16
+ rspec (0.6.4)
17
+ sequel (3.37.0)
18
+ sinatra (1.3.3)
19
+ rack (~> 1.3, >= 1.3.6)
20
+ rack-protection (~> 1.2)
21
+ tilt (~> 1.3, >= 1.3.3)
22
+ tilt (1.3.3)
23
+
24
+ PLATFORMS
25
+ ruby
26
+
27
+ DEPENDENCIES
28
+ rack-passbook!
29
+ rake (~> 0.9.2)
30
+ rspec (~> 0.6.1)
data/LICENSE ADDED
@@ -0,0 +1,19 @@
1
+ Copyright (c) 2012 Mattt Thompson (http://mattt.me/)
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ of this software and associated documentation files (the "Software"), to deal
5
+ in the Software without restriction, including without limitation the rights
6
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ copies of the Software, and to permit persons to whom the Software is
8
+ furnished to do so, subject to the following conditions:
9
+
10
+ The above copyright notice and this permission notice shall be included in
11
+ all copies or substantial portions of the Software.
12
+
13
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,126 @@
1
+ ![Passbook](http://cl.ly/JPjc/title_passbook.png)
2
+
3
+ # Rack::Passbook
4
+
5
+ > This is still in early stages of development, so proceed with caution when using this in a production application. Any bug reports, feature requests, or general feedback at this point would be greatly appreciated.
6
+
7
+ [Passbook](http://www.apple.com/ios/whats-new/#passbook) is an iOS 6 feature that manages boarding passes, movie tickets, retail coupons, & loyalty cards. Using the [PassKit API](https://developer.apple.com/library/prerelease/ios/#documentation/UserExperience/Reference/PassKit_Framework/_index.html), developers can register web services to automatically update content on the pass, such as gate changes on a boarding pass, or adding credit to a loyalty card.
8
+
9
+ Apple [provides a specification](https://developer.apple.com/library/prerelease/ios/#documentation/PassKit/Reference/PassKit_WebService/WebService.html) for a REST-style web service protocol to communicate with Passbook, with endpoints to get the latest version of a pass, register / unregister devices to receive push notifications for a pass, and query for passes registered for a device.
10
+
11
+ This project is an example implementation of this web service specification in Rails, and will serve the basis for a more comprehensive Rails generator in the near future.
12
+
13
+ > If you're just starting out Passbook development, you should definitely check out [this great two-part tutorial](http://www.raywenderlich.com/20734/beginning-passbook-part-1) by [Marin Todorov](http://www.raywenderlich.com/about#marintodorov) ([Part 1](http://www.raywenderlich.com/20734/beginning-passbook-part-1) [Part 2](http://www.raywenderlich.com/20785/beginning-passbook-in-ios-6-part-22)).
14
+
15
+ ## Requirements
16
+
17
+ - Ruby 1.9
18
+ - PostgreSQL 9.1 running locally ([Postgres.app](http://postgresapp.com) is the easiest way to get a Postgres server running on your Mac)
19
+
20
+ ## Example Usage
21
+
22
+ Rack::Passbook can be run as Rack middleware or as a single web application. All that is required is a connection to a Postgres database.
23
+
24
+ ### config.ru
25
+
26
+ ```ruby
27
+ require 'bundler'
28
+ Bundler.require
29
+
30
+ run Rack::Passbook
31
+ ```
32
+
33
+ An example application can be found in the `/example` directory of this repository.
34
+
35
+ ---
36
+
37
+ ## Specification
38
+
39
+ What follows is a summary of the specification. The complete specification can be found in the [Passbook Web Service Reference](https://developer.apple.com/library/prerelease/ios/#documentation/PassKit/Reference/PassKit_WebService/WebService.html).
40
+
41
+ ### Getting the Latest Version of a Pass
42
+
43
+ ```
44
+ GET http://example.com/v1/passes/:passTypeIdentifier/:serialNumber
45
+ ```
46
+
47
+ - **passTypeIdentifier** The pass’s type, as specified in the pass.
48
+ - **serialNumber** The unique pass identifier, as specified in the pass.
49
+
50
+ **Response**
51
+
52
+ - If request is authorized, return HTTP status 200 with a payload of the pass data.
53
+ - If the request is not authorized, return HTTP status 401.
54
+ - Otherwise, return the appropriate standard HTTP status.
55
+
56
+ ### Getting the Serial Numbers for Passes Associated with a Device
57
+
58
+ ```
59
+ GET http://example.com/v1/devices/:deviceLibraryIdentifier/registrations/:passTypeIdentifier[?passesUpdatedSince=tag]
60
+ ```
61
+
62
+ - **deviceLibraryIdentifier** A unique identifier that is used to identify and authenticate the device.
63
+ - **passTypeIdentifier** The pass’s type, as specified in the pass.
64
+ - **serialNumber** The unique pass identifier, as specified in the pass.
65
+ - **passesUpdatedSince** _Optional_ A tag from a previous request.
66
+
67
+ **Response**
68
+
69
+ If the `passesUpdatedSince` parameter is present, return only the passes that have been updated since the time indicated by tag. Otherwise, return all passes.
70
+
71
+ - If there are matching passes, return HTTP status 200 with a JSON dictionary with the following keys and values:
72
+ - **lastUpdated** _(string)_ The current modification tag.
73
+ - **serialNumbers** _(array of strings)_ The serial numbers of the matching passes.
74
+ - If there are no matching passes, return HTTP status 204.
75
+ - Otherwise, return the appropriate standard HTTP status.
76
+
77
+ ### Registering a Device to Receive Push Notifications for a Pass
78
+
79
+ ```
80
+ POST http://example.com/v1/devices/:deviceLibraryIdentifier/registrations/:passTypeIdentifier/:serialNumber
81
+ ```
82
+
83
+ - **deviceLibraryIdentifier** A unique identifier that is used to identify and authenticate the device.
84
+ - **passTypeIdentifier** The pass’s type, as specified in the pass.
85
+ - **serialNumber** The unique pass identifier, as specified in the pass.
86
+
87
+ The POST payload is a JSON dictionary, containing a single key and value:
88
+
89
+ - **pushToken** The push token that the server can use to send push notifications to this device.
90
+
91
+ **Response**
92
+
93
+ - If the serial number is already registered for this device, return HTTP status 200.
94
+ - If registration succeeds, return HTTP status 201.
95
+ - If the request is not authorized, return HTTP status 401.
96
+ - Otherwise, return the appropriate standard HTTP status.
97
+
98
+ ### Unregistering a Device
99
+
100
+ ```
101
+ DELETE http://example.com/v1/devices/:deviceLibraryIdentifier/registrations/:passTypeIdentifier/:serialNumber
102
+ ```
103
+
104
+ - **deviceLibraryIdentifier** A unique identifier that is used to identify and authenticate the device.
105
+ - **passTypeIdentifier** The pass’s type, as specified in the pass.
106
+ - **serialNumber** The unique pass identifier, as specified in the pass.
107
+
108
+ **Response**
109
+
110
+ - If disassociation succeeds, return HTTP status 200.
111
+ - If the request is not authorized, return HTTP status 401.
112
+ - Otherwise, return the appropriate standard HTTP status.
113
+
114
+ ---
115
+
116
+ ## Contact
117
+
118
+ Mattt Thompson
119
+
120
+ - http://github.com/mattt
121
+ - http://twitter.com/mattt
122
+ - m@mattt.me
123
+
124
+ ## License
125
+
126
+ Rack::Passbook is available under the MIT license. See the LICENSE file for more info.
data/Rakefile ADDED
@@ -0,0 +1,10 @@
1
+ require "bundler"
2
+ Bundler.setup
3
+
4
+ gemspec = eval(File.read("rack-passbook.gemspec"))
5
+
6
+ task :build => "#{gemspec.full_name}.gem"
7
+
8
+ file "#{gemspec.full_name}.gem" => gemspec.files + ["rack-passbook.gemspec"] do
9
+ system "gem build rack-passbook.gemspec"
10
+ end
@@ -0,0 +1,102 @@
1
+ require 'rack'
2
+ require 'rack/contrib'
3
+
4
+ require 'sinatra/base'
5
+ require 'sinatra/param'
6
+
7
+ require 'sequel'
8
+
9
+ module Rack
10
+ class Passbook < Sinatra::Base
11
+ VERSION = '0.0.1'
12
+
13
+ use Rack::PostBodyContentTypeParser
14
+ helpers Sinatra::Param
15
+
16
+ Sequel.extension :core_extensions, :migration, :pg_hstore, :pg_hstore_ops
17
+
18
+ autoload :Pass, ::File.join(::File.dirname(__FILE__), 'passbook/models/pass')
19
+ autoload :Registration, ::File.join(::File.dirname(__FILE__), 'passbook/models/registration')
20
+
21
+ disable :raise_errors, :show_exceptions
22
+
23
+ before do
24
+ content_type :json
25
+ end
26
+
27
+ # Get the latest version of a pass.
28
+ get '/passes/:pass_type_identifier/:serial_number/?' do
29
+ @pass = Pass.filter(pass_type_identifier: params[:pass_type_identifier], serial_number: params[:serial_number]).first
30
+ halt 404 if @pass.nil?
31
+ filter_authorization_for_pass!(@pass)
32
+
33
+ last_modified @pass.updated_at.utc
34
+
35
+ @pass.to_json
36
+ end
37
+
38
+
39
+ # Get the serial numbers for passes associated with a device.
40
+ # This happens the first time a device communicates with our web service.
41
+ # Additionally, when a device gets a push notification, it asks our
42
+ # web service for the serial numbers of passes that have changed since
43
+ # a given update tag (timestamp).
44
+ get '/devices/:device_library_identifier/registrations/:pass_type_identifier/?' do
45
+ @passes = Pass.filter(pass_type_identifier: params[:pass_type_identifier]).join(Registration.dataset, device_library_identifier: params[:device_library_identifier])
46
+ halt 404 if @passes.empty?
47
+
48
+ @passes = @passes.filter('passes.updated_at > ?', params[:passesUpdatedSince]) if params[:passesUpdatedSince]
49
+
50
+ if @passes.any?
51
+ {
52
+ lastUpdated: @passes.collect(&:updated_at).max,
53
+ serialNumbers: @passes.collect(&:serial_number).collect(&:to_s)
54
+ }.to_json
55
+ else
56
+ halt 204
57
+ end
58
+ end
59
+
60
+
61
+ # Register a device to receive push notifications for a pass.
62
+ post '/devices/:device_library_identifier/registrations/:pass_type_identifier/:serial_number/?' do
63
+ @pass = Pass.where(pass_type_identifier: params[:pass_type_identifier], serial_number: params[:serial_number]).first
64
+ halt 404 if @pass.nil?
65
+ filter_authorization_for_pass!(@pass)
66
+
67
+ param :pushToken, String, required: true
68
+
69
+ @registration = @pass.registrations.detect{|registration| registration.device_library_identifier == params[:device_library_identifier]}
70
+ @registration ||= Registration.new(pass_id: @pass.id, device_library_identifier: params[:device_library_identifier])
71
+ @registration.push_token = params[:pushToken]
72
+
73
+ status = @registration.new? ? 201 : 200
74
+
75
+ @registration.save
76
+ p @registration
77
+ halt 406 unless @registration.valid?
78
+
79
+ halt status
80
+ end
81
+
82
+ # Unregister a device so it no longer receives push notifications for a pass.
83
+ delete '/devices/:device_library_identifier/registrations/:pass_type_identifier/:serial_number/?' do
84
+ @pass = Pass.filter(pass_type_identifier: params[:pass_type_identifier], serial_number: params[:serial_number]).first
85
+ halt 404 if @pass.nil?
86
+ filter_authorization_for_pass!(@pass)
87
+
88
+ @registration = @pass.registrations.detect{|registration| registration.device_library_identifier == params[:device_library_identifier]}
89
+ halt 404 if @registration.nil?
90
+
91
+ @registration.destroy
92
+
93
+ halt 200
94
+ end
95
+
96
+ private
97
+
98
+ def filter_authorization_for_pass!(pass)
99
+ halt 401 if request.env['HTTP_AUTHORIZATION'] != "ApplePass #{pass.authentication_token}"
100
+ end
101
+ end
102
+ end
@@ -0,0 +1,36 @@
1
+ Sequel.migration do
2
+ up do
3
+ run %{CREATE EXTENSION hstore;}
4
+
5
+ create_table :passbook_devices do
6
+ primary_key :id
7
+
8
+ column :pass_type_identifier, :varchar, unique: true, empty: false
9
+ column :serial_number, :varchar, empty: false
10
+ column :authentication_token, :varchar
11
+ column :data, :hstore
12
+ column :created_at, :timestamp
13
+ column :updated_at, :timestamp
14
+
15
+ index :pass_type_identifier
16
+ index :serial_number
17
+ end
18
+
19
+ create_table :passbook_registrations do
20
+ primary_key :id
21
+
22
+ column :pass_id, :int8, null: false
23
+ column :device_library_identifier, :varchar, empty: false
24
+ column :push_token, :varchar
25
+ column :created_at, :timestamp
26
+ column :updated_at, :timestamp
27
+
28
+ index :device_library_identifier
29
+ end
30
+ end
31
+
32
+ down do
33
+ drop_table :devices
34
+ drop_table :registrations
35
+ end
36
+ end
@@ -0,0 +1,28 @@
1
+ module Rack
2
+ class Passbook
3
+ DB = Sequel.connect(ENV['DATABASE_URL'] || "postgres://localhost:5432/passbook_example")
4
+ Sequel::Migrator.run(DB, ::File.join(::File.dirname(__FILE__), "../migrations"))
5
+
6
+ class Pass < Sequel::Model
7
+ plugin :json_serializer, naked: true, except: :id
8
+ plugin :validation_helpers
9
+ plugin :timestamps, force: true, update_on_create: true
10
+ plugin :schema
11
+ plugin :typecast_on_load
12
+
13
+ self.dataset = :passbook_devices
14
+ self.strict_param_setting = false
15
+ self.raise_on_save_failure = false
16
+
17
+ one_to_many :registrations, class_name: "Rack::Passbook::Registration"
18
+
19
+ def validate
20
+ super
21
+
22
+ validates_presence [:pass_type_identifier, :serial_number]
23
+ validates_unique :pass_type_identifier
24
+ validates_unique [:serial_number, :pass_type_identifier]
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,33 @@
1
+ module Rack
2
+ class Passbook
3
+ class Registration < Sequel::Model
4
+ plugin :json_serializer, naked: true, except: :id
5
+ plugin :validation_helpers
6
+ plugin :timestamps, force: true, update_on_create: true
7
+ plugin :schema
8
+
9
+ self.dataset = :passbook_registrations
10
+ self.strict_param_setting = false
11
+ self.raise_on_save_failure = false
12
+
13
+ def before_validation
14
+ normalize_push_token! if self.push_token
15
+ end
16
+
17
+ def validate
18
+ super
19
+
20
+ validates_presence :device_library_identifier
21
+ validates_unique [:device_library_identifier, :pass_id]
22
+ validates_format /[[:xdigit:]]+/, :push_token
23
+ validates_exact_length 40, :push_token
24
+ end
25
+
26
+ private
27
+
28
+ def normalize_push_token!
29
+ self.push_token = self.push_token.strip.gsub(/[<\s>]/, '')
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,26 @@
1
+ # -*- encoding: utf-8 -*-
2
+ $:.push File.expand_path("../lib", __FILE__)
3
+ require "rack/passbook"
4
+
5
+ Gem::Specification.new do |s|
6
+ s.name = "rack-passbook"
7
+ s.authors = ["Mattt Thompson"]
8
+ s.email = "m@mattt.me"
9
+ s.homepage = "http://mattt.me"
10
+ s.version = Rack::Passbook::VERSION
11
+ s.platform = Gem::Platform::RUBY
12
+ s.summary = "Rack::Passbook"
13
+ s.description = "Automatically generate REST APIs for Passbook registration."
14
+
15
+ s.add_development_dependency "rspec", "~> 0.6.1"
16
+ s.add_development_dependency "rake", "~> 0.9.2"
17
+
18
+ s.add_dependency "rack", "~> 1.4"
19
+ s.add_dependency "sinatra", "~> 1.3.2"
20
+ s.add_dependency "sequel", "~> 3.37.0"
21
+
22
+ s.files = Dir["./**/*"].reject { |file| file =~ /\.\/(bin|example|log|pkg|script|spec|test|vendor)/ }
23
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
24
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
25
+ s.require_paths = ["lib"]
26
+ end
metadata ADDED
@@ -0,0 +1,140 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: rack-passbook
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Mattt Thompson
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2013-01-03 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: 0.6.1
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: 0.6.1
30
+ - !ruby/object:Gem::Dependency
31
+ name: rake
32
+ requirement: !ruby/object:Gem::Requirement
33
+ none: false
34
+ requirements:
35
+ - - ~>
36
+ - !ruby/object:Gem::Version
37
+ version: 0.9.2
38
+ type: :development
39
+ prerelease: false
40
+ version_requirements: !ruby/object:Gem::Requirement
41
+ none: false
42
+ requirements:
43
+ - - ~>
44
+ - !ruby/object:Gem::Version
45
+ version: 0.9.2
46
+ - !ruby/object:Gem::Dependency
47
+ name: rack
48
+ requirement: !ruby/object:Gem::Requirement
49
+ none: false
50
+ requirements:
51
+ - - ~>
52
+ - !ruby/object:Gem::Version
53
+ version: '1.4'
54
+ type: :runtime
55
+ prerelease: false
56
+ version_requirements: !ruby/object:Gem::Requirement
57
+ none: false
58
+ requirements:
59
+ - - ~>
60
+ - !ruby/object:Gem::Version
61
+ version: '1.4'
62
+ - !ruby/object:Gem::Dependency
63
+ name: sinatra
64
+ requirement: !ruby/object:Gem::Requirement
65
+ none: false
66
+ requirements:
67
+ - - ~>
68
+ - !ruby/object:Gem::Version
69
+ version: 1.3.2
70
+ type: :runtime
71
+ prerelease: false
72
+ version_requirements: !ruby/object:Gem::Requirement
73
+ none: false
74
+ requirements:
75
+ - - ~>
76
+ - !ruby/object:Gem::Version
77
+ version: 1.3.2
78
+ - !ruby/object:Gem::Dependency
79
+ name: sequel
80
+ requirement: !ruby/object:Gem::Requirement
81
+ none: false
82
+ requirements:
83
+ - - ~>
84
+ - !ruby/object:Gem::Version
85
+ version: 3.37.0
86
+ type: :runtime
87
+ prerelease: false
88
+ version_requirements: !ruby/object:Gem::Requirement
89
+ none: false
90
+ requirements:
91
+ - - ~>
92
+ - !ruby/object:Gem::Version
93
+ version: 3.37.0
94
+ description: Automatically generate REST APIs for Passbook registration.
95
+ email: m@mattt.me
96
+ executables: []
97
+ extensions: []
98
+ extra_rdoc_files: []
99
+ files:
100
+ - ./Gemfile
101
+ - ./Gemfile.lock
102
+ - ./lib/rack/passbook/migrations/001_base_schema.rb
103
+ - ./lib/rack/passbook/models/pass.rb
104
+ - ./lib/rack/passbook/models/registration.rb
105
+ - ./lib/rack/passbook.rb
106
+ - ./LICENSE
107
+ - ./rack-passbook.gemspec
108
+ - ./Rakefile
109
+ - ./README.md
110
+ homepage: http://mattt.me
111
+ licenses: []
112
+ post_install_message:
113
+ rdoc_options: []
114
+ require_paths:
115
+ - lib
116
+ required_ruby_version: !ruby/object:Gem::Requirement
117
+ none: false
118
+ requirements:
119
+ - - ! '>='
120
+ - !ruby/object:Gem::Version
121
+ version: '0'
122
+ segments:
123
+ - 0
124
+ hash: -4311652005948836172
125
+ required_rubygems_version: !ruby/object:Gem::Requirement
126
+ none: false
127
+ requirements:
128
+ - - ! '>='
129
+ - !ruby/object:Gem::Version
130
+ version: '0'
131
+ segments:
132
+ - 0
133
+ hash: -4311652005948836172
134
+ requirements: []
135
+ rubyforge_project:
136
+ rubygems_version: 1.8.24
137
+ signing_key:
138
+ specification_version: 3
139
+ summary: Rack::Passbook
140
+ test_files: []