reagent-fleakr 0.3.0 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (51) hide show
  1. data/README.rdoc +115 -20
  2. data/Rakefile +1 -1
  3. data/lib/fleakr.rb +66 -7
  4. data/lib/fleakr/api.rb +7 -0
  5. data/lib/fleakr/api/file_parameter.rb +47 -0
  6. data/lib/fleakr/api/method_request.rb +57 -0
  7. data/lib/fleakr/api/parameter.rb +35 -0
  8. data/lib/fleakr/api/parameter_list.rb +96 -0
  9. data/lib/fleakr/api/response.rb +2 -2
  10. data/lib/fleakr/api/upload_request.rb +64 -0
  11. data/lib/fleakr/api/value_parameter.rb +36 -0
  12. data/lib/fleakr/core_ext.rb +1 -0
  13. data/lib/fleakr/core_ext/hash.rb +22 -0
  14. data/lib/fleakr/objects.rb +9 -0
  15. data/lib/fleakr/objects/authentication_token.rb +43 -0
  16. data/lib/fleakr/objects/contact.rb +5 -5
  17. data/lib/fleakr/objects/error.rb +2 -2
  18. data/lib/fleakr/objects/group.rb +2 -2
  19. data/lib/fleakr/objects/image.rb +7 -7
  20. data/lib/fleakr/objects/photo.rb +69 -5
  21. data/lib/fleakr/objects/search.rb +3 -6
  22. data/lib/fleakr/objects/set.rb +11 -5
  23. data/lib/fleakr/objects/user.rb +14 -26
  24. data/lib/fleakr/support.rb +2 -0
  25. data/lib/fleakr/support/attribute.rb +30 -12
  26. data/lib/fleakr/support/object.rb +20 -4
  27. data/lib/fleakr/version.rb +1 -1
  28. data/test/fixtures/auth.checkToken.xml +8 -0
  29. data/test/fixtures/auth.getFullToken.xml +8 -0
  30. data/test/fixtures/people.getInfo.xml +1 -1
  31. data/test/fixtures/photos.getInfo.xml +20 -0
  32. data/test/test_helper.rb +18 -3
  33. data/test/unit/fleakr/api/file_parameter_test.rb +63 -0
  34. data/test/unit/fleakr/api/method_request_test.rb +103 -0
  35. data/test/unit/fleakr/api/parameter_list_test.rb +161 -0
  36. data/test/unit/fleakr/api/parameter_test.rb +34 -0
  37. data/test/unit/fleakr/api/upload_request_test.rb +133 -0
  38. data/test/unit/fleakr/api/value_parameter_test.rb +41 -0
  39. data/test/unit/fleakr/core_ext/hash_test.rb +32 -0
  40. data/test/unit/fleakr/objects/authentication_token_test.rb +47 -0
  41. data/test/unit/fleakr/objects/image_test.rb +10 -5
  42. data/test/unit/fleakr/objects/photo_test.rb +96 -36
  43. data/test/unit/fleakr/objects/search_test.rb +1 -1
  44. data/test/unit/fleakr/objects/set_test.rb +12 -1
  45. data/test/unit/fleakr/objects/user_test.rb +2 -16
  46. data/test/unit/fleakr/support/attribute_test.rb +82 -24
  47. data/test/unit/fleakr/support/object_test.rb +26 -3
  48. data/test/unit/fleakr_test.rb +65 -6
  49. metadata +27 -4
  50. data/lib/fleakr/api/request.rb +0 -58
  51. data/test/unit/fleakr/api/request_test.rb +0 -93
@@ -9,13 +9,10 @@ module Fleakr
9
9
 
10
10
  # Retrieve search results from the API
11
11
  def results
12
- if @results.nil?
13
- response = Fleakr::Api::Request.with_response!('photos.search', parameters)
14
- @results = (response.body/'rsp/photos/photo').map do |flickr_photo|
15
- Photo.new(flickr_photo)
16
- end
12
+ @results ||= begin
13
+ response = Fleakr::Api::MethodRequest.with_response!('photos.search', parameters)
14
+ (response.body/'rsp/photos/photo').map {|p| Photo.new(p) }
17
15
  end
18
- @results
19
16
  end
20
17
 
21
18
  private
@@ -8,6 +8,7 @@ module Fleakr
8
8
  # [id] The ID for this photoset
9
9
  # [title] The title of this photoset
10
10
  # [description] The description of this set
11
+ # [count] Count of photos in this set
11
12
  #
12
13
  # == Associations
13
14
  #
@@ -19,26 +20,31 @@ module Fleakr
19
20
 
20
21
  has_many :photos, :using => :photoset_id
21
22
 
22
- flickr_attribute :id, :attribute => 'id'
23
+ flickr_attribute :id
23
24
  flickr_attribute :title
24
25
  flickr_attribute :description
26
+ flickr_attribute :count, :from => '@photos'
25
27
 
26
28
  find_all :by_user_id, :call => 'photosets.getList', :path => 'photosets/photoset'
27
29
 
28
- # Save all photos in this set to the specified directory using the specified size. Allowed
30
+ # Save all photos in this set to the specified directory for the specified size. Allowed
29
31
  # Sizes include <tt>:square</tt>, <tt>:small</tt>, <tt>:thumbnail</tt>, <tt>:medium</tt>,
30
- # <tt>:large</tt>, and <tt>:original</tt>. When saving the set, this # method will create
32
+ # <tt>:large</tt>, and <tt>:original</tt>. When saving the set, this method will create
31
33
  # a subdirectory based on the set's title.
32
34
  #
33
35
  def save_to(path, size)
34
36
  target = "#{path}/#{self.title}"
35
37
  FileUtils.mkdir(target) unless File.exist?(target)
36
38
 
37
- self.photos.each do |photo|
39
+ self.photos.each_with_index do |photo, index|
38
40
  image = photo.send(size)
39
- image.save_to(target) unless image.nil?
41
+ image.save_to(target, file_prefix(index)) unless image.nil?
40
42
  end
41
43
  end
44
+
45
+ def file_prefix(index) # :nodoc:
46
+ sprintf("%0#{self.count.length}d_", (index + 1))
47
+ end
42
48
 
43
49
  end
44
50
  end
@@ -11,6 +11,7 @@ module Fleakr
11
11
  # [id] The ID for this user (also referred to as the NSID in the API docs)
12
12
  # [username] This user's username
13
13
  # [name] This user's full name (if entered)
14
+ # [location] This user's location (if entered)
14
15
  # [photos_url] The direct URL to this user's photostream
15
16
  # [profile_url] The direct URL to this user's profile
16
17
  # [photos_count] The number of photos that this user has uploaded
@@ -42,37 +43,24 @@ module Fleakr
42
43
 
43
44
  include Fleakr::Support::Object
44
45
 
45
- def self.lazily_load(*attributes)
46
- options = attributes.extract_options!
47
-
48
- attributes.each do |attribute|
49
- class_eval <<-CODE
50
- def #{attribute}_with_loading
51
- self.send(:#{options[:with]}) if @#{attribute}.nil?
52
- #{attribute}_without_loading
53
- end
54
- alias_method_chain :#{attribute}, :loading
55
- CODE
56
- end
57
- end
58
-
59
- flickr_attribute :id, :xpath => 'rsp/user', :attribute => 'nsid'
60
- flickr_attribute :username, :xpath => 'rsp/user/username'
61
- flickr_attribute :name, :xpath => 'rsp/person/realname'
62
- flickr_attribute :photos_url, :xpath => 'rsp/person/photosurl'
63
- flickr_attribute :profile_url, :xpath => 'rsp/person/profileurl'
64
- flickr_attribute :photos_count, :xpath => 'rsp/person/photos/count'
65
- flickr_attribute :icon_server, :xpath => 'rsp/person', :attribute => 'iconserver'
66
- flickr_attribute :icon_farm, :xpath => 'rsp/person', :attribute => 'iconfarm'
67
- flickr_attribute :pro, :xpath => 'rsp/person', :attribute => 'ispro'
68
- flickr_attribute :admin, :xpath => 'rsp/person', :attribute => 'isadmin'
46
+ flickr_attribute :id, :from => 'user@nsid'
47
+ flickr_attribute :username
48
+ flickr_attribute :name, :from => 'person/realname'
49
+ flickr_attribute :location
50
+ flickr_attribute :photos_url, :from => 'person/photosurl'
51
+ flickr_attribute :profile_url, :from => 'person/profileurl'
52
+ flickr_attribute :photos_count, :from => 'person/photos/count'
53
+ flickr_attribute :icon_server, :from => 'person@iconserver'
54
+ flickr_attribute :icon_farm, :from => 'person@iconfarm'
55
+ flickr_attribute :pro, :from => 'person@ispro'
56
+ flickr_attribute :admin, :from => 'person@isadmin'
69
57
 
70
58
  has_many :sets, :groups, :photos, :contacts
71
59
 
72
60
  find_one :by_username, :call => 'people.findByUsername'
73
61
  find_one :by_email, :using => :find_email, :call => 'people.findByEmail'
74
62
 
75
- lazily_load :name, :photos_url, :profile_url, :photos_count, :with => :load_info
63
+ lazily_load :name, :photos_url, :profile_url, :photos_count, :location, :with => :load_info
76
64
  lazily_load :icon_server, :icon_farm, :pro, :admin, :with => :load_info
77
65
 
78
66
  scoped_search
@@ -97,7 +85,7 @@ module Fleakr
97
85
  end
98
86
 
99
87
  def load_info # :nodoc:
100
- response = Fleakr::Api::Request.with_response!('people.getInfo', :user_id => self.id)
88
+ response = Fleakr::Api::MethodRequest.with_response!('people.getInfo', :user_id => self.id)
101
89
  self.populate_from(response.body)
102
90
  end
103
91
 
@@ -0,0 +1,2 @@
1
+ require 'fleakr/support/attribute'
2
+ require 'fleakr/support/object'
@@ -2,25 +2,43 @@ module Fleakr
2
2
  module Support # :nodoc:all
3
3
  class Attribute
4
4
 
5
- attr_reader :name, :xpath, :attribute
5
+ # TODO: Refactor the location / attribute logic into a Source class
6
6
 
7
- def initialize(name, options = {})
7
+ attr_reader :name, :sources
8
+
9
+ def initialize(name, sources = nil)
8
10
  @name = name.to_sym
9
- @attribute = options[:attribute]
11
+
12
+ @sources = Array(sources)
13
+ @sources << @name.to_s if @sources.empty?
14
+ end
15
+
16
+ def split(source)
17
+ location, attribute = source.split('@')
18
+ location = self.name.to_s if location.blank?
19
+
20
+ [location, attribute]
21
+ end
22
+
23
+ def node_for(document, source)
24
+ document.at(location(source)) || document.search("//[@#{attribute(source)}]").first
25
+ end
10
26
 
11
- @xpath = options[:xpath]
12
- @xpath ||= @name.to_s unless @attribute
27
+ def attribute(source)
28
+ location, attribute = source.split('@')
29
+ attribute || location
30
+ end
31
+
32
+ def location(source)
33
+ split(source).first
13
34
  end
14
35
 
15
36
  def value_from(document)
16
- node = document
17
-
18
- begin
19
- node = document.at(self.xpath) if self.xpath
20
- self.attribute.nil? ? node.inner_text : node[self.attribute]
21
- rescue NoMethodError
22
- nil
37
+ values = sources.map do |source|
38
+ node = node_for(document, source)
39
+ (node.attributes[attribute(source)] || node.inner_text) unless node.nil?
23
40
  end
41
+ values.compact.first
24
42
  end
25
43
 
26
44
  end
@@ -9,7 +9,7 @@ module Fleakr
9
9
  end
10
10
 
11
11
  def flickr_attribute(name, options = {})
12
- self.attributes << Attribute.new(name, options)
12
+ self.attributes << Attribute.new(name, options[:from])
13
13
  class_eval "attr_accessor :#{name}"
14
14
  end
15
15
 
@@ -34,7 +34,7 @@ module Fleakr
34
34
 
35
35
  class_eval <<-CODE
36
36
  def self.find_all_#{condition}(value)
37
- response = Fleakr::Api::Request.with_response!('#{options[:call]}', :#{attribute} => value)
37
+ response = Fleakr::Api::MethodRequest.with_response!('#{options[:call]}', :#{attribute} => value)
38
38
  (response.body/'rsp/#{options[:path]}').map {|e| #{target_class}.new(e) }
39
39
  end
40
40
  CODE
@@ -44,8 +44,10 @@ module Fleakr
44
44
  attribute = options[:using].nil? ? condition.to_s.sub(/^by_/, '') : options[:using]
45
45
 
46
46
  class_eval <<-CODE
47
- def self.find_#{condition}(value)
48
- response = Fleakr::Api::Request.with_response!('#{options[:call]}', :#{attribute} => value)
47
+ def self.find_#{condition}(value, options = {})
48
+ options.merge!(:#{attribute} => value)
49
+
50
+ response = Fleakr::Api::MethodRequest.with_response!('#{options[:call]}', options)
49
51
  #{self.name}.new(response.body)
50
52
  end
51
53
  CODE
@@ -60,6 +62,20 @@ module Fleakr
60
62
  end
61
63
  CODE
62
64
  end
65
+
66
+ def lazily_load(*attributes)
67
+ options = attributes.extract_options!
68
+
69
+ attributes.each do |attribute|
70
+ class_eval <<-CODE
71
+ def #{attribute}_with_loading
72
+ self.send(:#{options[:with]}) if @#{attribute}.nil?
73
+ #{attribute}_without_loading
74
+ end
75
+ alias_method_chain :#{attribute}, :loading
76
+ CODE
77
+ end
78
+ end
63
79
 
64
80
  end
65
81
 
@@ -2,7 +2,7 @@ module Fleakr
2
2
  module Version # :nodoc:
3
3
 
4
4
  MAJOR = 0
5
- MINOR = 3
5
+ MINOR = 4
6
6
  TINY = 0
7
7
 
8
8
  def self.to_s
@@ -0,0 +1,8 @@
1
+ <?xml version="1.0" encoding="utf-8"?>
2
+ <rsp stat="ok">
3
+ <auth>
4
+ <token>abc-123</token>
5
+ <perms>delete</perms>
6
+ <user nsid="31066442@N69" fullname="Sir Froot Pants" username="frootpantz"></user>
7
+ </auth>
8
+ </rsp>
@@ -0,0 +1,8 @@
1
+ <?xml version="1.0" encoding="utf-8"?>
2
+ <rsp stat="ok">
3
+ <auth>
4
+ <token>abc-123</token>
5
+ <perms>delete</perms>
6
+ <user nsid="31066442@N69" fullname="Sir Froot Pants" username="frootpantz"></user>
7
+ </auth>
8
+ </rsp>
@@ -4,7 +4,7 @@
4
4
  <username>frootpantz</username>
5
5
  <realname>Sir Froot Pantz</realname>
6
6
  <mbox_sha1sum>e52ed1e5b91c763694995460e9796fc2adc02019</mbox_sha1sum>
7
- <location />
7
+ <location>The Moon</location>
8
8
  <photosurl>http://www.flickr.com/photos/frootpantz/</photosurl>
9
9
  <profileurl>http://www.flickr.com/people/frootpantz/</profileurl>
10
10
  <mobileurl>http://m.flickr.com/photostream.gne?id=34225</mobileurl>
@@ -0,0 +1,20 @@
1
+ <?xml version="1.0" encoding="utf-8" ?>
2
+ <rsp stat="ok">
3
+ <photo id="1" secret="secret" server="3085" farm="4" dateuploaded="1230274722" isfavorite="0" license="0" rotation="0" originalsecret="sekrit" originalformat="jpg" media="photo">
4
+ <owner nsid="31066442@N69" username="frootpantz" realname="" location="" />
5
+ <title>Tree</title>
6
+ <description>A Tree</description>
7
+ <visibility ispublic="1" isfriend="0" isfamily="0" />
8
+ <dates posted="1230274722" taken="2008-12-25 18:26:55" takengranularity="0" lastupdate="1230276652" />
9
+
10
+ <editability cancomment="0" canaddmeta="0" />
11
+ <usage candownload="1" canblog="0" canprint="0" />
12
+ <comments>0</comments>
13
+ <notes />
14
+ <tags />
15
+ <urls>
16
+ <url type="photopage">http://www.flickr.com/photos/yes/1</url>
17
+ </urls>
18
+
19
+ </photo>
20
+ </rsp>
data/test/test_helper.rb CHANGED
@@ -9,6 +9,19 @@ require File.dirname(__FILE__) + '/../lib/fleakr'
9
9
 
10
10
  class Test::Unit::TestCase
11
11
 
12
+ def self.should_autoload_when_accessing(*attributes)
13
+ options = attributes.extract_options!
14
+ attributes.each do |accessor_name|
15
+ it "should load the additional user information when accessing the :#{accessor_name} attribute" do
16
+ klass = self.class.name.sub(/Test$/, '').constantize
17
+
18
+ object = klass.new
19
+ object.expects(options[:with]).with()
20
+ object.send(accessor_name)
21
+ end
22
+ end
23
+ end
24
+
12
25
  def self.should_have_a_value_for(attribute_test)
13
26
  it "should have a value for :#{attribute_test.keys.first}" do
14
27
  @object.send(attribute_test.keys.first).should == attribute_test.values.first
@@ -64,12 +77,14 @@ class Test::Unit::TestCase
64
77
  klass = "Fleakr::Objects::#{class_name}".constantize
65
78
  object_type = class_name.downcase
66
79
 
80
+ condition_value = '1'
81
+
67
82
  options[:with] = options[:by] if options[:with].nil?
83
+ params = {options[:with] => condition_value}
68
84
 
69
85
  it "should be able to find a #{thing} by #{options[:by]}" do
70
- condition_value = '1'
71
86
  stub = stub()
72
- response = mock_request_cycle :for => options[:call], :with => {options[:with] => condition_value}
87
+ response = mock_request_cycle :for => options[:call], :with => params
73
88
 
74
89
  klass.expects(:new).with(response.body).returns(stub)
75
90
  klass.send("find_by_#{options[:by]}".to_sym, condition_value).should == stub
@@ -108,7 +123,7 @@ class Test::Unit::TestCase
108
123
 
109
124
  def mock_request_cycle(options)
110
125
  response = stub(:body => Hpricot.XML(read_fixture(options[:for])))
111
- Fleakr::Api::Request.expects(:with_response!).with(options[:for], options[:with]).returns(response)
126
+ Fleakr::Api::MethodRequest.expects(:with_response!).with(options[:for], options[:with]).returns(response)
112
127
 
113
128
  response
114
129
  end
@@ -0,0 +1,63 @@
1
+ require File.dirname(__FILE__) + '/../../../test_helper'
2
+
3
+ module Fleakr::Api
4
+ class FileParameterTest < Test::Unit::TestCase
5
+
6
+ describe "An instance of the FileParameter class" do
7
+
8
+ before do
9
+ @temp_dir = File.expand_path(create_temp_directory)
10
+ @filename = "#{@temp_dir}/image.jpg"
11
+ end
12
+
13
+ after do
14
+ FileUtils.rm_rf(@temp_dir)
15
+ end
16
+
17
+ it "should know not to include itself in the parameter signature" do
18
+ parameter = FileParameter.new('photo', @filename)
19
+ parameter.include_in_signature?.should be(false)
20
+ end
21
+
22
+ {'jpg' => 'image/jpeg', 'png' => 'image/png', 'gif' => 'image/gif'}.each do |ext, mime_type|
23
+ it "should know the correct MIME type for an extension of #{ext}" do
24
+ parameter = FileParameter.new('photo', "#{@temp_dir}/image.#{ext}")
25
+ parameter.mime_type.should == mime_type
26
+ end
27
+ end
28
+
29
+ it "should retrieve the contents of the file when accessing the value" do
30
+ File.expects(:read).with(@filename).returns('bopbip')
31
+
32
+ parameter = FileParameter.new('photo', @filename)
33
+ parameter.value.should == 'bopbip'
34
+ end
35
+
36
+ it "should cache the file contents after retrieving them" do
37
+ File.expects(:read).with(@filename).once.returns('bopbip')
38
+
39
+ parameter = FileParameter.new('photo', @filename)
40
+ 2.times { parameter.value }
41
+ end
42
+
43
+ it "should know how to generate a form representation of itself" do
44
+ filename = 'image.jpg'
45
+ mime_type = 'image/jpeg'
46
+
47
+ parameter = FileParameter.new('photo', filename)
48
+ parameter.stubs(:mime_type).with().returns(mime_type)
49
+ parameter.stubs(:value).with().returns('data')
50
+
51
+ expected =
52
+ "Content-Disposition: form-data; name=\"photo\"; filename=\"#{filename}\"\r\n" +
53
+ "Content-Type: image/jpeg\r\n" +
54
+ "\r\n" +
55
+ "data\r\n"
56
+
57
+ parameter.to_form.should == expected
58
+ end
59
+
60
+ end
61
+
62
+ end
63
+ end
@@ -0,0 +1,103 @@
1
+ require File.dirname(__FILE__) + '/../../../test_helper'
2
+
3
+ module Fleakr::Api
4
+ class MethodRequestTest < Test::Unit::TestCase
5
+
6
+ describe "An instance of MethodRequest" do
7
+
8
+ context "with API credentials" do
9
+
10
+ before do
11
+ @api_key = 'f00b4r'
12
+ Fleakr.stubs(:api_key).with().returns(@api_key)
13
+ Fleakr.stubs(:shared_secret).with().returns('sekrit')
14
+ end
15
+
16
+ it "should know the full query parameters" do
17
+ request = MethodRequest.new('flickr.people.findByUsername', :username => 'foobar')
18
+
19
+ request.parameters[:api_key].value.should == @api_key
20
+ request.parameters[:method].value.should == 'flickr.people.findByUsername'
21
+ request.parameters[:username].value.should == 'foobar'
22
+ end
23
+
24
+ it "should translate a shorthand API call" do
25
+ request = MethodRequest.new('people.findByUsername')
26
+ request.parameters[:method].value.should == 'flickr.people.findByUsername'
27
+ end
28
+
29
+ it "should know that it needs to sign the request" do
30
+ ParameterList.expects(:new).with(:sign? => true).returns(stub(:<< => nil))
31
+ request = MethodRequest.new('people.findByUsername', :sign? => true)
32
+ end
33
+
34
+ it "should know that it needs to authenticate the request" do
35
+ ParameterList.expects(:new).with(:authenticate? => true).returns(stub(:<< => nil))
36
+ request = MethodRequest.new('activity.userPhotos', :authenticate? => true)
37
+ end
38
+
39
+ it "should know the endpoint with full parameters" do
40
+ query_parameters = 'foo=bar'
41
+
42
+ request = MethodRequest.new('people.getInfo')
43
+ request.parameters.stubs(:to_query).returns(query_parameters)
44
+
45
+ uri_mock = mock() {|m| m.expects(:query=).with(query_parameters)}
46
+ URI.expects(:parse).with("http://api.flickr.com/services/rest/").returns(uri_mock)
47
+
48
+ request.__send__(:endpoint_uri).should == uri_mock
49
+ end
50
+
51
+ it "should be able to make a request" do
52
+ endpoint_uri = stub()
53
+
54
+ request = MethodRequest.new('people.findByUsername')
55
+ request.stubs(:endpoint_uri).with().returns(endpoint_uri)
56
+
57
+ Net::HTTP.expects(:get).with(endpoint_uri).returns('<xml>')
58
+
59
+ request.send
60
+ end
61
+
62
+ it "should create a response from the request" do
63
+ response_xml = '<xml>'
64
+ response_stub = stub()
65
+
66
+ Net::HTTP.stubs(:get).returns(response_xml)
67
+ Response.expects(:new).with(response_xml).returns(response_stub)
68
+
69
+ request = MethodRequest.new('people.findByUsername')
70
+ request.stubs(:endpoint_uri)
71
+
72
+ request.send.should == response_stub
73
+ end
74
+
75
+ it "should be able to make a full request and response cycle" do
76
+ method = 'flickr.people.findByUsername'
77
+ params = {:username => 'foobar'}
78
+
79
+ response = stub(:error? => false)
80
+
81
+ MethodRequest.expects(:new).with(method, params).returns(stub(:send => response))
82
+
83
+ MethodRequest.with_response!(method, params).should == response
84
+ end
85
+
86
+ it "should raise an exception when the full request / response cycle has errors" do
87
+ method = 'flickr.people.findByUsername'
88
+ params = {:username => 'foobar'}
89
+
90
+ response = stub(:error? => true, :error => stub(:code => '1', :message => 'User not found'))
91
+
92
+ MethodRequest.expects(:new).with(method, params).returns(stub(:send => response))
93
+
94
+ lambda do
95
+ MethodRequest.with_response!('flickr.people.findByUsername', :username => 'foobar')
96
+ end.should raise_error(Fleakr::ApiError)
97
+ end
98
+
99
+ end
100
+ end
101
+
102
+ end
103
+ end