snapsearch-client-ruby 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.
- checksums.yaml +7 -0
- data/Gemfile +9 -0
- data/Gemfile.lock +83 -0
- data/LICENSE +20 -0
- data/README.md +109 -0
- data/Rakefile +20 -0
- data/VERSION +1 -0
- data/examples/rack/Gemfile +5 -0
- data/examples/rack/config.ru +88 -0
- data/examples/rack/public/index.html +15 -0
- data/examples/sinatra/Gemfile +5 -0
- data/examples/sinatra/Gemfile.lock +90 -0
- data/examples/sinatra/config.ru +15 -0
- data/examples/sinatra/lib/sinatra_snap_search.rb +19 -0
- data/examples/sinatra/public/index.html +15 -0
- data/lib/rack/snap_search.rb +143 -0
- data/lib/rack/snap_search/config.rb +85 -0
- data/lib/snap_search.rb +14 -0
- data/lib/snap_search/client.rb +147 -0
- data/lib/snap_search/connection_exception.rb +15 -0
- data/lib/snap_search/detector.rb +248 -0
- data/lib/snap_search/exception.rb +8 -0
- data/lib/snap_search/interceptor.rb +66 -0
- data/lib/snap_search/validation_exception.rb +17 -0
- data/resources/cacert.pem +3785 -0
- data/resources/extensions.json +26 -0
- data/resources/robots.json +208 -0
- data/snapsearch.gemspec +31 -0
- data/spec/lib/rack/qs_spec.rb +34 -0
- data/spec/lib/rack/snap_search/config_spec.rb +56 -0
- data/spec/lib/snap_search/detector_spec.rb +362 -0
- data/spec/lib/snap_search/interceptor_spec.rb +116 -0
- data/spec/spec_helper.rb +6 -0
- metadata +216 -0
@@ -0,0 +1,15 @@
|
|
1
|
+
# Notes to run:
|
2
|
+
# gem install bundler
|
3
|
+
# bundle install
|
4
|
+
# rackup
|
5
|
+
#
|
6
|
+
# Testing:
|
7
|
+
# Visit http://localhost:9292/
|
8
|
+
# Visit http://localhost:9292/?_escaped_fragment_
|
9
|
+
|
10
|
+
require 'bundler/setup'
|
11
|
+
require 'pathname'
|
12
|
+
$:.unshift( Pathname.new(__FILE__).join('..', 'lib').expand_path.to_s )
|
13
|
+
require 'sinatra_snap_search'
|
14
|
+
|
15
|
+
run SinatraSnapSearch
|
@@ -0,0 +1,19 @@
|
|
1
|
+
require 'bundler/setup'
|
2
|
+
require 'sinatra/base'
|
3
|
+
require 'pathname'
|
4
|
+
require 'snap_search'
|
5
|
+
|
6
|
+
class SinatraSnapSearch < Sinatra::Base
|
7
|
+
|
8
|
+
configure do
|
9
|
+
set :root, SnapSearch.root.join('examples', 'sinatra')
|
10
|
+
enable :sessions, :logging, :method_override, :static
|
11
|
+
|
12
|
+
use Rack::SnapSearch, email: 'email', key: 'key'
|
13
|
+
end
|
14
|
+
|
15
|
+
get '/' do
|
16
|
+
redirect to('/index.html')
|
17
|
+
end
|
18
|
+
|
19
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
<!DOCTYPE html>
|
2
|
+
<html lang='en'>
|
3
|
+
<head>
|
4
|
+
<meta charset='utf-8'>
|
5
|
+
<title>SnapSearch Example</title>
|
6
|
+
<link href="/css/blah.css" media="all" rel="stylesheet" />
|
7
|
+
<!--[if lt IE 9]>
|
8
|
+
<script src="http://html5shim.googlecode.com/svn/trunk/html5.js"></script>
|
9
|
+
<![endif]-->
|
10
|
+
<!-- <script src="/js/blah.js"></script> -->
|
11
|
+
</head>
|
12
|
+
<body>
|
13
|
+
<h1>SnapSearch Example</h1>
|
14
|
+
</body>
|
15
|
+
</html>
|
@@ -0,0 +1,143 @@
|
|
1
|
+
require 'snap_search'
|
2
|
+
require 'rack/snap_search/config'
|
3
|
+
|
4
|
+
module Rack
|
5
|
+
|
6
|
+
# Use to enable SnapSearch detection within your web application.
|
7
|
+
class SnapSearch
|
8
|
+
|
9
|
+
# Initialize the middleware.
|
10
|
+
#
|
11
|
+
# @param [#call] app The Rack application.
|
12
|
+
# @param [Hash, #to_h] options Options to configure this middleware with.
|
13
|
+
# @yield [Rack::SnapSearch::Config, SnapSearch::Detector] A block to further modify the middleware.
|
14
|
+
# @yieldparam [Rack::SnapSearch::Config] config Options to configure this middleware with, optionally preset with the `options` param.
|
15
|
+
# @yieldparam [SnapSearch::Detector] detector The instance of the Detector class which will be used for detecting whether requests are coming from a robot.
|
16
|
+
def initialize(app, options={}, &block)
|
17
|
+
raise TypeError, 'app must respond to #call' unless app.respond_to?(:call)
|
18
|
+
raise TypeError, 'options must be a Hash or respond to #to_h or #to_hash' unless options.is_a?(Hash) || options.respond_to?(:to_h) || options.respond_to?(:to_hash)
|
19
|
+
options = options.to_h rescue options.to_hash
|
20
|
+
|
21
|
+
@app = app
|
22
|
+
|
23
|
+
setup_config(options)
|
24
|
+
|
25
|
+
block.call(@config) if block_given?
|
26
|
+
|
27
|
+
setup_client
|
28
|
+
setup_detector
|
29
|
+
setup_interceptor
|
30
|
+
end
|
31
|
+
|
32
|
+
# Run the middleware.
|
33
|
+
#
|
34
|
+
# @param [Hash, to_h] app The Rack environment
|
35
|
+
def call(environment)
|
36
|
+
raise TypeError, 'environment must be a Hash or respond to #to_h or #to_hash' unless environment.is_a?(Hash) || environment.respond_to?(:to_h) || environment.respond_to?(:to_hash)
|
37
|
+
environment = environment.to_h rescue environment.to_hash
|
38
|
+
|
39
|
+
setup_x_forwarded_proto(environment) if @config.x_forwarded_proto
|
40
|
+
|
41
|
+
setup_api_response_from_environment(environment) # Will set @api_response if one is given from the API
|
42
|
+
|
43
|
+
if @api_response
|
44
|
+
setup_attributes_from_api_response
|
45
|
+
else
|
46
|
+
setup_attributes_from_app(environment)
|
47
|
+
end
|
48
|
+
|
49
|
+
@config.response_callback.nil? ? rack_response : @config.response_callback.call(*rack_response)
|
50
|
+
end
|
51
|
+
|
52
|
+
protected
|
53
|
+
|
54
|
+
# == Initialization Helpers
|
55
|
+
|
56
|
+
# Setup the Config instance from the given options.
|
57
|
+
def setup_config(options)
|
58
|
+
@config = Rack::SnapSearch::Config.new(options)
|
59
|
+
|
60
|
+
@config.x_forwarded_proto ||= true
|
61
|
+
end
|
62
|
+
|
63
|
+
# Setup the Client instance from the @config.
|
64
|
+
def setup_client
|
65
|
+
@client = ::SnapSearch::Client.new(
|
66
|
+
email: @config.email,
|
67
|
+
key: @config.key,
|
68
|
+
parameters: @config.parameters,
|
69
|
+
api_url: @config.api_url,
|
70
|
+
ca_cert_file: @config.ca_cert_file
|
71
|
+
)
|
72
|
+
end
|
73
|
+
|
74
|
+
# Setup the Detector instance from the @config.
|
75
|
+
def setup_detector
|
76
|
+
@detector = ::SnapSearch::Detector.new(
|
77
|
+
matched_routes: @config.matched_routes,
|
78
|
+
ignored_routes: @config.ignored_routes,
|
79
|
+
robots_json: @config.robots_json,
|
80
|
+
extensions_json: @config.extensions_json,
|
81
|
+
check_static_files: @config.check_static_files
|
82
|
+
)
|
83
|
+
end
|
84
|
+
|
85
|
+
# Setup the Interceptor instance using the @client and @detector, then setup callbacks if needed.
|
86
|
+
def setup_interceptor
|
87
|
+
@interceptor = ::SnapSearch::Interceptor.new(@client, @detector)
|
88
|
+
|
89
|
+
@interceptor.before_intercept(&@config.before_intercept) unless @config.before_intercept.nil?
|
90
|
+
@interceptor.after_intercept(&@config.after_intercept) unless @config.before_intercept.nil?
|
91
|
+
end
|
92
|
+
|
93
|
+
# == Running Helpers
|
94
|
+
|
95
|
+
# Alter the environment if the X-FORWARDED-PROTO header is given.
|
96
|
+
def setup_x_forwarded_proto(environment)
|
97
|
+
# Check X-Forwarded-Proto because Heroku SSL Support terminates at the load balancer
|
98
|
+
if environment['X-FORWARDED-PROTO']
|
99
|
+
environment['HTTPS'] = true && environment['rack.url_scheme'] = 'https' && environment['SERVER_PORT'] = 443 if environment['X-FORWARDED-PROTO'] == 'https'
|
100
|
+
environment['HTTPS'] = false && environment['rack.url_scheme'] = 'http' && environment['SERVER_PORT'] = 80 if env['X-FORWARDED-PROTO'] == 'http'
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
# Intercept and return the response.
|
105
|
+
def setup_api_response_from_environment(environment)
|
106
|
+
begin
|
107
|
+
request = Rack::Request.new(environment.to_h)
|
108
|
+
@api_response = @interceptor.intercept(request: request)
|
109
|
+
rescue SnapSearch::Exception => exception
|
110
|
+
@config.on_exception.nil? ? raise(exception) : @config.on_exception.call(exception)
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
# Setup the Location header in the response.
|
115
|
+
def setup_location_header
|
116
|
+
response_location_header = @api_response['headers'].find { |header| header['name'] == 'Location' }
|
117
|
+
|
118
|
+
@headers['Location'] = response_location_header['value'] unless response_location_header.nil?
|
119
|
+
end
|
120
|
+
|
121
|
+
# Setup the Status header and body in the response.
|
122
|
+
def setup_status_and_body
|
123
|
+
@status, @body = @api_response['status'], [ @api_response['html'] ] # Note that the body in the Rack response must be an interable containing Strings, we it's wrapped within an Array.
|
124
|
+
end
|
125
|
+
|
126
|
+
# Setup Location and Status headers, as well as teh body, if we got a response from SnapSearch.
|
127
|
+
def setup_attributes_from_api_response
|
128
|
+
setup_location_header
|
129
|
+
# TODO: Should setup_content_length_header?
|
130
|
+
setup_status_and_body
|
131
|
+
end
|
132
|
+
|
133
|
+
def setup_attributes_from_app(environment)
|
134
|
+
@status, @headers, @body = @app.call(environment)
|
135
|
+
end
|
136
|
+
|
137
|
+
def rack_response
|
138
|
+
[ @status, @headers, @body ]
|
139
|
+
end
|
140
|
+
|
141
|
+
end
|
142
|
+
|
143
|
+
end
|
@@ -0,0 +1,85 @@
|
|
1
|
+
module Rack
|
2
|
+
class SnapSearch
|
3
|
+
|
4
|
+
# The configuration class for the Rack middleware.
|
5
|
+
# Holds the attributes to initialize the Client, Interceptor, and Detector with.
|
6
|
+
class Config
|
7
|
+
|
8
|
+
ATTRIBUTES = [
|
9
|
+
:email, :key, :api_url, :ca_cert_file, :x_forwarded_proto, :parameters, # Client options
|
10
|
+
:matched_routes, :ignored_routes, :robots_json, :extensions_json, :check_static_files # Detector options
|
11
|
+
]
|
12
|
+
|
13
|
+
attr_accessor *ATTRIBUTES # Setup reader & writer instance methods for each attribute
|
14
|
+
|
15
|
+
# Create a new instance.
|
16
|
+
#
|
17
|
+
# @param [Hash] options The options to initialize this instance with.
|
18
|
+
# @option options [String] :email The email to authenticate with.
|
19
|
+
# @option options [String] :key The key to authenticate with.
|
20
|
+
# @option options [String] :api_url The API URL to send requests to.
|
21
|
+
# @option options [String] :ca_cert_file The CA Cert file to use when sending HTTPS requests to the API.
|
22
|
+
# @option options [String] :x_forwarded_proto Check X-Forwarded-Proto because Heroku SSL Support terminates at the load balancer
|
23
|
+
# @option options [String] :parameters Extra parameters to send to the API.
|
24
|
+
# @option options [String] :matched_routes Whitelisted routes. Should be an Array of Regexp instances.
|
25
|
+
# @option options [String] :ignored_routes Blacklisted routes. Should be an Array of Regexp instances.
|
26
|
+
# @option options [String] :robots_json A path of the JSON file containing the user agent whitelist & blacklist.
|
27
|
+
# @option options [String] :extensions_json A path to the JSON file containing a single Hash with the keys `ignore` and `match`. These keys contain Arrays of Strings (user agents)
|
28
|
+
# @option options [String] :check_static_files Set to `true` to ignore direct requests to files.
|
29
|
+
# @option options [Proc, #call] :on_exception The block to run when an exception within SnapSearch occurs.
|
30
|
+
# @option options [Proc, #call] :before_intercept A block to run before the interception of a bot.
|
31
|
+
# @option options [Proc, #call] :after_intercept A block to run after the interception of a bot.
|
32
|
+
# @option options [Proc, #call] :response_callback A block to manipulate the response from the SnapSearch API.
|
33
|
+
def initialize(options={})
|
34
|
+
raise TypeError, 'options must be a Hash or respond to #to_h' unless options.is_a?(Hash) || options.respond_to?(:to_h) || options.respond_to?(:to_hash)
|
35
|
+
options = options.to_h rescue options.to_hash
|
36
|
+
|
37
|
+
ATTRIBUTES.each do |attribute|
|
38
|
+
send( "#{attribute}=", options[attribute] ) if options.has_key?(attribute)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
# Getter/Setter for the `on_exception` attribute.
|
43
|
+
#
|
44
|
+
# @yield If given, the Proc or callable to set the attribute as.
|
45
|
+
# @return [Proc] The value of the attribute.
|
46
|
+
def on_exception(&block)
|
47
|
+
@on_exception = block if block_given?
|
48
|
+
|
49
|
+
@on_exception
|
50
|
+
end
|
51
|
+
|
52
|
+
# Getter/Setter for the `before_intercept` attribute on the Interceptor.
|
53
|
+
#
|
54
|
+
# @yield If given, the Proc or callable to set the attribute as.
|
55
|
+
# @return [Proc] The value of the attribute.
|
56
|
+
def before_intercept(&block)
|
57
|
+
@before_intercept = block if block_given?
|
58
|
+
|
59
|
+
@before_intercept
|
60
|
+
end
|
61
|
+
|
62
|
+
# Getter/Setter for the `after_intercept` attribute on the Interceptor.
|
63
|
+
#
|
64
|
+
# @yield If given, the Proc or callable to set the attribute as.
|
65
|
+
# @return [Proc] The value of the attribute.
|
66
|
+
def after_intercept(&block)
|
67
|
+
@after_intercept = block if block_given?
|
68
|
+
|
69
|
+
@after_intercept
|
70
|
+
end
|
71
|
+
|
72
|
+
# Getter/Setter for the `response_callback` attribute on the Interceptor.
|
73
|
+
#
|
74
|
+
# @yield If given, the Proc or callable to set the attribute as.
|
75
|
+
# @return [Proc] The value of the attribute.
|
76
|
+
def response_callback(&block)
|
77
|
+
@response_callback = block if block_given?
|
78
|
+
|
79
|
+
@response_callback
|
80
|
+
end
|
81
|
+
|
82
|
+
end
|
83
|
+
|
84
|
+
end
|
85
|
+
end
|
data/lib/snap_search.rb
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
require 'pathname'
|
2
|
+
|
3
|
+
module SnapSearch
|
4
|
+
|
5
|
+
def self.root
|
6
|
+
@root ||= Pathname.new(__FILE__).join('..', '..').expand_path
|
7
|
+
end
|
8
|
+
|
9
|
+
end
|
10
|
+
|
11
|
+
require 'snap_search/client'
|
12
|
+
require 'snap_search/detector'
|
13
|
+
require 'snap_search/interceptor'
|
14
|
+
require 'rack/snap_search'
|
@@ -0,0 +1,147 @@
|
|
1
|
+
require 'json'
|
2
|
+
require 'httpi'
|
3
|
+
require 'snap_search/connection_exception'
|
4
|
+
require 'snap_search/validation_exception'
|
5
|
+
|
6
|
+
module SnapSearch
|
7
|
+
|
8
|
+
# The Client sends an authenticated HTTP request to the SnapChat API and returns the `content`
|
9
|
+
# field from the JSON response body.
|
10
|
+
class Client
|
11
|
+
|
12
|
+
attr_reader :email, :key, :parameters, :api_url, :ca_cert_file
|
13
|
+
|
14
|
+
# Create a new Client instance.
|
15
|
+
#
|
16
|
+
# @param [Hash, #to_h] options The options to create the Client with.
|
17
|
+
# @option options [String, #to_s] :email The email to authenticate with.
|
18
|
+
# @option options [String, #to_s] :key The secret authentication key.
|
19
|
+
# @option options [Hash, #to_h] :parameters ({}) The parameters to send with the HTTP request.
|
20
|
+
# @option options [String, #to_s] :api_url (https://snapsearch.io/api/v1/robot) The URL to send the HTTP request to.
|
21
|
+
# @option options [String, #to_s] :ca_cert_file (ROOT/resources/cacert.pem) The CA cert file to use with request.
|
22
|
+
def initialize(options={})
|
23
|
+
initialize_attributes(options)
|
24
|
+
end
|
25
|
+
|
26
|
+
# Validate and set the value of the `email` attribute.
|
27
|
+
#
|
28
|
+
# @param [String, #to_s] value The value to set the attribute to.
|
29
|
+
# @return [String] The new value of the attribute.
|
30
|
+
def email=(value)
|
31
|
+
raise TypeError, 'email must be a String or respond to #to_s' unless value.is_a?(String) || respond_to?(:to_s)
|
32
|
+
|
33
|
+
value = value.to_s
|
34
|
+
raise ArgumentError, 'email must be an email address' unless value.include?(?@)
|
35
|
+
|
36
|
+
@email = value
|
37
|
+
end
|
38
|
+
|
39
|
+
# Validate and set the value of the `key` attribute.
|
40
|
+
#
|
41
|
+
# @param [String, #to_s] value The value to set the attribute to.
|
42
|
+
# @return [String] The new value of the attribute.
|
43
|
+
def key=(value)
|
44
|
+
@key = value.nil? ? nil : value.to_s
|
45
|
+
end
|
46
|
+
|
47
|
+
# Validate and set the value of the `parameters` attribute.
|
48
|
+
#
|
49
|
+
# @param [Hash, #to_h] value The value to set the attribute to.
|
50
|
+
# @return [Hash] The new value of the attribute.
|
51
|
+
def parameters=(value)
|
52
|
+
raise TypeError, 'parameters must be a Hash or respond to #to_h or #to_hash' unless value.is_a?(Hash) || value.respond_to?(:to_h) || value.respond_to?(:to_hash)
|
53
|
+
value = value.to_h rescue value.to_hash
|
54
|
+
|
55
|
+
@parameters = value.to_h
|
56
|
+
end
|
57
|
+
|
58
|
+
# Validate and set the value of the `api_url` attribute.
|
59
|
+
#
|
60
|
+
# @param [String, #to_s] value The value to set the attribute to.
|
61
|
+
# @return [String] The new value of the attribute.
|
62
|
+
def api_url=(value)
|
63
|
+
raise TypeError, 'api_url must be a String or respond_to #to_s' unless value.is_a?(String) || value.respond_to?(:to_s)
|
64
|
+
|
65
|
+
@api_url = value.to_s
|
66
|
+
end
|
67
|
+
|
68
|
+
# Validate and set the value of the `ca_cert_file` attribute.
|
69
|
+
#
|
70
|
+
# @param [String, #to_s] value The value to set the attribute to.
|
71
|
+
# @return [String] The new value of the attribute.
|
72
|
+
def ca_cert_file=(value)
|
73
|
+
raise TypeError, 'ca_cert_file must be a String or respond_to #to_s' unless value.is_a?(String) || value.respond_to?(:to_s)
|
74
|
+
|
75
|
+
@ca_cert_file = value.to_s
|
76
|
+
end
|
77
|
+
|
78
|
+
# Send an authenticated HTTP request to the `api_url` and return the `content` field from the JSON response body.
|
79
|
+
#
|
80
|
+
# @param [String, #to_s] url The url to send in the parameters to the `api_url`.
|
81
|
+
# @return [String] The `content` field from the JSON response body.
|
82
|
+
def request(url)
|
83
|
+
raise TypeError, 'url must be a String or respond_to #to_s' unless url.is_a?(String) || url.respond_to?(:to_s)
|
84
|
+
@parameters['url'] = url.to_s # The URL must contain the entire URL with the _escaped_fragment_ parsed out
|
85
|
+
|
86
|
+
content_from_response(send_request)
|
87
|
+
end
|
88
|
+
|
89
|
+
protected
|
90
|
+
|
91
|
+
# Initialize this instance based on options passed.
|
92
|
+
#
|
93
|
+
# @param [Hash, #to_h] options The options to create the Client with.
|
94
|
+
# @option options [String, #to_s] :email The email to authenticate with.
|
95
|
+
# @option options [String, #to_s] :key The secret authentication key.
|
96
|
+
# @option options [Hash, #to_h] :parameters ({}) The parameters to send with the HTTP request.
|
97
|
+
# @option options [String, #to_s] :api_url (https://snapsearch.io/api/v1/robot) The URL to send the HTTP request to.
|
98
|
+
# @option options [String, #to_s] :ca_cert_file (ROOT/resources/cacert.pem) The CA cert file to use with request.
|
99
|
+
def initialize_attributes(options)
|
100
|
+
raise TypeError, 'options must be a Hash or respond to #to_h' unless options.is_a?(Hash) || options.respond_to?(:to_h) || options.respond_to?(:to_hash)
|
101
|
+
options = options.to_h rescue options.to_hash
|
102
|
+
|
103
|
+
options = {
|
104
|
+
parameters: {},
|
105
|
+
api_url: 'https://snapsearch.io/api/v1/robot',
|
106
|
+
ca_cert_file: SnapSearch.root.join('resources', 'cacert.pem')
|
107
|
+
}.merge(options.to_h)
|
108
|
+
|
109
|
+
self.email, self.key, self.parameters, self.api_url, self.ca_cert_file = options.values_at(:email, :key, :parameters, :api_url, :ca_cert_file)
|
110
|
+
end
|
111
|
+
|
112
|
+
# Send an authenticated HTTP POST request encoded in JSON to the API URL
|
113
|
+
# using the HTTP client adapter of the developer's choice.
|
114
|
+
#
|
115
|
+
# @return [HTTPI::Response] The HTTP response object.
|
116
|
+
def send_request
|
117
|
+
request = HTTPI::Request.new
|
118
|
+
request.url = api_url
|
119
|
+
request.auth.basic(email, key)
|
120
|
+
request.auth.ssl.ca_cert_file = ca_cert_file
|
121
|
+
request.auth.ssl.verify_mode = :peer
|
122
|
+
request.open_timeout = 30
|
123
|
+
request.headers['Content-Type'] = 'application/json'
|
124
|
+
request.headers['Accept-Encoding'] = 'gzip, deflate, identity'
|
125
|
+
request.body = @parameters.to_json
|
126
|
+
|
127
|
+
HTTPI.post(request)
|
128
|
+
rescue
|
129
|
+
raise ConnectionException
|
130
|
+
end
|
131
|
+
|
132
|
+
# Retrieve the `content` or raise an error based on the `code` field in the JSON response.
|
133
|
+
#
|
134
|
+
# @return [HTTPI::Response] The HTTP response object.
|
135
|
+
def content_from_response(response)
|
136
|
+
body = JSON.parse(response.body)
|
137
|
+
|
138
|
+
case body['code']
|
139
|
+
when 'success' then body['content']
|
140
|
+
when 'validation_error' then raise( ValidationException, body['content'] )
|
141
|
+
else; false # System error on SnapSearch; Nothing we can do # TODO: Raise exception?
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
145
|
+
end
|
146
|
+
|
147
|
+
end
|