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