rack-passbook 0.0.1

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