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
@@ -0,0 +1,91 @@
|
|
1
|
+
require 'pp'
|
2
|
+
require 'cgi'
|
3
|
+
require 'rack/utils'
|
4
|
+
|
5
|
+
module HaveAPI::ClientExamples
|
6
|
+
class Http < HaveAPI::ClientExample
|
7
|
+
label 'HTTP'
|
8
|
+
code :http
|
9
|
+
order 100
|
10
|
+
|
11
|
+
def init
|
12
|
+
<<END
|
13
|
+
OPTIONS /v#{version}/ HTTP/1.1
|
14
|
+
Host: #{host}
|
15
|
+
|
16
|
+
END
|
17
|
+
end
|
18
|
+
|
19
|
+
def auth(method, desc)
|
20
|
+
case method
|
21
|
+
when :basic
|
22
|
+
<<END
|
23
|
+
GET / HTTP/1.1
|
24
|
+
Host: #{host}
|
25
|
+
Authorization: Basic dXNlcjpzZWNyZXQ=
|
26
|
+
|
27
|
+
END
|
28
|
+
|
29
|
+
when :token
|
30
|
+
<<END
|
31
|
+
POST /_auth/token/tokens HTTP/1.1
|
32
|
+
Host: #{host}
|
33
|
+
Content-Type: application/json
|
34
|
+
|
35
|
+
#{JSON.pretty_generate({token: {login: 'user', password: 'secret', lifetime: 'fixed'}})}
|
36
|
+
END
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def request(sample)
|
41
|
+
path = resolve_path(
|
42
|
+
action[:method],
|
43
|
+
action[:url],
|
44
|
+
sample[:url_params] || [],
|
45
|
+
sample[:request]
|
46
|
+
)
|
47
|
+
|
48
|
+
req = "#{action[:method]} #{path} HTTP/1.1\n"
|
49
|
+
req << "Host: #{host}\n"
|
50
|
+
req << "Content-Type: application/json\n\n"
|
51
|
+
|
52
|
+
if action[:method] != 'GET' && sample[:request] && !sample[:request].empty?
|
53
|
+
req << JSON.pretty_generate({action[:input][:namespace] => sample[:request]})
|
54
|
+
end
|
55
|
+
|
56
|
+
req
|
57
|
+
end
|
58
|
+
|
59
|
+
def response(sample)
|
60
|
+
content = JSON.pretty_generate({
|
61
|
+
status: sample[:status],
|
62
|
+
message: sample[:message],
|
63
|
+
response: {action[:output][:namespace] => sample[:response]},
|
64
|
+
errors: sample[:errors],
|
65
|
+
})
|
66
|
+
|
67
|
+
status_msg = Rack::Utils::HTTP_STATUS_CODES[sample[:http_status]]
|
68
|
+
|
69
|
+
res = "HTTP/1.1 #{sample[:http_status]} #{status_msg}\n"
|
70
|
+
res << "Content-Type: application/json;charset=utf-8\n"
|
71
|
+
res << "Content-Length: #{content.size}\n\n"
|
72
|
+
res << content
|
73
|
+
res
|
74
|
+
end
|
75
|
+
|
76
|
+
def resolve_path(method, url, url_params, input_params)
|
77
|
+
ret = url.clone
|
78
|
+
|
79
|
+
url_params.each do |v|
|
80
|
+
ret.sub!(/:[a-zA-Z\-_]+/, v.to_s)
|
81
|
+
end
|
82
|
+
|
83
|
+
return ret if method != 'GET' || !input_params || input_params.empty?
|
84
|
+
|
85
|
+
ret << '?'
|
86
|
+
ret << input_params.map { |k, v| "#{k}=#{CGI.escape(v.to_s)}" }.join('&')
|
87
|
+
|
88
|
+
ret
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
@@ -0,0 +1,149 @@
|
|
1
|
+
module HaveAPI::ClientExamples
|
2
|
+
class JsClient < HaveAPI::ClientExample
|
3
|
+
label 'JavaScript'
|
4
|
+
code :javascript
|
5
|
+
order 10
|
6
|
+
|
7
|
+
def init
|
8
|
+
<<END
|
9
|
+
import HaveAPI from 'haveapi-client'
|
10
|
+
|
11
|
+
var api = new HaveAPI.Client("#{base_url}", {version: "#{version}"});
|
12
|
+
END
|
13
|
+
end
|
14
|
+
|
15
|
+
def auth(method, desc)
|
16
|
+
case method
|
17
|
+
when :basic
|
18
|
+
<<END
|
19
|
+
#{init}
|
20
|
+
|
21
|
+
api.authenticate("basic", {
|
22
|
+
username: "user",
|
23
|
+
password: "secret"
|
24
|
+
}, function (client, status) {
|
25
|
+
console.log("Authenticated?", status);
|
26
|
+
});
|
27
|
+
END
|
28
|
+
|
29
|
+
when :token
|
30
|
+
<<END
|
31
|
+
#{init}
|
32
|
+
|
33
|
+
// Request a new token
|
34
|
+
api.authenticate("token", {
|
35
|
+
username: "user",
|
36
|
+
password: "secret"
|
37
|
+
}, function (client, status) {
|
38
|
+
console.log("Authenticated?", status);
|
39
|
+
|
40
|
+
if (status)
|
41
|
+
console.log("Token is", client.authProvider.token);
|
42
|
+
});
|
43
|
+
|
44
|
+
// Use an existing token
|
45
|
+
api.authenticate("token", {
|
46
|
+
token: "qwertyuiop..."
|
47
|
+
}, function (client, status) {
|
48
|
+
console.log("Authenticated?", status);
|
49
|
+
});
|
50
|
+
END
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
def example(sample)
|
55
|
+
args = []
|
56
|
+
|
57
|
+
args.concat(sample[:url_params]) if sample[:url_params]
|
58
|
+
|
59
|
+
if sample[:request] && !sample[:request].empty?
|
60
|
+
args << JSON.pretty_generate(sample[:request])
|
61
|
+
end
|
62
|
+
|
63
|
+
out = "#{init}\n"
|
64
|
+
out << "api.#{resource_path.join('.')}.#{action_name}"
|
65
|
+
out << '('
|
66
|
+
out << "#{args.join(', ')}, " unless args.empty?
|
67
|
+
|
68
|
+
callback = "function (client, reply) {\n"
|
69
|
+
callback << " console.log('Response', reply);\n"
|
70
|
+
|
71
|
+
if sample[:status]
|
72
|
+
callback << response(sample)
|
73
|
+
|
74
|
+
else
|
75
|
+
callback << error(sample)
|
76
|
+
end
|
77
|
+
|
78
|
+
callback << "}"
|
79
|
+
|
80
|
+
out << callback.strip
|
81
|
+
out << ');'
|
82
|
+
out
|
83
|
+
end
|
84
|
+
|
85
|
+
def response(sample)
|
86
|
+
out = ''
|
87
|
+
|
88
|
+
case action[:output][:layout]
|
89
|
+
when :hash
|
90
|
+
out << "# reply is an instance of HaveAPI.Client.Response\n"
|
91
|
+
out << "# reply.response() returns an object with output parameters:\n"
|
92
|
+
out << JSON.pretty_generate(sample[:response] || {}).split("\n").map do |v|
|
93
|
+
" // #{v}"
|
94
|
+
end.join("\n")
|
95
|
+
|
96
|
+
when :hash_list
|
97
|
+
out << "# reply is an instance of HaveAPI.Client.Response\n"
|
98
|
+
out << "# reply.response() returns an array of objects:\n"
|
99
|
+
out << JSON.pretty_generate(sample[:response] || []).split("\n").map do |v|
|
100
|
+
" // #{v}"
|
101
|
+
end.join("\n")
|
102
|
+
|
103
|
+
when :object
|
104
|
+
out << " // reply is an instance of HaveAPI.Client.ResourceInstance\n"
|
105
|
+
|
106
|
+
(sample[:response] || {}).each do |k, v|
|
107
|
+
param = action[:output][:parameters][k]
|
108
|
+
|
109
|
+
if param[:type] == 'Resource'
|
110
|
+
out << " // reply.#{k} = HaveAPI.Client.ResourceInstance("
|
111
|
+
out << "resource: #{param[:resource].join('.')}, "
|
112
|
+
|
113
|
+
if v.is_a?(::Hash)
|
114
|
+
out << v.map { |k,v| "#{k}: #{PP.pp(v, '').strip}" }.join(', ')
|
115
|
+
else
|
116
|
+
out << "id: #{v}"
|
117
|
+
end
|
118
|
+
|
119
|
+
out << ")\n"
|
120
|
+
|
121
|
+
elsif param[:type] == 'Custom' && (v.is_a?(::Hash) || v.is_a?(::Array))
|
122
|
+
json = JSON.pretty_generate(v).split("\n").map do |v|
|
123
|
+
" // #{v}"
|
124
|
+
end.join("\n")
|
125
|
+
|
126
|
+
out << " // reply.#{k} = #{json}"
|
127
|
+
|
128
|
+
else
|
129
|
+
out << " // reply.#{k} = #{PP.pp(v, '')}"
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
when :object_list
|
134
|
+
out << " // reply is an instance of HaveAPI.Client.ResourceInstanceList\n"
|
135
|
+
end
|
136
|
+
|
137
|
+
out
|
138
|
+
|
139
|
+
end
|
140
|
+
|
141
|
+
def error(sample)
|
142
|
+
out = ''
|
143
|
+
out << " // reply.isOk() returns false\n"
|
144
|
+
out << " // reply.message() returns the error message\n"
|
145
|
+
out << " // reply.envelope.errors contains parameter errors\n"
|
146
|
+
out
|
147
|
+
end
|
148
|
+
end
|
149
|
+
end
|
@@ -0,0 +1,122 @@
|
|
1
|
+
module HaveAPI::ClientExamples
|
2
|
+
class PhpClient < HaveAPI::ClientExample
|
3
|
+
label 'PHP'
|
4
|
+
code :php
|
5
|
+
order 20
|
6
|
+
|
7
|
+
def init
|
8
|
+
<<END
|
9
|
+
$api = new \\HaveAPI\\Client("#{base_url}", "#{version}");
|
10
|
+
END
|
11
|
+
end
|
12
|
+
|
13
|
+
def auth(method, desc)
|
14
|
+
case method
|
15
|
+
when :basic
|
16
|
+
<<END
|
17
|
+
#{init}
|
18
|
+
|
19
|
+
$api->authenticate("basic", ["username" => "user", "password" => "secret"]);
|
20
|
+
END
|
21
|
+
|
22
|
+
when :token
|
23
|
+
<<END
|
24
|
+
#{init}
|
25
|
+
|
26
|
+
// Get token using username and password
|
27
|
+
$api->authenticate("token", ["username" => "user", "password" => "secret"]);
|
28
|
+
|
29
|
+
echo "Token = ".$api->getAuthenticationProvider()->getToken();
|
30
|
+
|
31
|
+
// Next time, the client can authenticate using the token directly
|
32
|
+
$api->authenticate("token", ["token" => $savedToken]);
|
33
|
+
END
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def example(sample)
|
38
|
+
args = []
|
39
|
+
|
40
|
+
args.concat(sample[:url_params]) if sample[:url_params]
|
41
|
+
|
42
|
+
if sample[:request] && !sample[:request].empty?
|
43
|
+
args << format_parameters(:input, sample[:request])
|
44
|
+
end
|
45
|
+
|
46
|
+
out = "#{init}\n"
|
47
|
+
out << "$reply = $api->#{resource_path.join('->')}->#{action_name}"
|
48
|
+
out << "(#{args.join(', ')});\n"
|
49
|
+
|
50
|
+
return (out << response(sample)) if sample[:status]
|
51
|
+
|
52
|
+
out << "// Throws exception \\HaveAPI\\Client\\Exception\\ActionFailed"
|
53
|
+
out
|
54
|
+
end
|
55
|
+
|
56
|
+
def response(sample)
|
57
|
+
out = "\n"
|
58
|
+
|
59
|
+
case action[:output][:layout]
|
60
|
+
when :hash
|
61
|
+
out << "// $reply is an instance of \\HaveAPI\\Client\\Response\n"
|
62
|
+
out << "// $reply->getResponse() returns an associative array of output parameters:\n"
|
63
|
+
out << format_parameters(:output, sample[:response] || {}, "// ")
|
64
|
+
|
65
|
+
when :hash_list
|
66
|
+
out << "// $reply is an instance of \\HaveAPI\\Client\\Response\n"
|
67
|
+
out << "// $reply->getResponse() returns an array of associative arrays:\n"
|
68
|
+
|
69
|
+
when :object
|
70
|
+
out << "// $reply is an instance of \\HaveAPI\\Client\\ResourceInstance\n"
|
71
|
+
|
72
|
+
(sample[:response] || {}).each do |k, v|
|
73
|
+
param = action[:output][:parameters][k]
|
74
|
+
|
75
|
+
if param[:type] == 'Resource'
|
76
|
+
out << "// $reply->#{k} = \\HaveAPI\\Client\\ResourceInstance("
|
77
|
+
out << "resource: #{param[:resource].join('.')}, "
|
78
|
+
|
79
|
+
if v.is_a?(::Hash)
|
80
|
+
out << v.map { |k,v| "#{k}: #{PP.pp(v, '').strip}" }.join(', ')
|
81
|
+
else
|
82
|
+
out << "id: #{v}"
|
83
|
+
end
|
84
|
+
|
85
|
+
out << ")\n"
|
86
|
+
|
87
|
+
elsif param[:type] == 'Custom'
|
88
|
+
out << "// $reply->#{k} is a custom type"
|
89
|
+
|
90
|
+
else
|
91
|
+
out << "// $reply->#{k} = #{PP.pp(v, '')}"
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
when :object_list
|
96
|
+
out << "// $reply is an instance of \\HaveAPI\\Client\\ResourceInstanceList"
|
97
|
+
end
|
98
|
+
|
99
|
+
out
|
100
|
+
end
|
101
|
+
|
102
|
+
def format_parameters(dir, params, prefix = '')
|
103
|
+
ret = []
|
104
|
+
|
105
|
+
params.each do |k, v|
|
106
|
+
if action[dir][:parameters][k][:type] == 'Custom'
|
107
|
+
ret << "#{prefix} \"#{k}\" => custom type}"
|
108
|
+
|
109
|
+
else
|
110
|
+
ret << "#{prefix} \"#{k}\" => #{value(v)}"
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
"#{prefix}[\n#{ret.join(",\n")}\n#{prefix}]"
|
115
|
+
end
|
116
|
+
|
117
|
+
def value(v)
|
118
|
+
return v if v.is_a?(::Numeric) || v === true || v === false
|
119
|
+
"\"#{v}\""
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|
@@ -0,0 +1,117 @@
|
|
1
|
+
module HaveAPI
|
2
|
+
module CLI ; end
|
3
|
+
end
|
4
|
+
|
5
|
+
require 'haveapi/cli/output_formatter'
|
6
|
+
|
7
|
+
module HaveAPI::ClientExamples
|
8
|
+
class RubyCli < HaveAPI::ClientExample
|
9
|
+
label 'CLI'
|
10
|
+
code :bash
|
11
|
+
order 30
|
12
|
+
|
13
|
+
def init
|
14
|
+
"$ haveapi-cli -u #{base_url} --version #{version}"
|
15
|
+
end
|
16
|
+
|
17
|
+
def auth(method, desc)
|
18
|
+
case method
|
19
|
+
when :basic
|
20
|
+
<<END
|
21
|
+
# Provide credentials on command line
|
22
|
+
#{init} --auth basic --username user --password secret
|
23
|
+
|
24
|
+
# If username or password isn't provided, the user is asked on stdin
|
25
|
+
#{init} --auth basic --username user
|
26
|
+
Password: secret
|
27
|
+
END
|
28
|
+
|
29
|
+
when :token
|
30
|
+
<<END
|
31
|
+
# Get token using username and password and save it to disk
|
32
|
+
# Note that the client always has to call some action. APIs should provide
|
33
|
+
# an action to get information about the current user, so that's what we're
|
34
|
+
# calling now.
|
35
|
+
#{init} --auth token --username user --save user current
|
36
|
+
Password: secret
|
37
|
+
|
38
|
+
# Now the token is read from disk and the user does not have to provide username
|
39
|
+
# nor password and be authenticated
|
40
|
+
#{init} user current
|
41
|
+
END
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def example(sample)
|
46
|
+
cmd = [init]
|
47
|
+
cmd << resource_path.join('.')
|
48
|
+
cmd << action_name
|
49
|
+
cmd.concat(sample[:url_params]) if sample[:url_params]
|
50
|
+
|
51
|
+
if sample[:request] && !sample[:request].empty?
|
52
|
+
cmd << "-- \\\n"
|
53
|
+
|
54
|
+
res = cmd.join(' ') + sample[:request].map do |k, v|
|
55
|
+
' '*14 + input_param(k, v)
|
56
|
+
end.join(" \\\n")
|
57
|
+
|
58
|
+
else
|
59
|
+
res = cmd.join(' ')
|
60
|
+
end
|
61
|
+
|
62
|
+
return response(sample, res) if sample[:status]
|
63
|
+
|
64
|
+
res << "\nAction failed: #{sample[:message]}\n"
|
65
|
+
|
66
|
+
if sample[:errors] && sample[:errors].any?
|
67
|
+
res << "Errors:\n"
|
68
|
+
sample[:errors].each do |param, e|
|
69
|
+
res << "\t#{param}: #{e.join('; ')}\n"
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
res
|
74
|
+
end
|
75
|
+
|
76
|
+
def response(sample, res)
|
77
|
+
return res if sample[:response].nil? || sample[:response].empty?
|
78
|
+
|
79
|
+
cols = []
|
80
|
+
|
81
|
+
action[:output][:parameters].each do |name, param|
|
82
|
+
col = {
|
83
|
+
name: name,
|
84
|
+
align: %w(Integer Float).include?(param[:type]) ? 'right' : 'left',
|
85
|
+
label: param[:label] && !param[:label].empty? ? param[:label] : name.upcase,
|
86
|
+
}
|
87
|
+
|
88
|
+
if param[:type] == 'Resource'
|
89
|
+
col[:display] = Proc.new do |r|
|
90
|
+
next '' unless r
|
91
|
+
next r unless r.is_a?(::Hash)
|
92
|
+
|
93
|
+
"#{r[ param[:value_label].to_sym ]} (##{r[ param[:value_id].to_sym ]})"
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
cols << col
|
98
|
+
end
|
99
|
+
|
100
|
+
res << "\n" << HaveAPI::CLI::OutputFormatter.format(
|
101
|
+
sample[:response],
|
102
|
+
cols
|
103
|
+
)
|
104
|
+
res
|
105
|
+
end
|
106
|
+
|
107
|
+
def input_param(name, value)
|
108
|
+
option = name.to_s.gsub(/_/, '-')
|
109
|
+
|
110
|
+
if action[:input][:parameters][name][:type] == 'Boolean'
|
111
|
+
return value ? "--#{option}" : "--no-#{name}"
|
112
|
+
end
|
113
|
+
|
114
|
+
"--#{option} '#{value}'"
|
115
|
+
end
|
116
|
+
end
|
117
|
+
end
|