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