opera-mobile-store-sdk 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.
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