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
@@ -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
|