opera-mobile-store-sdk 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (34) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +10 -0
  3. data/.rspec +2 -0
  4. data/.travis.yml +3 -0
  5. data/CODE_OF_CONDUCT.md +13 -0
  6. data/Gemfile +9 -0
  7. data/LICENSE.txt +21 -0
  8. data/README.md +39 -0
  9. data/Rakefile +2 -0
  10. data/bin/console +14 -0
  11. data/bin/setup +7 -0
  12. data/lib/opera-mobile-store-sdk.rb +69 -0
  13. data/lib/opera-mobile-store-sdk/version.rb +5 -0
  14. data/lib/opera/mobile_store/author.rb +31 -0
  15. data/lib/opera/mobile_store/build.rb +88 -0
  16. data/lib/opera/mobile_store/build_file.rb +27 -0
  17. data/lib/opera/mobile_store/category.rb +31 -0
  18. data/lib/opera/mobile_store/developer.rb +70 -0
  19. data/lib/opera/mobile_store/payment_info.rb +105 -0
  20. data/lib/opera/mobile_store/product.rb +157 -0
  21. data/lib/opera/mobile_store/product_image.rb +42 -0
  22. data/lib/opera/mobile_store/product_localization.rb +35 -0
  23. data/lib/opera/mobile_store_sdk/api_accessible.rb +44 -0
  24. data/lib/opera/mobile_store_sdk/api_object_list.rb +174 -0
  25. data/lib/opera/mobile_store_sdk/config.rb +71 -0
  26. data/lib/opera/mobile_store_sdk/errors.rb +10 -0
  27. data/lib/opera/mobile_store_sdk/faraday_middleware.rb +4 -0
  28. data/lib/opera/mobile_store_sdk/faraday_middleware/authentication.rb +76 -0
  29. data/lib/opera/mobile_store_sdk/faraday_middleware/required_response_format.rb +14 -0
  30. data/lib/opera/mobile_store_sdk/faraday_middleware/response_parser.rb +45 -0
  31. data/lib/opera/mobile_store_sdk/faraday_middleware/sdk_benchmark.rb +47 -0
  32. data/lib/opera/mobile_store_sdk/identity_mapable.rb +35 -0
  33. data/opera-mobile-store-sdk.gemspec +35 -0
  34. metadata +174 -0
@@ -0,0 +1,105 @@
1
+ module Opera
2
+ module MobileStore
3
+ class PaymentInfo
4
+
5
+ include ActiveModel::Model
6
+
7
+ def self.build_from_nokogiri_node(node)
8
+ if node.present?
9
+ type = node.xpath("string(@type)").strip.downcase
10
+
11
+ case type
12
+ when "check" then Check.build_from_nokogiri_node node
13
+ when "wired" then Wired.build_from_nokogiri_node node
14
+ when "paypal" then PayPal.build_from_nokogiri_node node
15
+ when "none" then nil
16
+ else raise "WTF?"
17
+ end
18
+ end
19
+ end
20
+
21
+ def type
22
+ self.class.name.demodulize.downcase
23
+ end
24
+
25
+ def inspect
26
+ attr_inspect = attributes.inject [] do |attributes, keyval|
27
+ key, val = keyval
28
+ attributes + ["#{key}: \"#{val}\""]
29
+ end.join(", ")
30
+
31
+ "<#{self.class.name} #{attr_inspect}>"
32
+ end
33
+
34
+ class Check < PaymentInfo
35
+ attr_accessor :name, :address
36
+
37
+ def self.build_from_nokogiri_node(node)
38
+ self.new(
39
+ name: node.xpath("string(payment_check_name)"),
40
+ address: node.xpath("string(payment_check_address)")
41
+ )
42
+ end
43
+
44
+ def attributes
45
+ [:name, :address].inject({}) do |hash, method|
46
+ value = self.public_send method
47
+ hash[method] = value unless value.nil?
48
+ hash
49
+ end
50
+ end
51
+ end
52
+
53
+ class Wired < PaymentInfo
54
+ attr_accessor :bank_account,
55
+ :bank_name,
56
+ :bank_address,
57
+ :bank_swiftbic,
58
+ :bank_iban,
59
+ :bank_routing_number,
60
+ :intermediary_bank_name,
61
+ :intermediary_bank_address,
62
+ :intermediary_bank_swiftbic,
63
+ :intermediary_bank_iban
64
+
65
+ def self.build_from_nokogiri_node(node)
66
+ data = [
67
+ :bank_account, :bank_name, :bank_address, :bank_swiftbic, :bank_iban,
68
+ :bank_routing_number, :intermediary_bank_name, :intermediary_bank_address,
69
+ :intermediary_bank_swiftbic, :intermediary_bank_iban
70
+ ].inject({}) do |hash, attribute_name|
71
+ value = node.xpath("string(payment_wired_#{attribute_name})").strip
72
+ hash[attribute_name] = value if value.present?
73
+ hash
74
+ end
75
+
76
+ self.new data
77
+ end
78
+
79
+ def attributes
80
+ [
81
+ :bank_account, :bank_name, :bank_address, :bank_swiftbic, :bank_iban,
82
+ :bank_routing_number, :intermediary_bank_name, :intermediary_bank_address,
83
+ :intermediary_bank_swiftbic, :intermediary_bank_iban
84
+ ].inject({}) do |hash, method|
85
+ value = self.public_send method
86
+ hash[method] = value unless value.nil?
87
+ hash
88
+ end
89
+ end
90
+ end
91
+
92
+ class PayPal < PaymentInfo
93
+ attr_accessor :account
94
+ def self.build_from_nokogiri_node(node)
95
+ self.new(account: node.xpath("string(account)").strip)
96
+ end
97
+
98
+ def attributes
99
+ { account: account }
100
+ end
101
+ end
102
+
103
+ end
104
+ end
105
+ end
@@ -0,0 +1,157 @@
1
+ require "active_model"
2
+
3
+ module Opera
4
+ module MobileStore
5
+ class Product
6
+
7
+ include ActiveModel::Model
8
+
9
+ include Opera::MobileStoreSDK::APIAccessible
10
+
11
+ # All attributes are Read-Only...
12
+ attr_accessor :id, :code, :category_id, :cp_product_id,
13
+ :app_type,
14
+ :released_at, # Release date
15
+ :download_count, # Download count at Opera Mobile Store
16
+ :author_id,
17
+ :support_url,
18
+ :version,
19
+ :requirements,
20
+ :price,
21
+ :adult_content,
22
+ :rating,
23
+ :currency,
24
+ :product_type,
25
+ :weight,
26
+ :updated_at,
27
+ :added_at,
28
+ :keywords,
29
+ :rating,
30
+ :images,
31
+ :eula,
32
+ :subsites,
33
+ :builds,
34
+ :i18n
35
+
36
+ def category
37
+ Category.find category_id if category_id.present?
38
+ end
39
+
40
+ def author
41
+ Author.find author_id if author_id.present?
42
+ end
43
+
44
+ def title(key = "en")
45
+ required_data = i18n.detect do |x|
46
+ x.language_iso_code == key.to_s
47
+ end.title
48
+ required_data.present? ? required_data : title('en')
49
+ end
50
+
51
+ def short_description(key = "en")
52
+ required_data = i18n.detect do |x|
53
+ x.language_iso_code == key.to_s
54
+ end.short_description
55
+ required_data.present? ? required_data : short_description('en')
56
+ end
57
+
58
+ def long_description(key = "en")
59
+ required_data = i18n.detect do |x|
60
+ x.language_iso_code == key.to_s
61
+ end.long_description
62
+ required_data.present? ? required_data : long_description('en')
63
+ end
64
+
65
+ def available_language_codes
66
+ i18n.map(&:language_iso_code)
67
+ end
68
+
69
+ def self.build_from_nokogiri_node(node)
70
+
71
+ category_id = node.xpath("number(category/@id)").to_i
72
+ author_id = node.xpath("number(author/@id)").to_i
73
+
74
+ # Register the category unless it's already registered in the ID Map:
75
+ Category.register(
76
+ id: category_id,
77
+ code: node.xpath("string(category/@code)"),
78
+ name: node.xpath("string(category)")
79
+ ) unless Category.registered? category_id
80
+
81
+ # Register the author unless it's already registered in the ID Map:
82
+ unless Author.registered? author_id
83
+ author_attributes = {
84
+ id: author_id,
85
+ name: node.xpath("string(author)"),
86
+ email: node.xpath("string(author_email)")
87
+ }
88
+ author_attributes.delete(:email) unless author_attributes[:email].present?
89
+
90
+ Author.register author_attributes
91
+ end
92
+
93
+ data = {
94
+ id: node.xpath("number(@id)").to_i,
95
+ code: node.xpath("string(@code)"),
96
+ cp_product_id: node.xpath("number(cp_product_id)").to_i,
97
+
98
+ category_id: category_id,
99
+ author_id: author_id,
100
+ app_type: node.xpath("string(apptype)"),
101
+ released_at: Time.parse(node.xpath "string(release_date)"),
102
+ download_count: node.xpath("number(downloads_count)").to_i,
103
+ support_url: node.xpath("string(support_url)"),
104
+ version: node.xpath("string(version)"),
105
+ # TODO: process requirements node
106
+
107
+ # Product localization in English:
108
+ i18n: [
109
+ ProductLocalization.new(
110
+ language_code: "en",
111
+ language_iso_code: "en",
112
+ language_name: "English",
113
+ title: node.xpath("string(product_name)").strip,
114
+ short_description: node.xpath("string(short_description)").strip,
115
+ long_description: node.xpath("string(long_description)").strip
116
+ )
117
+ ],
118
+
119
+ price: node.xpath("number(price)"),
120
+ currency: node.xpath("string(currency)"),
121
+ product_type: node.xpath("string(type)"),
122
+ weight: node.xpath("number(weight)").to_i,
123
+ updated_at: Time.parse(node.xpath "string(update_date)"),
124
+ added_at: Time.parse(node.xpath "string(add_date)"),
125
+
126
+ keywords: node.xpath("keywords/keyword").map do |x|
127
+ value = x.text.strip
128
+ value.present? ? Opera::MobileStoreSDK.html_entities.decode(value) : nil
129
+ end.compact,
130
+
131
+ images: node.xpath("images/*").map do |i|
132
+ ProductImage.build_from_nokogiri_node i
133
+ end,
134
+
135
+ builds: node.xpath("builds/build").map do |b|
136
+ Build.build_from_nokogiri_node b
137
+ end,
138
+
139
+ }
140
+
141
+ node.xpath("translates/language").each do |language_node|
142
+ data[:i18n] << ProductLocalization.new(
143
+ language_code: language_node.xpath("string(@code)").strip,
144
+ language_iso_code: language_node.xpath("string(@iso)").strip,
145
+ language_name: language_node.xpath("string(@language)").strip,
146
+ title: node.xpath("string(title)").strip,
147
+ short_description: node.xpath("string(short_description)").strip,
148
+ long_description: node.xpath("string(description)").strip
149
+ )
150
+ end
151
+
152
+ self.new data
153
+ end
154
+
155
+ end
156
+ end
157
+ end
@@ -0,0 +1,42 @@
1
+ module Opera
2
+ module MobileStore
3
+ class ProductImage
4
+
5
+ include ActiveModel::Model
6
+
7
+ # All attributes are Read-Only...
8
+ attr_accessor :type, :width, :height, :url
9
+
10
+ def self.build_from_nokogiri_node(node)
11
+
12
+ data = {
13
+ type: node.name,
14
+ url: node.text.strip
15
+ }
16
+
17
+ # Extract width + height data:
18
+ width = node.xpath("string(@width)")
19
+ height = node.xpath("string(@height)")
20
+ data[:width] = width.to_i if width.present?
21
+ data[:height] = width.to_i if height.present?
22
+
23
+ self.new data
24
+ end
25
+
26
+ def inspect
27
+ info = { type: type }
28
+ info[:width] = width if width.present?
29
+ info[:height] = height if height.present?
30
+ info[:url] = url
31
+
32
+ info = info.inject [] do |attributes, keyval|
33
+ key, val = keyval
34
+ attributes + ["#{key}:#{val}"]
35
+ end
36
+
37
+ "<Opera::MobileStore::ProductImage " + info.join(', ') + ">"
38
+ end
39
+
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,35 @@
1
+ module Opera
2
+ module MobileStore
3
+ class ProductLocalization
4
+
5
+ include ActiveModel::Model
6
+
7
+ # All attributes are Read-Only...
8
+ attr_accessor :language_code,
9
+ :language_iso_code,
10
+ :language_name,
11
+ :title,
12
+ :short_description,
13
+ :long_description
14
+
15
+ def attributes
16
+ [:language_code, :language_iso_code, :language_name,
17
+ :title, :short_description, :long_description].inject({}) do |hash, method|
18
+ value = self.public_send method
19
+ hash[method] = value unless value.nil?
20
+ hash
21
+ end
22
+ end
23
+
24
+ def inspect
25
+ attr_inspect = attributes.inject [] do |attributes, keyval|
26
+ key, val = keyval
27
+ attributes + ["#{key}:#{val}"]
28
+ end.join(", ")
29
+
30
+ "<#{self.class.name} #{attr_inspect}>"
31
+ end
32
+
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,44 @@
1
+ require "active_support/concern"
2
+
3
+ module Opera
4
+ module MobileStoreSDK
5
+ module APIAccessible
6
+
7
+ extend ActiveSupport::Concern
8
+
9
+ included do
10
+
11
+ end
12
+
13
+ module ClassMethods
14
+
15
+ def where(given_options = {})
16
+ APIObjectList.new(name).where given_options
17
+ end
18
+
19
+ def per(given_item_count)
20
+ APIObjectList.new(name).per given_item_count
21
+ end
22
+
23
+ def page(given_page)
24
+ APIObjectList.new(name).page given_page
25
+ end
26
+
27
+ def all
28
+ APIObjectList.new(name)
29
+ end
30
+
31
+ def includes(*included_fields)
32
+ params = included_fields.map(&:to_s).map(&:downcase)
33
+ .select { |x| %w(original_images billing eula adult subsites compatibility profit rating).include? x }
34
+ .map { |x| x == "billing" ? "show_billing" : x }
35
+ .inject({}) { |hash, param_name| hash[param_name] = "1"; hash }
36
+
37
+ APIObjectList.new name, params
38
+ end
39
+
40
+ end
41
+
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,174 @@
1
+ require "faraday"
2
+ require "opera/mobile_store_sdk/faraday_middleware"
3
+
4
+ module Opera
5
+ module MobileStoreSDK
6
+
7
+ # Our version of Array... which will request each page upon iteration...
8
+ # see http://stackoverflow.com/questions/1571349/can-the-array-be-reinvented-in-ruby
9
+ class APIObjectList
10
+
11
+ include Enumerable
12
+
13
+ attr_reader :options
14
+
15
+ def initialize(klass, options = {})
16
+ @klass = klass
17
+ @options = options.with_indifferent_access
18
+ end
19
+
20
+ def length
21
+ response.body.count
22
+ end
23
+
24
+ def found_rows
25
+ response.env[:found_rows]
26
+ end
27
+ alias_method :total_count, :found_rows
28
+
29
+ def sdk_tms
30
+ {
31
+ api_calling: response.env[:opera_api_calling_tms],
32
+ api_response_parsing: response.env[:opera_api_response_parsing_tms],
33
+ }
34
+ end
35
+
36
+ def api_call_duration
37
+ sdk_tms[:api_calling].real
38
+ end
39
+
40
+ def api_response_parsing_duration
41
+ sdk_tms[:api_response_parsing].real
42
+ end
43
+
44
+ def api_response_datetime
45
+ response.env[:opera_api_response_datetime]
46
+ end
47
+
48
+ def timestamp
49
+ response.env[:timestamp]
50
+ end
51
+
52
+ def updated_at
53
+ Time.at(page.timestamp).to_datetime
54
+ end
55
+
56
+ def [](n)
57
+ response.body[n]
58
+ end
59
+
60
+ def each
61
+ 0.upto(length - 1) { |idx| yield self[idx] }
62
+ end
63
+
64
+ def to_a
65
+ response.body
66
+ end
67
+
68
+ # Chainable Methods ------------------------------------------------------
69
+ def where(given_options = {})
70
+
71
+ given_options = given_options.with_indifferent_access
72
+
73
+ ########################################################################
74
+ # Validate (& filter out conflicting) options:
75
+
76
+ # Product list API can use added_after/updated_after parameters:
77
+
78
+ if given_options.include? :updated_after
79
+ if given_options[:updated_after].is_a? String
80
+ begin
81
+ given_options[:updated_after] = DateTime.parse(given_options[:updated_after])
82
+ rescue ArgumentError => e
83
+ raise Opera::MobileStoreSDK::Errors::APIParamsError,
84
+ "Parameter 'updated_after' is not a date (#{given_options[:updated_after]})"
85
+ end
86
+ elsif !given_options[:updated_after].respond_to? :to_date
87
+ raise Opera::MobileStoreSDK::Errors::APIParamsError,
88
+ "Parameter 'updated_after' is not a date (#{given_options[:updated_after]})"
89
+ end
90
+ end
91
+
92
+ if given_options.key? :added_after
93
+ if given_options[:added_after].is_a? String
94
+ begin
95
+ given_options[:added_after] = DateTime.parse(given_options[:added_after])
96
+ rescue ArgumentError => e
97
+ raise Opera::MobileStoreSDK::Errors::APIParamsError,
98
+ "Parameter 'added_after' is not a date (#{given_options[:added_after]})"
99
+ end
100
+ elsif !given_options[:added_after].respond_to? :to_date
101
+ raise Opera::MobileStoreSDK::Errors::APIParamsError,
102
+ "Parameter 'added_after' is not a date (#{given_options[:added_after]})"
103
+ end
104
+
105
+ # Delete conflicting parameters:
106
+ given_options.delete :updated_after
107
+ end
108
+ ########################################################################
109
+
110
+ self.class.new @klass, @options.merge(given_options)
111
+ end
112
+
113
+ def page(given_page)
114
+ where @options.merge(page: given_page)
115
+ end
116
+
117
+ def offset(given_offset)
118
+ where @options.merge(offset: given_offset)
119
+ end
120
+
121
+ def per(given_item_count)
122
+ where @options.merge(items: given_item_count)
123
+ end
124
+
125
+ def includes(included_fields = [])
126
+ params = included_fields.map(&:to_s).map(&:downcase)
127
+ .select { |x| %w(original_images billing eula adult subsites compatibility profit rating).include? x }
128
+ .map { |x| x == "billing" ? "show_billing" : x }
129
+ .inject({}) { |hash, param_name| hash[param_name] = "1"; hash }
130
+
131
+ self.class.new @klass, @options.merge(params)
132
+ end
133
+ # ------------------------------------------------------------------------
134
+
135
+ protected
136
+
137
+ def response
138
+
139
+ converted_options = @options.inject({}) do |hsh, keyval|
140
+ key, val = keyval
141
+
142
+ hsh[key.to_s] = case key.to_sym
143
+ when :added_after, :updated_after then
144
+ # byebug
145
+ val.to_date.to_s
146
+ else val.to_s
147
+ end
148
+
149
+ hsh
150
+ end
151
+
152
+ # puts "Options: #{@options.inspect}"
153
+ # byebug
154
+
155
+ @_response ||= Opera::MobileStoreSDK.connection.get do |request|
156
+ request.url MobileStoreSDK.config.api_path, {
157
+ action: action_param
158
+ }.merge(converted_options)
159
+
160
+ request.options.timeout = MobileStoreSDK.config.api_call_timeout
161
+ request.options.open_timeout = MobileStoreSDK.config.api_call_open_timeout
162
+ end
163
+ end
164
+
165
+ def action_param
166
+ @_action_param ||= case @klass
167
+ when "Opera::MobileStore::Developer" then "full_developers"
168
+ else @klass.demodulize.underscore.pluralize
169
+ end
170
+ end
171
+
172
+ end
173
+ end
174
+ end