swiftype 0.0.5 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (97) hide show
  1. data/.gitignore +5 -0
  2. data/.travis.yml +6 -0
  3. data/Gemfile +1 -1
  4. data/README.md +202 -283
  5. data/lib/data/ca-bundle.crt +3554 -0
  6. data/lib/swiftype.rb +7 -31
  7. data/lib/swiftype/client.rb +439 -11
  8. data/lib/swiftype/configuration.rb +29 -4
  9. data/lib/swiftype/exceptions.rb +2 -0
  10. data/lib/swiftype/ext/backport-uri.rb +33 -0
  11. data/lib/swiftype/request.rb +88 -20
  12. data/lib/swiftype/result_set.rb +43 -6
  13. data/lib/swiftype/sso.rb +20 -0
  14. data/lib/swiftype/version.rb +1 -1
  15. data/spec/client_spec.rb +576 -0
  16. data/spec/configuration_spec.rb +36 -0
  17. data/spec/deprecated_spec.rb +20 -0
  18. data/spec/fixtures/vcr/analytics_autoselects.yml +55 -0
  19. data/spec/fixtures/vcr/analytics_autoselects_with_document_type.yml +55 -0
  20. data/spec/fixtures/vcr/analytics_autoselects_with_document_type_and_time_range.yml +55 -0
  21. data/spec/fixtures/vcr/analytics_autoselects_with_time_range.yml +55 -0
  22. data/spec/fixtures/vcr/analytics_clicks.yml +55 -0
  23. data/spec/fixtures/vcr/analytics_clicks_with_document_type.yml +55 -0
  24. data/spec/fixtures/vcr/analytics_clicks_with_document_type_and_time_range.yml +55 -0
  25. data/spec/fixtures/vcr/analytics_clicks_with_time_range.yml +55 -0
  26. data/spec/fixtures/vcr/analytics_searches.yml +55 -0
  27. data/spec/fixtures/vcr/analytics_searches_with_document_type_and_time_range.yml +55 -0
  28. data/spec/fixtures/vcr/analytics_searches_with_time_range.yml +55 -0
  29. data/spec/fixtures/vcr/analytics_searchs_with_document_type.yml +55 -0
  30. data/spec/fixtures/vcr/analytics_top_no_result_queries.yml +55 -0
  31. data/spec/fixtures/vcr/analytics_top_no_result_queries_paginated.yml +55 -0
  32. data/spec/fixtures/vcr/analytics_top_queries.yml +55 -0
  33. data/spec/fixtures/vcr/analytics_top_queries_paginated.yml +55 -0
  34. data/spec/fixtures/vcr/analytics_top_queries_too_large.yml +51 -0
  35. data/spec/fixtures/vcr/bulk_create_documents.yml +49 -0
  36. data/spec/fixtures/vcr/bulk_create_or_update_documents_failure.yml +45 -0
  37. data/spec/fixtures/vcr/bulk_create_or_update_documents_success.yml +49 -0
  38. data/spec/fixtures/vcr/bulk_destroy_documents.yml +45 -0
  39. data/spec/fixtures/vcr/crawl_url.yml +45 -0
  40. data/spec/fixtures/vcr/create_document.yml +48 -0
  41. data/spec/fixtures/vcr/create_document_type.yml +45 -0
  42. data/spec/fixtures/vcr/create_domain.yml +45 -0
  43. data/spec/fixtures/vcr/create_engine.yml +45 -0
  44. data/spec/fixtures/vcr/create_or_update_document_create.yml +47 -0
  45. data/spec/fixtures/vcr/create_or_update_document_update.yml +47 -0
  46. data/spec/fixtures/vcr/create_user.yml +45 -0
  47. data/spec/fixtures/vcr/destroy_document.yml +41 -0
  48. data/spec/fixtures/vcr/destroy_document_type.yml +41 -0
  49. data/spec/fixtures/vcr/destroy_domain.yml +41 -0
  50. data/spec/fixtures/vcr/destroy_engine.yml +41 -0
  51. data/spec/fixtures/vcr/destroy_non_existent_document_type.yml +43 -0
  52. data/spec/fixtures/vcr/document_type_search.yml +60 -0
  53. data/spec/fixtures/vcr/document_type_search_pagination.yml +45 -0
  54. data/spec/fixtures/vcr/document_type_suggest.yml +50 -0
  55. data/spec/fixtures/vcr/document_type_suggest_pagination.yml +45 -0
  56. data/spec/fixtures/vcr/engine_search.yml +64 -0
  57. data/spec/fixtures/vcr/engine_search_facets.yml +162 -0
  58. data/spec/fixtures/vcr/engine_search_pagination.yml +45 -0
  59. data/spec/fixtures/vcr/engine_suggest.yml +52 -0
  60. data/spec/fixtures/vcr/engine_suggest_pagination.yml +45 -0
  61. data/spec/fixtures/vcr/find_document.yml +47 -0
  62. data/spec/fixtures/vcr/find_document_type.yml +45 -0
  63. data/spec/fixtures/vcr/find_domain.yml +45 -0
  64. data/spec/fixtures/vcr/find_domain_failure.yml +43 -0
  65. data/spec/fixtures/vcr/find_engine.yml +45 -0
  66. data/spec/fixtures/vcr/list_document_type.yml +45 -0
  67. data/spec/fixtures/vcr/list_documents.yml +68 -0
  68. data/spec/fixtures/vcr/list_documents_with_pagination.yml +68 -0
  69. data/spec/fixtures/vcr/list_domains.yml +45 -0
  70. data/spec/fixtures/vcr/list_engines.yml +46 -0
  71. data/spec/fixtures/vcr/list_users.yml +45 -0
  72. data/spec/fixtures/vcr/list_users_with_pagination.yml +45 -0
  73. data/spec/fixtures/vcr/log_clickthrough_failure.yml +43 -0
  74. data/spec/fixtures/vcr/log_clickthrough_success.yml +45 -0
  75. data/spec/fixtures/vcr/recrawl_domain_failure.yml +44 -0
  76. data/spec/fixtures/vcr/recrawl_domain_success.yml +45 -0
  77. data/spec/fixtures/vcr/show_user.yml +45 -0
  78. data/spec/fixtures/vcr/update_document.yml +47 -0
  79. data/spec/fixtures/vcr/update_document_unknown_field_failure.yml +45 -0
  80. data/spec/fixtures/vcr/update_documents_failure_non_existent_document.yml +45 -0
  81. data/spec/fixtures/vcr/update_documents_success.yml +45 -0
  82. data/spec/fixtures/vcr/users_client_secret_incorrect.yml +42 -0
  83. data/spec/fixtures/vcr/users_no_api_key.yml +42 -0
  84. data/spec/fixtures/vcr/users_no_client_id_or_secret.yml +43 -0
  85. data/spec/platform_spec.rb +95 -0
  86. data/spec/spec_helper.rb +26 -0
  87. data/spec/ssl_spec.rb +34 -0
  88. data/spec/sso_spec.rb +24 -0
  89. data/swiftype.gemspec +10 -11
  90. metadata +183 -52
  91. data/lib/swiftype/base_model.rb +0 -89
  92. data/lib/swiftype/connection.rb +0 -47
  93. data/lib/swiftype/document.rb +0 -17
  94. data/lib/swiftype/document_type.rb +0 -69
  95. data/lib/swiftype/easy.rb +0 -77
  96. data/lib/swiftype/engine.rb +0 -33
  97. data/lib/swiftype/search.rb +0 -23
@@ -1,3 +1,4 @@
1
+ require 'uri'
1
2
  require 'swiftype/version'
2
3
 
3
4
  module Swiftype
@@ -6,10 +7,10 @@ module Swiftype
6
7
  DEFAULT_USER_AGENT = "Swiftype-Ruby/#{Swiftype::VERSION}"
7
8
 
8
9
  VALID_OPTIONS_KEYS = [
9
- :username,
10
- :password,
11
10
  :api_key,
12
11
  :user_agent,
12
+ :platform_client_id,
13
+ :platform_client_secret,
13
14
  :endpoint
14
15
  ]
15
16
 
@@ -19,24 +20,48 @@ module Swiftype
19
20
  base.reset
20
21
  end
21
22
 
23
+ # Reset configuration to default values.
22
24
  def reset
23
- self.username = nil
24
- self.password = nil
25
25
  self.api_key = nil
26
26
  self.endpoint = DEFAULT_ENDPOINT
27
27
  self.user_agent = DEFAULT_USER_AGENT
28
+ self.platform_client_id = nil
29
+ self.platform_client_secret = nil
28
30
  self
29
31
  end
30
32
 
33
+ # Yields the Swiftype::Configuration module which can be used to set configuration options.
34
+ #
35
+ # @return self
31
36
  def configure
32
37
  yield self
33
38
  self
34
39
  end
35
40
 
41
+ # Return a hash of the configured options.
36
42
  def options
37
43
  options = {}
38
44
  VALID_OPTIONS_KEYS.each{|k| options[k] = send(k)}
39
45
  options
40
46
  end
47
+
48
+ # Set api_key and endpoint based on a URL with HTTP authentication.
49
+ # Useful if you're using the Swiftype Heroku add-on.
50
+ def authenticated_url=(url)
51
+ uri = URI(url)
52
+ self.api_key = uri.user
53
+ uri.user = nil
54
+ uri.password = nil
55
+ self.endpoint = uri.to_s
56
+ end
57
+
58
+ # setter for endpoint that ensures it always ends in '/'
59
+ def endpoint=(endpoint)
60
+ if endpoint.end_with?('/')
61
+ @endpoint = endpoint
62
+ else
63
+ @endpoint = "#{endpoint}/"
64
+ end
65
+ end
41
66
  end
42
67
  end
@@ -3,5 +3,7 @@ module Swiftype
3
3
  class NonExistentRecord < ClientException; end
4
4
  class RecordAlreadyExists < ClientException; end
5
5
  class InvalidCredentials < ClientException; end
6
+ class BadRequest < ClientException; end
7
+ class Forbidden < ClientException; end
6
8
  class UnexpectedHTTPException < ClientException; end
7
9
  end
@@ -0,0 +1,33 @@
1
+ # :stopdoc:
2
+
3
+ # Stolen from ruby core's uri/common.rb, with modifications to support 1.8.x
4
+ #
5
+ # https://github.com/ruby/ruby/blob/trunk/lib/uri/common.rb
6
+ #
7
+ #
8
+
9
+ module URI
10
+ def self.encode_www_form(enum)
11
+ enum.map do |k,v|
12
+ if v.nil?
13
+ encode_www_form_component(k)
14
+ elsif v.respond_to?(:to_ary)
15
+ v.to_ary.map do |w|
16
+ str = encode_www_form_component(k)
17
+ unless w.nil?
18
+ str << '='
19
+ str << encode_www_form_component(w)
20
+ end
21
+ end.join('&')
22
+ else
23
+ str = encode_www_form_component(k)
24
+ str << '='
25
+ str << encode_www_form_component(v)
26
+ end
27
+ end.join('&')
28
+ end
29
+
30
+ def self.encode_www_form_component(str)
31
+ str.to_s.gsub(/[^*\-.0-9A-Z_a-z]/) { |chr| TBLENCWWWCOMP_[chr] }
32
+ end
33
+ end
@@ -1,34 +1,102 @@
1
+ require 'net/https'
2
+ if RUBY_VERSION < "1.9"
3
+ require 'swiftype/ext/backport-uri'
4
+ else
5
+ require 'uri'
6
+ end
7
+ require 'json'
8
+ require 'swiftype/exceptions'
9
+ require 'openssl'
10
+
1
11
  module Swiftype
2
12
  module Request
3
- def get(path, params={}, options={})
4
- request(:get, path, params, options)
13
+ def get(path, params={})
14
+ request(:get, path, params)
15
+ end
16
+
17
+ def post(path, params={})
18
+ request(:post, path, params)
5
19
  end
6
20
 
7
- def delete(path, params={}, options={})
8
- request(:delete, path, params, options)
21
+ def put(path, params={})
22
+ request(:put, path, params)
9
23
  end
10
24
 
11
- def post(path, params={}, options={})
12
- request(:post, path, params, options)
25
+ def delete(path, params={})
26
+ request(:delete, path, params)
13
27
  end
14
28
 
15
- def put(path, params={}, options={})
16
- request(:put, path, params, options)
29
+ def request(method, path, params={})
30
+ uri = URI.parse("#{Swiftype.endpoint}#{path}")
31
+
32
+ request = build_request(method, uri, params)
33
+ http = Net::HTTP.new(uri.host, uri.port)
34
+
35
+ if uri.scheme == 'https'
36
+ http.use_ssl = true
37
+ http.verify_mode = OpenSSL::SSL::VERIFY_PEER
38
+ http.ca_file = File.join(__FILE__, '..', 'data', 'ca-bundle.crt')
39
+ end
40
+
41
+ response = http.request(request)
42
+
43
+ handle_errors(response)
44
+
45
+ JSON.parse(response.body) if response.body && response.body.strip != ''
17
46
  end
18
47
 
19
48
  private
20
- def request(method, path, params, options)
21
- params.merge!({:auth_token => Swiftype.api_key}) if Swiftype.api_key
22
- response = connection.send(method) do |request|
23
- case method.to_sym
24
- when :delete, :get
25
- request.url(path, params)
26
- when :post, :put
27
- request.path = path
28
- request.body = params unless params.empty?
29
- end
49
+
50
+ def handle_errors(response)
51
+ case response
52
+ when Net::HTTPSuccess
53
+ response
54
+ when Net::HTTPUnauthorized
55
+ raise Swiftype::InvalidCredentials
56
+ when Net::HTTPNotFound
57
+ raise Swiftype::NonExistentRecord
58
+ when Net::HTTPConflict
59
+ raise Swiftype::RecordAlreadyExists
60
+ when Net::HTTPBadRequest
61
+ raise Swiftype::BadRequest
62
+ when Net::HTTPForbidden
63
+ raise Swiftype::Forbidden
64
+ else
65
+ raise Swiftype::UnexpectedHTTPException, "#{response.code} #{response.body}"
66
+ end
67
+ end
68
+
69
+ def build_request(method, uri, params)
70
+ klass = case method
71
+ when :get
72
+ Net::HTTP::Get
73
+ when :post
74
+ Net::HTTP::Post
75
+ when :put
76
+ Net::HTTP::Put
77
+ when :delete
78
+ Net::HTTP::Delete
79
+ end
80
+
81
+ case method
82
+ when :get, :delete
83
+ uri.query = URI.encode_www_form(params) if params && !params.empty?
84
+ req = klass.new(uri.request_uri)
85
+ when :post, :put
86
+ req = klass.new(uri.request_uri)
87
+ req.body = JSON.generate(params) unless params.length == 0
30
88
  end
31
- options[:raw] ? response : response.body
89
+
90
+ req['User-Agent'] = Swiftype.user_agent
91
+ req['Content-Type'] = 'application/json'
92
+
93
+ if platform_access_token
94
+ req['Authorization'] = "Bearer #{platform_access_token}"
95
+ elsif api_key
96
+ req.basic_auth api_key, ''
97
+ end
98
+
99
+ req
32
100
  end
33
101
  end
34
- end
102
+ end
@@ -1,31 +1,66 @@
1
1
  module Swiftype
2
+ # The Swiftype::ResultSet class represents a {search}[https://swiftype.com/documentation/searching]
3
+ # or {suggest result}[https://swiftype.com/documentation/autocomplete] returned by the Swiftype API.
2
4
  class ResultSet
3
- attr_accessor :records, :info
4
-
5
+ # @attribute errors [r]
6
+ # a hash of errors for the search (for example filtering on a missing attribute) keyed by DocumentType slug
7
+ attr_reader :errors
8
+
9
+ # @attribute records [r]
10
+ # a hash of results for the search keyed by DocumentType slug. Use `[]` to access results more easily.
11
+ attr_reader :records
12
+
13
+ # @attribute info [r]
14
+ # a hash of extra query info (for example, facets and number of results) keyed by DocumentType slug.
15
+ # Use the convenience methods of this class for easier access.
16
+ attr_reader :info
17
+
18
+ # Create a Swiftype::ResultSet from deserialized JSON.
5
19
  def initialize(results)
6
- @records = {}
7
- results['records'].each do |document_type, documents|
8
- @records[document_type] = documents.map { |d| Swiftype::Document.new(d) }
9
- end
20
+ @records = results['records']
10
21
  @info = results['info']
22
+ @errors = results['errors']
11
23
  end
12
24
 
25
+ # Get results for the provided DocumentType
26
+ #
27
+ # @param [String] document_type the DocumentType slug to get results for
13
28
  def [](document_type)
14
29
  records[document_type]
15
30
  end
16
31
 
32
+ # Return a list of DocumentType slugs represented in the ResultSet.
33
+ def document_types
34
+ records.keys
35
+ end
36
+
37
+ # Return the search facets for the provided DocumentType. Will be
38
+ # empty unless a facets parameter was provided when calling the
39
+ # search API.
40
+ #
41
+ # @param [String] document_type the DocumentType slug to get facets for
17
42
  def facets(document_type)
18
43
  info[document_type]['facets']
19
44
  end
20
45
 
46
+ # Return the page of results for this ResultSet
21
47
  def current_page
22
48
  info[info.keys.first]['current_page']
23
49
  end
24
50
 
51
+ # Return the number of results per page.
25
52
  def per_page
26
53
  info[info.keys.first]['per_page']
27
54
  end
28
55
 
56
+ # Return the number of pages. Since a search can cover multiple
57
+ # DocumentTypes with different numbers of results, the number of
58
+ # pages can vary between DocumentTypes. With no argument, it
59
+ # returns the maximum num_pages for all DocumentTypes in this
60
+ # ResultSet. With a DocumentType slug, it returns the number of
61
+ # pages for that DocumentType.
62
+ #
63
+ # @param [String] document_type the DocumentType slug to return the number of pages for
29
64
  def num_pages(document_type=nil)
30
65
  if document_type
31
66
  info[document_type]['num_pages']
@@ -34,10 +69,12 @@ module Swiftype
34
69
  end
35
70
  end
36
71
 
72
+ # Return the total number of results for the query
37
73
  def total_result_count(document_type)
38
74
  info[document_type]['total_result_count']
39
75
  end
40
76
 
77
+ # Return the query used for this search
41
78
  def query
42
79
  info[info.keys.first]['query']
43
80
  end
@@ -0,0 +1,20 @@
1
+ require 'digest/sha1'
2
+
3
+ module Swiftype
4
+ # Single sign-on for the Swiftype Dashboard.
5
+ module SSO
6
+ BASE_URL = 'https://swiftype.com/sso'
7
+
8
+ # Generate a URL that a user can click on to be logged into the Swiftype Dashboard.
9
+ # This requires the +platform_client_id+ and +platform_client_secret+ configuration options be set.
10
+ def self.url(user_id)
11
+ timestamp = Time.now.to_i
12
+
13
+ "#{BASE_URL}?user_id=#{user_id}&client_id=#{Swiftype.platform_client_id}&timestamp=#{timestamp}&token=#{token(user_id, timestamp)}"
14
+ end
15
+
16
+ def self.token(user_id, timestamp)
17
+ Digest::SHA1.hexdigest("#{user_id}:#{Swiftype.platform_client_secret}:#{timestamp}")
18
+ end
19
+ end
20
+ end
@@ -1,3 +1,3 @@
1
1
  module Swiftype
2
- VERSION = "0.0.5"
2
+ VERSION = "1.0.0"
3
3
  end
@@ -0,0 +1,576 @@
1
+ require 'spec_helper'
2
+
3
+ describe Swiftype::Client do
4
+ let(:engine_slug) { 'swiftype-api-example' }
5
+ let(:client) { Swiftype::Client.new }
6
+
7
+ before :each do
8
+ Swiftype.api_key = 'hello'
9
+ end
10
+
11
+ context 'Search' do
12
+ context '#search' do
13
+ it 'searches all DocumentTypes in the engine' do
14
+ VCR.use_cassette(:engine_search) do
15
+ results = client.search(engine_slug, 'cat')
16
+ results.document_types.size.should == 2
17
+ results['videos'].size.should == 2
18
+ results['channels'].size.should == 1
19
+ end
20
+ end
21
+
22
+ it 'searches the engine with options' do
23
+ VCR.use_cassette(:engine_search_pagination) do
24
+ results = client.search(engine_slug, 'cat', {:page => 2})
25
+ results.document_types.size.should == 2
26
+ results['videos'].size.should == 0
27
+ results['channels'].size.should == 0
28
+ end
29
+ end
30
+
31
+ it 'includes facets when requested' do
32
+ VCR.use_cassette(:engine_search_facets) do
33
+ results = client.search(engine_slug, nil, {:facets => {:videos => ['category_id']}})
34
+ results.document_types.size.should == 2
35
+ results.facets('channels').should be_empty
36
+ results.facets('videos')['category_id'].should == {
37
+ "23" => 4,
38
+ "28" => 2,
39
+ "15" => 2,
40
+ "25" => 1,
41
+ "22" => 1,
42
+ "2" => 1,
43
+ "10" => 1
44
+ }
45
+ end
46
+ end
47
+ end
48
+
49
+ context '#search_document_type' do
50
+ let(:document_type_slug) { 'videos' }
51
+
52
+ it 'searches only the provided DocumentType' do
53
+ VCR.use_cassette(:document_type_search) do
54
+ results = client.search_document_type(engine_slug, document_type_slug, 'cat')
55
+ results.document_types.should == ['videos']
56
+ results['videos'].size.should == 2
57
+ end
58
+ end
59
+
60
+ it 'searches the DocumentType with options' do
61
+ VCR.use_cassette(:document_type_search_pagination) do
62
+ results = client.search_document_type(engine_slug, document_type_slug, 'cat', {:page => 2})
63
+ results.document_types.should == ['videos']
64
+ results[document_type_slug].size.should == 0
65
+ end
66
+ end
67
+ end
68
+ end
69
+
70
+ context 'Suggest' do
71
+ context '#suggest' do
72
+ it 'does prefix searches for all DocumentTypes in the engine' do
73
+ VCR.use_cassette(:engine_suggest) do
74
+ results = client.suggest(engine_slug, 'goo')
75
+ results.document_types.size.should == 2
76
+ results['videos'].size.should == 1
77
+ results['channels'].size.should == 1
78
+ end
79
+ end
80
+
81
+ it 'suggests for an engine with options' do
82
+ VCR.use_cassette(:engine_suggest_pagination) do
83
+ results = client.suggest(engine_slug, 'goo', {:page => 2})
84
+ results.document_types.size.should == 2
85
+ results['videos'].size.should == 0
86
+ results['channels'].size.should == 0
87
+ end
88
+ end
89
+ end
90
+
91
+ context '#suggest_document_type' do
92
+ let(:document_type_slug) { 'videos' }
93
+
94
+ it 'does a prefix search on the provided DocumentType' do
95
+ VCR.use_cassette(:document_type_suggest) do
96
+ results = client.suggest_document_type(engine_slug, document_type_slug, 'goo')
97
+ results.document_types.should == ['videos']
98
+ results['videos'].size.should == 1
99
+ end
100
+ end
101
+
102
+ it 'suggests for a document types with options' do
103
+ VCR.use_cassette(:document_type_suggest_pagination) do
104
+ results = client.suggest_document_type(engine_slug, document_type_slug, 'goo', {:page => 2})
105
+ results.document_types.should == ['videos']
106
+ results[document_type_slug].size.should == 0
107
+ end
108
+ end
109
+ end
110
+ end
111
+
112
+
113
+ context 'Engine' do
114
+ it 'gets all engines' do
115
+ VCR.use_cassette(:list_engines) do
116
+ engines = client.engines
117
+ engines.size.should == 6
118
+ end
119
+ end
120
+
121
+ it 'gets an engine' do
122
+ VCR.use_cassette(:find_engine) do
123
+ engine = client.engine(engine_slug)
124
+ engine['slug'].should == engine_slug
125
+ end
126
+ end
127
+
128
+ it 'creates engines' do
129
+ VCR.use_cassette(:create_engine) do
130
+ engine = client.create_engine('new engine from spec')
131
+ engine['slug'].should == 'new-engine-from-spec'
132
+ end
133
+ end
134
+
135
+ it 'destroys engines' do
136
+ VCR.use_cassette(:destroy_engine) do
137
+ response = client.destroy_engine('new-engine-from-spec')
138
+ response.should be_nil
139
+ end
140
+ end
141
+ end
142
+
143
+ context 'DocumentType' do
144
+ let(:document_type_slug) { 'videos' }
145
+
146
+ it 'gets all document types' do
147
+ VCR.use_cassette(:list_document_type) do
148
+ document_types = client.document_types(engine_slug)
149
+ document_types.size.should == 2
150
+ document_types.map { |dt| dt['name'] }.sort.should == ['channels', 'videos']
151
+ end
152
+ end
153
+
154
+ it 'gets a document type' do
155
+ VCR.use_cassette(:find_document_type) do
156
+ document_type = client.document_type(engine_slug, document_type_slug)
157
+ document_type['slug'].should == document_type_slug
158
+ end
159
+ end
160
+
161
+ it 'creates a document type' do
162
+ VCR.use_cassette(:create_document_type) do
163
+ name = document_type_slug
164
+ document_type = client.create_document_type(engine_slug, 'new_doc_type')
165
+ document_type['name'].should == 'new_doc_type'
166
+ document_type['slug'].should == 'new-doc-type'
167
+ end
168
+ end
169
+
170
+ it 'destroys document types' do
171
+ VCR.use_cassette(:destroy_document_type) do
172
+ response = client.destroy_document_type(engine_slug, 'new-doc-type')
173
+ response.should be_nil
174
+ end
175
+ end
176
+
177
+ it 'raises an error if deleting a non-existent DocumentType' do
178
+ VCR.use_cassette(:destroy_non_existent_document_type) do
179
+ expect do
180
+ response = client.destroy_document_type(engine_slug, 'not_there')
181
+ end.to raise_error
182
+ end
183
+ end
184
+ end
185
+
186
+ context 'Document' do
187
+ let(:document_type_slug) { 'videos' }
188
+ let(:document_id) { 'FtX8nswnUKU'}
189
+ let(:documents) do
190
+ [{'external_id'=>'INscMGmhmX4',
191
+ 'fields' => [{'name'=>'url', 'value'=>'http://www.youtube.com/watch?v=v1uyQZNg2vE', 'type'=>'enum'},
192
+ {'name'=>'thumbnail_url', 'value'=>'https://i.ytimg.com/vi/INscMGmhmX4/mqdefault.jpg', 'type'=>'enum'},
193
+ {'name'=>'channel_id', 'value'=>'UCTzVrd9ExsI3Zgnlh3_btLg', 'type'=>'enum'},
194
+ {'name'=>'title', 'value'=>'The Original Grumpy Cat', 'type'=>'string'},
195
+ {'name'=>'category_name', 'value'=>'Pets &amp; Animals', 'type'=>'string'}]},
196
+ {'external_id'=>'XfY9Dsg_DZk',
197
+ 'fields' => [{'name'=>'url', 'value'=>'http://www.youtube.com/watch?v=XfY9Dsg_DZk', 'type'=>'enum'},
198
+ {'name'=>'thumbnail_url', 'value'=>'https://i.ytimg.com/vi/XfY9Dsg_DZk/mqdefault.jpg', 'type'=>'enum'},
199
+ {'name'=>'channel_id', 'value'=>'UC5VA5j05FjETg-iLekcyiBw', 'type'=>'enum'},
200
+ {'name'=>'title', 'value'=>'Corgi talks to cat', 'type'=>'string'},
201
+ {'name'=>'category_name', 'value'=>'Pets &amp; Animals', 'type'=>'string'}]}]
202
+ end
203
+
204
+ it 'lists documents in a document type' do
205
+ VCR.use_cassette(:list_documents) do
206
+ documents = client.documents(engine_slug, document_type_slug)
207
+ documents.size.should == 2
208
+ end
209
+ end
210
+
211
+ it 'lists documents with pagination' do
212
+ VCR.use_cassette(:list_documents_with_pagination) do
213
+ documents = client.documents(engine_slug, document_type_slug, 2, 2)
214
+ documents.size.should == 2
215
+ end
216
+ end
217
+
218
+ it 'find a document' do
219
+ VCR.use_cassette(:find_document) do
220
+ document = client.document(engine_slug, document_type_slug, document_id)
221
+ document['external_id'].should == document_id
222
+ end
223
+ end
224
+
225
+ it 'creates a document' do
226
+ VCR.use_cassette(:create_document) do
227
+ document = client.create_document(engine_slug, document_type_slug, documents.first)
228
+ document['external_id'].should == 'INscMGmhmX4'
229
+ end
230
+ end
231
+
232
+ it 'bulk create multiple documents' do
233
+ VCR.use_cassette(:bulk_create_documents) do
234
+ response = client.create_documents(engine_slug, document_type_slug, documents)
235
+ response.should == [true, true]
236
+ end
237
+ end
238
+
239
+ it 'destroys a document' do
240
+ VCR.use_cassette(:destroy_document) do
241
+ response = client.destroy_document(engine_slug, document_type_slug, 'INscMGmhmX4')
242
+ response.should be_nil
243
+ end
244
+ end
245
+
246
+ it 'destroys multiple documents' do
247
+ VCR.use_cassette(:bulk_destroy_documents) do
248
+ response = client.destroy_documents(engine_slug, document_type_slug, ['INscMGmhmX4', 'XfY9Dsg_DZk'])
249
+ response.should == [true, true]
250
+ end
251
+ end
252
+
253
+ context '#create_or_update_document' do
254
+ it 'creates a document' do
255
+ VCR.use_cassette(:create_or_update_document_create) do
256
+ response = client.create_or_update_document(engine_slug, document_type_slug, {:external_id => 'foobar', :fields => [{:type => :string, :name => 'title', :value => 'new document'}]})
257
+ response['external_id'].should == 'foobar'
258
+ response['title'].should == 'new document'
259
+ end
260
+ end
261
+
262
+ it 'updates an existing document' do
263
+ VCR.use_cassette(:create_or_update_document_update) do
264
+ response = client.create_or_update_document(engine_slug, document_type_slug, {:external_id => document_id, :fields => [{:type => :string, :name => 'title', :value => 'new title'}]})
265
+ response['external_id'].should == document_id
266
+ response['title'].should == 'new title'
267
+ end
268
+ end
269
+ end
270
+
271
+ context '#bulk_create_or_update_documents' do
272
+ it 'returns true for all documents successfully created or updated' do
273
+ VCR.use_cassette(:bulk_create_or_update_documents_success) do
274
+ response = client.create_or_update_documents(engine_slug, document_type_slug, documents)
275
+ response.should == [true, true]
276
+ end
277
+ end
278
+
279
+ it 'returns false if a document cannot be created or updated due to an error' do
280
+ documents = [{:external_id => 'failed_doc', :fields => [{:type => :string, :name => :title}]}] # missing value
281
+
282
+ VCR.use_cassette(:bulk_create_or_update_documents_failure) do
283
+ response = client.create_or_update_documents(engine_slug, document_type_slug, documents)
284
+ response.should == [false]
285
+ end
286
+ end
287
+ end
288
+
289
+ context '#update_document' do
290
+ it 'updates a document given its id and fields to update' do
291
+ fields = {:title => 'awesome new title', :channel_id => 'UC5VA5j05FjETg-iLekcyiBw'}
292
+ VCR.use_cassette(:update_document) do
293
+ response = client.update_document(engine_slug, document_type_slug, document_id, fields)
294
+ response['external_id'].should == document_id
295
+ response['title'].should == 'awesome new title'
296
+ response['channel_id'].should == 'UC5VA5j05FjETg-iLekcyiBw'
297
+ end
298
+ end
299
+
300
+ it 'raises an error if a unknown field is included' do
301
+ fields = {:not_a_field => 'not a field'}
302
+
303
+ VCR.use_cassette(:update_document_unknown_field_failure) do
304
+ expect do
305
+ response = client.update_document(engine_slug, document_type_slug, document_id, fields)
306
+ end.to raise_error
307
+ end
308
+ end
309
+ end
310
+
311
+ context "#update_documents" do
312
+ it 'returns true for each document successfully updated' do
313
+ documents = [{:external_id => 'INscMGmhmX4', :fields => {:title => 'hi'}}, {:external_id => 'XfY9Dsg_DZk', :fields => {:title => 'bye'}}]
314
+
315
+ VCR.use_cassette(:update_documents_success) do
316
+ response = client.update_documents(engine_slug, document_type_slug, documents)
317
+ response.should == [true, true]
318
+ end
319
+ end
320
+
321
+ it 'returns false if document is not successfully updated' do
322
+ documents = [{:external_id => 'not_there', :fields => [{:name => :title, :value => 'hi', :type => :string}]}]
323
+
324
+ VCR.use_cassette(:update_documents_failure_non_existent_document) do
325
+ response = client.update_documents(engine_slug, document_type_slug, documents)
326
+ response.should == [false]
327
+ end
328
+ end
329
+ end
330
+ end
331
+
332
+ context 'Analytics' do
333
+ let(:engine_slug) { 'recursion' }
334
+
335
+ context '#analytics_searches' do
336
+ it 'returns search counts for the default time frame' do
337
+ VCR.use_cassette(:analytics_searches) do
338
+ searches = client.analytics_searches(engine_slug)
339
+ searches.size.should == 15 # FIXME: is this a bug in the API?
340
+ searches.first.should == ['2013-09-13', 0]
341
+ end
342
+ end
343
+
344
+ it 'returns search counts for a specified time range' do
345
+ VCR.use_cassette(:analytics_searches_with_time_range) do
346
+ searches = client.analytics_searches(engine_slug, :start_date => '2013-01-01', :end_date => '2013-01-07')
347
+ searches.size.should == 7
348
+ searches.first.should == ['2013-01-07', 0]
349
+ end
350
+ end
351
+
352
+ it 'returns search counts for a specified DocumentType' do
353
+ VCR.use_cassette(:analytics_searchs_with_document_type) do
354
+ searches = client.analytics_searches(engine_slug, :document_type_id => 'page')
355
+ searches.size.should == 15
356
+ searches.first.should == ['2013-09-16', 0]
357
+ end
358
+ end
359
+
360
+ it 'returns search counts for a specified DocumentType and time range' do
361
+ VCR.use_cassette(:analytics_searches_with_document_type_and_time_range) do
362
+ searches = client.analytics_autoselects(engine_slug, :document_type_id => 'page', :start_date => '2013-07-01', :end_date => '2013-07-07')
363
+ searches.size.should == 7
364
+ searches.first.should == ['2013-07-07', 0]
365
+ end
366
+ end
367
+ end
368
+
369
+ context '#analytics_autoselects' do
370
+ it 'returns autoselect counts for the default time frame' do
371
+ VCR.use_cassette(:analytics_autoselects) do
372
+ autoselects = client.analytics_autoselects(engine_slug)
373
+ autoselects.size.should == 15
374
+ autoselects.first.should == ['2013-09-13', 0]
375
+ end
376
+ end
377
+
378
+ it 'returns autoselects counts for a specified time range' do
379
+ VCR.use_cassette(:analytics_autoselects_with_time_range) do
380
+ autoselects = client.analytics_autoselects(engine_slug, :start_date => '2013-07-01', :end_date => '2013-07-07')
381
+ autoselects.size.should == 7
382
+ end
383
+ end
384
+
385
+ it 'returns autoselect counts for a specified DocumentType' do
386
+ VCR.use_cassette(:analytics_autoselects_with_document_type) do
387
+ autoselects = client.analytics_autoselects(engine_slug, :document_type_id => 'page')
388
+ autoselects.size.should == 15
389
+ end
390
+ end
391
+
392
+ it 'returns autoselect counts for a specified DocumentType and time range' do
393
+ VCR.use_cassette(:analytics_autoselects_with_document_type_and_time_range) do
394
+ autoselects = client.analytics_autoselects(engine_slug, :document_type_id => 'page', :start_date => '2013-07-01', :end_date => '2013-07-07')
395
+ autoselects.size.should == 7
396
+ end
397
+ end
398
+ end
399
+
400
+ context '#analytics_clicks' do
401
+ it 'returns click counts for the default time frame' do
402
+ VCR.use_cassette(:analytics_clicks) do
403
+ clicks = client.analytics_clicks(engine_slug)
404
+ clicks.size.should == 15
405
+ clicks.first.should == ['2013-09-17', 0]
406
+ end
407
+ end
408
+
409
+ it 'returns clicks counts for a specified time range' do
410
+ VCR.use_cassette(:analytics_clicks_with_time_range) do
411
+ clicks = client.analytics_clicks(engine_slug, :start_date => '2013-07-01', :end_date => '2013-07-07')
412
+ clicks.size.should == 7
413
+ clicks.first.should == ['2013-07-07', 0]
414
+ end
415
+ end
416
+
417
+ it 'returns click counts for a specified DocumentType' do
418
+ VCR.use_cassette(:analytics_clicks_with_document_type) do
419
+ clicks = client.analytics_clicks(engine_slug, :document_type_id => 'page')
420
+ clicks.size.should == 15
421
+ end
422
+ end
423
+
424
+ it 'returns click counts for a specified DocumentType and time range' do
425
+ VCR.use_cassette(:analytics_clicks_with_document_type_and_time_range) do
426
+ clicks = client.analytics_clicks(engine_slug, :document_type_id => 'page', :start_date => '2013-07-01', :end_date => '2013-07-07')
427
+ clicks.size.should == 7
428
+ clicks.first.should == ['2013-07-07', 0]
429
+ end
430
+ end
431
+ end
432
+
433
+ context '#analytics_top_queries' do
434
+ it 'returns top queries' do
435
+ VCR.use_cassette(:analytics_top_queries) do
436
+ top_queries = client.analytics_top_queries(engine_slug)
437
+ top_queries.size.should == 3
438
+ top_queries.first.should == ['"fire watch"', 1]
439
+ end
440
+ end
441
+
442
+ it 'returns top queries with pagination' do
443
+ VCR.use_cassette(:analytics_top_queries_paginated) do
444
+ top_queries = client.analytics_top_queries(engine_slug, :start_date => '2013-08-01', :end_date => '2013-08-30', :per_page => 5, :page => 2)
445
+ top_queries.size.should == 5
446
+ top_queries.first.should == ['no simple victory', 1]
447
+ end
448
+ end
449
+
450
+ it 'raises an error if the timeframe is to large' do
451
+ VCR.use_cassette(:analytics_top_queries_too_large) do
452
+ expect do
453
+ top_queries = client.analytics_top_queries(engine_slug, :start_date => '2013-01-01', :end_date => '2013-05-01')
454
+ end.to raise_error(Swiftype::BadRequest)
455
+ end
456
+ end
457
+ end
458
+
459
+ context 'analytics_top_no_result_queries' do
460
+ it 'returns top queries with no results for the default time range' do
461
+ VCR.use_cassette(:analytics_top_no_result_queries) do
462
+ top_no_result_queries = client.analytics_top_no_result_queries(engine_slug)
463
+ top_no_result_queries.size.should == 2
464
+ top_no_result_queries.first.should == ['no results', 10]
465
+ end
466
+ end
467
+
468
+ it 'has top no result queries in date ranges' do
469
+ VCR.use_cassette(:analytics_top_no_result_queries_paginated) do
470
+ top_no_result_queries = client.analytics_top_no_result_queries(engine_slug, :start_date => '2013-08-01', :end_date => '2013-08-30', :per_page => 5, :page => 2)
471
+ top_no_result_queries.size.should == 1
472
+ top_no_result_queries.first.should == ['no result again', 2]
473
+ end
474
+ end
475
+ end
476
+ end
477
+
478
+ context 'Domain' do
479
+ let(:engine_slug) { 'crawler-demo-site' }
480
+ let(:domain_id) { '51534c6e2ed960cc79000001' }
481
+
482
+ it 'gets all domains' do
483
+ VCR.use_cassette(:list_domains) do
484
+ domains = client.domains(engine_slug)
485
+ domains.size.should == 1
486
+ domains.first['id'].should == domain_id
487
+ end
488
+ end
489
+
490
+ context '#domain' do
491
+ it 'shows a domain if it exists' do
492
+ VCR.use_cassette(:find_domain) do
493
+ domain = client.domain(engine_slug, domain_id)
494
+ domain['id'].should == domain_id
495
+ end
496
+ end
497
+
498
+ it 'raises an error if the domain does not exist' do
499
+ VCR.use_cassette(:find_domain_failure) do
500
+ expect do
501
+ domain = client.domain(engine_slug, 'bogus')
502
+ end.to raise_error(Swiftype::NonExistentRecord)
503
+ end
504
+ end
505
+ end
506
+
507
+ context '#create_domain' do
508
+ it 'creates a domain' do
509
+ VCR.use_cassette(:create_domain) do
510
+ url = 'http://www.zombo.com/'
511
+ domain = client.create_domain(engine_slug, url)
512
+ domain['submitted_url'].should == url
513
+ end
514
+ end
515
+ end
516
+
517
+ it 'destroys a domain' do
518
+ VCR.use_cassette(:destroy_domain) do
519
+ response = client.destroy_domain(engine_slug, '52324b132ed960589800004a')
520
+ response.should be_nil
521
+ end
522
+ end
523
+
524
+ context '#recrawl_domain' do
525
+ it 'enqueues a request to recrawl a domain' do
526
+ VCR.use_cassette(:recrawl_domain_success) do
527
+ domain = client.recrawl_domain(engine_slug, domain_id)
528
+ domain['id'].should == domain_id
529
+ end
530
+ end
531
+
532
+ it 'raises an exception if domain recrawl is not allowed' do
533
+ VCR.use_cassette(:recrawl_domain_failure) do
534
+ expect do
535
+ domain = client.recrawl_domain(engine_slug, domain_id)
536
+ end.to raise_error(Swiftype::Forbidden)
537
+ end
538
+ end
539
+ end
540
+
541
+ context '#crawl_url' do
542
+ it 'enqueues a request to crawl a URL on a domain' do
543
+ VCR.use_cassette(:crawl_url) do
544
+ url = 'http://crawler-demo-site.herokuapp.com/2012/01/01/first-post.html'
545
+ crawled_url = client.crawl_url(engine_slug, domain_id, url)
546
+ crawled_url['url'].should == url
547
+ end
548
+ end
549
+ end
550
+ end
551
+
552
+ context 'Clickthrough' do
553
+ let(:query) { 'foo' }
554
+ let(:document_type_slug) { 'videos' }
555
+ let(:external_id) { 'FtX8nswnUKU'}
556
+
557
+ context "#log_clickthough" do
558
+ # Not thrilled with this test, but since nothing is returned all we
559
+ # can reasonably check is that an error isn't raised
560
+ it 'returns nil' do
561
+ VCR.use_cassette(:log_clickthrough_success) do
562
+ response = client.log_clickthrough(engine_slug, document_type_slug, query, external_id)
563
+ response.should == nil
564
+ end
565
+ end
566
+
567
+ it 'raises an error when missing params' do
568
+ VCR.use_cassette(:log_clickthrough_failure) do
569
+ expect do
570
+ client.log_clickthrough(engine_slug, document_type_slug, nil, external_id)
571
+ end.to raise_error(Swiftype::BadRequest)
572
+ end
573
+ end
574
+ end
575
+ end
576
+ end