haveapi 0.6.0 → 0.7.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (42) hide show
  1. checksums.yaml +4 -4
  2. data/.editorconfig +15 -0
  3. data/CHANGELOG +15 -0
  4. data/README.md +66 -47
  5. data/doc/create-client.md +14 -5
  6. data/doc/json-schema.erb +16 -2
  7. data/doc/protocol.md +25 -3
  8. data/doc/protocol.plantuml +14 -8
  9. data/haveapi.gemspec +4 -2
  10. data/lib/haveapi.rb +5 -3
  11. data/lib/haveapi/action.rb +34 -6
  12. data/lib/haveapi/action_state.rb +92 -0
  13. data/lib/haveapi/authentication/basic/provider.rb +7 -0
  14. data/lib/haveapi/authentication/token/provider.rb +5 -0
  15. data/lib/haveapi/client_example.rb +83 -0
  16. data/lib/haveapi/client_examples/curl.rb +86 -0
  17. data/lib/haveapi/client_examples/fs_client.rb +116 -0
  18. data/lib/haveapi/client_examples/http.rb +91 -0
  19. data/lib/haveapi/client_examples/js_client.rb +149 -0
  20. data/lib/haveapi/client_examples/php_client.rb +122 -0
  21. data/lib/haveapi/client_examples/ruby_cli.rb +117 -0
  22. data/lib/haveapi/client_examples/ruby_client.rb +106 -0
  23. data/lib/haveapi/context.rb +3 -2
  24. data/lib/haveapi/example.rb +29 -2
  25. data/lib/haveapi/extensions/action_exceptions.rb +2 -2
  26. data/lib/haveapi/extensions/base.rb +1 -1
  27. data/lib/haveapi/extensions/exception_mailer.rb +339 -0
  28. data/lib/haveapi/hooks.rb +1 -1
  29. data/lib/haveapi/parameters/typed.rb +5 -3
  30. data/lib/haveapi/public/css/highlight.css +99 -0
  31. data/lib/haveapi/public/doc/protocol.png +0 -0
  32. data/lib/haveapi/public/js/highlight.pack.js +2 -0
  33. data/lib/haveapi/public/js/highlighter.js +9 -0
  34. data/lib/haveapi/public/js/main.js +32 -0
  35. data/lib/haveapi/public/js/nojs-tabs.js +196 -0
  36. data/lib/haveapi/resources/action_state.rb +196 -0
  37. data/lib/haveapi/server.rb +96 -27
  38. data/lib/haveapi/version.rb +2 -2
  39. data/lib/haveapi/views/main_layout.erb +14 -0
  40. data/lib/haveapi/views/version_page.erb +187 -13
  41. data/lib/haveapi/views/version_sidebar.erb +37 -3
  42. 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