app_store 0.0.4 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/README.rdoc CHANGED
@@ -21,9 +21,9 @@ It provides way to search for applications, navigate through categories.
21
21
  @fontpicker.company.title # => "Etienne Segonzac"
22
22
 
23
23
  * Medias
24
- @fontpicker.screenshots # => [#<AppStore::Image:0x1017683e0 @width=320, @ra ...
25
- @fontpicker.screenshots.first.url # => "http://a1.phobos.apple.com/us/r1000/017/Purple/c4/99/6d/mzl.jtoxfers.480x480-75.jpg"
26
- @fontpicker.icon.url # => "http://a1.phobos.apple.com/us/r1000/026/Purple/39/40/54/mzl.yrrhymuu.100x100-75.jpg"
24
+ @fontpicker.screenshots # => [#<AppStore::Image:0x1017683e0 @width=320, @ra ...
25
+ @fontpicker.screenshots.first.url # => "http://a1.phobos.apple.com/us/r1000/017/Purple/c4/99/6d/mzl.jtoxfers.480x480-75.jpg"
26
+ @fontpicker.icon.url # => "http://a1.phobos.apple.com/us/r1000/026/Purple/39/40/54/mzl.yrrhymuu.100x100-75.jpg"
27
27
 
28
28
  * User reviews
29
29
  @remote = AppStore::Application.find_by_id(284417350)
@@ -46,15 +46,46 @@ It provides way to search for applications, navigate through categories.
46
46
  @category = @categories.first
47
47
  @category.title # => "Games"
48
48
  @category.item_count # => 1573
49
-
50
- # Go through subcategories and applications
51
- @subcategory = @category.items.last
52
- @subcategory.class # => AppStore::Category
53
- @subcategory.title # => "Word"
54
- @subcategory.items.length # => 26
55
- @subcategory.items.first.class # => AppStore::Application
56
- @subcategory.items.last.class # => AppStore::Category
57
- @subcategory.items.last.title # => "Twenty Five More..."
49
+
50
+ # Category by id
51
+ @category = AppStore::Category.find_by_id(6014)
52
+ @category.title # => "Games"
53
+
54
+ # Iterate through subcategories and applications
55
+ # This example will display all categories with all applications available in the current AppStore
56
+ def go_deeper(category)
57
+ puts "Category #{category.title}"
58
+ category.items.each do |item|
59
+ if item.is_a?(AppStore::Category)
60
+ go_deeper item
61
+ else
62
+ puts " => #{item.title} has id #{item.item_id}"
63
+ end
64
+ end
65
+ end
66
+
67
+ AppStore::Category.featured.each {|category| go_deeper category}
68
+
69
+ === List
70
+ Every list elements returned by the Apple AppStore are limited to 25 elements.
71
+ An additional element is provided at the end of the list to fetch the next elements.
72
+ AppStore::List provides an abstraction to this list and allows to iterate through each element like a traditional enumerable object.
73
+
74
+ list = AppStore::Category.featured.first
75
+ list = Category.featured.first.items
76
+ list.count # => 23453
77
+ list.elements # => [AppStore::Category, AppStore::Category, ...]
78
+ list.collect {|item| item.item_id} # => [9843509, 9028423, 8975435, 987345, ...]
79
+
80
+ === Agent
81
+ There are several Apple AppStore defined by their country. You will have different results (applications, comments) from different store front. You can use AppStore::Agent to ensure you make all yours calls to a specific AppStore (store front).
82
+ You have a list of available store fronts in the constant StoreFronts defined in AppStore::Client.
83
+
84
+ agent = AppStore::Agent.new(:store_front => :fr)
85
+ agent.category.featured # => will call Category.featured with store front set to :fr
86
+ agent.application.find_by_id(42) # => will call Application.find_by_id(42) with store front set to :fr
87
+ agent.application.find_by_id(42, :bleh => 'yeah') # => also accepts extra arguments and merge them with store front
88
+
58
89
 
59
90
  == REQUIREMENTS
60
91
 
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.0.4
1
+ 0.1.0
@@ -0,0 +1,25 @@
1
+ require 'app_store/client'
2
+ require 'app_store/application'
3
+ require 'app_store/helpers/string'
4
+ require 'app_store/helpers/proxy'
5
+
6
+ # Define an agent which is linked to a specific store front and can perform all calls to this specific store.
7
+ # Accepts every AppStore class as a method, ex :
8
+ # = Examples
9
+ # agent = AppStore::Agent.new(:store_front => :fr)
10
+ # agent.category.featured # => will call Category.featured with store front set to :fr
11
+ # agent.application.find_by_id(42) # => will call Application.find_by_id(42) with store front set to :fr
12
+ # agent.application.find_by_id(42, :bleh => 'yeah') # => also accepts extra arguments and merge them with store front
13
+ class AppStore::Agent
14
+ def initialize(args = {})
15
+ @store_front = AppStore::Client::StoreFronts[args[:store_front]] || AppStore::Client::DefaultStoreFront
16
+ @client = AppStore::Client.new(:store_front => @store_front)
17
+ end
18
+
19
+ private
20
+ def method_missing(method, *args)
21
+ # OPTIMIZE: we should use a 'camelize' method instead of capitalize
22
+ AppStore::Helper::Proxy.new :to => "AppStore::#{method.to_s.capitalize}".constantize,
23
+ :extra => {:client => @client}
24
+ end
25
+ end
@@ -66,17 +66,19 @@ class AppStore::Application < AppStore::Base
66
66
  }
67
67
 
68
68
  # Search an Application by its <tt>id</tt>. Accepts only one <tt>id</tt> and returns an Application instance.
69
- def self.find_by_id(id)
70
- plist = AppStore::Caller.get(AppStore::Caller::ApplicationURL, :id => id)
69
+ def self.find_by_id(id, options = {})
70
+ client = options[:client] || AppStore::Client.new
71
+ plist = client.get(AppStore::Client::ApplicationURL, :id => id)
71
72
  # TODO : Check if everything was right before instancianting
72
- new :plist => plist['item-metadata']
73
+ new :client => client, :plist => plist['item-metadata']
73
74
  end
74
75
 
75
76
  # Search an Application by a <tt>text</tt>.
76
77
  # Returns an array with matching application or an empty array if no result found.
77
- def self.search(text)
78
- plist = AppStore::Caller.get(AppStore::Caller::SearchURL, :media => 'software', :term => text)
79
- AppStore::List.new :list => plist['items']
78
+ def self.search(text, options = {})
79
+ client = options[:client] || AppStore::Client.new
80
+ plist = client.get(AppStore::Client::SearchURL, :media => 'software', :term => text)
81
+ AppStore::List.new :client => client, :list => plist['items']
80
82
  end
81
83
 
82
84
  def initialize(attrs = {})
@@ -87,7 +89,7 @@ class AppStore::Application < AppStore::Base
87
89
  # Returns an AppStore::List of UserReview objects.
88
90
  def user_reviews
89
91
  if @user_reviews.nil?
90
- plist = AppStore::Caller.get(@raw['view-user-reviews-url'])
92
+ plist = @client.get(@raw['view-user-reviews-url'])
91
93
  @user_reviews = AppStore::List.new(:list => plist['items'])
92
94
  end
93
95
  @user_reviews
@@ -99,7 +101,7 @@ class AppStore::Application < AppStore::Base
99
101
 
100
102
  def icon
101
103
  if @icon.nil?
102
- parsed = AppStore::Caller.itunes_get(AppStore::Caller::ApplicationURL, :id => item_id)
104
+ parsed = @client.itunes_get(AppStore::Client::ApplicationURL, :id => item_id)
103
105
  @icon = AppStore::Image.new(:plist => parsed.search('PictureView[@height="100"][@width="100"]').first)
104
106
  end
105
107
 
@@ -1,4 +1,4 @@
1
- require "app_store/caller"
1
+ require "app_store/client"
2
2
  require "app_store/helpers/plist"
3
3
 
4
4
  # Implements basic operations for AppStore objects : initialization, plist parsing, ...
@@ -11,7 +11,8 @@ class AppStore::Base
11
11
  # <tt>attrs</tt> accepts:
12
12
  # * <tt>:plist</tt>: a plist object to be parsed
13
13
  def initialize(attrs = {})
14
- init_from_plist attrs[:plist] if attrs[:plist]
14
+ init_from_plist attrs.delete(:plist) if attrs[:plist]
15
+ attrs.each { |key, value| instance_variable_set "@#{key}", value}
15
16
  end
16
17
 
17
18
  protected
@@ -1,11 +1,40 @@
1
1
  require "app_store/base"
2
2
  require "app_store/application"
3
- require 'pp'
4
3
 
5
4
  # A category like categories on the AppStore.
6
5
  # Available attributes:
7
6
  # * <tt>item-count</tt>: total items count for this category.
8
7
  # * <tt>title</tt>: title for the category.
8
+ # = Examples
9
+ # === Fetch featured categories
10
+ # @categories = AppStore::Category.featured
11
+ # @categories.class # => Array
12
+ # @categories.length # => 20
13
+ #
14
+ # === Use category
15
+ # @category = @categories.first
16
+ # @category.title # => "Games"
17
+ # @category.item_count # => 1573
18
+ #
19
+ # === Category by id
20
+ # @category = AppStore::Category.find_by_id(6014)
21
+ # @category.title # => "Games"
22
+ #
23
+ # === Iterate through subcategories and applications
24
+ # This example will display all categories with all applications available in the current AppStore
25
+ # def go_deeper(category)
26
+ # puts "Category #{category.title}"
27
+ # category.items.each do |item|
28
+ # if item.is_a?(AppStore::Category)
29
+ # go_deeper item
30
+ # else
31
+ # puts " => #{item.title} has id #{item.item_id}"
32
+ # end
33
+ # end
34
+ # end
35
+ #
36
+ # AppStore::Category.featured.each {|category| go_deeper category}
37
+ #
9
38
  class AppStore::Category < AppStore::Base
10
39
  plist :mapping => {
11
40
  'item-count' => :item_count,
@@ -14,33 +43,37 @@ class AppStore::Category < AppStore::Base
14
43
 
15
44
  # Returns an array of featured categories (main categories).
16
45
  # It is the same list as the one displayed in the iPhone AppStore.
17
- def self.featured
18
- plist = AppStore::Caller.get(AppStore::Caller::FeaturedCategoriesURL)
19
- plist['items'].collect { |item| new :plist => item }
46
+ def self.featured(options = {})
47
+ client = options[:client] || AppStore::Client.new
48
+ plist = client.get(AppStore::Client::FeaturedCategoriesURL)
49
+ plist['items'].collect { |item| new :client => client, :plist => item }
20
50
  end
21
51
 
22
- def self.find_by_id(id)
23
- # TODO: pass item_id to new
24
- category = new(:plist => AppStore::Caller.get(AppStore::Caller::CategoryURL, :id => id))
52
+ # Search a Category by its <tt>id</tt>. Accepts only one <tt>id</tt> and returns a Category instance.
53
+ def self.find_by_id(id, options = {})
54
+ client = options[:client] || AppStore::Client.new
55
+ new :item_id => id,
56
+ :client => client,
57
+ :plist => client.get(AppStore::Client::CategoryURL, :id => id)
25
58
  end
26
59
 
27
60
  # Returns id for this category
28
61
  def item_id
29
- @item_id ||= @raw['url'].match("id=([0-9]+)")[1]
62
+ @item_id ||= @raw['url'].match("mobile-software-applications/id([0-9]+)")[1]
30
63
  end
31
64
 
32
- # Returns an array of items contained in the category, with a maximum of 25 items (Apple limitation).
33
- # If there are more than 25 items in the category, an extra item is added at the end of the list
34
- # as a link to the next 25 entries.
35
- # Each element can be either a Category or an Application.
65
+ # Returns an instance of List which contains items elements.
66
+ # Each element in the list can be either another category (subcategory) or a Link to an application.
36
67
  def items
37
68
  if @items.nil?
38
- plist = @raw['items'] ? @raw : AppStore::Caller.get(@raw['url'])
69
+ plist = @raw['items'] ? @raw : @client.get(@raw['url'])
39
70
  @items = AppStore::List.new(
71
+ :client => @client,
40
72
  :list => plist['items'],
41
73
  :element_type => 'link',
42
74
  :element_initializer => lambda {|element|
43
- (element['link-type'] == 'software' ? AppStore::Link : AppStore::Category).new(:plist => element)
75
+ (element['link-type'] == 'software' ? AppStore::Link : AppStore::Category).new(:client => @client,
76
+ :plist => element)
44
77
  }
45
78
  )
46
79
  end
@@ -5,35 +5,42 @@ require "plist"
5
5
 
6
6
  require "app_store"
7
7
 
8
- # Caller regroups all the calling and xml parsing mechanism to call the AppStore.
9
- module AppStore::Caller
10
- extend self
11
-
8
+ # Client regroups all the calling and xml parsing mechanism to call the AppStore.
9
+ class AppStore::Client
10
+
11
+ # Urls
12
12
  FeaturedCategoriesURL = "http://ax.itunes.apple.com/WebObjects/MZStore.woa/wa/viewFeaturedSoftwareCategories"
13
13
  ApplicationURL = "http://ax.itunes.apple.com/WebObjects/MZStore.woa/wa/viewSoftware"
14
14
  CategoryURL = "http://ax.itunes.apple.com/WebObjects/MZStore.woa/wa/viewGenre"
15
15
  SearchURL = "http://ax.search.itunes.apple.com/WebObjects/MZSearch.woa/wa/search"
16
-
17
- # Call the Apple AppStore using given <tt>url</tt> and passing <tt>params</tt> with an HTTP Get method.
18
- def iphone_get(url, params = {})
16
+
17
+ # Store Front
18
+ StoreFronts = {
19
+ :us => "143441",
20
+ :fr => "143442"
21
+ }
22
+ DefaultStoreFront = StoreFronts[:us]
23
+
24
+ def initialize(args = {})
19
25
  # About the X-Apple-Store-Front header: this is used to select which store and which language.
20
26
  # Format is XXXXXX-Y,Z where XXXXXX is the store number (us, french, ...), Y the language and Z the return format.
21
27
  # If you omit the language, the default one for the store is used.
22
28
  # Return format can be either "1" or "2" : "1" returns data to be directly displayed and "2" is a more structured format.
23
- # apple app store codes:
24
- # * 143441 : US
25
- # * 143442 : FR
26
- answer = iphone_agent.get(:url => url, :headers => {"X-Apple-Store-Front" => '143441,2'}, :params => params)
27
- raise AppStore::RequestError if answer.code.to_s != '200'
28
- Plist::parse_xml answer.body
29
+ @store_front = "#{args[:store_front] || DefaultStoreFront},2"
30
+ end
31
+
32
+ # Call the Apple AppStore using given <tt>url</tt> and passing <tt>params</tt> with an HTTP Get method.
33
+ # Call is seen as a fake iPhone iTunes client.
34
+ def iphone_get(url, params = {})
35
+ Plist::parse_xml make_call_and_handle_answer(iphone_agent, url, params).body
29
36
  end
30
37
 
31
38
  alias :get :iphone_get
32
39
 
40
+ # Call the Apple AppStore using given <tt>url</tt> and passing <tt>params</tt> with HTTP Get method.
41
+ # Call is seen as a fake Mac OS X iTunes client.
33
42
  def itunes_get(url, params = {})
34
- answer = itunes_agent.get(url, params)
35
- raise AppStore::RequestError if answer.code.to_s != '200'
36
- Nokogiri.parse answer.body
43
+ Nokogiri.parse make_call_and_handle_answer(itunes_agent, url, params).body
37
44
  end
38
45
 
39
46
  protected
@@ -44,5 +51,19 @@ module AppStore::Caller
44
51
  def itunes_agent
45
52
  @itunes_agent ||= WWW::Mechanize.new { |a| a.user_agent = 'iTunes/9.0.1 (Macintosh; Intel Mac OS X 10.6.1) AppleWebKit/531.9' }
46
53
  end
54
+
55
+ def headers
56
+ {
57
+ "X-Apple-Store-Front" => @store_front
58
+ }
59
+ end
60
+
61
+ # Make call using given <tt>agent</tt>, <tt>url</tt> and <tt>params</tt>.
62
+ # Handle answer code and returns answer if answer code was correct.
63
+ def make_call_and_handle_answer(agent, url, params)
64
+ answer = agent.get(:url => url, :headers => headers, :params => params)
65
+ raise AppStore::RequestError if answer.code.to_s != '200'
66
+ answer
67
+ end
47
68
 
48
69
  end
@@ -0,0 +1,28 @@
1
+ require 'app_store/helper'
2
+
3
+ # Proxy class, call class methods on given class (<tt>to</tt>) with <tt>extra</tt> arguments
4
+ # for each method called on a proxy instance
5
+ class AppStore::Helper::Proxy
6
+ # Instanciate a new proxy object. Acceptable arguments :
7
+ # * <tt>to</tt>: receiver class
8
+ # * <tt>extra</tt>: extra arguments passed to each methods
9
+ def initialize(args = {})
10
+ @to = args[:to]
11
+ @extra = args[:extra]
12
+ end
13
+
14
+ private
15
+ def method_missing(method, *args)
16
+ raise "method #{method} not found for #{@to}" unless @to.methods.include?(method.to_s)
17
+ # OPTIMIZE: define method instead of sending each time
18
+ hash = args.last.is_a?(Hash) ? args.last : {}
19
+ case @to.method(method).arity
20
+ when -2
21
+ @to.send(method, args.first, @extra.merge(hash))
22
+ when -1
23
+ @to.send(method, @extra.merge(hash))
24
+ else
25
+ raise "method #{method} not supported by proxy"
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,13 @@
1
+ class String
2
+ # Taken from ActiveSupport::Inflector
3
+ def constantize
4
+ names = split('::')
5
+ names.shift if names.empty? || names.first.empty?
6
+
7
+ constant = Object
8
+ names.each do |name|
9
+ constant = constant.const_defined?(name) ? constant.const_get(name) : constant.const_missing(name)
10
+ end
11
+ constant
12
+ end
13
+ end
@@ -1,5 +1,4 @@
1
1
  require "app_store/base"
2
- require "app_store/caller"
3
2
 
4
3
  class AppStore::Link < AppStore::Base
5
4
  plist :mapping => {
@@ -8,11 +7,11 @@ class AppStore::Link < AppStore::Base
8
7
  'title' => :title,
9
8
  'url' => :url
10
9
  }
11
-
10
+
12
11
  def destination
13
12
  @destination ||= case @item_type
14
13
  when 'software'
15
- AppStore::Application.new :plist => AppStore::Caller.get(@url)['item-metadata']
14
+ AppStore::Application.new :plist => @client.get(@url)['item-metadata']
16
15
  else
17
16
  raise 'unsupported'
18
17
  end
@@ -5,6 +5,12 @@ require 'app_store'
5
5
  # sends only 24 elements followed by a link for the next 24 elements.
6
6
  # This class represents an abstraction of Apple AppStore lists, have
7
7
  # a real count attribute and is enumerable over the entire list.
8
+ # = Example
9
+ # list = Category.featured.first.items
10
+ # list.count # => 23453
11
+ # list.elements # => [AppStore::Category, AppStore::Category, ...]
12
+ # list.collect {|item| item.item_id} # => [9843509, 9028423, 8975435, 987345, ...]
13
+ #
8
14
  class AppStore::List
9
15
  include Enumerable
10
16
 
@@ -17,6 +23,7 @@ class AppStore::List
17
23
  def initialize(attrs = {})
18
24
  @element_initializer = attrs[:element_initializer]
19
25
  @element_type = attrs[:element_type]
26
+ @client = attrs[:client] || AppStore::Client.new
20
27
  @elements ||= []
21
28
 
22
29
  process_new_elements attrs[:list]
@@ -28,11 +35,14 @@ class AppStore::List
28
35
  @count ||= @elements.count
29
36
  end
30
37
 
38
+ # Iterates the given block using each object in the list.
31
39
  def each(&block)
32
40
  collect(&block)
33
41
  @elements
34
42
  end
35
43
 
44
+ # Iterates the given block using each object in the list,
45
+ # returns an array containing the execution block result for each element.
36
46
  def collect
37
47
  # First, iterate through already fetched elements
38
48
  result = @elements.collect {|element| yield element}
@@ -51,7 +61,7 @@ class AppStore::List
51
61
  # Return last fetched elements if any, nil otherwise
52
62
  def fetch_next_elements
53
63
  return nil if @link_to_next_elements.nil?
54
- process_new_elements(AppStore::Caller.get(@link_to_next_elements)['items'])
64
+ process_new_elements(@client.get(@link_to_next_elements)['items'])
55
65
  end
56
66
 
57
67
  def process_new_elements(new_elements)
@@ -63,11 +73,11 @@ class AppStore::List
63
73
  when @element_type
64
74
  result << initialize_element_and_append(element)
65
75
  when 'software'
66
- result << append_element(AppStore::Application.new(:plist => element))
76
+ result << append_element(AppStore::Application.new(:client => @client, :plist => element))
67
77
  when 'review'
68
- result << append_element(AppStore::UserReview.new(:plist => element))
78
+ result << append_element(AppStore::UserReview.new(:client => @client, :plist => element))
69
79
  when 'link'
70
- result << append_element(AppStore::Link.new(:plist => element))
80
+ result << append_element(AppStore::Link.new(:client => @client, :plist => element))
71
81
  when 'more'
72
82
  @count = element['total-items']
73
83
  @link_to_next_elements = element['url']
File without changes
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: app_store
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.4
4
+ version: 0.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Fabien Jakimowicz
@@ -9,7 +9,7 @@ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
11
 
12
- date: 2009-11-07 00:00:00 +01:00
12
+ date: 2009-11-09 00:00:00 +01:00
13
13
  default_executable:
14
14
  dependencies:
15
15
  - !ruby/object:Gem::Dependency
@@ -79,14 +79,17 @@ files:
79
79
  - Rakefile
80
80
  - VERSION
81
81
  - lib/app_store.rb
82
+ - lib/app_store/agent.rb
82
83
  - lib/app_store/application.rb
83
84
  - lib/app_store/artwork.rb
84
85
  - lib/app_store/base.rb
85
- - lib/app_store/caller.rb
86
86
  - lib/app_store/category.rb
87
+ - lib/app_store/client.rb
87
88
  - lib/app_store/company.rb
88
89
  - lib/app_store/helper.rb
89
90
  - lib/app_store/helpers/plist.rb
91
+ - lib/app_store/helpers/proxy.rb
92
+ - lib/app_store/helpers/string.rb
90
93
  - lib/app_store/image.rb
91
94
  - lib/app_store/link.rb
92
95
  - lib/app_store/list.rb
@@ -94,7 +97,7 @@ files:
94
97
  - test/application_test.rb
95
98
  - test/artwork_test.rb
96
99
  - test/base_test.rb
97
- - test/caller_test.rb
100
+ - test/client_test.rb
98
101
  - test/image_test.rb
99
102
  - test/output.txt
100
103
  - test/plist_test.rb
@@ -137,7 +140,7 @@ test_files:
137
140
  - test/application_test.rb
138
141
  - test/artwork_test.rb
139
142
  - test/base_test.rb
140
- - test/caller_test.rb
143
+ - test/client_test.rb
141
144
  - test/image_test.rb
142
145
  - test/plist_test.rb
143
146
  - test/test_helper.rb