motion-addressbook 0.1.1 → 0.1.2

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore CHANGED
@@ -3,8 +3,7 @@ build
3
3
  resources/*.nib
4
4
  resources/*.momd
5
5
  resources/*.storyboardc
6
-
7
- resources/photos/[1-9]*/*
6
+ Gemfile.lock
8
7
 
9
8
  vendor/Pods/Pods.xcodeproj/project.pbxproj
10
9
  vendor/Pods/build*
data/README.md CHANGED
@@ -20,6 +20,62 @@ Or install it yourself as:
20
20
 
21
21
  ## Usage
22
22
 
23
+ ### Requesting access
24
+
25
+ iOS 6 requires asking the user for permission before it allows an app to access the AddressBook. There are 3 ways to interact with this
26
+
27
+ 1 - Let the gem take care of it for you
28
+
29
+ ```ruby
30
+ people = AddressBook::Person.all
31
+ # A dialog may be presented to the user before "people" was returned
32
+ ```
33
+
34
+ 2 - Manually decide when to ask the user for authorization
35
+
36
+ ```ruby
37
+ # asking whether we are already authorized
38
+ if AddressBook.authorized?
39
+ puts "This app is authorized?"
40
+ else
41
+ puts "This app is not authorized?"
42
+ end
43
+
44
+ # ask the user to authorize us
45
+ if AddressBook.authorize
46
+ # do something now that the user has said "yes"
47
+ else
48
+ # do something now that the user has said "no"
49
+ end
50
+ ```
51
+
52
+ 3 - Manually ask the user but do it asynchronously (this is how Apple's API works)
53
+
54
+ ```ruby
55
+ # ask the user to authorize us
56
+ if AddressBook.authorize do |granted|
57
+ # this block is invoked sometime later
58
+ if granted
59
+ # do something now that the user has said "yes"
60
+ else
61
+ # do something now that the user has said "no"
62
+ end
63
+ end
64
+ # do something here before the user has decided
65
+ ```
66
+
67
+ ### Showing the ABPeoplePicker
68
+
69
+ ```ruby
70
+ AddressBook.pick { |person|
71
+ if person
72
+ # person is an AddressBook::Person object
73
+ else
74
+ # canceled
75
+ end
76
+ }
77
+ ```
78
+
23
79
  ### Instantiating a person object
24
80
 
25
81
  There are 3 ways to instantiate a person object
data/Rakefile CHANGED
@@ -5,4 +5,4 @@ require "bundler/gem_tasks"
5
5
  Bundler.setup
6
6
  Bundler.require
7
7
 
8
- require 'bubble-wrap/test'
8
+ require 'bubble-wrap/test'
@@ -1,5 +1,5 @@
1
1
  module Motion
2
2
  module Addressbook
3
- VERSION = "0.1.1"
3
+ VERSION = "0.1.2"
4
4
  end
5
5
  end
@@ -1,5 +1,10 @@
1
1
  require "motion-addressbook/version"
2
2
 
3
- BubbleWrap.require 'motion/address_book.rb'
3
+ BubbleWrap.require 'motion/address_book.rb' do
4
+ file('motion/address_book.rb').uses_framework('AddressBook')
5
+ end
4
6
  BW.require 'motion/address_book/multi_value.rb'
5
7
  BW.require 'motion/address_book/person.rb'
8
+ BW.require 'motion/address_book/picker.rb' do
9
+ file('motion/address_book/picker.rb').uses_framework('AddressBookUI')
10
+ end
@@ -17,14 +17,14 @@ module AddressBook
17
17
  def attribute_map
18
18
  self.class.attribute_map
19
19
  end
20
-
20
+
21
21
  def alex
22
22
  ABMultiValueGetIdentifierAtIndex @ab_multi_values, 0
23
23
  end
24
24
 
25
25
  def initialize(attributes={}, existing_ab_multi_values=nil)
26
26
  @attributes = {}
27
- if existing_ab_multi_values
27
+ if existing_ab_multi_values
28
28
  @ab_multi_values = ABMultiValueCreateMutableCopy(existing_ab_multi_values)
29
29
  load_attributes_from_ab
30
30
  else
@@ -49,7 +49,7 @@ module AddressBook
49
49
  return false if attribute_name.nil?
50
50
  attribute_map.include?(attribute_name.to_sym) || [:email, :phone_number].include?( attribute_name.to_sym)
51
51
  end
52
-
52
+
53
53
  def getter?(method_name)
54
54
  if self.class.is_attribute? method_name
55
55
  method_name
@@ -69,7 +69,7 @@ module AddressBook
69
69
  def get(attribute_name)
70
70
  attributes[attribute_name.to_sym] ||= get_field(attribute_map[attribute_name])
71
71
  end
72
-
72
+
73
73
  def set(attribute_name, value)
74
74
  set_field(attribute_map[attribute_name.to_sym], value)
75
75
  attributes[attribute_name.to_sym] = value
@@ -117,4 +117,4 @@ module AddressBook
117
117
  end
118
118
 
119
119
  end
120
- end
120
+ end
@@ -14,10 +14,11 @@ module AddressBook
14
14
  end
15
15
 
16
16
  def self.all
17
- address_book = ABAddressBookCreate()
18
- ABAddressBookCopyArrayOfAllPeople(address_book).map do |ab_person|
17
+ people = ABAddressBookCopyArrayOfAllPeople(AddressBook.address_book).map do |ab_person|
19
18
  new({}, ab_person)
20
19
  end
20
+ people.sort! { |a,b| "#{a.first_name} #{a.last_name}" <=> "#{b.first_name} #{b.last_name}" }
21
+ people
21
22
  end
22
23
 
23
24
  def self.create(attributes)
@@ -32,13 +33,13 @@ module AddressBook
32
33
  @address_book = nil #force refresh
33
34
  @new_record = false
34
35
  end
35
-
36
+
36
37
  def self.where(conditions)
37
38
  all.select do |person|
38
39
  person.meets? conditions
39
40
  end
40
41
  end
41
-
42
+
42
43
  def meets?(conditions)
43
44
  conditions.keys.all? do |attribute|
44
45
  send(attribute) == conditions[attribute]
@@ -77,12 +78,12 @@ module AddressBook
77
78
  else
78
79
  super
79
80
  end
80
- end
81
+ end
81
82
  def self.is_attribute?(attribute_name)
82
83
  return false if attribute_name.nil?
83
84
  attribute_map.include?(attribute_name.to_sym) || [:email, :phone_number].include?( attribute_name.to_sym)
84
85
  end
85
-
86
+
86
87
  def getter?(method_name)
87
88
  if self.class.is_attribute? method_name
88
89
  method_name
@@ -122,16 +123,16 @@ module AddressBook
122
123
  nil
123
124
  end
124
125
  end
125
-
126
+
126
127
  def get(attribute_name)
127
128
  attributes[attribute_name.to_sym] ||= get_field(attribute_map[attribute_name])
128
129
  end
129
-
130
+
130
131
  def set(attribute_name, value)
131
132
  set_field(attribute_map[attribute_name.to_sym], value)
132
133
  attributes[attribute_name.to_sym] = value
133
134
  end
134
-
135
+
135
136
  def self.find_all_by(attribute_name, criteria)
136
137
  where({attribute_name.to_sym => criteria})
137
138
  end
@@ -141,7 +142,7 @@ module AddressBook
141
142
  def self.find_or_new_by(attribute_name, criteria)
142
143
  find_by(attribute_name, criteria) || new({attribute_name.to_sym => criteria})
143
144
  end
144
-
145
+
145
146
  def photo
146
147
  ABPersonCopyImageData(ab_person)
147
148
  end
@@ -166,7 +167,7 @@ module AddressBook
166
167
  def email_values
167
168
  emails.values
168
169
  end
169
-
170
+
170
171
  # UGH - kinda arbitrary way to deal with multiple values. DO SOMETHING BETTER.
171
172
  def email
172
173
  @attributes[:email] ||= email_values.first
@@ -247,8 +248,8 @@ module AddressBook
247
248
  end
248
249
 
249
250
  def address_book
250
- @address_book ||= ABAddressBookCreate()
251
+ @address_book ||= AddressBook.address_book
251
252
  end
252
253
 
253
254
  end
254
- end
255
+ end
@@ -0,0 +1,60 @@
1
+ module AddressBook
2
+ class Picker
3
+ class << self
4
+ attr_accessor :showing
5
+ end
6
+ def self.show(&after)
7
+ raise "Cannot show two Pickers" if showing?
8
+ @picker = Picker.new(&after)
9
+ @picker.show
10
+ @picker
11
+ end
12
+
13
+ def self.showing?
14
+ !!showing
15
+ end
16
+
17
+ def initialize(&after)
18
+ @after = after
19
+ end
20
+
21
+ def show
22
+ self.class.showing = true
23
+
24
+ @people_picker_ctlr = ABPeoplePickerNavigationController.alloc.init
25
+ @people_picker_ctlr.peoplePickerDelegate = self
26
+ UIApplication.sharedApplication.keyWindow.rootViewController.presentViewController(@people_picker_ctlr, animated:true, completion:nil)
27
+ end
28
+
29
+ def hide(ab_person=nil)
30
+ person = ab_person ? AddressBook::Person.new({}, ab_person) : nil
31
+
32
+ UIApplication.sharedApplication.keyWindow.rootViewController.dismissViewControllerAnimated(true, completion:lambda{
33
+ @after.call(person) if @after
34
+ self.class.showing = false
35
+ })
36
+ end
37
+
38
+ def peoplePickerNavigationController(people_picker, shouldContinueAfterSelectingPerson:ab_person)
39
+ hide(ab_person)
40
+ false
41
+ end
42
+
43
+ def peoplePickerNavigationController(people_picker, shouldContinueAfterSelectingPerson:ab_person, property:property, identifier:id)
44
+ hide(ab_person)
45
+ false
46
+ end
47
+
48
+ def peoplePickerNavigationControllerDidCancel(people_picker)
49
+ hide
50
+ end
51
+ end
52
+ end
53
+
54
+ module AddressBook
55
+ module_function
56
+ def pick(&after)
57
+ AddressBook::Picker.show &after
58
+ end
59
+ end
60
+
@@ -0,0 +1,60 @@
1
+ module AddressBook
2
+ module_function
3
+
4
+ def address_book
5
+ if UIDevice.currentDevice.systemVersion >= '6'
6
+ ios6_create
7
+ else
8
+ ios5_create
9
+ end
10
+ end
11
+
12
+ def ios6_create
13
+ error = nil
14
+ address_book = ABAddressBookCreateWithOptions(nil, error)
15
+ request_authorization unless authorized?
16
+ address_book
17
+ end
18
+
19
+ def ios5_create
20
+ ABAddressBookCreate()
21
+ end
22
+
23
+ def request_authorization(&block)
24
+ synchronous = !!block
25
+ access_callback = lambda { |granted, error|
26
+ # not sure what to do with error ... so we're ignoring it
27
+ @address_book_access_granted = granted
28
+ block.call(@address_book_access_granted) unless block.nil?
29
+ }
30
+
31
+ ABAddressBookRequestAccessWithCompletion address_book, access_callback
32
+ if synchronous
33
+ # Wait on the asynchronous callback before returning.
34
+ while @address_book_access_granted.nil? do
35
+ sleep 0.1
36
+ end
37
+ end
38
+ @address_book_access_granted
39
+ end
40
+
41
+ def authorized?
42
+ authorization_status == :authorized
43
+ end
44
+
45
+ def authorization_status
46
+ return :authorized unless UIDevice.currentDevice.systemVersion >= '6'
47
+
48
+ status_map = { KABAuthorizationStatusNotDetermined => :not_determined,
49
+ KABAuthorizationStatusRestricted => :restricted,
50
+ KABAuthorizationStatusDenied => :denied,
51
+ KABAuthorizationStatusAuthorized => :authorized
52
+ }
53
+ status_map[ABAddressBookGetAuthorizationStatus()]
54
+ end
55
+
56
+ def create_with_options_available?
57
+ error = nil
58
+ ABAddressBookCreateWithOptions(nil, error) rescue false
59
+ end
60
+ end
@@ -2,12 +2,12 @@ describe AddressBook::MultiValue do
2
2
  describe 'properties on a new multivalue' do
3
3
  describe 'initializing with values' do
4
4
  before do
5
- @attributes = { :mobile => '123-456-7890', :iphone => '222-333-4444', :main => '555-1212',
5
+ @attributes = { :mobile => '123-456-7890', :iphone => '222-333-4444', :main => '555-1212',
6
6
  :home_fax => '1-617-555-8000', :work_fax => '1-212-555-0000', :pager => '99-9999-9999',
7
7
  :work => 'alex@work.com', :home => 'alex@home.com', :other => 'alex@other.com'}
8
8
  @multi_value = AddressBook::MultiValue.new @attributes
9
9
  end
10
-
10
+
11
11
  it 'should be able to get each of the single value fields' do
12
12
  @multi_value.mobile.should.equal @attributes[:mobile ]
13
13
  @multi_value.iphone.should.equal @attributes[:iphone ]
@@ -19,7 +19,7 @@ describe AddressBook::MultiValue do
19
19
  @multi_value.home.should.equal @attributes[:home ]
20
20
  @multi_value.other.should.equal @attributes[:other ]
21
21
  end
22
-
22
+
23
23
  it 'should give all the values in a list' do
24
24
  @multi_value.values.should == ["123-456-7890", "222-333-4444", "555-1212", "1-617-555-8000", "1-212-555-0000", "99-9999-9999", "alex@work.com", "alex@home.com", "alex@other.com"]
25
25
  end
@@ -68,13 +68,13 @@ describe AddressBook::MultiValue do
68
68
  end
69
69
  end
70
70
  end
71
-
71
+
72
72
  describe 'an existing multivalue' do
73
73
  before do
74
74
  @first_multi_value = AddressBook::MultiValue.new :mobile => '123-456-7890', :iphone => '99-8888888-7777777-66'
75
75
  @multi_value = AddressBook::MultiValue.new({:mobile => '987654321', :home_fax => '777-6666-4444'}, @first_multi_value.ab_multi_values)
76
76
  end
77
-
77
+
78
78
  it 'should ' do
79
79
  @multi_value.mobile.should == '987654321'
80
80
  @multi_value.iphone.should == '99-8888888-7777777-66'
@@ -1,5 +1,4 @@
1
1
  describe AddressBook::Person do
2
-
3
2
  describe 'ways of creating and finding people' do
4
3
  describe 'new' do
5
4
  before do
@@ -12,7 +11,7 @@ describe AddressBook::Person do
12
11
  @alex.email_values.should == ['alex_testy@example.com']
13
12
  end
14
13
  end
15
-
14
+
16
15
  describe 'existing' do
17
16
  before do
18
17
  @email = unique_email
@@ -60,7 +59,7 @@ describe AddressBook::Person do
60
59
  alexes.should == []
61
60
  end
62
61
  end
63
-
62
+
64
63
  describe '.all' do
65
64
  it 'should have the person we created' do
66
65
  all_names = AddressBook::Person.all.map do |person|
@@ -68,13 +67,13 @@ describe AddressBook::Person do
68
67
  end
69
68
  all_names.should.include? [@alex.first_name, @alex.last_name]
70
69
  end
71
-
70
+
72
71
  it 'should get bigger when we create another' do
73
72
  initial_people_count = AddressBook::Person.all.size
74
73
  @person = AddressBook::Person.create({:first_name => 'Alex2', :last_name=>'Rothenberg2'})
75
74
  AddressBook::Person.all.size.should == (initial_people_count + 1)
76
75
  end
77
- end
76
+ end
78
77
  end
79
78
 
80
79
  describe '.find_or_new_by_XXX - new or existing' do
@@ -107,17 +106,17 @@ describe AddressBook::Person do
107
106
  :email => unique_email
108
107
  }
109
108
  end
110
-
109
+
111
110
  describe 'a new person' do
112
111
  before do
113
112
  @ab_person = AddressBook::Person.new(@attributes)
114
113
  end
115
-
114
+
116
115
  it 'should not be existing' do
117
116
  @ab_person.should.be.new_record
118
117
  @ab_person.should.not.be.exists
119
118
  end
120
-
119
+
121
120
  it 'should be able to get each of the single value fields' do
122
121
  @ab_person.first_name.should.equal @attributes[:first_name ]
123
122
  @ab_person.last_name.should.equal @attributes[:last_name ]
@@ -125,7 +124,7 @@ describe AddressBook::Person do
125
124
  @ab_person.department.should.equal @attributes[:department ]
126
125
  @ab_person.organization.should.equal @attributes[:organization]
127
126
  end
128
-
127
+
129
128
  describe 'setting each field' do
130
129
  it 'should be able to set the first name' do
131
130
  @ab_person.first_name = 'new first name'
@@ -147,7 +146,7 @@ describe AddressBook::Person do
147
146
  @ab_person.organization = 'new organization'
148
147
  @ab_person.organization.should.equal 'new organization'
149
148
  end
150
-
149
+
151
150
  it 'should be able to set the phot' do
152
151
  image = CIImage.emptyImage
153
152
  data = UIImagePNGRepresentation(UIImage.imageWithCIImage image)
@@ -155,11 +154,11 @@ describe AddressBook::Person do
155
154
  UIImagePNGRepresentation(@ab_person.photo).should.equal data
156
155
  end
157
156
  end
158
-
157
+
159
158
  it 'should be able to get the phone numbers' do
160
159
  @ab_person.phone_number_values.should.equal [@attributes[:mobile_phone], @attributes[:office_phone] ]
161
160
  end
162
-
161
+
163
162
  it 'should be able to get the emails' do
164
163
  @ab_person.email_values.should.equal [@attributes[:email] ]
165
164
  end
@@ -173,7 +172,7 @@ describe AddressBook::Person do
173
172
  end
174
173
  end
175
174
  end
176
-
175
+
177
176
  describe 'updating an existing person' do
178
177
  before do
179
178
  AddressBook::Person.new(@attributes).save
@@ -182,14 +181,14 @@ describe AddressBook::Person do
182
181
  @attributes[:department ] = nil
183
182
  @ab_person = AddressBook::Person.find_or_new_by_email(@attributes[:email])
184
183
  end
185
-
184
+
186
185
  it 'should know it is not new' do
187
186
  @ab_person.should.not.be.new_record
188
187
  @ab_person.should.be.exists
189
188
  @ab_person.first_name.should == 'Alex'
190
189
  @ab_person.department.should == 'Development'
191
190
  end
192
-
191
+
193
192
  describe 'updating' do
194
193
  it 'should be able to get each of the single value fields' do
195
194
  @ab_person.save
@@ -199,11 +198,11 @@ describe AddressBook::Person do
199
198
  AddressBook::Person.find_by_email(@ab_person.email).first_name.should == 'New First Name'
200
199
  end
201
200
  end
202
-
201
+
203
202
  end
204
-
203
+
205
204
  end
206
-
205
+
207
206
  describe 'method missing magic' do
208
207
  before do
209
208
  @person = AddressBook::Person.new
@@ -0,0 +1,34 @@
1
+ # I don't know how to test a class wrapping a controller like this yet
2
+ #
3
+ # describe AddressBook::Picker do
4
+ # describe 'IOS UI for finding people' do
5
+ # before do
6
+ # @selected_person = nil
7
+ # @picker = AddressBook.pick do |person|
8
+ # @selected_person = person
9
+ # end
10
+ # end
11
+
12
+ # it 'should yield the selected person' do
13
+ # ab_person = AddressBook::Person.new(first_name: 'Colin').ab_person
14
+ # @picker.peoplePickerNavigationController(@picker_nav_controller, shouldContinueAfterSelectingPerson: ab_person)
15
+ # @selected_person.should.not == nil
16
+ # @selected_person.first_name.should == 'Colin'
17
+ # end
18
+
19
+ # it 'should yield the selected person' do
20
+ # property = :some_property
21
+ # id = :some_id
22
+ # ab_person = AddressBook::Person.new(first_name: 'Colin').ab_person
23
+ # @picker.peoplePickerNavigationController(@picker_nav_controller, shouldContinueAfterSelectingPerson: ab_person, property:property, identifier:id)
24
+ # @selected_person.should.not == nil
25
+ # @selected_person.first_name.should == 'Colin'
26
+ # end
27
+
28
+ # it 'should yield nil when cancelled' do
29
+ # ab_person = AddressBook::Person.new(first_name: 'Colin').ab_person
30
+ # @picker.peoplePickerNavigationControllerDidCancel(@picker_nav_controller)
31
+ # @selected_person.should == nil
32
+ # end
33
+ # end
34
+ # end
@@ -1,3 +1,6 @@
1
- # The value of these constants is undefined until one of the following functions has been called: ABAddressBookCreate, ABPersonCreate, ABGroupCreate.
2
- # see https://developer.apple.com/library/ios/#documentation/AddressBook/Reference/ABPersonRef_iPhoneOS/Reference/reference.html and search for "Discussion"
3
- ABAddressBookCreate()
1
+ # The value of these constants is undefined until one of the following functions has been called:
2
+ # ABAddressBookCreate, ABPersonCreate, ABGroupCreate.
3
+ # see https://developer.apple.com/library/ios/#documentation/AddressBook/Reference/ABPersonRef_iPhoneOS/Reference/reference.html
4
+ # and search for "Discussion"
5
+ # WTF: Why would an API define a constant this way?!?!
6
+ AddressBook.address_book
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: motion-addressbook
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.1
4
+ version: 0.1.2
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2012-08-08 00:00:00.000000000 Z
12
+ date: 2012-11-08 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: bubble-wrap
@@ -68,17 +68,19 @@ extra_rdoc_files: []
68
68
  files:
69
69
  - .gitignore
70
70
  - Gemfile
71
- - Gemfile.lock
72
71
  - LICENSE
73
72
  - README.md
74
73
  - Rakefile
75
74
  - lib/motion-addressbook.rb
76
75
  - lib/motion-addressbook/version.rb
77
76
  - motion-addressbook.gemspec
77
+ - motion/address_book.rb
78
78
  - motion/address_book/multi_value.rb
79
79
  - motion/address_book/person.rb
80
+ - motion/address_book/picker.rb
80
81
  - spec/address_book/multi_value_spec.rb
81
82
  - spec/address_book/person_spec.rb
83
+ - spec/address_book/picker_spec.rb
82
84
  - spec/helpers/bacon_matchers.rb
83
85
  - spec/helpers/hacks.rb
84
86
  - spec/helpers/person_helpers.rb
@@ -96,7 +98,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
96
98
  version: '0'
97
99
  segments:
98
100
  - 0
99
- hash: 2733254916178873065
101
+ hash: 4255124381672907442
100
102
  required_rubygems_version: !ruby/object:Gem::Requirement
101
103
  none: false
102
104
  requirements:
@@ -105,7 +107,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
105
107
  version: '0'
106
108
  segments:
107
109
  - 0
108
- hash: 2733254916178873065
110
+ hash: 4255124381672907442
109
111
  requirements: []
110
112
  rubyforge_project:
111
113
  rubygems_version: 1.8.24
@@ -115,6 +117,7 @@ summary: A RubyMotion wrapper around the iOS Address Book framework
115
117
  test_files:
116
118
  - spec/address_book/multi_value_spec.rb
117
119
  - spec/address_book/person_spec.rb
120
+ - spec/address_book/picker_spec.rb
118
121
  - spec/helpers/bacon_matchers.rb
119
122
  - spec/helpers/hacks.rb
120
123
  - spec/helpers/person_helpers.rb
data/Gemfile.lock DELETED
@@ -1,29 +0,0 @@
1
- PATH
2
- remote: .
3
- specs:
4
- motion-addressbook (0.1.1)
5
- bubble-wrap
6
-
7
- GEM
8
- remote: https://rubygems.org/
9
- specs:
10
- bubble-wrap (1.1.1)
11
- diff-lcs (1.1.3)
12
- rake (0.9.2.2)
13
- rspec (2.10.0)
14
- rspec-core (~> 2.10.0)
15
- rspec-expectations (~> 2.10.0)
16
- rspec-mocks (~> 2.10.0)
17
- rspec-core (2.10.1)
18
- rspec-expectations (2.10.0)
19
- diff-lcs (~> 1.1.3)
20
- rspec-mocks (2.10.1)
21
-
22
- PLATFORMS
23
- ruby
24
-
25
- DEPENDENCIES
26
- bubble-wrap
27
- motion-addressbook!
28
- rake
29
- rspec