candy_check 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +5 -0
- data/.rubocop.yml +6 -0
- data/.ruby-version +1 -0
- data/.travis.yml +16 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +157 -0
- data/Rakefile +15 -0
- data/bin/cc_appstore +90 -0
- data/bin/cc_playstore +119 -0
- data/candy_check.gemspec +31 -0
- data/lib/candy_check/app_store/client.rb +57 -0
- data/lib/candy_check/app_store/config.rb +30 -0
- data/lib/candy_check/app_store/receipt.rb +83 -0
- data/lib/candy_check/app_store/verification.rb +49 -0
- data/lib/candy_check/app_store/verification_failure.rb +60 -0
- data/lib/candy_check/app_store/verifier.rb +69 -0
- data/lib/candy_check/app_store.rb +12 -0
- data/lib/candy_check/play_store/client.rb +102 -0
- data/lib/candy_check/play_store/config.rb +51 -0
- data/lib/candy_check/play_store/discovery_repository.rb +33 -0
- data/lib/candy_check/play_store/receipt.rb +81 -0
- data/lib/candy_check/play_store/verification.rb +46 -0
- data/lib/candy_check/play_store/verification_failure.rb +30 -0
- data/lib/candy_check/play_store/verifier.rb +52 -0
- data/lib/candy_check/play_store.rb +15 -0
- data/lib/candy_check/utils/attribute_reader.rb +30 -0
- data/lib/candy_check/utils/config.rb +40 -0
- data/lib/candy_check/utils.rb +2 -0
- data/lib/candy_check/version.rb +4 -0
- data/lib/candy_check.rb +8 -0
- data/spec/app_store/client_spec.rb +55 -0
- data/spec/app_store/config_spec.rb +41 -0
- data/spec/app_store/receipt_spec.rb +92 -0
- data/spec/app_store/verifcation_failure_spec.rb +28 -0
- data/spec/app_store/verification_spec.rb +66 -0
- data/spec/app_store/verifier_spec.rb +110 -0
- data/spec/candy_check_spec.rb +9 -0
- data/spec/fixtures/api_cache.dump +1 -0
- data/spec/fixtures/play_store/api_cache.dump +1 -0
- data/spec/fixtures/play_store/auth_failure.txt +18 -0
- data/spec/fixtures/play_store/auth_success.txt +20 -0
- data/spec/fixtures/play_store/discovery.txt +2841 -0
- data/spec/fixtures/play_store/dummy.p12 +0 -0
- data/spec/fixtures/play_store/empty.txt +17 -0
- data/spec/fixtures/play_store/products_failure.txt +29 -0
- data/spec/fixtures/play_store/products_success.txt +22 -0
- data/spec/play_store/client_spec.rb +104 -0
- data/spec/play_store/config_spec.rb +96 -0
- data/spec/play_store/discovery_respository_spec.rb +31 -0
- data/spec/play_store/receipt_spec.rb +88 -0
- data/spec/play_store/verification_failure_spec.rb +35 -0
- data/spec/play_store/verification_spec.rb +80 -0
- data/spec/play_store/verifier_spec.rb +95 -0
- data/spec/spec_helper.rb +35 -0
- data/spec/support/with_fixtures.rb +9 -0
- data/spec/support/with_temp_file.rb +23 -0
- metadata +270 -0
data/candy_check.gemspec
ADDED
@@ -0,0 +1,31 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'candy_check/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = 'candy_check'
|
8
|
+
spec.version = CandyCheck::VERSION
|
9
|
+
spec.authors = ['Jonas Thiel']
|
10
|
+
spec.email = ['jonas@thiel.io']
|
11
|
+
spec.summary = 'Check and verify in-app receipts'
|
12
|
+
spec.homepage = 'https://github.com/jnbt/candy_check'
|
13
|
+
spec.license = 'MIT'
|
14
|
+
|
15
|
+
spec.files = `git ls-files -z`.split("\x0")
|
16
|
+
spec.executables = spec.files.grep(/^bin\//) { |f| File.basename(f) }
|
17
|
+
spec.test_files = spec.files.grep(/^(test|spec|features)\//)
|
18
|
+
spec.require_paths = ['lib']
|
19
|
+
|
20
|
+
spec.add_dependency 'multi_json', '~> 1.10'
|
21
|
+
spec.add_dependency 'google-api-client', '~> 0.8'
|
22
|
+
|
23
|
+
spec.add_development_dependency 'rubocop', '~> 0.28'
|
24
|
+
spec.add_development_dependency 'inch', '~> 0.5'
|
25
|
+
spec.add_development_dependency 'bundler', '~> 1.7'
|
26
|
+
spec.add_development_dependency 'rake', '~> 10.0'
|
27
|
+
spec.add_development_dependency 'coveralls', '~> 0.7'
|
28
|
+
spec.add_development_dependency 'minitest', '~> 5.5'
|
29
|
+
spec.add_development_dependency 'minitest-around', '~> 0.3'
|
30
|
+
spec.add_development_dependency 'webmock', '~> 1.20'
|
31
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
require 'multi_json'
|
2
|
+
|
3
|
+
module CandyCheck
|
4
|
+
module AppStore
|
5
|
+
# Simple HTTP client to load the receipt's data from Apple's verification
|
6
|
+
# servers (either sandbox or production).
|
7
|
+
class Client
|
8
|
+
# Mimetype for JSON objects
|
9
|
+
JSON_MIME_TYPE = 'application/json'
|
10
|
+
|
11
|
+
# Initialize a new client bound to an endpoint
|
12
|
+
# @param endpoint_url [String]
|
13
|
+
def initialize(endpoint_url)
|
14
|
+
@uri = URI(endpoint_url)
|
15
|
+
end
|
16
|
+
|
17
|
+
# Contacts the configured endpoint and requests the receipt's data
|
18
|
+
# @param receipt_data [String] base64 encoded data string from the app
|
19
|
+
# @param secret [String] the password for auto-renewable subscriptions
|
20
|
+
# @return [Hash]
|
21
|
+
def verify(receipt_data, secret = nil)
|
22
|
+
request = build_request(build_request_parameters(receipt_data, secret))
|
23
|
+
response = perform_request(request)
|
24
|
+
MultiJson.load(response.body)
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
|
29
|
+
def perform_request(request)
|
30
|
+
build_http_connector.request(request)
|
31
|
+
end
|
32
|
+
|
33
|
+
def build_http_connector
|
34
|
+
Net::HTTP.new(@uri.host, @uri.port).tap do |net|
|
35
|
+
net.use_ssl = true
|
36
|
+
net.verify_mode = OpenSSL::SSL::VERIFY_PEER
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def build_request(parameters)
|
41
|
+
Net::HTTP::Post.new(@uri.request_uri).tap do |post|
|
42
|
+
post['Accept'] = JSON_MIME_TYPE
|
43
|
+
post['Content-Type'] = JSON_MIME_TYPE
|
44
|
+
post.body = MultiJson.dump(parameters)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def build_request_parameters(receipt_data, secret)
|
49
|
+
{
|
50
|
+
'receipt-data' => receipt_data
|
51
|
+
}.tap do |h|
|
52
|
+
h['password'] = secret if secret
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
module CandyCheck
|
2
|
+
module AppStore
|
3
|
+
# Configure the verifier
|
4
|
+
class Config < Utils::Config
|
5
|
+
# @return [Symbol] the used environment
|
6
|
+
attr_reader :environment
|
7
|
+
|
8
|
+
# Initializes a new configuration from a hash
|
9
|
+
# @param attributes [Hash]
|
10
|
+
# @example
|
11
|
+
# Config.new(
|
12
|
+
# environment: :production # or :sandbox
|
13
|
+
# )
|
14
|
+
def initialize(attributes)
|
15
|
+
super
|
16
|
+
end
|
17
|
+
|
18
|
+
# @return [Boolean] if it is production environment
|
19
|
+
def production?
|
20
|
+
environment == :production
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
|
25
|
+
def validate!
|
26
|
+
validates_inclusion(:environment, :production, :sandbox)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,83 @@
|
|
1
|
+
module CandyCheck
|
2
|
+
module AppStore
|
3
|
+
# Describes a successful response from the AppStore verification server
|
4
|
+
class Receipt
|
5
|
+
include Utils::AttributeReader
|
6
|
+
|
7
|
+
# @return [Hash] the raw attributes returned from the server
|
8
|
+
attr_reader :attributes
|
9
|
+
|
10
|
+
# Initializes a new instance which bases on a JSON result
|
11
|
+
# from Apple's verification server
|
12
|
+
# @param attributes [Hash]
|
13
|
+
def initialize(attributes)
|
14
|
+
@attributes = attributes
|
15
|
+
end
|
16
|
+
|
17
|
+
# In most cases a receipt is a valid transaction except when the
|
18
|
+
# transaction was canceled.
|
19
|
+
# @return [Boolean]
|
20
|
+
def valid?
|
21
|
+
!has?('cancellation_date')
|
22
|
+
end
|
23
|
+
|
24
|
+
# The receipt's transaction id
|
25
|
+
# @return [String]
|
26
|
+
def transaction_id
|
27
|
+
read('transaction_id')
|
28
|
+
end
|
29
|
+
|
30
|
+
# The receipt's original transaction id which might differ from
|
31
|
+
# the transaction id for restored products
|
32
|
+
# @return [String]
|
33
|
+
def original_transaction_id
|
34
|
+
read('original_transaction_id')
|
35
|
+
end
|
36
|
+
|
37
|
+
# The version number for the app
|
38
|
+
# @return [String]
|
39
|
+
def app_version
|
40
|
+
read('bvrs')
|
41
|
+
end
|
42
|
+
|
43
|
+
# The app's bundle identifier
|
44
|
+
# @return [String]
|
45
|
+
def bundle_identifier
|
46
|
+
read('bid')
|
47
|
+
end
|
48
|
+
|
49
|
+
# The app's item id of the product
|
50
|
+
# @return [String]
|
51
|
+
def item_id
|
52
|
+
read('item_id')
|
53
|
+
end
|
54
|
+
|
55
|
+
# The quantity of the product
|
56
|
+
# @return [Fixnum]
|
57
|
+
def quantity
|
58
|
+
read_integer('quantity')
|
59
|
+
end
|
60
|
+
|
61
|
+
# The purchase date
|
62
|
+
# @return [DateTime]
|
63
|
+
def purchase_date
|
64
|
+
read_datetime_from_string('purchase_date')
|
65
|
+
end
|
66
|
+
|
67
|
+
# The original purchase date which might differ from the
|
68
|
+
# actual purchase date for restored products
|
69
|
+
# @return [DateTime]
|
70
|
+
def original_purchase_date
|
71
|
+
read_datetime_from_string('original_purchase_date')
|
72
|
+
end
|
73
|
+
|
74
|
+
# The date of when Apple has canceled this transaction.
|
75
|
+
# From Apple's documentation: "Treat a canceled receipt
|
76
|
+
# the same as if no purchase had ever been made."
|
77
|
+
# @return [DateTime]
|
78
|
+
def cancellation_date
|
79
|
+
read_datetime_from_string('cancellation_date')
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
module CandyCheck
|
2
|
+
module AppStore
|
3
|
+
# Verifies a receipt block against a verification server.
|
4
|
+
# The call return either an {Receipt} or a {VerificationFailure}
|
5
|
+
class Verification
|
6
|
+
# @return [String] the verification URL to use
|
7
|
+
attr_reader :endpoint_url
|
8
|
+
# @return [String] the raw data to be verified
|
9
|
+
attr_reader :receipt_data
|
10
|
+
# @return [String] the optional shared secret
|
11
|
+
attr_reader :secret
|
12
|
+
|
13
|
+
# Constant for successful responses
|
14
|
+
STATUS_OK = 0
|
15
|
+
|
16
|
+
# Builds a fresh verification run
|
17
|
+
# @param endpoint_url [String] the verification URL to use
|
18
|
+
# @param receipt_data [String] the raw data to be verified
|
19
|
+
# @param secret [String] the optional shared secret
|
20
|
+
def initialize(endpoint_url, receipt_data, secret = nil)
|
21
|
+
@endpoint_url, @receipt_data = endpoint_url, receipt_data
|
22
|
+
@secret = secret
|
23
|
+
end
|
24
|
+
|
25
|
+
# Performs the verification against the remote server
|
26
|
+
# @return [Receipt] if successful
|
27
|
+
# @return [VerificationFailure] otherwise
|
28
|
+
def call!
|
29
|
+
verify!
|
30
|
+
if valid?
|
31
|
+
Receipt.new(@response['receipt'])
|
32
|
+
else
|
33
|
+
VerificationFailure.fetch(@response['status'])
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
private
|
38
|
+
|
39
|
+
def valid?
|
40
|
+
@response && @response['status'] == STATUS_OK && @response['receipt']
|
41
|
+
end
|
42
|
+
|
43
|
+
def verify!
|
44
|
+
client = Client.new(endpoint_url)
|
45
|
+
@response = client.verify(receipt_data, secret)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
module CandyCheck
|
2
|
+
module AppStore
|
3
|
+
# Represents a failing call against the verification server
|
4
|
+
class VerificationFailure < Struct.new(:code, :message)
|
5
|
+
# @!attribute code
|
6
|
+
# @return [Fixnum] the code of the failure
|
7
|
+
# @!attribute message
|
8
|
+
# @return [String] the message of the failure
|
9
|
+
|
10
|
+
class << self
|
11
|
+
# Gets a known failure or build an unknown failure
|
12
|
+
# without description
|
13
|
+
# @param code [Fixnum]
|
14
|
+
# @return [VerificationFailure]
|
15
|
+
def fetch(code)
|
16
|
+
known.fetch(code) do
|
17
|
+
fallback(code)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
private
|
22
|
+
|
23
|
+
def fallback(code)
|
24
|
+
new(code || -1, 'Unknown error')
|
25
|
+
end
|
26
|
+
|
27
|
+
def known
|
28
|
+
@known ||= {}
|
29
|
+
end
|
30
|
+
|
31
|
+
def add(code, name)
|
32
|
+
known[code] = new(code, name)
|
33
|
+
end
|
34
|
+
|
35
|
+
def freeze!
|
36
|
+
known.freeze
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
add 21_000, 'The App Store could not read the JSON object you provided.'
|
41
|
+
add 21_002, 'The data in the receipt-data property was malformed' \
|
42
|
+
' or missing.'
|
43
|
+
add 21_003, 'The receipt could not be authenticated.'
|
44
|
+
add 21_004, 'The shared secret you provided does not match the shared' \
|
45
|
+
' secret on file for your account.'
|
46
|
+
add 21_005, 'The receipt server is not currently available.'
|
47
|
+
add 21_006, 'This receipt is valid but the subscription has expired.' \
|
48
|
+
' When this status code is returned to your server, the' \
|
49
|
+
' receipt data is also decoded and returned as part of'\
|
50
|
+
' the response.'
|
51
|
+
add 21_007, 'This receipt is from the test environment, but it was' \
|
52
|
+
' sent to the production environment for verification.' \
|
53
|
+
' Send it to the test environment instead.'
|
54
|
+
add 21_008, 'This receipt is from the production environment, but it' \
|
55
|
+
' was sent to the test environment for verification.' \
|
56
|
+
' Send it to the production environment instead.'
|
57
|
+
freeze!
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
@@ -0,0 +1,69 @@
|
|
1
|
+
module CandyCheck
|
2
|
+
module AppStore
|
3
|
+
# Verifies receipts against the verification servers.
|
4
|
+
# The call return either an {Receipt} or a {VerificationFailure}
|
5
|
+
class Verifier
|
6
|
+
# HTTPS endpoint for production receipts
|
7
|
+
PRODUCTION_ENDPOINT = 'https://buy.itunes.apple.com/verifyReceipt'
|
8
|
+
# HTTPS endpoint for sandbox receipts
|
9
|
+
SANDBOX_ENDPOINT = 'https://sandbox.itunes.apple.com/verifyReceipt'
|
10
|
+
# Status code from production endpoint when receiving a sandbox
|
11
|
+
# receipt which occurs during the app's review process
|
12
|
+
REDIRECT_TO_SANDBOX_CODE = 21_007
|
13
|
+
# Status code from the sandbox endpoint when receiving a production
|
14
|
+
# receipt
|
15
|
+
REDIRECT_TO_PRODUCTION_CODE = 21_008
|
16
|
+
|
17
|
+
# @return [Config] the current configuration
|
18
|
+
attr_reader :config
|
19
|
+
|
20
|
+
# Initializes a new verifier for the application which is bound
|
21
|
+
# to a configuration
|
22
|
+
# @param config [Config]
|
23
|
+
def initialize(config)
|
24
|
+
@config = config
|
25
|
+
end
|
26
|
+
|
27
|
+
# Calls a verification for the given input
|
28
|
+
# @param receipt_data [String] the raw data to be verified
|
29
|
+
# @param secret [String] the optional shared secret
|
30
|
+
# @return [Receipt] if successful
|
31
|
+
# @return [VerificationFailure] otherwise
|
32
|
+
def verify(receipt_data, secret = nil)
|
33
|
+
default_endpoint, opposite_endpoint = endpoints
|
34
|
+
result = call_for(default_endpoint, receipt_data, secret)
|
35
|
+
if should_retry?(result)
|
36
|
+
return call_for(opposite_endpoint, receipt_data, secret)
|
37
|
+
end
|
38
|
+
result
|
39
|
+
end
|
40
|
+
|
41
|
+
private
|
42
|
+
|
43
|
+
def call_for(endpoint_url, receipt_data, secret)
|
44
|
+
Verification.new(endpoint_url, receipt_data, secret).call!
|
45
|
+
end
|
46
|
+
|
47
|
+
def should_retry?(result)
|
48
|
+
result.is_a?(VerificationFailure) && redirect?(result)
|
49
|
+
end
|
50
|
+
|
51
|
+
def endpoints
|
52
|
+
if config.production?
|
53
|
+
[PRODUCTION_ENDPOINT, SANDBOX_ENDPOINT]
|
54
|
+
else
|
55
|
+
[SANDBOX_ENDPOINT, PRODUCTION_ENDPOINT]
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
def redirect_code
|
60
|
+
config.production? ? REDIRECT_TO_SANDBOX_CODE :
|
61
|
+
REDIRECT_TO_PRODUCTION_CODE
|
62
|
+
end
|
63
|
+
|
64
|
+
def redirect?(failure)
|
65
|
+
failure.code == redirect_code
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
require 'candy_check/app_store/client'
|
2
|
+
require 'candy_check/app_store/config'
|
3
|
+
require 'candy_check/app_store/receipt'
|
4
|
+
require 'candy_check/app_store/verification'
|
5
|
+
require 'candy_check/app_store/verification_failure'
|
6
|
+
require 'candy_check/app_store/verifier'
|
7
|
+
|
8
|
+
module CandyCheck
|
9
|
+
# Module to request and verify a AppStore receipt
|
10
|
+
module AppStore
|
11
|
+
end
|
12
|
+
end
|
@@ -0,0 +1,102 @@
|
|
1
|
+
module CandyCheck
|
2
|
+
module PlayStore
|
3
|
+
# A client which uses the official Google API SDK to authenticate
|
4
|
+
# and request product information from Google's API.
|
5
|
+
#
|
6
|
+
# @example Usage
|
7
|
+
# config = ClientConfig.new({...})
|
8
|
+
# client = Client.new(config)
|
9
|
+
# client.boot! # a single time
|
10
|
+
# client.verify('my.bundle', 'product_1', 'a-very-long-secure-token')
|
11
|
+
# # ... multiple calls from now on
|
12
|
+
# client.verify('my.bundle', 'product_1', 'another-long-token')
|
13
|
+
class Client
|
14
|
+
# Error thrown if the discovery of the API wasn't successful
|
15
|
+
class DiscoveryError < RuntimeError; end
|
16
|
+
|
17
|
+
# API endpoint
|
18
|
+
API_URL = 'https://accounts.google.com/o/oauth2/token'
|
19
|
+
# API scope for Android services
|
20
|
+
API_SCOPE = 'https://www.googleapis.com/auth/androidpublisher'
|
21
|
+
# API discovery namespace
|
22
|
+
API_DISCOVER = 'androidpublisher'
|
23
|
+
# API version
|
24
|
+
API_VERSION = 'v2'
|
25
|
+
|
26
|
+
# Initializes a client using a configuration.
|
27
|
+
# @param config [ClientConfig]
|
28
|
+
def initialize(config)
|
29
|
+
self.config = config
|
30
|
+
end
|
31
|
+
|
32
|
+
# Boots a client by discovering the API's services and then authorizes
|
33
|
+
# by fetching an access token.
|
34
|
+
# If the config has a cache_file the client tries to load discovery
|
35
|
+
def boot!
|
36
|
+
self.api_client = Google::APIClient.new(
|
37
|
+
application_name: config.application_name,
|
38
|
+
application_version: config.application_version
|
39
|
+
)
|
40
|
+
discover!
|
41
|
+
authorize!
|
42
|
+
end
|
43
|
+
|
44
|
+
# Calls the remote API to load the product information for a specific
|
45
|
+
# combination of parameter which should be loaded from the client.
|
46
|
+
# @param package [String] the app's package name
|
47
|
+
# @param product_id [String] the app's item id
|
48
|
+
# @param token [String] the purchase token
|
49
|
+
# @return [Hash] result of the API call
|
50
|
+
def verify(package, product_id, token)
|
51
|
+
api_client.execute(
|
52
|
+
api_method: rpc.purchases.products.get,
|
53
|
+
parameters: {
|
54
|
+
'packageName' => package,
|
55
|
+
'productId' => product_id,
|
56
|
+
'token' => token
|
57
|
+
}
|
58
|
+
).data.to_hash
|
59
|
+
end
|
60
|
+
|
61
|
+
private
|
62
|
+
|
63
|
+
attr_accessor :config, :api_client, :rpc
|
64
|
+
|
65
|
+
def discover!
|
66
|
+
self.rpc = load_discover_dump || request_discover
|
67
|
+
validate_rpc!
|
68
|
+
write_discover_dump
|
69
|
+
end
|
70
|
+
|
71
|
+
def request_discover
|
72
|
+
api_client.discovered_api(API_DISCOVER, API_VERSION)
|
73
|
+
end
|
74
|
+
|
75
|
+
def authorize!
|
76
|
+
api_client.authorization = Signet::OAuth2::Client.new(
|
77
|
+
token_credential_uri: API_URL,
|
78
|
+
audience: API_URL,
|
79
|
+
scope: API_SCOPE,
|
80
|
+
issuer: config.issuer,
|
81
|
+
signing_key: config.api_key
|
82
|
+
)
|
83
|
+
api_client.authorization.fetch_access_token!
|
84
|
+
end
|
85
|
+
|
86
|
+
def validate_rpc!
|
87
|
+
return if rpc.purchases.products.get
|
88
|
+
fail DiscoveryError, 'Unable to get the API discovery'
|
89
|
+
rescue NoMethodError
|
90
|
+
raise DiscoveryError, 'Unable to get the API discovery'
|
91
|
+
end
|
92
|
+
|
93
|
+
def load_discover_dump
|
94
|
+
DiscoveryRepository.new(config.cache_file).load
|
95
|
+
end
|
96
|
+
|
97
|
+
def write_discover_dump
|
98
|
+
DiscoveryRepository.new(config.cache_file).save(rpc)
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|