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,106 @@
1
+ require 'pp'
2
+
3
+ module HaveAPI::ClientExamples
4
+ class RubyClient < HaveAPI::ClientExample
5
+ label 'Ruby'
6
+ code :ruby
7
+ order 0
8
+
9
+ def init
10
+ <<END
11
+ require 'haveapi-client'
12
+
13
+ client = HaveAPI::Client.new("#{base_url}", version: "#{version}")
14
+ END
15
+ end
16
+
17
+ def auth(method, desc)
18
+ case method
19
+ when :basic
20
+ <<END
21
+ #{init}
22
+
23
+ client.authenticate(:basic, username: "user", password: "secret")
24
+ END
25
+
26
+ when :token
27
+ <<END
28
+ #{init}
29
+
30
+ # Get token using username and password
31
+ client.authenticate(:token, username: "user", password: "secret")
32
+
33
+ puts "Token = \#{client.auth.token}"
34
+
35
+ # Next time, the client can authenticate using the token directly
36
+ client.authenticate(:token, token: saved_token)
37
+ END
38
+ end
39
+ end
40
+
41
+ def example(sample)
42
+ args = []
43
+
44
+ args.concat(sample[:url_params]) if sample[:url_params]
45
+
46
+ if sample[:request] && !sample[:request].empty?
47
+ args << PP.pp(sample[:request], '').strip
48
+ end
49
+
50
+ out = "#{init}\n"
51
+ out << "reply = client.#{resource_path.join('.')}.#{action_name}"
52
+ out << "(#{args.join(', ')})" unless args.empty?
53
+
54
+ return (out << response(sample)) if sample[:status]
55
+
56
+ out << "\n"
57
+ out << "# Raises exception HaveAPI::Client::ActionFailed"
58
+ out
59
+ end
60
+
61
+ def response(sample)
62
+ out = "\n\n"
63
+
64
+ case action[:output][:layout]
65
+ when :hash
66
+ out << "# reply is an instance of HaveAPI::Client::Response\n"
67
+ out << "# reply.response() returns a hash of output parameters:\n"
68
+ out << PP.pp(sample[:response] || {}, '').split("\n").map { |v| "# #{v}" }.join("\n")
69
+
70
+ when :hash_list
71
+ out << "# reply is an instance of HaveAPI::Client::Response\n"
72
+ out << "# reply.response() returns an array of hashes:\n"
73
+ out << PP.pp(sample[:response] || [], '').split("\n").map { |v| "# #{v}" }.join("\n")
74
+
75
+ when :object
76
+ out << "# reply is an instance of HaveAPI::Client::ResourceInstance\n"
77
+
78
+ (sample[:response] || {}).each do |k, v|
79
+ param = action[:output][:parameters][k]
80
+
81
+ if param[:type] == 'Resource'
82
+ out << "# reply.#{k} = HaveAPI::Client::ResourceInstance("
83
+ out << "resource: #{param[:resource].join('.')}, "
84
+
85
+ if v.is_a?(::Hash)
86
+ out << v.map { |k,v| "#{k}: #{PP.pp(v, '').strip}" }.join(', ')
87
+ else
88
+ out << "id: #{v}"
89
+ end
90
+
91
+ out << ")\n"
92
+
93
+ else
94
+ out << "# reply.#{k} = #{PP.pp(v, '')}"
95
+ end
96
+ end
97
+
98
+ when :object_list
99
+ out << "# reply is an instance of HaveAPI::Client::ResourceInstanceList,\n"
100
+ out << "# which is a subclass of Array"
101
+ end
102
+
103
+ out
104
+ end
105
+ end
106
+ end
@@ -1,14 +1,15 @@
1
1
  module HaveAPI
2
2
  class Context
3
- attr_accessor :server, :version, :resource, :action, :url, :args,
3
+ attr_accessor :server, :version, :request, :resource, :action, :url, :args,
4
4
  :params, :current_user, :authorization, :endpoint,
5
5
  :action_instance, :action_prepare, :layout
6
6
 
7
- def initialize(server, version: nil, resource: [], action: nil,
7
+ def initialize(server, version: nil, request: nil, resource: [], action: nil,
8
8
  url: nil, args: nil, params: nil, user: nil,
9
9
  authorization: nil, endpoint: nil)
10
10
  @server = server
11
11
  @version = version
12
+ @request = request
12
13
  @resource = resource
13
14
  @action = action
14
15
  @url = url
@@ -4,6 +4,10 @@ module HaveAPI
4
4
  @title = title
5
5
  end
6
6
 
7
+ def url_params(*params)
8
+ @url_params = params
9
+ end
10
+
7
11
  def request(f)
8
12
  @request = f
9
13
  end
@@ -12,21 +16,44 @@ module HaveAPI
12
16
  @response = f
13
17
  end
14
18
 
19
+ def status(status)
20
+ @status = status
21
+ end
22
+
23
+ def message(msg)
24
+ @message = msg
25
+ end
26
+
27
+ def errors(errs)
28
+ @errors = errs
29
+ end
30
+
31
+ def http_status(code)
32
+ @http_status = code
33
+ end
34
+
15
35
  def comment(str)
16
36
  @comment = str
17
37
  end
18
38
 
19
39
  def provided?
20
- @request || @response || @comment
40
+ instance_variables.detect do |v|
41
+ instance_variable_get(v)
42
+ end ? true : false
21
43
  end
22
44
 
23
45
  def describe
24
46
  if provided?
25
47
  {
26
48
  title: @title,
49
+ comment: @comment,
50
+ url_params: @url_params,
27
51
  request: @request,
28
52
  response: @response,
29
- comment: @comment
53
+ status: @status.nil? ? true : @status,
54
+ message: @message,
55
+ errors: @errors,
56
+ http_status: @http_status || 200,
30
57
  }
31
58
  else
32
59
  {}
@@ -1,8 +1,8 @@
1
1
  module HaveAPI::Extensions
2
2
  class ActionExceptions < Base
3
3
  class << self
4
- def enabled
5
- HaveAPI::Action.connect_hook(:exec_exception) do |ret, action, e|
4
+ def enabled(server)
5
+ HaveAPI::Action.connect_hook(:exec_exception) do |ret, context, e|
6
6
  break(ret) unless @exceptions
7
7
 
8
8
  @exceptions.each do |handler|
@@ -1,7 +1,7 @@
1
1
  module HaveAPI
2
2
  module Extensions
3
3
  class Base
4
- def self.enabled
4
+ def self.enabled(server)
5
5
 
6
6
  end
7
7
  end
@@ -0,0 +1,339 @@
1
+ require 'net/smtp'
2
+ require 'mail'
3
+
4
+ module HaveAPI::Extensions
5
+ # This extension mails exceptions raised during action execution and description
6
+ # construction to specified e-mail address.
7
+ #
8
+ # The template is based on {Sinatra::ShowExceptions::TEMPLATE}, but the JavaScript
9
+ # functions are removed, since e-mail doesn't support it. HaveAPI-specific content
10
+ # is added. Some helper methods are taken either from Sinatra or Rack.
11
+ class ExceptionMailer < Base
12
+ # @param opts [Hash] options
13
+ # @option opts to [String] recipient address
14
+ # @option opts from [String] sender address
15
+ # @option opts subject [String] '%s' is replaced by the error message
16
+ # @option opts smtp [Hash, falsy] smtp options, sendmail is used if not provided
17
+ def initialize(opts)
18
+ @opts = opts
19
+ end
20
+
21
+ def enabled(server)
22
+ HaveAPI::Action.connect_hook(:exec_exception) do |ret, context, e|
23
+ log(context, e)
24
+ ret
25
+ end
26
+
27
+ server.connect_hook(:description_exception) do |ret, context, e|
28
+ log(context, e)
29
+ ret
30
+ end
31
+ end
32
+
33
+ def log(context, exception)
34
+ req = context.request.request
35
+ path = (req.script_name + req.path_info).squeeze("/")
36
+
37
+ frames = exception.backtrace.map { |line|
38
+ frame = OpenStruct.new
39
+
40
+ if line =~ /(.*?):(\d+)(:in `(.*)')?/
41
+ frame.filename = $1
42
+ frame.lineno = $2.to_i
43
+ frame.function = $4
44
+
45
+ begin
46
+ lineno = frame.lineno-1
47
+ lines = ::File.readlines(frame.filename)
48
+ frame.context_line = lines[lineno].chomp
49
+ rescue
50
+ end
51
+
52
+ frame
53
+ else
54
+ nil
55
+ end
56
+ }.compact
57
+
58
+ env = context.request.env
59
+
60
+ mail(context, exception, TEMPLATE.result(binding))
61
+ end
62
+
63
+ def mail(context, exception, body)
64
+ mail = ::Mail.new({
65
+ from: @opts[:from],
66
+ to: @opts[:to],
67
+ subject: @opts[:subject] % [exception.to_s],
68
+ body: body,
69
+ content_type: 'text/html; charset=UTF-8',
70
+ })
71
+
72
+ if @opts[:smtp]
73
+ mail.delivery_method(:smtp, @opts[:smtp])
74
+
75
+ else
76
+ mail.delivery_method(:sendmail)
77
+ end
78
+
79
+ mail.deliver!
80
+ mail
81
+ end
82
+
83
+ protected
84
+ # From {Sinatra::ShowExceptions}
85
+ def frame_class(frame)
86
+ if frame.filename =~ /lib\/sinatra.*\.rb/
87
+ "framework"
88
+ elsif (defined?(Gem) && frame.filename.include?(Gem.dir)) ||
89
+ frame.filename =~ /\/bin\/(\w+)$/
90
+ "system"
91
+ else
92
+ "app"
93
+ end
94
+ end
95
+
96
+ # From {Rack::ShowExceptions}
97
+ def h(obj)
98
+ case obj
99
+ when String
100
+ Rack::Utils.escape_html(obj)
101
+ else
102
+ Rack::Utils.escape_html(obj.inspect)
103
+ end
104
+ end
105
+
106
+ TEMPLATE = ERB.new(<<END
107
+ <!DOCTYPE html>
108
+ <html>
109
+ <head>
110
+ <meta charset="utf-8">
111
+ <title><%=h exception.class %> at <%=h path %></title>
112
+ <style type="text/css">
113
+ * {margin: 0; padding: 0; border: 0; outline: 0;}
114
+ div.clear {clear: both;}
115
+ body {background: #EEEEEE; margin: 0; padding: 0;
116
+ font-family: 'Lucida Grande', 'Lucida Sans Unicode',
117
+ 'Garuda';}
118
+ code {font-family: 'Lucida Console', monospace;
119
+ font-size: 12px;}
120
+ li {height: 18px;}
121
+ ul {list-style: none; margin: 0; padding: 0;}
122
+ ol:hover {cursor: pointer;}
123
+ ol li {white-space: pre;}
124
+ #explanation {font-size: 12px; color: #666666;
125
+ margin: 20px 0 0 100px;}
126
+ /* WRAP */
127
+ #wrap {width: 1000px; background: #FFFFFF; margin: 0 auto;
128
+ padding: 30px 5px 20px 5px;
129
+ border-left: 1px solid #DDDDDD;
130
+ border-right: 1px solid #DDDDDD;}
131
+ /* HEADER */
132
+ #header {margin: 0 auto 25px auto;}
133
+ #header #summary {margin: 12px 0 0 20px;
134
+ font-family: 'Lucida Grande', 'Lucida Sans Unicode';}
135
+ h1 {margin: 0; font-size: 36px; color: #981919;}
136
+ h2 {margin: 0; font-size: 22px; color: #333333;}
137
+ #header ul {margin: 0; font-size: 12px; color: #666666;}
138
+ #header ul li strong{color: #444444;}
139
+ #header ul li {display: inline; padding: 0 10px;}
140
+ #header ul li.first {padding-left: 0;}
141
+ #header ul li.last {border: 0; padding-right: 0;}
142
+ /* BODY */
143
+ #backtrace,
144
+ #get,
145
+ #post,
146
+ #cookies,
147
+ #rack, #context {width: 980px; margin: 0 auto 10px auto;}
148
+ p#nav {float: right; font-size: 14px;}
149
+ /* BACKTRACE */
150
+ h3 {float: left; width: 100px; margin-bottom: 10px;
151
+ color: #981919; font-size: 14px; font-weight: bold;}
152
+ #nav a {color: #666666; text-decoration: none; padding: 0 5px;}
153
+ #backtrace li.frame-info {background: #f7f7f7; padding-left: 10px;
154
+ font-size: 12px; color: #333333;}
155
+ #backtrace ul {list-style-position: outside; border: 1px solid #E9E9E9;
156
+ border-bottom: 0;}
157
+ #backtrace ol {width: 920px; margin-left: 50px;
158
+ font: 10px 'Lucida Console', monospace; color: #666666;}
159
+ #backtrace ol li {border: 0; border-left: 1px solid #E9E9E9;
160
+ padding: 2px 0;}
161
+ #backtrace ol code {font-size: 10px; color: #555555; padding-left: 5px;}
162
+ #backtrace-ul li {border-bottom: 1px solid #E9E9E9; height: auto;
163
+ padding: 3px 0;}
164
+ #backtrace-ul .code {padding: 6px 0 4px 0;}
165
+ /* REQUEST DATA */
166
+ p.no-data {padding-top: 2px; font-size: 12px; color: #666666;}
167
+ table.req {width: 980px; text-align: left; font-size: 12px;
168
+ color: #666666; padding: 0; border-spacing: 0;
169
+ border: 1px solid #EEEEEE; border-bottom: 0;
170
+ border-left: 0;
171
+ clear:both}
172
+ table.req tr th {padding: 2px 10px; font-weight: bold;
173
+ background: #F7F7F7; border-bottom: 1px solid #EEEEEE;
174
+ border-left: 1px solid #EEEEEE;}
175
+ table.req tr td {padding: 2px 20px 2px 10px;
176
+ border-bottom: 1px solid #EEEEEE;
177
+ border-left: 1px solid #EEEEEE;}
178
+ /* HIDE PRE/POST CODE AT START */
179
+ .pre-context,
180
+ .post-context {display: none;}
181
+ table td.code {width:750px}
182
+ table td.code div {width:750px;overflow:hidden}
183
+ </style>
184
+ </head>
185
+ <body>
186
+ <div id="wrap">
187
+ <div id="header">
188
+ <div id="summary">
189
+ <h1><strong><%=h exception.class %></strong> at <strong><%=h path %>
190
+ </strong></h1>
191
+ <h2><%=h exception.message %></h2>
192
+ <ul>
193
+ <li class="first"><strong>file:</strong> <code>
194
+ <%=h frames.first.filename.split("/").last %></code></li>
195
+ <li><strong>location:</strong> <code><%=h frames.first.function %>
196
+ </code></li>
197
+ <li class="last"><strong>line:
198
+ </strong> <%=h frames.first.lineno %></li>
199
+ </ul>
200
+ </div>
201
+ <div class="clear"></div>
202
+ </div>
203
+ <div id="context">
204
+ <h3>Context</h3>
205
+ <table class="req">
206
+ <tr>
207
+ <th>API version</th>
208
+ <td><%=h context.version %></td>
209
+ </tr>
210
+ <tr>
211
+ <th>Action</th>
212
+ <td><%= h(context.action && context.action.to_s) %></td>
213
+ </tr>
214
+ <tr>
215
+ <th>Arguments</th>
216
+ <td><%=h context.args %></td>
217
+ </tr>
218
+ <tr>
219
+ <th>Parameters</th>
220
+ <td><%=h context.params %></td>
221
+ </tr>
222
+ <tr>
223
+ <th>User</th>
224
+ <td><%=h context.request.current_user %></td>
225
+ </tr>
226
+ </table>
227
+ <div class="clear"></div>
228
+ </div>
229
+ <div id="backtrace">
230
+ <h3>BACKTRACE</h3>
231
+ <p id="nav"><strong>JUMP TO:</strong>
232
+ <a href="#get-info">GET</a>
233
+ <a href="#post-info">POST</a>
234
+ <a href="#cookie-info">COOKIES</a>
235
+ <a href="#env-info">ENV</a>
236
+ </p>
237
+ <div class="clear"></div>
238
+ <ul id="backtrace-ul">
239
+ <% frames.each do |frame| %>
240
+ <li class="frame-info <%= frame_class(frame) %>">
241
+ <code><%=h frame.filename %></code> in
242
+ <code><strong><%=h frame.function %></strong></code>
243
+ </li>
244
+ <li class="code <%= frame_class(frame) %>">
245
+ <ol start="<%= frame.lineno %>" class="context">
246
+ <li class="context-line">
247
+ <code><%=h frame.context_line %></code>
248
+ </li>
249
+ </ol>
250
+ <div class="clear"></div>
251
+ </li>
252
+ <% end %>
253
+ </ul>
254
+ </div> <!-- /BACKTRACE -->
255
+ <div id="get">
256
+ <h3 id="get-info">GET</h3>
257
+ <% if req.GET and not req.GET.empty? %>
258
+ <table class="req">
259
+ <tr>
260
+ <th>Variable</th>
261
+ <th>Value</th>
262
+ </tr>
263
+ <% req.GET.sort_by { |k, v| k.to_s }.each { |key, val| %>
264
+ <tr>
265
+ <td><%=h key %></td>
266
+ <td class="code"><div><%=h val.inspect %></div></td>
267
+ </tr>
268
+ <% } %>
269
+ </table>
270
+ <% else %>
271
+ <p class="no-data">No GET data.</p>
272
+ <% end %>
273
+ <div class="clear"></div>
274
+ </div> <!-- /GET -->
275
+ <div id="post">
276
+ <h3 id="post-info">POST</h3>
277
+ <% if req.POST and not req.POST.empty? %>
278
+ <table class="req">
279
+ <tr>
280
+ <th>Variable</th>
281
+ <th>Value</th>
282
+ </tr>
283
+ <% req.POST.sort_by { |k, v| k.to_s }.each { |key, val| %>
284
+ <tr>
285
+ <td><%=h key %></td>
286
+ <td class="code"><div><%=h val.inspect %></div></td>
287
+ </tr>
288
+ <% } %>
289
+ </table>
290
+ <% else %>
291
+ <p class="no-data">No POST data.</p>
292
+ <% end %>
293
+ <div class="clear"></div>
294
+ </div> <!-- /POST -->
295
+ <div id="cookies">
296
+ <h3 id="cookie-info">COOKIES</h3>
297
+ <% unless req.cookies.empty? %>
298
+ <table class="req">
299
+ <tr>
300
+ <th>Variable</th>
301
+ <th>Value</th>
302
+ </tr>
303
+ <% req.cookies.each { |key, val| %>
304
+ <tr>
305
+ <td><%=h key %></td>
306
+ <td class="code"><div><%=h val.inspect %></div></td>
307
+ </tr>
308
+ <% } %>
309
+ </table>
310
+ <% else %>
311
+ <p class="no-data">No cookie data.</p>
312
+ <% end %>
313
+ <div class="clear"></div>
314
+ </div> <!-- /COOKIES -->
315
+ <div id="rack">
316
+ <h3 id="env-info">Rack ENV</h3>
317
+ <table class="req">
318
+ <tr>
319
+ <th>Variable</th>
320
+ <th>Value</th>
321
+ </tr>
322
+ <% env.sort_by { |k, v| k.to_s }.each { |key, val| %>
323
+ <tr>
324
+ <td><%=h key %></td>
325
+ <td class="code"><div><%=h val %></div></td>
326
+ </tr>
327
+ <% } %>
328
+ </table>
329
+ <div class="clear"></div>
330
+ </div> <!-- /RACK ENV -->
331
+ <p id="explanation">You're seeing this error because you have
332
+ enabled HaveAPI Extension <code>HaveAPI::Extensions::ExceptionMailer</code>.</p>
333
+ </div> <!-- /WRAP -->
334
+ </body>
335
+ </html>
336
+ END
337
+ )
338
+ end
339
+ end