acceleration 0.0.17

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: c83f243d7a8d0d21a3f97522c0b465628d07d0ff
4
+ data.tar.gz: a47dc95e3a994f0ffc5ef1d13a24584bbde040d9
5
+ SHA512:
6
+ metadata.gz: 8c4502e59c604b8f23dd9bc20290b0e6b55bb8ef11083d49037d5157af2761a0ca02a8f4b795aee89732ce50c8322b2a072aa15d3be999f78ba5cd6f0e00969a
7
+ data.tar.gz: 55dbd72a3eaa6d674e766166d23ccaf650cd02b99210ba52ef44643b4128882ddf36ac367f8136e0b3aca09bc3e74f7d33f72bf69396a8c9199cdc5f3dd3da3e
@@ -0,0 +1,7 @@
1
+ *.gem
2
+ .bundle
3
+ Gemfile.lock
4
+ pkg/*
5
+ *~
6
+ *.swp
7
+ doc
@@ -0,0 +1,51 @@
1
+ # This configuration was generated by
2
+ # `rubocop --auto-gen-config`
3
+ # on 2016-12-27 15:51:09 -0500 using RuboCop version 0.46.0.
4
+ # The point is for the user to remove these configuration records
5
+ # one by one as the offenses are removed from the code base.
6
+ # Note that changes in the inspected code, or installation of new
7
+ # versions of RuboCop, may require this file to be generated again.
8
+
9
+ # Offense count: 2
10
+ Metrics/AbcSize:
11
+ Max: 20
12
+
13
+ # Offense count: 1
14
+ # Configuration parameters: CountComments.
15
+ Metrics/BlockLength:
16
+ Max: 26
17
+
18
+ # Offense count: 1
19
+ # Configuration parameters: CountComments.
20
+ Metrics/ClassLength:
21
+ Max: 120
22
+
23
+ # Offense count: 4
24
+ # Configuration parameters: AllowHeredoc, AllowURI, URISchemes, IgnoreCopDirectives, IgnoredPatterns.
25
+ # URISchemes: http, https
26
+ Metrics/LineLength:
27
+ Max: 104
28
+
29
+ # Offense count: 2
30
+ # Configuration parameters: CountComments.
31
+ Metrics/MethodLength:
32
+ Max: 15
33
+
34
+ # Offense count: 1
35
+ Metrics/PerceivedComplexity:
36
+ Max: 8
37
+
38
+ # Offense count: 1
39
+ Style/AccessorMethodName:
40
+ Exclude:
41
+ - 'lib/acceleration/velocity.rb'
42
+
43
+ # Offense count: 2
44
+ # Configuration parameters: NamePrefix, NamePrefixBlacklist, NameWhitelist.
45
+ # NamePrefix: is_, has_, have_
46
+ # NamePrefixBlacklist: is_, has_, have_
47
+ # NameWhitelist: is_a?
48
+ Style/PredicateName:
49
+ Exclude:
50
+ - 'spec/**/*'
51
+ - 'lib/acceleration/velocity.rb'
@@ -0,0 +1 @@
1
+ acceleration
@@ -0,0 +1 @@
1
+ 2.0.0
data/.semver ADDED
@@ -0,0 +1,5 @@
1
+ ---
2
+ :major: 0
3
+ :minor: 0
4
+ :patch: 17
5
+ :special: ''
data/Gemfile ADDED
@@ -0,0 +1,2 @@
1
+ source 'https://rubygems.org'
2
+ gemspec
@@ -0,0 +1,25 @@
1
+ filter(/\.txt$/, /.*\.zip/)
2
+
3
+ notification :gntp
4
+
5
+ guard :bundler do
6
+ watch 'Gemfile'
7
+ watch(/\.gemspec$/)
8
+ end
9
+
10
+ group :red_green_refactor, halt_on_fail: true do
11
+ # guard :rspec,
12
+ # cmd: 'bundle exec rspec',
13
+ # failed_mode: :keep do
14
+ # watch 'spec/spec_helper.rb'
15
+ # watch(/^spec\/.+_spec\.rb/)
16
+ # watch(/^lib\/(.+)\.rb/)
17
+ # end
18
+
19
+ guard :rubocop do
20
+ watch(/.+\.rb$/)
21
+ watch(%r{/(?:.+\/)?\.rubocop\.yml$/}) { |m| File.dirname(m[0]) }
22
+ end
23
+ end
24
+
25
+ scope group: :red_green_refactor
@@ -0,0 +1,7 @@
1
+ Copyright 2016 IBM Corporation
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
4
+
5
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
6
+
7
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,60 @@
1
+ Acceleration
2
+ ============
3
+
4
+ A succinct interface to the IBM Watson Explorer Foundational Components Engine REST API
5
+
6
+ by Colin Dean <colindean@us.ibm.com>
7
+
8
+ Introduction
9
+ ------------
10
+
11
+ Acceleration provides a succinct, ActiveResource-style interface to a IBM Watson Explorer Foundational Components (WEX-FC) Engine search platform instance's REST API.
12
+
13
+ The name comes from WEX-FC's pre-acquisition name, Vivísimo Velocity. Acceleration is derived from Velocity. Get it?
14
+
15
+ License
16
+ -------
17
+
18
+ This library is property of IBM Corporation and licensed under the MIT license.
19
+ See LICENSE.md for license terms.
20
+
21
+ (C) Copyright IBM Corporation. 2012-2016. AWSOM WAT056420161228.
22
+
23
+ Installation
24
+ ------------
25
+
26
+ _to be completed_
27
+
28
+ Contributing
29
+ ------------
30
+
31
+ Please test all changes against Ruby 1.9.3+ and JRuby 1.7+. Proper testing
32
+ infrastructure is more than welcome!
33
+
34
+ ### Getting started
35
+
36
+ Check out the source:
37
+
38
+ git clone git@github.com:Watson-Explorer/acceleration-ruby.git
39
+ cd acceleration
40
+
41
+ Install dependencies:
42
+
43
+ gem install bundler
44
+ bundle install
45
+
46
+ Generate documentation:
47
+
48
+ rake doc
49
+
50
+ Now you're clear for hacking. Open the docs with `open doc/index.html` to learn
51
+ how to use it. The top-level class is actually **Velocity**.
52
+
53
+ ### Releasing
54
+
55
+ Acceleration uses semantic versioning. Once all work for a version is
56
+ committed, increment the version number in lib/acceleration/version.rb and
57
+ execute `semver inc patch`, or whatever else is appropriate for the release.
58
+ Then, commit the changes to `lib/acceleration/version.rb` and `.semver` with
59
+ `git commit -a -m "version $(semver tag)"` and then tag it with `git tag
60
+ $(semver tag)`.
@@ -0,0 +1,16 @@
1
+ require 'bundler'
2
+ Bundler::GemHelper.install_tasks
3
+
4
+ require 'rubocop/rake_task'
5
+
6
+ RuboCop::RakeTask.new
7
+
8
+ desc 'Open an IRB session preloaded with Acceleration'
9
+ task :console do
10
+ sh 'pry -rubygems -I lib -racceleration'
11
+ end
12
+ desc 'Compile documentation using RDoc'
13
+ task :doc do
14
+ sh 'rdoc --main Velocity lib'
15
+ end
16
+
@@ -0,0 +1,35 @@
1
+ # -*- encoding: utf-8 -*-
2
+ $LOAD_PATH.push File.expand_path '../lib', __FILE__
3
+ require 'acceleration/version'
4
+
5
+ Gem::Specification.new do |s|
6
+ s.name = 'acceleration'
7
+ s.version = Acceleration::VERSION
8
+ s.platform = Gem::Platform::RUBY
9
+ s.authors = ['Colin Dean']
10
+ s.email = ['colindean@us.ibm.com']
11
+ s.homepage = 'https://github.com/watson-explorer/acceleration-ruby'
12
+ product_name = 'IBM Watson Explorer Foundational Components Engine'
13
+ s.summary = "A succinct interface to to the #{product_name} REST API"
14
+ s.description = <<-END.gsub(/^ {6}/, '')
15
+ Acceleration provides a succinct, ActiveResource-style interface to a the
16
+ #{product_name} search platform instance's REST API. Acceleration is
17
+ derived from Velocity, the original name for Engine.
18
+ END
19
+
20
+ ['nokogiri', 'rest-client'].each { |d| s.add_runtime_dependency d }
21
+ %w(semver pry bundler rake).each do |version_unspecified|
22
+ s.add_development_dependency version_unspecified
23
+ end
24
+
25
+ s.add_development_dependency 'guard', '~> 2.14.0'
26
+ s.add_development_dependency 'guard-bundler', '~> 2.1.0'
27
+ s.add_development_dependency 'guard-rubocop', '~> 1.2.0'
28
+ s.add_development_dependency 'ruby_gntp', '~> 0.3.0'
29
+
30
+ s.files = `git ls-files`.split("\n")
31
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
32
+ s.executables = `git ls-files -- bin/*`
33
+ .split("\n").map { |f| File.basename(f) }
34
+ s.require_paths = ['lib']
35
+ end
@@ -0,0 +1,17 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ begin
4
+ require 'acceleration'
5
+ rescue
6
+ require 'rubygems'
7
+ require 'acceleration'
8
+ end
9
+
10
+ require 'irb'
11
+ require 'irb/completion'
12
+ ENV['IRBRC'] = '.irbrc' if File.exist? '.irbrc'
13
+
14
+ ARGV.clear
15
+
16
+ IRB.start
17
+ exit!
@@ -0,0 +1,3 @@
1
+ # Licensed materials property of IBM Corporation.
2
+ # (C) Copyright IBM Corporation. 2012-2016.
3
+ require 'acceleration/velocity'
@@ -0,0 +1,46 @@
1
+ # Licensed materials property of IBM Corporation.
2
+ # (C) Copyright IBM Corporation. 2012-2016.
3
+
4
+ ##
5
+ # Monkeypatches on String to provide convenience methods
6
+ #
7
+ # _Warning:_ these could go away at any time, so do not rely on their continued
8
+ # existence. They should only be used internally within the Acceleration gem.
9
+ class String
10
+ ##
11
+ # Convenience function for converting Ruby method names to Velocity API
12
+ # method names by replacing underscores with dashes.
13
+ #
14
+ def dasherize
15
+ downcase.tr('_', '-')
16
+ end
17
+
18
+ ##
19
+ # Convenience function for converting Velocity API method names to Ruby
20
+ # method or symbol names.
21
+ def dedasherize
22
+ tr('-', '_')
23
+ end
24
+ end
25
+
26
+ ##
27
+ # Monkeypatches on Symbol to provide convenience methods
28
+ #
29
+ # _Warning:_ these could go away at any time, so do not rely on their continued
30
+ # existence. They should only be used internally within the Acceleration gem.
31
+ class Symbol
32
+ ##
33
+ # Convenience function for converting Ruby method names to Velocity API
34
+ # method names by replacing underscores with dashes.
35
+ #
36
+ def dasherize
37
+ to_s.downcase.tr('_', '-').to_sym
38
+ end
39
+
40
+ ##
41
+ # Convenience function for converting Velocity API method names to Ruby
42
+ # method or symbol names.
43
+ def dedasherize
44
+ to_s.tr('-', '_').to_sym
45
+ end
46
+ end
@@ -0,0 +1,1342 @@
1
+ require 'logger'
2
+ require 'restclient'
3
+ require 'nokogiri'
4
+ require 'acceleration/monkeypatches'
5
+ ##
6
+ # :main:Acceleration
7
+ # == Acceleration
8
+ #
9
+ # by Colin Dean <colindean@us.ibm.com>
10
+ #
11
+ # Licensed materials property of IBM Corporation.
12
+ # (C) Copyright IBM Corporation. 2012-2016.
13
+ #
14
+ # For services and training around this library, please contact an IBM Watson
15
+ # Solution Architect.
16
+ #
17
+ # == Introduction
18
+ #
19
+ # *Acceleration* exists to provide a simple, object-oriented interface to
20
+ # a Vivisimo Velocity search platform instance in an interface familiar to
21
+ # Rubyists. It communicates with the instance via REST and parses the responses
22
+ # using Nokogiri, a very fast and well-tested XML library.
23
+ #
24
+ # Acceleration is derived from Velocity ;-)
25
+ #
26
+ # == Interface
27
+ #
28
+ # Acceleration makes an effort to provide an ActiveRecord-style interface while
29
+ # still allowing the end user to access directly the XML returned from the
30
+ # Velocity API. Acceleration wraps most of the responses in a convenient object
31
+ # which provides a series a methods to accelerate development using the
32
+ # Velocity API.
33
+ #
34
+ # i = Velocity::Instance.new endpoint: api_endpoint,
35
+ # username: api_username,
36
+ # password: api-password
37
+ # if i.ping
38
+ # puts "Working"
39
+ # i.collections.each do |c|
40
+ # puts c.status.crawler.elapsed_time
41
+ # end
42
+ # end
43
+ # c = i.collection("wiki") # => <#Velocity::API::Collection name=wiki>
44
+ #
45
+ # c.status.crawler.n_docs if c.status.has_data? # => get the number of docs
46
+ #
47
+ # == Reference material:
48
+ # * http://rdoc.info/github/archiloque/rest-client/master/file/README.rdoc
49
+ # * http://nokogiri.org/Nokogiri.html
50
+ #
51
+ module Velocity
52
+ ##
53
+ # Velocity::Logger is just an instance of stdlib Logger. It can be easily
54
+ # replaced by Rails logger or whatever
55
+ Logger = ::Logger.new(STDERR)
56
+ Logger.level = ::Logger::WARN
57
+
58
+ ##
59
+ # Models the instance. This is the top level object in the Velocity API. It
60
+ # models the actual Velocity server or instance.
61
+ #
62
+ class Instance
63
+ # The v_app in use. Defaults to +api-rest+.
64
+ attr_accessor :v_app
65
+ # The URL of the velocity CGI application on the instance.
66
+ attr_accessor :endpoint
67
+ # The username for the API user. Create this in the Admin Tool.
68
+ attr_accessor :username
69
+ # The password of the user.
70
+ attr_accessor :password
71
+ # How long Acceleration should wait for a response. Default is 120 seconds.
72
+ attr_accessor :read_timeout
73
+ # How long Acceleration should wait to connect to the instance. Default is
74
+ # 30 seconds.
75
+ attr_accessor :open_timeout
76
+ # The error a ping encounters.
77
+ attr_reader :error
78
+
79
+ ##
80
+ # call-seq:
81
+ # new(:endpoint => endpoint, :username => username, :password => password)
82
+ #
83
+ # Create a new instance of Instance. This is the model central to the gem.
84
+ # It facilitates all communication with the Velocity instance.
85
+ #
86
+ # Args passed in as a hash may include any attributes except +:error+.
87
+ #
88
+ def initialize(args)
89
+ @v_app = args[:v_app] || 'api-rest'
90
+ @endpoint = args[:endpoint]
91
+ @username = args[:username]
92
+ @password = args[:password]
93
+ @read_timeout = args[:read_timeout] || 120
94
+ @open_timeout = args[:open_timeout] || 30
95
+ end
96
+
97
+ ##
98
+ # Prepare and eventually execute a Velocity API function call.
99
+ #
100
+ # This function is generally meant to be called from within
101
+ # APIModel#method_missing, but a method can call it directly if something
102
+ # special must be done with the returned Nokogiri::XML object. The classes
103
+ # that do that generally wrap around the object to provide convenience
104
+ # methods.
105
+ #
106
+ def call(function, args = {})
107
+ sanity_check
108
+ Logger.info "calling #{function} with args: #{args}"
109
+ if (args.class == Array) && args.empty?
110
+ args = {}
111
+ elsif !args.empty? && (args.first.class == Hash)
112
+ args = args.first
113
+ end
114
+ params = base_parameters.merge({ 'v.function' => function }.merge(args))
115
+ result = Nokogiri::XML(rest_call(params))
116
+ raise VelocityException, result if VelocityException.exception? result
117
+ @error = nil
118
+ result
119
+ end
120
+
121
+ ##
122
+ # Perform the actual REST action
123
+ #
124
+ def rest_call(params)
125
+ # restclient stupidly puts query params in the...headers?
126
+ req = { method: :get, url: endpoint, headers: { params: params } }
127
+ req[:timeout] = read_timeout if read_timeout
128
+ req[:open_timeout] = open_timeout if open_timeout
129
+ Logger.info "#hitting #{endpoint} with params: #{clean_password(params.clone)}"
130
+ begin
131
+ RestClient::Request.execute(req)
132
+ rescue RestClient::RequestURITooLong => e
133
+ Logger.info "Server says #{e}, retrying with POST..."
134
+ # try a post. I don't like falling back like this, but pretty much
135
+ # everything but repository actions will be under the standard limit
136
+ req.delete(:headers)
137
+ req[:payload] = params
138
+ req[:method] = :post
139
+ RestClient::Request.execute(req)
140
+ end
141
+ end
142
+
143
+ def clean_password(params_hash)
144
+ params_hash.each_pair do |key, value|
145
+ params_hash[key] = if key.to_s.include? 'password'
146
+ 'md5:' + Digest::MD5.hexdigest(value)
147
+ else
148
+ value
149
+ end
150
+ end
151
+ params_hash
152
+ end
153
+ private :clean_password
154
+
155
+ ##
156
+ # Assemble a hash with the basic parameters for the instance.
157
+ #
158
+ def base_parameters
159
+ { 'v.app' => v_app,
160
+ 'v.username' => username,
161
+ 'v.password' => password }
162
+ end
163
+
164
+ ##
165
+ # Perform a simple ping against the instance using the API function
166
+ # appropriately named "ping".
167
+ #
168
+ # If Instance#ping returns false, check Instance#error for the exception
169
+ # that was thrown. Instance#ping should always have a boolean return.
170
+ #
171
+ def ping
172
+ begin
173
+ n = call 'ping'
174
+ return true if n.root.name == 'pong'
175
+ rescue StandardError => e
176
+ @error = e
177
+ end
178
+ false
179
+ end
180
+
181
+ ##
182
+ # List all collections available on the instance.
183
+ #
184
+ def collections
185
+ n = call 'search-collection-list-xml'
186
+ n.xpath('/vse-collections/vse-collection').collect do |c|
187
+ # initialize a new one, set its instance to me
188
+ SearchCollection.new_from_xml(xml: c, instance: self)
189
+ end
190
+ end
191
+
192
+ ##
193
+ # Get just one collection
194
+ #
195
+ def collection(name)
196
+ c = SearchCollection.new(name)
197
+ c.instance = self
198
+ c
199
+ end
200
+
201
+ ##
202
+ # List all dictionaries available on the instance.
203
+ #
204
+ def dictionaries
205
+ n = call 'dictionary-list-xml'
206
+ n.xpath('/dictionaries/dictionary').collect do |d|
207
+ Dictionary.new_from_xml(xml: d, instance: self)
208
+ end
209
+ end
210
+
211
+ ##
212
+ # Ensure that all instance variables necessary to communicate with the API
213
+ # are set.
214
+ #
215
+ def sanity_check
216
+ raise ArgumentError, 'You must specify a v.app.' if v_app.nil?
217
+ raise ArgumentError, 'You must specify a username.' if username.nil?
218
+ raise ArgumentError, 'You must specify a password.' if password.nil?
219
+ raise ArgumentError, 'You must specify an endpoint.' if endpoint.nil?
220
+ end
221
+
222
+ ##
223
+ # Determine the AXL service status
224
+ #
225
+ # Optionally supply a +:pool+ option.
226
+ #
227
+ # TODO: implement response wrapper
228
+ #
229
+ def axl_service_status(args = {})
230
+ call __method__.dasherize, args
231
+ end
232
+
233
+ ##
234
+ # Write a list of feature environments to disk.
235
+ #
236
+ # Expects a +:environment_list+ option containing a list of environments
237
+ # and their IDs.
238
+ #
239
+ # TODO: implement response wrapper
240
+ #
241
+ def write_environment_list(args = {})
242
+ call __method__.dasherize, args
243
+ end
244
+
245
+ ##
246
+ # The APIModel is a very simple interface for building more complex API
247
+ # function models. It shouldn't ever be instantiated itself.
248
+ #
249
+ # TODO: refactor some of this method into something includable
250
+ #
251
+ class APIModel
252
+ # A handle on the instance
253
+ attr_accessor :instance
254
+ ##
255
+ # Create a new APIModel instance
256
+ #
257
+ def initialize(instance)
258
+ @instance = instance
259
+ end
260
+
261
+ ##
262
+ # Build the API function name based off the prefix and the desired
263
+ # operation.
264
+ #
265
+ def resolve(operation)
266
+ [prefix, operation.dasherize].join '-'
267
+ end
268
+
269
+ ##
270
+ # Get the hardcoded prefix for this model.
271
+ #
272
+ # All classes extending APIModel should implement this method.
273
+ #
274
+ def prefix
275
+ nil
276
+ end
277
+ private :prefix
278
+
279
+ ##
280
+ # This magical method enables a direct pass-through of methods if no
281
+ # special logic is required to handle the response.
282
+ def method_missing(function, *args)
283
+ instance.call resolve(function), args
284
+ rescue
285
+ super
286
+ end
287
+
288
+ def respond_to_missing?(_function, _include_private = false)
289
+ true
290
+ end
291
+ end
292
+
293
+ ##
294
+ # Query models a query executed through the API. There are a very large
295
+ # number of arguments that can be passed to Query#search and similar
296
+ # methods. See the API documentation for a complete list.
297
+ #
298
+ # Acquire a Query by executing Velocity::Instance#query; do not instantiate
299
+ # one yourself.
300
+ #
301
+ class Query < APIModel
302
+ ##
303
+ # The prefix for the query model
304
+ #
305
+ def prefix
306
+ 'query'
307
+ end
308
+
309
+ ##
310
+ # Execute a standard search using a source the instance.
311
+ #
312
+ # You'll want to supply at least a +:sources+ option and likely
313
+ # a +:query+ option.
314
+ def search(args)
315
+ QueryResponse.new(@instance.call(resolve('search'), args))
316
+ end
317
+
318
+ ##
319
+ # Execute a browse query, having already executed a regular Query#search
320
+ # and passing the +:browse+ option set to true.
321
+ #
322
+ # You must supply a +:file+ corresponding to the file that was returned
323
+ # from the original query. This is not checked here, so _caveat_
324
+ # _implementor_.
325
+ #
326
+ def browse(args)
327
+ QueryResponse.new(@instance.call(resolve('browse'), args))
328
+ end
329
+
330
+ ##
331
+ # Execute a similar documents query.
332
+ #
333
+ # You must supply a +:document+ containing something that will resolve to
334
+ # an XML nodeset containing document nodes. This is not checked here, so
335
+ # _caveat_ _implementor_.
336
+ #
337
+ def similar_documents(args)
338
+ QueryResponse.new(@instance.call(resolve('similar-documents'), args))
339
+ end
340
+
341
+ ##
342
+ # This helper provides a programatic way to construct +:sort-xpaths+ XML
343
+ # for +query-search+.
344
+ #
345
+ # The expected usage of this is to put any Sorts in an array and then
346
+ # Array#join them when setting the +:sort-xpaths+ parameter of
347
+ # Query#search.
348
+ class Sort
349
+ # The xpath to the content to be sorted
350
+ attr_accessor :xpath
351
+
352
+ # The order in which it should be sorted
353
+ attr_accessor :order
354
+
355
+ # valid orders
356
+ VALID_ORDERS = [:ascending, :descending, nil].freeze
357
+
358
+ # call-seq:
359
+ # new(:order => order, :xpath => xpath)
360
+ #
361
+ # Create a new Sort helper
362
+ def initialize(args)
363
+ @xpath = args[:xpath]
364
+ @order = args[:order] || VALID_ORDERS.first
365
+ end
366
+
367
+ # Create an XML string from the Sort object
368
+ def to_s
369
+ sane?
370
+ builder = Nokogiri::XML::Builder.new do |xml|
371
+ xml.sort(xpath: xpath, order: order)
372
+ end
373
+ # this is necessary to suppress the xml version declaration
374
+ Nokogiri::XML(builder.to_xml).root.to_xml
375
+ end
376
+
377
+ private
378
+
379
+ # Set the order, ensuring that it's valid
380
+ def sane?
381
+ raise ArgumentError, ":order must be one of #{VALID_ORDERS}" unless VALID_ORDERS.member? order
382
+ end
383
+ end
384
+ end
385
+
386
+ ##
387
+ # QueryResponse wraps the XML output from a Query#search in an object which
388
+ # provides several convenience methods in addition to exposing the
389
+ # underlying XML document comprising the response.
390
+ #
391
+ class QueryResponse
392
+ # A handle on the XML document behind the response
393
+ attr_accessor :doc
394
+
395
+ ##
396
+ # Create a new QueryResponse given the response XML from Velocity
397
+ #
398
+ def initialize(doc)
399
+ @doc = doc
400
+ end
401
+
402
+ ##
403
+ # Indicates if a query response actually contains documents
404
+ #
405
+ def results?
406
+ !documents.empty?
407
+ end
408
+
409
+ ##
410
+ # Retrieve all documents from the query response
411
+ #
412
+ def documents
413
+ doc.xpath('/query-results/list/document').collect do |d|
414
+ Document.new d
415
+ end
416
+ end
417
+
418
+ ##
419
+ # Retrieve the file name of the browse file, a.k.a. +v:file+.
420
+ #
421
+ # Pass this as the +:file+ option to Query#browse in order for that
422
+ # method to work properly.
423
+ def file
424
+ doc.xpath('/query-results/@file').first.value
425
+ end
426
+ end
427
+
428
+ ##
429
+ # Document wraps the XML for an individual Velocity document in order to
430
+ # provide several convenience methods.
431
+ #
432
+ class Document
433
+ # A handle on the XML of the document
434
+ attr_accessor :doc
435
+
436
+ ##
437
+ # Create a new document XML element wrapper
438
+ #
439
+ def initialize(node)
440
+ @doc = node
441
+ end
442
+
443
+ ##
444
+ # Retrieve all contents
445
+ #
446
+ def contents
447
+ doc.xpath 'content'
448
+ end
449
+
450
+ ##
451
+ # Retrieve a single content.
452
+ #
453
+ # _Warning:_ This will actually return an array and that array may
454
+ # contain multiple elements if there are multiple contents with the same
455
+ # name attribute.
456
+ #
457
+ # document.content 'author'
458
+ # document.content("title").first
459
+ #
460
+ def content(name)
461
+ doc.xpath "content[@name='#{name}']"
462
+ end
463
+
464
+ ##
465
+ # Retrieve a single attribute from the document.
466
+ #
467
+ # document.attribute "url"
468
+ #
469
+ def attribute(name)
470
+ doc.attribute name
471
+ end
472
+
473
+ ##
474
+ # Retrieve all document attributes
475
+ #
476
+ def attributes
477
+ doc.attributes
478
+ end
479
+
480
+ ##
481
+ # Direct passthrough of the xpath in order to execute more complex XPath
482
+ # queries on the source document XML.
483
+ #
484
+ def xpath(xpath)
485
+ doc.xpath xpath
486
+ end
487
+ end
488
+
489
+ ##
490
+ # Create a new query
491
+ #
492
+ def query
493
+ Query.new(self)
494
+ end
495
+
496
+ ##
497
+ # CollectionBroker models an instance's collection broker, which can start
498
+ # and stop collections on demand. It's especially useful for when an
499
+ # instance has tens or hundreds of collections which cannot be
500
+ # simultaneously held in memory.
501
+ #
502
+ # TODO: implement
503
+ class CollectionBroker < APIModel
504
+ # The CollectionBroker prefix is +collection-broker+.
505
+ def prefix
506
+ 'collection-broker'
507
+ end
508
+
509
+ ##
510
+ # Create a new wrapper for the collection broker functions.
511
+ #
512
+ def initialize
513
+ raise NotImplementedError
514
+ end
515
+ end
516
+
517
+ ##
518
+ # Reports models an instance's reports system.
519
+ #
520
+ # TODO: implement
521
+ #
522
+ class Reports < APIModel
523
+ # The Reports prefix is simply +reports+.
524
+ def prefix
525
+ 'reports'
526
+ end
527
+
528
+ # Create a new wrapper for reports management functions.
529
+ def initialize
530
+ raise NotImplementedError
531
+ end
532
+ end
533
+
534
+ ##
535
+ # Repository models an instance's configuration node repository, enabling
536
+ # a user to list, download, update, add, and delete configuration nodes.
537
+ #
538
+ # Create a new wrapper for the repository management functions. The
539
+ # following methods are handled via method_missing and are thus documented
540
+ # here.
541
+ #
542
+ # * <tt>add(:node => xml)</tt> -
543
+ # Add a node to the repository.
544
+ # * <tt>delete(:element => element, :name => name, :md5 => md5)</tt> -
545
+ # Delete a node from the repository. +:md5+ is optional.
546
+ # * <tt>get(:element => element, :name => name)</tt> -
547
+ # Get a node from the repository.
548
+ # * <tt>get_md5(:element => element, :name => name)</tt> -
549
+ # Get a node with its md5 hash from the repository.
550
+ # * <tt>list_xml()</tt> -
551
+ # List the xml nodes in the repository.
552
+ # * <tt>update(:node => xml, :md5 => md5)</tt> -
553
+ # Update a node that is already in the repository. +:md5+ is optional.
554
+ #
555
+ # Any return value will be raw +Nokogiri::XML::Document+ object.
556
+ #
557
+ class Repository < APIModel
558
+ # The Repository prefix is simply +repository+.
559
+ def prefix
560
+ 'repository'
561
+ end
562
+
563
+ # List all nodes as nodespecs
564
+ def list_xml_specs(internal = false)
565
+ arr = []
566
+ xml = list_xml
567
+ xml.child.children.each do |c|
568
+ if !c.has_attribute?('internal') || (c.has_attribute?('internal') && internal)
569
+ arr << '%s.%s'.format([c.name, c.attr('name')])
570
+ end
571
+ end
572
+ arr
573
+ end
574
+
575
+ # Get a node given its nodespec in the form +element.@name+
576
+ def get_nodespec(nodespec)
577
+ element, name = nodespec.split '.'
578
+ get element: element, name: name
579
+ end
580
+ end
581
+
582
+ # Get a handle on the repository
583
+ def repository
584
+ Repository.new(self)
585
+ end
586
+
587
+ ##
588
+ # Scheduler models an instance's scheduler service. It can start and stop
589
+ # the service, as well as retrieve its status and list jobs.
590
+ #
591
+ # The scheduler configuration can be only modified by updating the
592
+ # scheduler node in the repository.
593
+ #
594
+ # TODO: implement
595
+ #
596
+ class Scheduler < APIModel
597
+ # The Scheduler prefix is simply +scheduler+.
598
+ def prefix
599
+ 'scheduler'
600
+ end
601
+
602
+ # Create a new wrapper for the scheduler functions.
603
+ def initialize
604
+ raise NotImplementedError
605
+ end
606
+ end
607
+
608
+ ##
609
+ # SearchService models an instance's search service, or more commonly
610
+ # called the _query_ _service_.
611
+ #
612
+ # TODO: implement
613
+ #
614
+ class SearchService < APIModel
615
+ # The SearchService prefix is +search-service+.
616
+ def prefix
617
+ 'search-service'
618
+ end
619
+
620
+ # Create a new wrapper for the search-service functions.
621
+ def initialize
622
+ raise NotImplementedError
623
+ end
624
+ end
625
+
626
+ ##
627
+ # SourceTest models an instance's source testing, which can automatically
628
+ # execute a test to know if a source is correctly returning expected
629
+ # results.
630
+ #
631
+ # TODO: implement
632
+ #
633
+ class SourceTest < APIModel
634
+ # The SourceTest prefix is +source-test+.
635
+ def prefix
636
+ 'source-test'
637
+ end
638
+
639
+ # Create a new wrapper for the source-test functions.
640
+ def initialize
641
+ raise NotImplementedError
642
+ end
643
+ end
644
+
645
+ ##
646
+ # SearchCollection models a Velocity search collection and provides a set
647
+ # of convenience methods for accessing its status, controlling its
648
+ # activity, and even enqueuing documents and URLs.
649
+ #
650
+ class SearchCollection < APIModel
651
+ # The name of the collection.
652
+ attr_accessor :name
653
+ # The SearchCollection prefix is +search-collection+.
654
+ def prefix
655
+ 'search-collection'
656
+ end
657
+
658
+ # Create a new SearchCollection wrapper.
659
+ def initialize(collection_name)
660
+ @name = collection_name
661
+ end
662
+
663
+ ##
664
+ # call-seq:
665
+ # SearchCollection.new_from_xml(:xml => xml, :instance => instance)
666
+ #
667
+ # Factory method used by Instance#collections
668
+ #
669
+ def self.new_from_xml(args)
670
+ sc = SearchCollection.new(args[:xml].attributes['name'].to_s)
671
+ sc.instance = args[:instance]
672
+ sc
673
+ end
674
+
675
+ ##
676
+ # Get a handle on the crawler service.
677
+ #
678
+ def crawler
679
+ Crawler.new self
680
+ end
681
+
682
+ ##
683
+ # Get a handle on the indexer service.
684
+ #
685
+ def indexer
686
+ Indexer.new self
687
+ end
688
+
689
+ ##
690
+ # Retrieve the status of the collection.
691
+ #
692
+ # Optionally pass +:subcollection => 'live' or 'staging'+ to choose which
693
+ # subcollection. Default is +'live'+.
694
+ #
695
+ # Optionally pass +:'stale-ok'+ boolean to receive stats that may be
696
+ # behind.
697
+ #
698
+ def status(_args = {})
699
+ Status.new instance.call resolve('status'), collection: name
700
+ end
701
+
702
+ ##
703
+ # Refresh the tags on an auto-classified collection.
704
+ #
705
+ def auto_classify_refresh_tags
706
+ # api_method = __method__.dasherize
707
+ raise NotImplementedError
708
+ end
709
+
710
+ ##
711
+ # Set collection XML
712
+ #
713
+ # This is more appropriate for collections than Repository#update because
714
+ # it correctly separates parts of the collection configuration that must
715
+ # go into the repository from parts that are saved in a status file.
716
+ #
717
+ def set_xml(args = {})
718
+ instance.call resolve('set-xml'), args.merge(collection: name)
719
+ end
720
+
721
+ ##
722
+ # Get collection XML
723
+ #
724
+ # Pull the collection from the collection service or using a saved copy
725
+ #
726
+ # Optionally pass +:'stale-ok' => true or false+ to indicate if a stale
727
+ # copy is OK. Requesting a fresh copy may extend the request. Default is
728
+ # false.
729
+ #
730
+ def xml(args = {})
731
+ instance.call resolve('xml'),
732
+ { collection: name, :'stale-ok' => false }.merge(args)
733
+ end
734
+
735
+ ##
736
+ # Interact with annotations on a collection.
737
+ #
738
+ # TODO: implement
739
+ class Annotation < APIModel
740
+ # The Annotation prefix is simply +annotation+.
741
+ def prefix
742
+ 'annotation'
743
+ end
744
+
745
+ # Create a new wrapper for the annotation functions.
746
+ def initialize
747
+ raise NotImplementedError
748
+ end
749
+ end
750
+
751
+ ##
752
+ # This models the collection status XML returned by Velocity.
753
+ #
754
+ class Status
755
+ # The raw document describing the status
756
+ attr_accessor :doc
757
+
758
+ ##
759
+ # Create a new wrapper for the status XML
760
+ #
761
+ def initialize(doc)
762
+ @doc = doc
763
+ end
764
+
765
+ ##
766
+ # Get the crawler status node
767
+ #
768
+ def crawler
769
+ CrawlerStatus.new doc.xpath('/vse-status/crawler-status').first
770
+ end
771
+
772
+ ##
773
+ # Get the indexer status node
774
+ #
775
+ def indexer
776
+ IndexerStatus.new doc.xpath('/vse-status/vse-index-status').first
777
+ end
778
+
779
+ ##
780
+ # Check to see if the collection actually has a status.
781
+ #
782
+ # If false, then the collection isn't running and has no data.
783
+ # if true, then the collection _may_ be running but certainly has data.
784
+ def has_data?
785
+ doc.xpath('__CONTAINER__').empty?
786
+ end
787
+
788
+ ##
789
+ # An abstracted wrapper for the various parts of the collection status
790
+ # XML returned by Velocity.
791
+ #
792
+ class ServiceStatus
793
+ # The raw document describing the status
794
+ attr_accessor :doc
795
+ # Create a new service status wrapper
796
+ def initialize(doc)
797
+ @doc = doc
798
+ @attrs = {}
799
+ end
800
+
801
+ ##
802
+ # Ensure that the status is actually there
803
+ #
804
+ def has_status?
805
+ !doc.nil?
806
+ end
807
+
808
+ ##
809
+ # Return a symbol-keyed hash of all attributes
810
+ #
811
+ # This method resolves the value of all of the Nokogiri attributes so
812
+ # that you don't have to.
813
+ #
814
+ def attributes
815
+ return @attrs unless @attrs.empty?
816
+ doc.attributes.each do |key, nattr|
817
+ @attrs[key.to_sym.dedasherize] = nattr.value
818
+ end
819
+ @attrs
820
+ end
821
+
822
+ ##
823
+ # Get a single attribute
824
+ #
825
+ def attribute(attr)
826
+ doc.attribute(attr).value
827
+ end
828
+
829
+ ##
830
+ # Capture attributes accessed as instance variables
831
+ #
832
+ def method_missing(function, *args, &block)
833
+ f = function.to_s.dasherize
834
+ if doc.attributes.member? f
835
+ attribute f
836
+ elsif doc.attributes.member? 'n-' + f
837
+ attribute 'n-' + f
838
+ else
839
+ super
840
+ end
841
+ end
842
+
843
+ def respond_to_missing?(function, include_private = false)
844
+ f = function.to_s.dasherize
845
+ if doc.attributes.member?(f) || doc.attributes.member?('n-' + f)
846
+ true
847
+ else
848
+ super(function, include_private)
849
+ end
850
+ end
851
+ end
852
+
853
+ ##
854
+ # Wrapper for the crawler status object
855
+ #
856
+ class CrawlerStatus < ServiceStatus
857
+ ##
858
+ # Get the total number of time spent converting
859
+ #
860
+ def converter_timings_total_ms
861
+ doc.xpath('converter-timings/@total-ms').first.value.to_i
862
+ end
863
+
864
+ ##
865
+ # Get an array of hashes containing the timings for all converters
866
+ # that have run so far while crawling.
867
+ #
868
+ def converter_timings
869
+ doc.xpath('converter-timings/converter-timing').collect do |ct|
870
+ attrs = {}
871
+ ct.attributes.each do |key, nattr|
872
+ attrs[key] = nattr.value
873
+ end
874
+ attrs
875
+ end
876
+ end
877
+
878
+ ##
879
+ # Retrieve the number of documents output at each hop
880
+ #
881
+ def crawl_hops_output
882
+ crawl_hops :output
883
+ end
884
+
885
+ ##
886
+ # Retrieve the number of documents input at each hop
887
+ #
888
+ def crawl_hops_input
889
+ crawl_hops :input
890
+ end
891
+
892
+ ##
893
+ # Private method unifying how crawl-hop elements are presented
894
+ #
895
+ def crawl_hops(which)
896
+ doc.xpath('crawl-hops-' + which.to_s + '/crawl-hop').collect do |ch|
897
+ attrs = {}
898
+ ch.attributes.each do |key, nattr|
899
+ attrs[key] = nattr.value
900
+ end
901
+ attrs
902
+ end
903
+ end
904
+ private :crawl_hops
905
+
906
+ # TODO: crawl-remote-all-status/crawl-remote-{server,client,all}-status
907
+ end
908
+
909
+ ##
910
+ # Wrapper for the index status object
911
+ #
912
+ class IndexerStatus < ServiceStatus
913
+ # TODO: implement convenience methods
914
+ #
915
+ ##
916
+ # Get index serving status
917
+ #
918
+ def serving
919
+ doc.xpath('vse-serving').first do |s|
920
+ attrs = {}
921
+ s.attributes.each do |key, sattr|
922
+ attrs[key.dedasherize.to_sym] = sattr
923
+ end
924
+ attrs
925
+ end
926
+ end
927
+
928
+ ##
929
+ # Get information about the index files
930
+ #
931
+ # The content counts per file are available in a subarray at key
932
+ # +:contents+.
933
+ #
934
+ def files
935
+ doc.xpath('vse-index-file').collection do |f|
936
+ end
937
+ end
938
+ end
939
+ end
940
+
941
+ ##
942
+ # A model for the collections' services
943
+ #
944
+ class CollectionService < APIModel
945
+ # The collection being controlled
946
+ attr_accessor :collection
947
+
948
+ ##
949
+ # Create a new wrapper for collection services for the given
950
+ # collection.
951
+ #
952
+ def initialize(collection)
953
+ @collection = collection
954
+ end
955
+
956
+ ##
957
+ # Start the service.
958
+ #
959
+ # Valid option for either service is:
960
+ #
961
+ # * :subcollection => 'live' (default) or 'staging'
962
+ #
963
+ # Valid option only for crawler service:
964
+ #
965
+ # * :type => 'resume' 'resume-and-idle' 'refresh-inplace' 'refresh-new'
966
+ # 'new' 'apply-changes'
967
+ #
968
+ def start(options = {})
969
+ act 'start', options
970
+ end
971
+
972
+ ##
973
+ # Stop the service
974
+ #
975
+ # Valid options for either service are:
976
+ #
977
+ # * :subcollection => 'live' (default) or 'staging'
978
+ # * :kill => true or false
979
+ #
980
+ def stop(options = {})
981
+ act 'stop', options
982
+ end
983
+
984
+ ##
985
+ # Restart the service
986
+ #
987
+ # Valid options for either service are:
988
+ #
989
+ # * :subcollection => 'live' (default) or 'staging'
990
+ #
991
+ def restart(options = {})
992
+ act 'restart', options
993
+ end
994
+
995
+ private
996
+
997
+ ##
998
+ # Refactored interface for all collection services
999
+ #
1000
+ def act(action, options = {})
1001
+ collection.instance.call resolve(action),
1002
+ options.merge(collection: collection.name)
1003
+ end
1004
+ end
1005
+
1006
+ ##
1007
+ # The Crawler service of the collection
1008
+ #
1009
+ # Methods implied by method_missing:
1010
+ # - start
1011
+ # - stop
1012
+ # - restart
1013
+ #
1014
+ class Crawler < CollectionService
1015
+ # The prefix for interacting with the Velocity API.
1016
+ def prefix
1017
+ collection.prefix + '-crawler'
1018
+ end
1019
+
1020
+ ##
1021
+ # Get the status of the crawler
1022
+ #
1023
+ # This is a convenience method for Status#crawler. See
1024
+ # SearchCollection#status for optional arguments.
1025
+ #
1026
+ def status(args = {})
1027
+ collection.status(args).crawler
1028
+ end
1029
+ end
1030
+
1031
+ ##
1032
+ # The Indexer service of the collection
1033
+ #
1034
+ # Methods implied by method_missing:
1035
+ # - start
1036
+ # - stop
1037
+ # - restart
1038
+ #
1039
+ class Indexer < CollectionService
1040
+ #
1041
+ ##
1042
+ # The prefix for interacting via the Velocity API.
1043
+ def prefix
1044
+ collection.prefix + '-indexer'
1045
+ end
1046
+
1047
+ ##
1048
+ # Executes a full merge on the index. This reduces the number of files
1049
+ # across which the index is spread and also removes deleted data.
1050
+ #
1051
+ # * :subcollection => 'live' (default) or 'staging'
1052
+ def full_merge(options = {})
1053
+ act 'full-merge', options
1054
+ end
1055
+
1056
+ ##
1057
+ # Get the status of the indexer
1058
+ #
1059
+ # This is a convenience method for Status#indexer. See
1060
+ # SearchCollection#status for optional arguments.
1061
+ #
1062
+ def status(args = {})
1063
+ collection.status(args).indexer
1064
+ end
1065
+ end
1066
+ end # Velocity::Instance::SearchCollection
1067
+
1068
+ ##
1069
+ # Interact with a dictionary on the Velocity instance
1070
+ #
1071
+ # Note that +dictionary-list-xml+ is implemented as
1072
+ # Velocity::Instance#dictionaries.
1073
+ #
1074
+ class Dictionary < APIModel
1075
+ # The name of the dictionary.
1076
+ attr_accessor :name
1077
+ # The Dictionary prefix is simply +dictionary+.
1078
+ def prefix
1079
+ 'dictionary'
1080
+ end
1081
+ private :prefix
1082
+
1083
+ ##
1084
+ # call-seq:
1085
+ # Dictionary.new_from_xml(:xml => xml, :instance => instance)
1086
+ #
1087
+ # Factory method used by Instance#dictionaries
1088
+ #
1089
+ def self.new_from_xml(args)
1090
+ d = Dictionary.new(args[:xml].attributes['name'].to_s)
1091
+ d.instance = args[:instance]
1092
+ d
1093
+ end
1094
+
1095
+ ##
1096
+ # Create a new wrapper for a dictionary
1097
+ #
1098
+ def initialize(name)
1099
+ @name = name
1100
+ end
1101
+
1102
+ ##
1103
+ # Get the dictionary's status object
1104
+ #
1105
+ # TODO: wrap the XML returned
1106
+ #
1107
+ def status
1108
+ act 'status-xml'
1109
+ end
1110
+
1111
+ ##
1112
+ # Begin a build of the dictionary
1113
+ #
1114
+ def build
1115
+ act __method__
1116
+ end
1117
+
1118
+ ##
1119
+ # Create the dictionary
1120
+ #
1121
+ # Can optionally pass +:based_on+ String to use another dictionary as a
1122
+ # template
1123
+ #
1124
+ def create(_args = {})
1125
+ act __method__
1126
+ end
1127
+
1128
+ ##
1129
+ # Stop the dictionary build process
1130
+ #
1131
+ # Can optionally pass +:kill+ boolean if it should be killed immediately
1132
+ #
1133
+ def stop(_args = {})
1134
+ act __method__, {}
1135
+ end
1136
+
1137
+ ##
1138
+ # Delete the dictionary
1139
+ #
1140
+ def delete
1141
+ act __method__
1142
+ end
1143
+
1144
+ ##
1145
+ # call-seq:
1146
+ # autocomplete_suggest(:str => "")
1147
+ #
1148
+ # Provide an autocompletion
1149
+ #
1150
+ # You must provide a +:str+ option in order to receive results.
1151
+ #
1152
+ def autocomplete_suggest(args = {})
1153
+ api_method = __method__.dasherize
1154
+ asargs = args.merge(dictionary: name)
1155
+ AutocompleteSuggestionSet.new_from_xml instance.call(api_method, asargs)
1156
+ end
1157
+
1158
+ ##
1159
+ # A simple wrapper for autocomplete suggestions
1160
+ #
1161
+ # Created only by Dictionary#autocomplete_suggest. Note that the
1162
+ # suggestions will already be in descending order by number of
1163
+ # occurrences.
1164
+ #
1165
+ class AutocompleteSuggestionSet
1166
+ # The raw XML
1167
+ attr_accessor :doc
1168
+ # The original text to be autocompleted
1169
+ attr_reader :query
1170
+ # The suggestions array
1171
+ attr_reader :suggestions
1172
+ # Create a new set of suggestions
1173
+ def initialize(query, suggestions = {}, xml = nil)
1174
+ @query = query
1175
+ @suggestions = suggestions
1176
+ @doc = xml
1177
+ end
1178
+
1179
+ # Create a new set of suggestions given some XML from Velocity
1180
+ def self.new_from_xml(xml)
1181
+ query = xml.xpath('/suggestions/@query').first.value
1182
+ suggestions = xml.xpath('/suggestions/suggestion').collect do |s|
1183
+ AutocompleteSuggestion.new_from_xml s
1184
+ end
1185
+ AutocompleteSuggestionSet.new(query, suggestions, xml)
1186
+ end
1187
+ end
1188
+
1189
+ ##
1190
+ # A simple wrapper for an autocomplete suggestion
1191
+ #
1192
+ class AutocompleteSuggestion
1193
+ # The xml
1194
+ attr_accessor :doc
1195
+ # The phrase
1196
+ attr_reader :phrase
1197
+ # The number of occurrences
1198
+ attr_reader :count
1199
+ # Create a new suggestion
1200
+ def initialize(phrase, count = 0, xml = nil)
1201
+ @phrase = phrase
1202
+ @count = count
1203
+ @doc = xml
1204
+ end
1205
+
1206
+ # Create a new suggestion given some XML from Velocity
1207
+ def self.new_from_xml(xml)
1208
+ AutocompleteSuggestion.new(
1209
+ xml.children.first.text,
1210
+ xml.attributes['count'].value.to_i,
1211
+ xml
1212
+ )
1213
+ end
1214
+
1215
+ # This is really only ever going to be used as a string
1216
+ def to_s
1217
+ phrase
1218
+ end
1219
+ end
1220
+
1221
+ def act(action, args = {})
1222
+ instance.call resolve(action), args.merge(dictionary: name)
1223
+ end
1224
+ private :act
1225
+ end # Velocity::Instance::Dictionary
1226
+
1227
+ ##
1228
+ # Interacts with alerts registered on the instance
1229
+ #
1230
+ # TODO: implement
1231
+ #
1232
+ class Alert < APIModel
1233
+ # The prefix for Alert is simply +alert+.
1234
+ def prefix
1235
+ 'alert'
1236
+ end
1237
+
1238
+ ##
1239
+ # Create a new wrapper for the Alerts interface.
1240
+ #
1241
+ def initialize
1242
+ raise NotImplementedError
1243
+ end
1244
+ end
1245
+ end # Velocity::Instance
1246
+
1247
+ ##
1248
+ # Generic Velocity API exception thrown when Velocity doesn't like the
1249
+ # arguments supplied in a call or the credentials are incorrect.
1250
+ #
1251
+ # Don't ever raise this yourself; it should be raised only by
1252
+ # Velocity::Instance#call
1253
+ #
1254
+ class VelocityException < RuntimeError
1255
+ ##
1256
+ # Determines if a response from the API is an exception response
1257
+ #
1258
+ def self.exception?(node)
1259
+ node.root.name == 'exception'
1260
+ end
1261
+
1262
+ ##
1263
+ # Wrap this exception around the XML returned by Velocity
1264
+ #
1265
+ def initialize(node)
1266
+ @node = node
1267
+ super(api_message)
1268
+ end
1269
+
1270
+ ##
1271
+ # Get the string describing the thrown exception
1272
+ #
1273
+ def api_message
1274
+ @node.xpath('/exception//text()').to_a.join.strip
1275
+ end
1276
+
1277
+ ##
1278
+ # Convert this exception to a string
1279
+ #
1280
+ def to_s
1281
+ api_message
1282
+ end
1283
+ end # Velocity::VelocityException
1284
+
1285
+ ##
1286
+ # Chico is an AXL runner. It allows a user to try small snippets of AXL, the
1287
+ # language Velocity uses to glue its parts together.
1288
+ #
1289
+ # Warning: Velocity::Chico may move to Velocity::Instance::Chico in the
1290
+ # future.
1291
+ #
1292
+ class Chico < Instance
1293
+ # The content type to be sent. Default is text/xml.
1294
+ attr_reader :content_type
1295
+ ##
1296
+ # call-seq:
1297
+ # new(:endpoint => endpoint, :username => username, :password => password)
1298
+ #
1299
+ # Create a new Chico instance. This wraps around Instance constructor and
1300
+ # sets +:v_app+ to 'chico'.
1301
+ #
1302
+ def initialize(args = {})
1303
+ super(args.merge(v_app: 'chico'))
1304
+ @content_type = 'text/xml'
1305
+ end
1306
+
1307
+ ##
1308
+ # call-seq:
1309
+ # run(xml)
1310
+ # run(:xml => xml)
1311
+ #
1312
+ # Run an AXL snippet on Chico
1313
+ #
1314
+ # Expects a String or a Hash with a key +:xml+ containing the AXL to be run.
1315
+ #
1316
+ def run(xml)
1317
+ if !([String, Hash].member? xml.class) || xml.empty?
1318
+ raise ArgumentError, 'Need some AXL to process.'
1319
+ end
1320
+
1321
+ if (xml.class == Hash) && xml.key?(:xml)
1322
+ h = xml
1323
+ elsif xml.class == String
1324
+ h = { xml: xml }
1325
+ end
1326
+ run_with h
1327
+ end
1328
+
1329
+ ##
1330
+ # call-seq:
1331
+ # run_with(:xml => xml, ...)
1332
+ #
1333
+ # Run an XML snippet with more options, such as +:profile+ => 'profile'.
1334
+ #
1335
+ def run_with(args = {})
1336
+ if args.nil? || !args.key?(:xml) || args[:xml].empty?
1337
+ raise ArgumentError, 'Need an :xml key containing some AXL to process.'
1338
+ end
1339
+ call nil, { content_type: @content_type, backend: 'backend' }.merge(args)
1340
+ end
1341
+ end
1342
+ end