mswallet 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +64 -0
- data/.travis.yml +14 -0
- data/Gemfile +5 -0
- data/Gemfile.lock +67 -0
- data/LICENSE.txt +22 -0
- data/README.md +91 -0
- data/README.ru.md +90 -0
- data/Rakefile +1 -0
- data/WalletItem.xsd +243 -0
- data/lib/mswallet.rb +14 -0
- data/lib/mswallet/pass.rb +187 -0
- data/lib/mswallet/version.rb +5 -0
- data/lib/rack/mswallet_rack.rb +52 -0
- data/mswallet.gemspec +31 -0
- data/spec/mswallet/mswallet_spec.rb +8 -0
- data/spec/mswallet/pass_spec.rb +190 -0
- data/spec/rack/mswallet_rack_spec.rb +139 -0
- data/spec/spec_helper.rb +42 -0
- metadata +194 -0
data/lib/mswallet.rb
ADDED
@@ -0,0 +1,187 @@
|
|
1
|
+
require 'xml/libxml'
|
2
|
+
require 'zip'
|
3
|
+
|
4
|
+
module Mswallet
|
5
|
+
class Pass
|
6
|
+
TYPES = %W(Deal General PaymentInstrument Ticket BoardingPass MembershipCard )
|
7
|
+
|
8
|
+
attr_accessor :pass, :files, :locales
|
9
|
+
|
10
|
+
def initialize(pass = nil)
|
11
|
+
@pass = pass || self.class.init
|
12
|
+
@locales = {}
|
13
|
+
@files = []
|
14
|
+
yield(self) if block_given?
|
15
|
+
end
|
16
|
+
|
17
|
+
def []=(key, val)
|
18
|
+
result = convert_to_xml(key, val)
|
19
|
+
if result.is_a? Array
|
20
|
+
result.each { |item| @pass.root << item }
|
21
|
+
else
|
22
|
+
@pass.root << result
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def [](key)
|
27
|
+
@pass.root.find(key).first
|
28
|
+
end
|
29
|
+
|
30
|
+
def add_file(file)
|
31
|
+
@files << file
|
32
|
+
end
|
33
|
+
|
34
|
+
def add_locale(lang, file)
|
35
|
+
@locales[lang] = file
|
36
|
+
end
|
37
|
+
|
38
|
+
# Return a Tempfile containing our ZipStream
|
39
|
+
def file(options = {})
|
40
|
+
options[:file_name] ||= 'pass.mswallet'
|
41
|
+
temp_file = Tempfile.new(options[:file_name])
|
42
|
+
temp_file.write self.stream.string
|
43
|
+
temp_file.close
|
44
|
+
temp_file
|
45
|
+
end
|
46
|
+
# Return a ZipOutputStream
|
47
|
+
def stream
|
48
|
+
check_pass
|
49
|
+
output_zip
|
50
|
+
end
|
51
|
+
|
52
|
+
def self.init
|
53
|
+
doc = XML::Document.new
|
54
|
+
doc.encoding = XML::Encoding::UTF_8
|
55
|
+
root = XML::Node.new 'WalletItem'
|
56
|
+
doc.root = root
|
57
|
+
|
58
|
+
version_el = XML::Node.new('Version')
|
59
|
+
version_el.content = WALLET_VERSION
|
60
|
+
root << version_el
|
61
|
+
|
62
|
+
return doc
|
63
|
+
end
|
64
|
+
|
65
|
+
def valid?
|
66
|
+
check_pass
|
67
|
+
true
|
68
|
+
rescue
|
69
|
+
false
|
70
|
+
end
|
71
|
+
|
72
|
+
private
|
73
|
+
|
74
|
+
def convert_to_xml(key, val)
|
75
|
+
case val
|
76
|
+
when Hash
|
77
|
+
el = XML::Node.new(key)
|
78
|
+
val.each do |k,v|
|
79
|
+
res = convert_to_xml(k, v)
|
80
|
+
if res.is_a? Array
|
81
|
+
res.each {|item| el << item }
|
82
|
+
else
|
83
|
+
el << res
|
84
|
+
end
|
85
|
+
end
|
86
|
+
when Array
|
87
|
+
el = []
|
88
|
+
val.each do |v|
|
89
|
+
el << convert_to_xml(key, v)
|
90
|
+
end
|
91
|
+
else
|
92
|
+
el = XML::Node.new(key)
|
93
|
+
if val
|
94
|
+
el.content = val.to_s
|
95
|
+
else
|
96
|
+
text_node = XML::Node.new_text(' ')
|
97
|
+
text_node.output_escaping = false
|
98
|
+
el << text_node
|
99
|
+
end
|
100
|
+
|
101
|
+
end
|
102
|
+
el
|
103
|
+
end
|
104
|
+
|
105
|
+
def check_pass
|
106
|
+
# Check for default images
|
107
|
+
[99, 159, 336].each do |size|
|
108
|
+
raise "Logo#{size}x#{size} missing" unless @files.detect {|f| f[:name] == "Logo#{size}x#{size}.png" }
|
109
|
+
end
|
110
|
+
|
111
|
+
# Check for developer field in XML
|
112
|
+
|
113
|
+
fail 'Pass must be XML::Document object' unless @pass.is_a? XML::Document
|
114
|
+
|
115
|
+
root = @pass.find('WalletItem')
|
116
|
+
|
117
|
+
fail 'WalletItem node missing' unless root
|
118
|
+
|
119
|
+
{
|
120
|
+
'Version' => ['Format Version missing', Mswallet::WALLET_VERSION ],
|
121
|
+
'Kind' => ['Pass Kind Identifier missing', TYPES ],
|
122
|
+
'Id' => ['Id missing'],
|
123
|
+
'DisplayName' => ['Display Name missing' ],
|
124
|
+
'IssuerDisplayName' => ['Issuer Display Name missing'],
|
125
|
+
'HeaderColor' => ['Header Color missing', /^#(?:[\da-fA-F]{2}){3}$/],
|
126
|
+
'BodyColor' => ['Body Name missing', /#(?:[\d\w]){3}/]
|
127
|
+
|
128
|
+
}.each do |tag, params|
|
129
|
+
msg, value = params
|
130
|
+
el = @pass.find(tag).first
|
131
|
+
fail msg unless el
|
132
|
+
|
133
|
+
case value
|
134
|
+
when Array
|
135
|
+
fail "#{tag} not in #{value.join(',')}" unless value.include?(el.content) unless value.empty?
|
136
|
+
when Regexp
|
137
|
+
fail "#{tag} not match #{value.to_s}" unless el.content.match value
|
138
|
+
when nil
|
139
|
+
nil
|
140
|
+
else
|
141
|
+
fail "#{tag} must be #{value}" unless value == el.content
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
145
|
+
end
|
146
|
+
|
147
|
+
def output_zip
|
148
|
+
buffer = Zip::OutputStream.write_buffer do |zip|
|
149
|
+
zip.put_next_entry 'WalletItem.xml'
|
150
|
+
zip.write @pass.to_s
|
151
|
+
|
152
|
+
@files.each do |file|
|
153
|
+
if file.class == Hash
|
154
|
+
zip.put_next_entry file[:name]
|
155
|
+
zip.print file[:content]
|
156
|
+
else
|
157
|
+
zip.put_next_entry File.basename(file)
|
158
|
+
zip.print IO.read(file)
|
159
|
+
end
|
160
|
+
end
|
161
|
+
|
162
|
+
@locales.each do |lang, file|
|
163
|
+
zip.put_next_entry "#{lang}/strings.txt"
|
164
|
+
|
165
|
+
data = ''
|
166
|
+
case file
|
167
|
+
when String
|
168
|
+
data = file
|
169
|
+
when File
|
170
|
+
data = file.read
|
171
|
+
when Hash
|
172
|
+
file.each do |k, v|
|
173
|
+
data += "#{k}=#{v}\n"
|
174
|
+
end
|
175
|
+
else
|
176
|
+
fail 'wrong locale object'
|
177
|
+
end
|
178
|
+
|
179
|
+
zip.print data
|
180
|
+
end
|
181
|
+
end
|
182
|
+
|
183
|
+
buffer
|
184
|
+
end
|
185
|
+
|
186
|
+
end
|
187
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
module Rack
|
2
|
+
class MswalletRack
|
3
|
+
|
4
|
+
def initialize(app)
|
5
|
+
@app = app
|
6
|
+
@parameters = {}
|
7
|
+
end
|
8
|
+
|
9
|
+
def call(env)
|
10
|
+
@parameters['authToken'] = env['HTTP_AUTHENTICATIONTOKEN'] if env['HTTP_AUTHENTICATIONTOKEN']
|
11
|
+
@parameters.merge!(Rack::Utils.parse_nested_query(env['QUERY_STRING']))
|
12
|
+
@parameters['path'] = env['PATH_INFO']
|
13
|
+
serial_number = find_id @parameters['path']
|
14
|
+
@parameters['serialNumber'] = serial_number
|
15
|
+
if serial_number
|
16
|
+
handler = Mswallet.custom_rack_handler || Mswallet::Handler
|
17
|
+
response = handler.update(@parameters)
|
18
|
+
header = {'Content-Type' => 'application/vnd.ms.wallet',
|
19
|
+
'Content-Disposition' => 'attachment',
|
20
|
+
'filename' => "#{serial_number}.mswallet"}
|
21
|
+
case response
|
22
|
+
when Mswallet::Pass
|
23
|
+
[200, header, [response.stream.string]]
|
24
|
+
when String
|
25
|
+
[200, header, [response]]
|
26
|
+
when File, StringIO, Zip::OutputStream, Tempfile
|
27
|
+
[200, header, [response.read]]
|
28
|
+
when false
|
29
|
+
[304, {}, {}]
|
30
|
+
else
|
31
|
+
[401, {}, {}]
|
32
|
+
end
|
33
|
+
|
34
|
+
else
|
35
|
+
@app.call env
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def append_parameter_separator url
|
40
|
+
end
|
41
|
+
|
42
|
+
def each(&block)
|
43
|
+
end
|
44
|
+
|
45
|
+
def find_id(path)
|
46
|
+
return nil unless path =~ /\/#{Mswallet::RACK_API_VERSION}\/walletitems\/([\d\w]+)/
|
47
|
+
$1
|
48
|
+
end
|
49
|
+
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
data/mswallet.gemspec
ADDED
@@ -0,0 +1,31 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'mswallet/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = 'mswallet'
|
8
|
+
spec.version = Mswallet::VERSION
|
9
|
+
spec.authors = ['ajieks@vmp.ru']
|
10
|
+
spec.email = %w(ajieks@vmp.ru schalexe@gmail.com)
|
11
|
+
spec.summary = %q{A Windows Phone wallet pass generator.}
|
12
|
+
spec.description = %q{This gem allows you to create passes for Windows Phone Wallet. Unlike some, this works with Rails but does not require it.}
|
13
|
+
spec.homepage = 'http://github.com/fuCtor/mswallet'
|
14
|
+
spec.license = 'MIT'
|
15
|
+
|
16
|
+
spec.files = `git ls-files`.split($/)
|
17
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
18
|
+
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
19
|
+
spec.require_paths = ['lib']
|
20
|
+
|
21
|
+
spec.add_development_dependency 'bundler'
|
22
|
+
spec.add_development_dependency 'pry'
|
23
|
+
spec.add_development_dependency 'rake'
|
24
|
+
spec.add_development_dependency 'rspec'
|
25
|
+
spec.add_development_dependency 'rack-test', ['>= 0']
|
26
|
+
spec.add_development_dependency 'simplecov'
|
27
|
+
spec.add_dependency 'libxml-ruby'
|
28
|
+
spec.add_dependency 'rack'
|
29
|
+
spec.add_dependency('rubyzip', [">= 1.0.0"])
|
30
|
+
|
31
|
+
end
|
@@ -0,0 +1,190 @@
|
|
1
|
+
require 'rspec'
|
2
|
+
|
3
|
+
describe Mswallet::Pass do
|
4
|
+
|
5
|
+
let :fields do
|
6
|
+
{
|
7
|
+
'Version' => ['Format Version missing', '1' ],
|
8
|
+
'Kind' => ['Pass Kind Identifier missing', 'General' ],
|
9
|
+
'Id' => ['Id missing', '1'],
|
10
|
+
'DisplayName' => ['Display Name missing' ],
|
11
|
+
'IssuerDisplayName' => ['Issuer Display Name missing'],
|
12
|
+
'HeaderColor' => ['Header Color missing', %W(#000000 #AABBCC)],
|
13
|
+
'BodyColor' => ['Body Name missing', %W(#000000 #AABBCC)]
|
14
|
+
}
|
15
|
+
end
|
16
|
+
|
17
|
+
let :wallet do
|
18
|
+
w = Mswallet::Pass.init
|
19
|
+
fields.each do |*args|
|
20
|
+
tag, msg, *value = args.flatten.map(&:to_s)
|
21
|
+
el = XML::Node.new(tag)
|
22
|
+
w.root << el
|
23
|
+
|
24
|
+
case value
|
25
|
+
when Array
|
26
|
+
el.content = value.first.to_s
|
27
|
+
when nil
|
28
|
+
|
29
|
+
else
|
30
|
+
el.content = value
|
31
|
+
end
|
32
|
+
end
|
33
|
+
w
|
34
|
+
end
|
35
|
+
let :pass do Mswallet::Pass.new wallet end
|
36
|
+
|
37
|
+
it '#init' do
|
38
|
+
expect { wallet } .to_not raise_error
|
39
|
+
expect( wallet ).to be_instance_of(XML::Document)
|
40
|
+
expect( wallet.root.name ).to be_eql 'WalletItem'
|
41
|
+
expect( wallet.root.find('Version').empty? ).to be_falsey
|
42
|
+
expect( wallet.root.find('Version').first.content ).to be_eql(Mswallet::WALLET_VERSION)
|
43
|
+
end
|
44
|
+
|
45
|
+
it '#add_file' do
|
46
|
+
expect { pass } .to_not raise_error
|
47
|
+
expect( pass.files ).to be_empty
|
48
|
+
expect { pass.add_file name: 'Logo336x336.png', content: '' } .to_not raise_error
|
49
|
+
expect( pass.files ).to_not be_empty
|
50
|
+
end
|
51
|
+
|
52
|
+
context 'locale' do
|
53
|
+
it '#add_locale' do
|
54
|
+
|
55
|
+
expect { pass } .to_not raise_error
|
56
|
+
expect( pass.locales ).to be_empty
|
57
|
+
expect { pass.add_locale 'ru-RU', '' } .to_not raise_error
|
58
|
+
expect { pass.add_locale 'ru-RU', {} } .to_not raise_error
|
59
|
+
expect { pass.add_locale 'ru-RU', File.new(__FILE__, 'r') } .to_not raise_error
|
60
|
+
expect { pass.add_locale 'ru-RU', true } .to_not raise_error
|
61
|
+
|
62
|
+
expect( pass.locales ).to_not be_empty
|
63
|
+
expect( pass.locales['ru-RU'] ).to be_truthy
|
64
|
+
end
|
65
|
+
|
66
|
+
context 'serialize' do
|
67
|
+
before do
|
68
|
+
[99, 159, 336].each do |size|
|
69
|
+
pass.add_file name: "Logo#{size}x#{size}.png", content: ''
|
70
|
+
end
|
71
|
+
|
72
|
+
pass.add_locale 'ru-RU', locale
|
73
|
+
end
|
74
|
+
|
75
|
+
context 'from string' do
|
76
|
+
let(:locale) { '' }
|
77
|
+
it do
|
78
|
+
expect { pass.stream } .to_not raise_error
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
context 'from file' do
|
83
|
+
let(:locale) { File.new(__FILE__, 'r') }
|
84
|
+
it do
|
85
|
+
expect { pass.stream } .to_not raise_error
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
context 'from hash' do
|
90
|
+
let(:locale) { {} }
|
91
|
+
it do
|
92
|
+
expect { pass.stream } .to_not raise_error
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
context 'from other' do
|
97
|
+
let(:locale) { nil }
|
98
|
+
it do
|
99
|
+
expect { pass.stream } .to raise_error 'wrong locale object'
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
|
106
|
+
it '#pass_check' do
|
107
|
+
expect { pass } .to_not raise_error
|
108
|
+
|
109
|
+
[99, 159, 336].each do |size|
|
110
|
+
expect( pass.valid? ) .to be_falsey
|
111
|
+
expect { pass.stream } .to raise_error /Logo#{size}x#{size}/
|
112
|
+
expect { pass.add_file name: "Logo#{size}x#{size}.png", content: '' } .to_not raise_error
|
113
|
+
end
|
114
|
+
|
115
|
+
fields.keys.reverse do |tag|
|
116
|
+
msg, *value = fields[tag].map(&:to_s)
|
117
|
+
expect { pass.stream } .to_not raise_error msg
|
118
|
+
|
119
|
+
el = wallet.root.find(tag).first
|
120
|
+
|
121
|
+
if value
|
122
|
+
el.content = ''
|
123
|
+
expect { pass.stream } .to raise_error /#{tag}/
|
124
|
+
end
|
125
|
+
|
126
|
+
el.remove!
|
127
|
+
expect { pass.stream } .to raise_error msg
|
128
|
+
end
|
129
|
+
expect( pass.valid? ) .to be_truthy
|
130
|
+
end
|
131
|
+
|
132
|
+
|
133
|
+
context '#[]=' do
|
134
|
+
let(:key) {'key'}
|
135
|
+
after do
|
136
|
+
expect(wallet.root.find(key).first).to be_nil
|
137
|
+
|
138
|
+
expect{ pass[key] = value }.to_not raise_error
|
139
|
+
|
140
|
+
expect(wallet.root.find(key).first).to_not be_nil
|
141
|
+
expect(pass[key]).to be_instance_of XML::Node
|
142
|
+
end
|
143
|
+
|
144
|
+
context 'string value' do
|
145
|
+
let(:value) { 'content' }
|
146
|
+
it do; end
|
147
|
+
end
|
148
|
+
|
149
|
+
context 'hash value' do
|
150
|
+
let(:value) { { 'key2' => 'content', key3: 'content'} }
|
151
|
+
it do; end
|
152
|
+
end
|
153
|
+
|
154
|
+
context 'string array value' do
|
155
|
+
let(:value) { %w(a b c) }
|
156
|
+
it do; end
|
157
|
+
end
|
158
|
+
|
159
|
+
context 'hash array value' do
|
160
|
+
let(:value) { [{a: 1},{b: 2}] }
|
161
|
+
it do; end
|
162
|
+
end
|
163
|
+
|
164
|
+
context 'nil value' do
|
165
|
+
let(:value) { nil }
|
166
|
+
it do; end
|
167
|
+
end
|
168
|
+
|
169
|
+
end
|
170
|
+
|
171
|
+
context 'valid pass' do
|
172
|
+
before do
|
173
|
+
[99, 159, 336].each do |size|
|
174
|
+
pass.add_file name: "Logo#{size}x#{size}.png", content: ''
|
175
|
+
end
|
176
|
+
|
177
|
+
pass.add_locale 'ru-RU', ''
|
178
|
+
end
|
179
|
+
|
180
|
+
it '#stream' do
|
181
|
+
expect { pass.stream } .to_not raise_error
|
182
|
+
expect( pass.stream.string.empty? ) .to be_falsey
|
183
|
+
end
|
184
|
+
|
185
|
+
it '#file' do
|
186
|
+
expect { pass.file } .to_not raise_error
|
187
|
+
end
|
188
|
+
end
|
189
|
+
|
190
|
+
end
|