api_bucket 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (47) hide show
  1. data/.document +5 -0
  2. data/.gitignore +19 -0
  3. data/.rspec +1 -0
  4. data/Gemfile +11 -0
  5. data/LICENSE.txt +22 -0
  6. data/README.md +54 -0
  7. data/Rakefile +1 -0
  8. data/VERSION +1 -0
  9. data/api-bucket.gemspec +23 -0
  10. data/lib/api_bucket/amazon/client/http.rb +61 -0
  11. data/lib/api_bucket/amazon/client.rb +73 -0
  12. data/lib/api_bucket/amazon/configration.rb +13 -0
  13. data/lib/api_bucket/amazon/item.rb +63 -0
  14. data/lib/api_bucket/amazon/response.rb +28 -0
  15. data/lib/api_bucket/amazon.rb +12 -0
  16. data/lib/api_bucket/base/client/http.rb +40 -0
  17. data/lib/api_bucket/base/element.rb +54 -0
  18. data/lib/api_bucket/base/item.rb +39 -0
  19. data/lib/api_bucket/base/response.rb +20 -0
  20. data/lib/api_bucket/itunes/client/http.rb +9 -0
  21. data/lib/api_bucket/itunes/client.rb +61 -0
  22. data/lib/api_bucket/itunes/configuration.rb +13 -0
  23. data/lib/api_bucket/itunes/item.rb +71 -0
  24. data/lib/api_bucket/itunes/response.rb +35 -0
  25. data/lib/api_bucket/itunes.rb +13 -0
  26. data/lib/api_bucket/rakuten/client/http.rb +9 -0
  27. data/lib/api_bucket/rakuten/client.rb +80 -0
  28. data/lib/api_bucket/rakuten/configuration.rb +13 -0
  29. data/lib/api_bucket/rakuten/item.rb +52 -0
  30. data/lib/api_bucket/rakuten/response.rb +32 -0
  31. data/lib/api_bucket/rakuten.rb +13 -0
  32. data/lib/api_bucket/version.rb +3 -0
  33. data/lib/api_bucket/yahooauction/client/http.rb +26 -0
  34. data/lib/api_bucket/yahooauction/client.rb +65 -0
  35. data/lib/api_bucket/yahooauction/configuration.rb +13 -0
  36. data/lib/api_bucket/yahooauction/item.rb +29 -0
  37. data/lib/api_bucket/yahooauction/response.rb +39 -0
  38. data/lib/api_bucket/yahooauction.rb +16 -0
  39. data/lib/api_bucket.rb +91 -0
  40. data/spec/api_bucket/amazon_spec.rb +66 -0
  41. data/spec/api_bucket/itunes_spec.rb +65 -0
  42. data/spec/api_bucket/rakuten_spec.rb +67 -0
  43. data/spec/api_bucket/yahooauction_spec.rb +69 -0
  44. data/spec/api_bucket_spec.rb +90 -0
  45. data/spec/secret.rb.skel +14 -0
  46. data/spec/spec_helper.rb +12 -0
  47. metadata +146 -0
data/.document ADDED
@@ -0,0 +1,5 @@
1
+ lib/**/*.rb
2
+ bin/*
3
+ -
4
+ features/**/*.feature
5
+ LICENSE.txt
data/.gitignore ADDED
@@ -0,0 +1,19 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
18
+
19
+ spec/secret.rb
data/.rspec ADDED
@@ -0,0 +1 @@
1
+ --color
data/Gemfile ADDED
@@ -0,0 +1,11 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in api_bucket.gemspec
4
+ gemspec
5
+
6
+ gem 'nokogiri'
7
+ gem 'ruby-hmac'
8
+
9
+ group :test do
10
+ gem 'rspec', '>= 2.8.0'
11
+ end
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2012 nakajijapan
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,54 @@
1
+ # ApiBucket
2
+
3
+ We can use sevral APIs with common interface.
4
+
5
+ * APIs
6
+ * Amazon ECS
7
+ * iTunes
8
+ * Rakuten(楽天)
9
+ * Yahoo Auction
10
+
11
+ ## Installation
12
+
13
+ Add this line to your application's Gemfile:
14
+
15
+ gem 'api_bucket'
16
+
17
+ And then execute:
18
+
19
+ $ bundle
20
+
21
+ Or install it yourself as:
22
+
23
+ $ gem install api_bucket
24
+
25
+ ## Usage
26
+
27
+ ### use Amazon
28
+
29
+ ```ruby
30
+ require 'api_bucket'
31
+ require 'api_bucket/amazon'
32
+
33
+ ApiBucket::Amazon::Client.configure do |o|
34
+ o.a_w_s_access_key_id = 'hogehoge'
35
+ o.a_w_s_secret_key = 'mogemoge'
36
+ o.associate_tag = 'nakajijapan'
37
+ end
38
+
39
+ service = ApiBucket::Service::instance(:amazon)
40
+ res = service.item_search('ruby')
41
+
42
+ res.items.each do |item|
43
+ p item.product_code
44
+ p item.title
45
+ end
46
+ ```
47
+
48
+ ## Contributing
49
+
50
+ 1. Fork it
51
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
52
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
53
+ 4. Push to the branch (`git push origin my-new-feature`)
54
+ 5. Create new Pull Request
data/Rakefile ADDED
@@ -0,0 +1 @@
1
+ require "bundler/gem_tasks"
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.1.0
@@ -0,0 +1,23 @@
1
+ # -*- encoding: utf-8 -*-
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'api_bucket/version'
5
+
6
+ Gem::Specification.new do |gem|
7
+ gem.name = "api_bucket"
8
+ gem.version = ApiBucket::VERSION
9
+ gem.authors = ["nakajijapan"]
10
+ gem.email = ["pp.kupepo.gattyanmo@gmail.com"]
11
+ gem.description = %q{We can use sevral APIs with common interface.}
12
+ gem.summary = %q{We can use sevral APIs with common interface}
13
+ gem.homepage = "https://github.com/nakajijapan/api_bucket"
14
+
15
+ gem.files = `git ls-files`.split($/)
16
+ gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
17
+ gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
18
+ gem.require_paths = ["lib"]
19
+
20
+ gem.add_dependency "nokogiri"
21
+ gem.add_dependency "ruby-hmac"
22
+ gem.add_development_dependency('rspec', ['~> 2.8.0'])
23
+ end
@@ -0,0 +1,61 @@
1
+ module ApiBucket::Amazon
2
+ class Client
3
+ module Http
4
+ def send_request(options)
5
+ request_url = "#{REQUEST_URL}?#{self.prepare_query(options)}"
6
+ uri = URI::parse(request_url)
7
+ res = Net::HTTP.get_response(uri)
8
+ res.body
9
+ end
10
+
11
+ def sign_request(url, key)
12
+ openssl_digest = OpenSSL::Digest::Digest.new( 'sha256' )
13
+ signature = OpenSSL::HMAC.digest(openssl_digest, key, url)
14
+ signature = [signature].pack('m').chomp
15
+ signature = URI.escape(signature, Regexp.new("[+=]"))
16
+ return signature
17
+ end
18
+
19
+ def url_encode(string)
20
+ string.gsub( /([^a-zA-Z0-9_.~-]+)/ ) do
21
+ '%' + $1.unpack( 'H2' * $1.bytesize ).join( '%' ).upcase
22
+ end
23
+ end
24
+
25
+ def prepare_query(options)
26
+ query = ''
27
+
28
+ secret_key = options.delete(:a_w_s_secret_key)
29
+ request_host = URI.parse(REQUEST_URL).host
30
+
31
+ options = options.sort do |c,d|
32
+ c[0].to_s <=> d[0].to_s
33
+ end
34
+
35
+ options = options.collect do |a,b|
36
+ [camelize(a.to_s), b.to_s]
37
+ end
38
+
39
+ options.each do |key, value|
40
+ next if value.nil?
41
+ query << "&#{key}=#{self.url_encode(value)}"
42
+ end
43
+
44
+ query.slice!(0)
45
+
46
+ # only amazon
47
+ signature = ''
48
+ unless secret_key.nil?
49
+ request_to_sign="GET\n#{request_host}\n/onca/xml\n#{query}"
50
+ signature = "&Signature=#{sign_request(request_to_sign, secret_key)}"
51
+ end
52
+
53
+ "#{query}#{signature}"
54
+ end
55
+
56
+ def camelize(s)
57
+ s.to_s.gsub(/\/(.?)/) { "::" + $1.upcase }.gsub(/(^|_)(.)/) { $2.upcase }
58
+ end
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,73 @@
1
+ require "api_bucket/amazon/client/http"
2
+
3
+ module ApiBucket::Amazon
4
+ class Client
5
+ attr_accessor :version, :service, :a_w_s_access_key_id, :a_w_s_secret_key, :associate_tag
6
+ attr_accessor :country, :count, :item_page
7
+
8
+ REQUEST_URL = 'http://ecs.amazonaws.jp/onca/xml'
9
+
10
+ def categories
11
+ {
12
+ ALL: '----',
13
+ DVD: 'DVD / BD',
14
+ ForeignBooks: 'ForeignBooks',
15
+ VideoGames: 'VideoGames',
16
+ Music: 'Music',
17
+ Software: 'Software',
18
+ Electronics: 'Electronics',
19
+ Watches: 'Watches',
20
+ KindleStore: 'KindleStore',
21
+ }
22
+ end
23
+
24
+ def initialize(options={})
25
+ options = ApiBucket::Amazon.options.merge(options)
26
+ self.version = '2011-08-01'
27
+ self.service = 'AWSECommerceService'
28
+ self.a_w_s_access_key_id = options[:a_w_s_access_key_id]
29
+ self.a_w_s_secret_key = options[:a_w_s_secret_key]
30
+ self.associate_tag = options[:associate_tag]
31
+ self.country = 'ja'
32
+ self.count = 20
33
+ self.item_page = 1
34
+ end
35
+
36
+ def search(keywords, params={})
37
+ options = {
38
+ a_w_s_access_key_id: self.a_w_s_access_key_id,
39
+ a_w_s_secret_key: self.a_w_s_secret_key,
40
+ associate_tag: self.associate_tag,
41
+
42
+ operation: 'ItemSearch',
43
+ response_group: 'Large,EditorialReview,ItemAttributes',
44
+ item_page: @page,
45
+ count: @limit,
46
+ search_index: 'All',
47
+ keywords: keywords,
48
+ timestamp: Time.now.utc.strftime("%Y-%m-%dT%H:%M:%SZ"),
49
+ }
50
+ options.merge!(params)
51
+
52
+ ApiBucket::Amazon::Response.new(send_request(options))
53
+ end
54
+
55
+ def lookup(id, params={})
56
+ options = {
57
+ a_w_s_access_key_id: self.a_w_s_access_key_id,
58
+ a_w_s_secret_key: self.a_w_s_secret_key,
59
+ associate_tag: self.associate_tag,
60
+
61
+ operation: 'ItemLookup',
62
+ response_group: 'Large,EditorialReview,ItemAttributes',
63
+ item_id: id,
64
+ timestamp: Time.now.utc.strftime("%Y-%m-%dT%H:%M:%SZ"),
65
+ }
66
+ options.merge!(params)
67
+
68
+ ApiBucket::Amazon::Response.new(send_request(options))
69
+ end
70
+
71
+ include ApiBucket::Amazon::Client::Http
72
+ end
73
+ end
@@ -0,0 +1,13 @@
1
+ module ApiBucket::Amazon
2
+ module Configuration
3
+ attr_accessor :a_w_s_access_key_id, :a_w_s_secret_key, :associate_tag
4
+
5
+ def configure
6
+ yield self
7
+ end
8
+
9
+ def options
10
+ [:a_w_s_access_key_id, :a_w_s_secret_key, :associate_tag].inject({}){|o,k| o.merge!(k => send(k))}
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,63 @@
1
+ # coding: utf-8
2
+ require "api_bucket/base/item"
3
+ require "api_bucket/base/element"
4
+
5
+ module ApiBucket::Amazon
6
+ class Item < ApiBucket::Base::Item
7
+ def initialize(element)
8
+ @element = ApiBucket::Base::Element.new(element)
9
+
10
+ @product_code = @element.get('ASIN')
11
+ @detail_url = @element.get('DetailPageURL')
12
+
13
+ # get item attributes element
14
+ item_attributes = @element.get_element('ItemAttributes')
15
+
16
+ # 最安値を優先的に格納する
17
+ offers = @element.get_element('Offers/Offer/OfferListing')
18
+ if @element.get('Offers/LowestNewPrice')
19
+ @price = @element.get('Offers/LowestNewPrice')
20
+ # ?????
21
+ #elsif offers.hash('Price')
22
+ # @price = offers.hash('Price')['Amount']
23
+ else
24
+ @price= item_attributes.get('ListPrice/Amount')
25
+ end
26
+
27
+ @release_date = item_attributes.get('ReleaseDate')
28
+ @title = item_attributes.get('Title')
29
+
30
+ # image
31
+ @image = {}
32
+ keys = {
33
+ l: 'LargeImage',
34
+ m: 'MediumImage',
35
+ s: 'SmallImage'
36
+ }
37
+ keys.each do |key, attr|
38
+ image = @element.get_element(attr)
39
+ if image
40
+
41
+ @image[key] = {
42
+ url: image.get('URL'),
43
+ width: image.get('Width'),
44
+ height: image.get('Height')
45
+ }
46
+ else
47
+ @image[key] = {
48
+ url: nil,
49
+ width: 0,
50
+ height: 0
51
+ }
52
+ end
53
+ end
54
+
55
+ if @element.hash('Offers/Offer/OfferListing')
56
+ @availablity = @element.hash('Offers/Offer/OfferListing')['Availability']
57
+ end
58
+
59
+ editor_review = @element.get_element('EditorialReviews/EditorialReview')
60
+ @description = editor_review.get('Content') if editor_review
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,28 @@
1
+ require "api_bucket/base/response"
2
+
3
+ module ApiBucket::Amazon
4
+ class Response < ApiBucket::Base::Response
5
+ def items
6
+ @items ||= (@doc/"Item").collect { |item| ApiBucket::Amazon::Item.new(item) }
7
+ end
8
+
9
+ def first_item
10
+ @items[0]
11
+ end
12
+
13
+ # Return current page no if :item_page option is when initiating the request.
14
+ def item_page
15
+ @item_page ||= ApiBucket::Base::Element.get(@doc, "//ItemPage").to_i
16
+ end
17
+
18
+ # Return total results.
19
+ def total_results
20
+ @total_results ||= ApiBucket::Base::Element.get(@doc, "//TotalResults").to_i
21
+ end
22
+
23
+ # Return total pages.
24
+ def total_pages
25
+ @total_pages ||= ApiBucket::Base::Element.get(@doc, "//TotalPages").to_i
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,12 @@
1
+ # -*- coding: utf-8 -*-
2
+ require 'api_bucket'
3
+
4
+ require "api_bucket/amazon/configration"
5
+ require "api_bucket/amazon/item"
6
+ require "api_bucket/amazon/response"
7
+ require "api_bucket/amazon/client"
8
+
9
+ # ApiBucket::Amazon
10
+ module ApiBucket::Amazon
11
+ extend Configuration
12
+ end
@@ -0,0 +1,40 @@
1
+ module ApiBucket::Base
2
+ module Client
3
+ module Http
4
+ def send_request(options, url)
5
+ request_url = "#{url}?#{prepare_query(options)}"
6
+
7
+ res = nil
8
+ begin
9
+ res = open(request_url, {}) do |f|
10
+ @raw_response = f.read
11
+ JSON.parse(@raw_response)
12
+ end
13
+ rescue => e
14
+ #p "#{e.message} : #{request_url}"
15
+ end
16
+
17
+ res
18
+ end
19
+
20
+ def prepare_query(options)
21
+ query = ''
22
+
23
+ # sort to asc
24
+ options = options.sort do |c,d|
25
+ c[0].to_s <=> d[0].to_s
26
+ end
27
+
28
+ # generate query_string from options
29
+ options.each do |key, value|
30
+ next if value.nil?
31
+ query << "&#{key}=#{URI.encode(value.to_s)}"
32
+ end
33
+
34
+ # sub string first charactor
35
+ query.slice!(0)
36
+ query
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,54 @@
1
+ module ApiBucket::Base
2
+ class Element
3
+ def initialize(e)
4
+ @element = e
5
+ end
6
+
7
+ def get(path)
8
+ return nil unless @element
9
+ result = @element.at_xpath(path)
10
+ result = result.inner_html if result
11
+ result
12
+ end
13
+
14
+ def /(elements, path)
15
+ items = self./(elements, path)
16
+ return nil if items.size = 0
17
+ items
18
+ end
19
+
20
+ # Returns a Nokogiri::XML::NodeSet of elements matching the given path.
21
+ # Example: element/"author".
22
+ def /(path)
23
+ elements = @element/path
24
+ return nil if elements.size == 0
25
+ elements
26
+ end
27
+
28
+ # Return an array of Amazon::Element matching the given path
29
+ def get_elements(path)
30
+ elements = self./(path)
31
+ return nil unless elements
32
+ elements = elements.map{|element| Element.new(element)}
33
+ end
34
+
35
+ def get_element(path)
36
+ elements = self.get_elements(path)
37
+ elements[0] if elements
38
+ end
39
+
40
+ def hash(path='.')
41
+ return unless @element
42
+
43
+ result = @element.at_xpath(path)
44
+ if result
45
+ hash = {}
46
+ result = result.children
47
+ result.each do |item|
48
+ hash[item.name] = item.inner_html
49
+ end
50
+ hash
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,39 @@
1
+ module ApiBucket::Base
2
+ class Item
3
+ attr_accessor :product_code #:ASIN
4
+ attr_accessor :detail_url #:DetailPageURL
5
+ attr_accessor :preview_url #:PreviewURL
6
+ attr_accessor :price #:Amount
7
+ attr_accessor :title #:Title
8
+ attr_accessor :image
9
+ attr_accessor :image_l #:ImageL
10
+ attr_accessor :image_m #:ImageM
11
+ attr_accessor :image_s #:ImageS
12
+ attr_accessor :description #:Content
13
+ attr_accessor :release_date #:ReleaseDate
14
+
15
+ attr_accessor :availablity #:Availablity for amazon
16
+
17
+ def adult?
18
+ false
19
+ end
20
+
21
+ def hash_all
22
+ @image ||= {}
23
+ %w(l m s).collect {|size| @image[size] ||= {}}
24
+ {
25
+ product_code: @product_code,
26
+ detail_url: @detail_url,
27
+ preview_url: @preview_url,
28
+ price: @price,
29
+ title: @title,
30
+ image_l: @image[:l],
31
+ image_m: @image[:m],
32
+ image_s: @image[:s],
33
+ description: @description,
34
+ release_date: @release_date,
35
+ availablity: @availablity
36
+ }
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,20 @@
1
+ require "api_bucket/base/response"
2
+
3
+ module ApiBucket::Base
4
+ class Response
5
+ attr_accessor :page, :count
6
+
7
+ def initialize(xml)
8
+ @doc = Nokogiri::XML(xml, nil, 'UTF-8')
9
+ @doc.remove_namespaces!
10
+ end
11
+
12
+ def doc
13
+ @doc
14
+ end
15
+
16
+ def dump
17
+ @doc.to_s
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,9 @@
1
+ require "api_bucket/base/client/http"
2
+
3
+ module ApiBucket::Itunes
4
+ class Client
5
+ module Http
6
+ include ApiBucket::Base::Client::Http
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,61 @@
1
+ require "api_bucket/itunes/client/http"
2
+
3
+ module ApiBucket::Itunes
4
+ class Client
5
+ include ApiBucket::Itunes::Client::Http
6
+
7
+ attr_accessor :country, :limit
8
+
9
+ REQUEST_URL = 'http://itunes.apple.com/search'
10
+ REQUEST_URL_ITEM = 'http://itunes.apple.com/lookup'
11
+
12
+ def categories
13
+ {
14
+ all: 'All',
15
+ movie: 'Movie',
16
+ podcast: 'Podcast',
17
+ music: 'Music',
18
+ musicVideo: 'MusicVideo',
19
+ audiobook: 'Audiobook',
20
+ shortFilm: 'ShortFilm',
21
+ tvShow: 'TvShow',
22
+ software: 'Software',
23
+ ebook: 'Ebook',
24
+ }
25
+ end
26
+
27
+ def initialize(options={})
28
+ options = ApiBucket::Itunes.options.merge(options)
29
+ self.country = options[:country] || 'JP'
30
+ self.limit = options[:limit] || 20
31
+ end
32
+
33
+ def search(keywords, params={})
34
+ options = {
35
+ limit: self.limit,
36
+ country: self.country,
37
+ media: params[:search_index],
38
+ term: keywords,
39
+ }
40
+ options.merge!(params)
41
+
42
+ # delete no needed keys
43
+ options.delete(:keywords)
44
+ options.delete(:search_index)
45
+ options[:media] = 'all' if options[:media].nil?
46
+
47
+ ApiBucket::Itunes::Response.new(send_request(options, REQUEST_URL))
48
+ end
49
+
50
+ # lookup
51
+ def lookup(id, params={})
52
+ options = {
53
+ country: self.country,
54
+ id: id,
55
+ }
56
+ options.merge!(params)
57
+
58
+ ApiBucket::Itunes::Response.new(send_request(options, REQUEST_URL_ITEM))
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,13 @@
1
+ module ApiBucket::Itunes
2
+ module Configuration
3
+ attr_accessor :country, :limit
4
+
5
+ def configure
6
+ yield self
7
+ end
8
+
9
+ def options
10
+ [:country, :limit].inject({}){|o,k| o.merge!(k => send(k))}
11
+ end
12
+ end
13
+ end