mswallet 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.
- 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
|