swiftype 0.0.5 → 1.0.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 (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