acceleration 0.0.17
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +7 -0
- data/.rubocop.yml +51 -0
- data/.ruby-gemset +1 -0
- data/.ruby-version +1 -0
- data/.semver +5 -0
- data/Gemfile +2 -0
- data/Guardfile +25 -0
- data/LICENSE.md +7 -0
- data/README.md +60 -0
- data/Rakefile +16 -0
- data/acceleration.gemspec +35 -0
- data/bin/acceleration +17 -0
- data/lib/acceleration.rb +3 -0
- data/lib/acceleration/monkeypatches.rb +46 -0
- data/lib/acceleration/velocity.rb +1342 -0
- data/lib/acceleration/version.rb +12 -0
- data/spec/todo.txt +1 -0
- metadata +206 -0
checksums.yaml
ADDED
@@ -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
|
data/.gitignore
ADDED
data/.rubocop.yml
ADDED
@@ -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'
|
data/.ruby-gemset
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
acceleration
|
data/.ruby-version
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
2.0.0
|
data/.semver
ADDED
data/Gemfile
ADDED
data/Guardfile
ADDED
@@ -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
|
data/LICENSE.md
ADDED
@@ -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.
|
data/README.md
ADDED
@@ -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)`.
|
data/Rakefile
ADDED
@@ -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
|
data/bin/acceleration
ADDED
@@ -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!
|
data/lib/acceleration.rb
ADDED
@@ -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
|