app_store 0.0.4 → 0.1.0

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