haveapi 0.6.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.editorconfig +15 -0
- data/CHANGELOG +15 -0
- data/README.md +66 -47
- data/doc/create-client.md +14 -5
- data/doc/json-schema.erb +16 -2
- data/doc/protocol.md +25 -3
- data/doc/protocol.plantuml +14 -8
- data/haveapi.gemspec +4 -2
- data/lib/haveapi.rb +5 -3
- data/lib/haveapi/action.rb +34 -6
- data/lib/haveapi/action_state.rb +92 -0
- data/lib/haveapi/authentication/basic/provider.rb +7 -0
- data/lib/haveapi/authentication/token/provider.rb +5 -0
- data/lib/haveapi/client_example.rb +83 -0
- data/lib/haveapi/client_examples/curl.rb +86 -0
- data/lib/haveapi/client_examples/fs_client.rb +116 -0
- data/lib/haveapi/client_examples/http.rb +91 -0
- data/lib/haveapi/client_examples/js_client.rb +149 -0
- data/lib/haveapi/client_examples/php_client.rb +122 -0
- data/lib/haveapi/client_examples/ruby_cli.rb +117 -0
- data/lib/haveapi/client_examples/ruby_client.rb +106 -0
- data/lib/haveapi/context.rb +3 -2
- data/lib/haveapi/example.rb +29 -2
- data/lib/haveapi/extensions/action_exceptions.rb +2 -2
- data/lib/haveapi/extensions/base.rb +1 -1
- data/lib/haveapi/extensions/exception_mailer.rb +339 -0
- data/lib/haveapi/hooks.rb +1 -1
- data/lib/haveapi/parameters/typed.rb +5 -3
- data/lib/haveapi/public/css/highlight.css +99 -0
- data/lib/haveapi/public/doc/protocol.png +0 -0
- data/lib/haveapi/public/js/highlight.pack.js +2 -0
- data/lib/haveapi/public/js/highlighter.js +9 -0
- data/lib/haveapi/public/js/main.js +32 -0
- data/lib/haveapi/public/js/nojs-tabs.js +196 -0
- data/lib/haveapi/resources/action_state.rb +196 -0
- data/lib/haveapi/server.rb +96 -27
- data/lib/haveapi/version.rb +2 -2
- data/lib/haveapi/views/main_layout.erb +14 -0
- data/lib/haveapi/views/version_page.erb +187 -13
- data/lib/haveapi/views/version_sidebar.erb +37 -3
- metadata +49 -5
data/haveapi.gemspec
CHANGED
@@ -5,7 +5,7 @@ require 'haveapi/version'
|
|
5
5
|
Gem::Specification.new do |s|
|
6
6
|
s.name = 'haveapi'
|
7
7
|
s.version = HaveAPI::VERSION
|
8
|
-
s.date = '2016-
|
8
|
+
s.date = '2016-11-24'
|
9
9
|
s.summary =
|
10
10
|
s.description = 'Framework for creating self-describing APIs'
|
11
11
|
s.authors = 'Jakub Skokan'
|
@@ -13,7 +13,7 @@ Gem::Specification.new do |s|
|
|
13
13
|
s.files = `git ls-files -z`.split("\x0")
|
14
14
|
s.license = 'MIT'
|
15
15
|
|
16
|
-
s.required_ruby_version = '
|
16
|
+
s.required_ruby_version = '>= 2.0.0'
|
17
17
|
|
18
18
|
s.add_runtime_dependency 'require_all'
|
19
19
|
s.add_runtime_dependency 'json'
|
@@ -24,4 +24,6 @@ Gem::Specification.new do |s|
|
|
24
24
|
s.add_runtime_dependency 'rake'
|
25
25
|
s.add_runtime_dependency 'github-markdown', '~> 0.6.9'
|
26
26
|
s.add_runtime_dependency 'nesty', '~> 1.0.2'
|
27
|
+
s.add_runtime_dependency 'haveapi-client', '~> 0.6.0'
|
28
|
+
s.add_runtime_dependency 'mail'
|
27
29
|
end
|
data/lib/haveapi.rb
CHANGED
@@ -10,18 +10,20 @@ require 'github/markdown'
|
|
10
10
|
require 'json'
|
11
11
|
|
12
12
|
module HaveAPI
|
13
|
-
module
|
14
|
-
end
|
13
|
+
module Resources ; end
|
14
|
+
module Actions ; end
|
15
15
|
end
|
16
16
|
|
17
17
|
require_relative 'haveapi/params'
|
18
18
|
require_rel 'haveapi/parameters/'
|
19
19
|
require_rel 'haveapi/*.rb'
|
20
|
+
require_rel 'haveapi/actions/*.rb'
|
21
|
+
require_rel 'haveapi/resources/*.rb'
|
20
22
|
require_rel 'haveapi/model_adapters/hash'
|
21
23
|
require_rel 'haveapi/model_adapters/active_record' if ar
|
22
24
|
require_rel 'haveapi/authentication'
|
23
|
-
require_rel 'haveapi/actions/*.rb'
|
24
25
|
require_rel 'haveapi/output_formatters/base.rb'
|
25
26
|
require_rel 'haveapi/output_formatters/'
|
26
27
|
require_rel 'haveapi/validators/'
|
28
|
+
require_rel 'haveapi/client_examples/'
|
27
29
|
require_rel 'haveapi/extensions'
|
data/lib/haveapi/action.rb
CHANGED
@@ -8,13 +8,14 @@ module HaveAPI
|
|
8
8
|
has_attr :http_method, :get
|
9
9
|
has_attr :auth, true
|
10
10
|
has_attr :aliases, []
|
11
|
+
has_attr :blocking, false
|
11
12
|
|
12
13
|
include Hookable
|
13
14
|
|
14
15
|
has_hook :exec_exception,
|
15
16
|
desc: 'Called when unhandled exceptions occurs during Action.exec',
|
16
17
|
args: {
|
17
|
-
|
18
|
+
context: 'HaveAPI::Context instance',
|
18
19
|
exception: 'exception instance',
|
19
20
|
},
|
20
21
|
ret: {
|
@@ -86,6 +87,17 @@ module HaveAPI
|
|
86
87
|
model_adapter(input.layout).used_by(:input, self)
|
87
88
|
model_adapter(output.layout).used_by(:output, self)
|
88
89
|
|
90
|
+
if blocking
|
91
|
+
meta(:global) do
|
92
|
+
output do
|
93
|
+
integer :action_state_id,
|
94
|
+
label: 'Action state ID',
|
95
|
+
desc: 'ID of ActionState object for state querying. When null, the action '+
|
96
|
+
'is not blocking for the current invocation.'
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
89
101
|
if @meta
|
90
102
|
@meta.each_value do |m|
|
91
103
|
next unless m
|
@@ -193,6 +205,7 @@ module HaveAPI
|
|
193
205
|
auth: @auth,
|
194
206
|
description: @desc,
|
195
207
|
aliases: @aliases,
|
208
|
+
blocking: @blocking ? true : false,
|
196
209
|
input: @input ? @input.describe(context) : {parameters: {}},
|
197
210
|
output: @output ? @output.describe(context) : {parameters: {}},
|
198
211
|
meta: @meta ? @meta.merge(@meta) { |_, v| v && v.describe(context) } : nil,
|
@@ -319,7 +332,7 @@ module HaveAPI
|
|
319
332
|
pre_exec
|
320
333
|
exec
|
321
334
|
rescue Exception => e
|
322
|
-
tmp = call_class_hooks_as_for(Action, :exec_exception, args: [
|
335
|
+
tmp = call_class_hooks_as_for(Action, :exec_exception, args: [@context, e])
|
323
336
|
|
324
337
|
if tmp.empty?
|
325
338
|
p e.message
|
@@ -327,7 +340,9 @@ module HaveAPI
|
|
327
340
|
error('Server error occurred')
|
328
341
|
end
|
329
342
|
|
330
|
-
|
343
|
+
unless tmp[:status]
|
344
|
+
error(tmp[:message], {}, http_status: tmp[:http_status] || 500)
|
345
|
+
end
|
331
346
|
end
|
332
347
|
end
|
333
348
|
|
@@ -392,6 +407,10 @@ module HaveAPI
|
|
392
407
|
safe_ret = ret
|
393
408
|
end
|
394
409
|
|
410
|
+
if self.class.blocking
|
411
|
+
@reply_meta[:global][:action_state_id] = state_id
|
412
|
+
end
|
413
|
+
|
395
414
|
ns = {output.namespace => safe_ret}
|
396
415
|
ns[Metadata.namespace] = @reply_meta[:global] unless meta[:no]
|
397
416
|
|
@@ -402,7 +421,7 @@ module HaveAPI
|
|
402
421
|
end
|
403
422
|
|
404
423
|
else
|
405
|
-
[false, @message, @errors]
|
424
|
+
[false, @message, @errors, @http_status]
|
406
425
|
end
|
407
426
|
end
|
408
427
|
|
@@ -475,13 +494,22 @@ module HaveAPI
|
|
475
494
|
ret
|
476
495
|
end
|
477
496
|
|
478
|
-
|
497
|
+
# @param ret [Hash] response
|
498
|
+
# @param opts [Hash] options
|
499
|
+
# @option opts [Integer] http_status HTTP status code sent to the client
|
500
|
+
def ok(ret = {}, opts = {})
|
501
|
+
@http_status = opts[:http_status]
|
479
502
|
throw(:return, ret)
|
480
503
|
end
|
481
504
|
|
482
|
-
|
505
|
+
# @param msg [String] error message sent to the client
|
506
|
+
# @param errs [Hash<Array>] parameter errors sent to the client
|
507
|
+
# @param opts [Hash] options
|
508
|
+
# @option opts [Integer] http_status HTTP status code sent to the client
|
509
|
+
def error(msg, errs = {}, opts = {})
|
483
510
|
@message = msg
|
484
511
|
@errors = errs
|
512
|
+
@http_status = opts[:http_status]
|
485
513
|
throw(:return, false)
|
486
514
|
end
|
487
515
|
|
@@ -0,0 +1,92 @@
|
|
1
|
+
module HaveAPI
|
2
|
+
# This class is an interface between APIs and HaveAPI for handling of blocking actions.
|
3
|
+
# Blocking actions are not executed immediately, but their execution takes an unspecified
|
4
|
+
# amount of time. This interface allows to list actions that are pending completion and view
|
5
|
+
# their status.
|
6
|
+
#
|
7
|
+
# If method `poll` is defined, it is called by action Resources::ActionState::Poll.
|
8
|
+
# it can provide a more sophisticated polling implementation than the implicit one, which is
|
9
|
+
# to create a new instance of this class every second and check its state. `poll` is passed
|
10
|
+
# one argument, a hash of input parameters from Resources::ActionState::Poll.
|
11
|
+
class ActionState
|
12
|
+
# Return an array of objects representing actions that are pending completion.
|
13
|
+
# @param [Object] user
|
14
|
+
# @param [Integer] offset
|
15
|
+
# @param [Integer] limit
|
16
|
+
# @param [Symbol] order (:newest or :oldest)
|
17
|
+
# @return [Array<ActionState>]
|
18
|
+
def self.list_pending(user, offset, limit, order)
|
19
|
+
raise NotImplementedError
|
20
|
+
end
|
21
|
+
|
22
|
+
# The constructor either gets parameter `id` or `state`. If `state` is not provided,
|
23
|
+
# the method should find it using the `id`.
|
24
|
+
#
|
25
|
+
# When the client is asking about the state of a specific action, lookup using `id`
|
26
|
+
# is used. When the client is listing pending actions, instances of this class are
|
27
|
+
# created in self.list_pending and are passed the `state` parameter to avoid double
|
28
|
+
# lookups. `id` should lead to the same object that would be passed as `state`.
|
29
|
+
#
|
30
|
+
# @param [Object] user
|
31
|
+
# @param [Integer] id action state id
|
32
|
+
# @param [Object] state
|
33
|
+
def initialize(user, id: nil, state: nil)
|
34
|
+
raise NotImplementedError
|
35
|
+
end
|
36
|
+
|
37
|
+
# @return [Boolean] true if the action exists
|
38
|
+
def valid?
|
39
|
+
raise NotImplementedError
|
40
|
+
end
|
41
|
+
|
42
|
+
# @return [Boolean] true of the action is finished
|
43
|
+
def finished?
|
44
|
+
raise NotImplementedError
|
45
|
+
end
|
46
|
+
|
47
|
+
# @return [Boolean] true if the action was/is going to be successful
|
48
|
+
def status
|
49
|
+
raise NotImplementedError
|
50
|
+
end
|
51
|
+
|
52
|
+
# @return [Integer] action state id
|
53
|
+
def id
|
54
|
+
raise NotImplementedError
|
55
|
+
end
|
56
|
+
|
57
|
+
# @return [String] human-readable label of this action state
|
58
|
+
def label
|
59
|
+
raise NotImplementedError
|
60
|
+
end
|
61
|
+
|
62
|
+
# @return [Hash]
|
63
|
+
def progress
|
64
|
+
raise NotImplementedError
|
65
|
+
end
|
66
|
+
|
67
|
+
# @return [Time]
|
68
|
+
def created_at
|
69
|
+
|
70
|
+
end
|
71
|
+
|
72
|
+
# @return [Time]
|
73
|
+
def updated_at
|
74
|
+
|
75
|
+
end
|
76
|
+
|
77
|
+
# @return [Boolean] true if the action can be cancelled
|
78
|
+
def can_cancel?
|
79
|
+
false
|
80
|
+
end
|
81
|
+
|
82
|
+
# Stop action execution
|
83
|
+
# @raise [RuntimeError] if the cancellation failed
|
84
|
+
# @raise [NotImplementedError] if the cancellation is not supported
|
85
|
+
# @return [Integer] if the cancellation succeded and is a blocking action
|
86
|
+
# @return [truthy] if the cancellation succeeded
|
87
|
+
# @return [falsy] if the cancellation failed
|
88
|
+
def cancel
|
89
|
+
raise NotImplementedError, 'action cancellation is not implemented by this API'
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
@@ -26,6 +26,13 @@ module HaveAPI::Authentication
|
|
26
26
|
user
|
27
27
|
end
|
28
28
|
|
29
|
+
def describe
|
30
|
+
{
|
31
|
+
description: "Authentication using HTTP basic. Username and password is passed "+
|
32
|
+
"via HTTP header. Its use is forbidden from web browsers."
|
33
|
+
}
|
34
|
+
end
|
35
|
+
|
29
36
|
protected
|
30
37
|
# Reimplement this method. It has to return an authenticated
|
31
38
|
# user or nil.
|
@@ -100,6 +100,11 @@ module HaveAPI::Authentication
|
|
100
100
|
{
|
101
101
|
http_header: http_header,
|
102
102
|
query_parameter: query_parameter,
|
103
|
+
description: "The client authenticates with username and password and gets "+
|
104
|
+
"a token. From this point, the password can be forgotten and "+
|
105
|
+
"the token is used instead. Tokens can have different lifetimes, "+
|
106
|
+
"can be renewed and revoked. The token is passed either via HTTP "+
|
107
|
+
"header or query parameter."
|
103
108
|
}
|
104
109
|
end
|
105
110
|
|
@@ -0,0 +1,83 @@
|
|
1
|
+
module HaveAPI
|
2
|
+
module ClientExamples ; end
|
3
|
+
|
4
|
+
# All client example classes should inherit this class. Depending on the client,
|
5
|
+
# the subclass may choose to implement either method `example` or `request` and
|
6
|
+
# `response`. `example` should be implemented if the client example shows only
|
7
|
+
# the request or the request and response should be coupled together.
|
8
|
+
#
|
9
|
+
# Methods `example`, `request` and `response` take one argument, the example
|
10
|
+
# to describe.
|
11
|
+
class ClientExample
|
12
|
+
class << self
|
13
|
+
# All subclasses have to call this method to set their label and be
|
14
|
+
# registered.
|
15
|
+
def label(v = nil)
|
16
|
+
if v
|
17
|
+
@label = v
|
18
|
+
HaveAPI::ClientExample.register(self)
|
19
|
+
end
|
20
|
+
|
21
|
+
@label
|
22
|
+
end
|
23
|
+
|
24
|
+
# Code name is passed to the syntax highligher.
|
25
|
+
def code(v = nil)
|
26
|
+
@code = v if v
|
27
|
+
@code
|
28
|
+
end
|
29
|
+
|
30
|
+
# A number used for ordering client examples.
|
31
|
+
def order(v = nil)
|
32
|
+
@order = v if v
|
33
|
+
@order
|
34
|
+
end
|
35
|
+
|
36
|
+
def register(klass)
|
37
|
+
@clients ||= []
|
38
|
+
@clients << klass
|
39
|
+
end
|
40
|
+
|
41
|
+
# Shortcut to {ClientExample#init}
|
42
|
+
def init(*args)
|
43
|
+
new(*args).init
|
44
|
+
end
|
45
|
+
|
46
|
+
# Shortcut to {ClientExample#auth}
|
47
|
+
def auth(*args)
|
48
|
+
new(*args[0..-3]).auth(*args[-2..-1])
|
49
|
+
end
|
50
|
+
|
51
|
+
# Shortcut to {ClientExample#example}
|
52
|
+
def example(*args)
|
53
|
+
new(*args[0..-2]).example(args.last)
|
54
|
+
end
|
55
|
+
|
56
|
+
# @return [Array<ClientExample>] sorted array of classes
|
57
|
+
def clients
|
58
|
+
@clients.sort { |a, b| a.order <=> b.order }
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
attr_reader :resource_path, :resource, :action_name, :action, :host, :base_url, :version
|
63
|
+
|
64
|
+
def initialize(host, base_url, version, *args)
|
65
|
+
@host = host
|
66
|
+
@base_url = base_url
|
67
|
+
@version = version
|
68
|
+
@resource_path, @resource, @action_name, @action = args
|
69
|
+
end
|
70
|
+
|
71
|
+
def init
|
72
|
+
|
73
|
+
end
|
74
|
+
|
75
|
+
def auth(method, desc)
|
76
|
+
|
77
|
+
end
|
78
|
+
|
79
|
+
def version_url
|
80
|
+
File.join(base_url, "v#{version}", '/')
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
@@ -0,0 +1,86 @@
|
|
1
|
+
require 'pp'
|
2
|
+
|
3
|
+
module HaveAPI::ClientExamples
|
4
|
+
class Curl < Http
|
5
|
+
label 'curl'
|
6
|
+
code :bash
|
7
|
+
order 90
|
8
|
+
|
9
|
+
def init
|
10
|
+
"$ curl --request OPTIONS '#{version_url}'"
|
11
|
+
end
|
12
|
+
|
13
|
+
def auth(method, desc)
|
14
|
+
login = {login: 'user', password: 'password', lifetime: 'fixed'}
|
15
|
+
|
16
|
+
case method
|
17
|
+
when :basic
|
18
|
+
<<END
|
19
|
+
# Password is asked on standard input
|
20
|
+
$ curl --request OPTIONS \\
|
21
|
+
--user username \\
|
22
|
+
'#{base_url}'
|
23
|
+
Password: secret
|
24
|
+
|
25
|
+
# Password given on the command line
|
26
|
+
$ curl --request OPTIONS \\
|
27
|
+
--user username:secret \\
|
28
|
+
'#{base_url}'
|
29
|
+
END
|
30
|
+
|
31
|
+
when :token
|
32
|
+
<<END
|
33
|
+
# Acquire the token
|
34
|
+
$ curl --request POST \\
|
35
|
+
--header 'Content-Type: application/json' \\
|
36
|
+
--data-binary "#{format_data(token: login)}" \\
|
37
|
+
'#{File.join(base_url, '_auth', 'token', 'tokens')}'
|
38
|
+
|
39
|
+
# Use a previously acquired token
|
40
|
+
$ curl --request OPTIONS \\
|
41
|
+
--header '#{desc[:http_header]}: thetoken' \\
|
42
|
+
'#{base_url}'
|
43
|
+
END
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def request(sample)
|
48
|
+
url = File.join(
|
49
|
+
base_url,
|
50
|
+
resolve_path(
|
51
|
+
action[:method],
|
52
|
+
action[:url],
|
53
|
+
sample[:url_params] || [],
|
54
|
+
sample[:request]
|
55
|
+
)
|
56
|
+
)
|
57
|
+
|
58
|
+
data = format_data({
|
59
|
+
action[:input][:namespace] => sample[:request],
|
60
|
+
})
|
61
|
+
|
62
|
+
<<END
|
63
|
+
$ curl --request #{action[:method]} \\
|
64
|
+
--data-binary "#{data}" \\
|
65
|
+
'#{url}'
|
66
|
+
END
|
67
|
+
end
|
68
|
+
|
69
|
+
def response(sample)
|
70
|
+
JSON.pretty_generate({
|
71
|
+
status: sample[:status],
|
72
|
+
message: sample[:message],
|
73
|
+
response: {action[:output][:namespace] => sample[:response]},
|
74
|
+
errors: sample[:errors],
|
75
|
+
})
|
76
|
+
end
|
77
|
+
|
78
|
+
def format_data(data)
|
79
|
+
json = JSON.pretty_generate(data)
|
80
|
+
json.split("\n").map do |line|
|
81
|
+
out = ''
|
82
|
+
PP.pp(line, out).strip[1..-2]
|
83
|
+
end.join("\n")
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
@@ -0,0 +1,116 @@
|
|
1
|
+
module HaveAPI::ClientExamples
|
2
|
+
class FsClient < HaveAPI::ClientExample
|
3
|
+
label 'File system'
|
4
|
+
code :bash
|
5
|
+
order 40
|
6
|
+
|
7
|
+
def init
|
8
|
+
"# Mount the file system\n$ haveapi-fs #{base_url} #{mountpoint} -o version=#{version}"
|
9
|
+
end
|
10
|
+
|
11
|
+
def auth(method, desc)
|
12
|
+
case method
|
13
|
+
when :basic
|
14
|
+
<<END
|
15
|
+
# Provide credentials as file system options
|
16
|
+
#{init} -o auth_method=basic,user=myuser,password=secret
|
17
|
+
|
18
|
+
# If username or password isn't provided, the user is asked on stdin
|
19
|
+
#{init} -o auth_method=basic,user=myuser
|
20
|
+
Password: secret
|
21
|
+
END
|
22
|
+
|
23
|
+
when :token
|
24
|
+
<<END
|
25
|
+
# Authenticate using username and password
|
26
|
+
#{init} -o auth_method=token,user=myuser
|
27
|
+
Password: secret
|
28
|
+
|
29
|
+
# If you have generated a token, you can use it
|
30
|
+
#{init} -o auth_method=token,token=yourtoken
|
31
|
+
|
32
|
+
# Note that the file system can read config file from haveapi-client, so if
|
33
|
+
# you set up authentication there, the file system will use it.
|
34
|
+
END
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def example(sample)
|
39
|
+
cmd = [init]
|
40
|
+
|
41
|
+
path = [mountpoint].concat(resource_path)
|
42
|
+
|
43
|
+
unless class_action?
|
44
|
+
if !sample[:url_params] || sample[:url_params].empty?
|
45
|
+
fail "example {#{sample}} of action #{resource_path.join('.')}"+
|
46
|
+
".#{action_name} is for an instance action but does not include "+
|
47
|
+
"URL parameters"
|
48
|
+
end
|
49
|
+
|
50
|
+
path << sample[:url_params].first.to_s
|
51
|
+
end
|
52
|
+
|
53
|
+
path << 'actions' << action_name
|
54
|
+
|
55
|
+
cmd << "\n# Change to action directory"
|
56
|
+
cmd << "$ cd #{File.join(path)}"
|
57
|
+
|
58
|
+
if sample[:request] && !sample[:request].empty?
|
59
|
+
cmd << "\n# Prepare input parameters"
|
60
|
+
|
61
|
+
sample[:request].each do |k, v|
|
62
|
+
cmd << "$ echo '#{v}' > input/#{k}"
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
cmd << "\n# Execute the action"
|
67
|
+
cmd << "$ echo 1 > exec"
|
68
|
+
|
69
|
+
cmd << "\n# Query the action's result"
|
70
|
+
cmd << "$ cat status"
|
71
|
+
cmd << (sample[:status] ? '1' : '0')
|
72
|
+
|
73
|
+
if sample[:status]
|
74
|
+
if sample[:response] && !sample[:response].empty? \
|
75
|
+
&& %i(hash object).include?(action[:output][:layout])
|
76
|
+
cmd << "\n# Query the output parameters"
|
77
|
+
|
78
|
+
sample[:response].each do |k, v|
|
79
|
+
cmd << "$ cat output/#{k}"
|
80
|
+
|
81
|
+
if v === true
|
82
|
+
cmd << '1'
|
83
|
+
|
84
|
+
elsif v === false
|
85
|
+
cmd << '0'
|
86
|
+
|
87
|
+
else
|
88
|
+
cmd << "#{v.to_s}"
|
89
|
+
end
|
90
|
+
|
91
|
+
cmd << "\n"
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
else
|
96
|
+
cmd << "\n# Get the error message"
|
97
|
+
cmd << "$ cat message"
|
98
|
+
cmd << sample[:message]
|
99
|
+
|
100
|
+
cmd << "\n# Parameter errors can be seen in the `errors` directory"
|
101
|
+
cmd << "$ ls errors"
|
102
|
+
cmd << (sample[:errors] || {}).keys.join("\n")
|
103
|
+
end
|
104
|
+
|
105
|
+
cmd.join("\n")
|
106
|
+
end
|
107
|
+
|
108
|
+
def mountpoint
|
109
|
+
"/mnt/#{host}"
|
110
|
+
end
|
111
|
+
|
112
|
+
def class_action?
|
113
|
+
action[:url].index(/:[a-zA-Z\-_]+/).nil?
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|