haveapi 0.6.0 → 0.7.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|