rspec_api_documentation 0.5.2 → 0.6.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (30) hide show
  1. data/lib/rspec_api_documentation.rb +1 -1
  2. data/lib/rspec_api_documentation/api_documentation.rb +1 -0
  3. data/lib/rspec_api_documentation/client_base.rb +9 -14
  4. data/lib/rspec_api_documentation/curl.rb +4 -4
  5. data/lib/rspec_api_documentation/html_writer.rb +15 -0
  6. data/lib/rspec_api_documentation/oauth2_mac_client.rb +5 -1
  7. data/lib/rspec_api_documentation/rack_test_client.rb +5 -1
  8. data/lib/rspec_api_documentation/test_server.rb +3 -4
  9. data/lib/rspec_api_documentation/wurl_writer.rb +105 -0
  10. data/templates/assets/img/glyphicons-halflings-white.png +0 -0
  11. data/templates/assets/img/glyphicons-halflings.png +0 -0
  12. data/templates/assets/javascripts/application.js +250 -0
  13. data/templates/assets/javascripts/codemirror.js +3636 -0
  14. data/templates/assets/javascripts/jquery-1-7-2.js +9401 -0
  15. data/templates/assets/javascripts/jquery-base64.js +189 -0
  16. data/templates/assets/javascripts/jquery-livequery.js +226 -0
  17. data/templates/assets/javascripts/jquery-ui-1-8-16-min.js +791 -0
  18. data/templates/assets/javascripts/mode/css/css.js +124 -0
  19. data/templates/assets/javascripts/mode/htmlmixed/htmlmixed.js +85 -0
  20. data/templates/assets/javascripts/mode/javascript/javascript.js +361 -0
  21. data/templates/assets/javascripts/mode/xml/xml.js +325 -0
  22. data/templates/assets/stylesheets/application.css +68 -0
  23. data/templates/assets/stylesheets/bootstrap.css +4960 -0
  24. data/templates/assets/stylesheets/codemirror.css +230 -0
  25. data/templates/rspec_api_documentation/html_example.mustache +52 -9
  26. data/templates/rspec_api_documentation/html_index.mustache +22 -21
  27. data/templates/rspec_api_documentation/wurl_example.mustache +241 -0
  28. data/templates/rspec_api_documentation/wurl_index.mustache +26 -0
  29. metadata +47 -111
  30. data/lib/rspec_api_documentation/syntax.rb +0 -33
@@ -18,7 +18,6 @@ module RspecApiDocumentation
18
18
  autoload :Index
19
19
  autoload :ClientBase
20
20
  autoload :Headers
21
- autoload :Syntax
22
21
  end
23
22
 
24
23
  autoload :DSL
@@ -26,6 +25,7 @@ module RspecApiDocumentation
26
25
  autoload :OAuth2MACClient, "rspec_api_documentation/oauth2_mac_client"
27
26
  autoload :TestServer
28
27
  autoload :HtmlWriter
28
+ autoload :WurlWriter
29
29
  autoload :JsonWriter
30
30
  autoload :IndexWriter
31
31
  autoload :Curl
@@ -14,6 +14,7 @@ module RspecApiDocumentation
14
14
  FileUtils.rm_rf(docs_dir, :secure => true)
15
15
  end
16
16
  FileUtils.mkdir_p(docs_dir)
17
+ FileUtils.cp_r(File.join(configuration.template_path, "assets"), docs_dir)
17
18
  end
18
19
 
19
20
  def document_example(rspec_example)
@@ -1,7 +1,6 @@
1
1
  module RspecApiDocumentation
2
2
  class ClientBase < Struct.new(:context, :options)
3
3
  include Headers
4
- include Syntax
5
4
 
6
5
  delegate :example, :app, :to => :context
7
6
  delegate :metadata, :to => :example
@@ -40,13 +39,15 @@ module RspecApiDocumentation
40
39
 
41
40
  request_metadata[:request_method] = method
42
41
  request_metadata[:request_path] = path
43
- request_metadata[:request_body] = highlight_syntax(request_body, content_type, true)
44
- request_metadata[:request_headers] = format_headers(request_headers)
45
- request_metadata[:request_query_parameters] = format_query_hash(query_hash)
42
+ request_metadata[:request_body] = request_body.empty? ? nil : request_body
43
+ request_metadata[:request_headers] = request_headers
44
+ request_metadata[:request_query_parameters] = query_hash
45
+ request_metadata[:request_content_type] = request_content_type
46
46
  request_metadata[:response_status] = status
47
47
  request_metadata[:response_status_text] = Rack::Utils::HTTP_STATUS_CODES[status]
48
- request_metadata[:response_body] = highlight_syntax(response_body, response_headers['Content-Type'])
49
- request_metadata[:response_headers] = format_headers(response_headers)
48
+ request_metadata[:response_body] = response_body.empty? ? nil : response_body
49
+ request_metadata[:response_headers] = response_headers
50
+ request_metadata[:response_content_type] = response_content_type
50
51
  request_metadata[:curl] = Curl.new(method, path, request_body, request_headers)
51
52
 
52
53
  metadata[:requests] ||= []
@@ -56,18 +57,12 @@ module RspecApiDocumentation
56
57
  def query_hash
57
58
  strings = query_string.split("&")
58
59
  arrays = strings.map do |segment|
59
- segment.split("=")
60
+ k,v = segment.split("=")
61
+ [k, CGI.unescape(v)]
60
62
  end
61
63
  Hash[arrays]
62
64
  end
63
65
 
64
- def format_query_hash(query_hash)
65
- return if query_hash.blank?
66
- query_hash.map do |key, value|
67
- "#{key}: #{CGI.unescape(value)}"
68
- end.join("\n")
69
- end
70
-
71
66
  def headers(method, path, params)
72
67
  if options && options[:headers]
73
68
  options[:headers]
@@ -10,19 +10,19 @@ module RspecApiDocumentation
10
10
  end
11
11
 
12
12
  def post
13
- "curl #{url} #{post_data} -X POST #{headers}"
13
+ "curl \"#{url}\" #{post_data} -X POST #{headers}"
14
14
  end
15
15
 
16
16
  def get
17
- "curl #{url}#{get_data} -X GET #{headers}"
17
+ "curl \"#{url}#{get_data}\" -X GET #{headers}"
18
18
  end
19
19
 
20
20
  def put
21
- "curl #{url} #{post_data} -X PUT #{headers}"
21
+ "curl \"#{url}\" #{post_data} -X PUT #{headers}"
22
22
  end
23
23
 
24
24
  def delete
25
- "curl #{url} -X DELETE #{headers}"
25
+ "curl \"#{url}\" #{post_data} -X DELETE #{headers}"
26
26
  end
27
27
 
28
28
  def url
@@ -39,6 +39,10 @@ module RspecApiDocumentation
39
39
  IndexWriter.sections(examples, @configuration)
40
40
  end
41
41
 
42
+ def url_prefix
43
+ @configuration.url_prefix
44
+ end
45
+
42
46
  def examples
43
47
  @index.examples.map { |example| HtmlExample.new(example, @configuration) }
44
48
  end
@@ -70,6 +74,9 @@ module RspecApiDocumentation
70
74
 
71
75
  def requests
72
76
  super.map do |hash|
77
+ hash[:request_headers_text] = format_hash(hash[:request_headers])
78
+ hash[:request_query_parameters_text] = format_hash(hash[:request_query_parameters])
79
+ hash[:response_headers_text] = format_hash(hash[:response_headers])
73
80
  if @host
74
81
  hash[:curl] = hash[:curl].output(@host) if hash[:curl].is_a? RspecApiDocumentation::Curl
75
82
  else
@@ -82,5 +89,13 @@ module RspecApiDocumentation
82
89
  def url_prefix
83
90
  configuration.url_prefix
84
91
  end
92
+
93
+ private
94
+ def format_hash(hash = {})
95
+ return nil unless hash.present?
96
+ hash.collect do |k, v|
97
+ "#{k}: #{v}"
98
+ end.join("\n")
99
+ end
85
100
  end
86
101
  end
@@ -32,10 +32,14 @@ module RspecApiDocumentation
32
32
  last_response.body
33
33
  end
34
34
 
35
- def content_type
35
+ def request_content_type
36
36
  last_request.content_type
37
37
  end
38
38
 
39
+ def response_content_type
40
+ last_response.content_type
41
+ end
42
+
39
43
  protected
40
44
 
41
45
  def do_request(method, path, params)
@@ -24,10 +24,14 @@ module RspecApiDocumentation
24
24
  last_response.body
25
25
  end
26
26
 
27
- def content_type
27
+ def request_content_type
28
28
  last_request.content_type
29
29
  end
30
30
 
31
+ def response_content_type
32
+ last_response.content_type
33
+ end
34
+
31
35
  protected
32
36
 
33
37
  def do_request(method, path, params)
@@ -1,7 +1,6 @@
1
1
  module RspecApiDocumentation
2
2
  class TestServer < Struct.new(:context)
3
3
  include Headers
4
- include Syntax
5
4
 
6
5
  delegate :example, :to => :context
7
6
  delegate :metadata, :to => :example
@@ -18,10 +17,10 @@ module RspecApiDocumentation
18
17
 
19
18
  request_metadata = {}
20
19
 
21
- request_metadata[:request_method] = request_method
20
+ request_metadata[:request_method] = @request_method
22
21
  request_metadata[:request_path] = env["PATH_INFO"]
23
- request_metadata[:request_body] = highlight_syntax(request_body, request_headers["Content-Type"], true)
24
- request_metadata[:request_headers] = format_headers(@request_headers)
22
+ request_metadata[:request_body] = @request_body
23
+ request_metadata[:request_headers] = @request_headers
25
24
 
26
25
  metadata[:requests] ||= []
27
26
  metadata[:requests] << request_metadata
@@ -0,0 +1,105 @@
1
+ require 'mustache'
2
+
3
+ module RspecApiDocumentation
4
+ class WurlWriter
5
+ attr_accessor :index, :configuration
6
+
7
+ def initialize(index, configuration)
8
+ self.index = index
9
+ self.configuration = configuration
10
+ end
11
+
12
+ def self.write(index, configuration)
13
+ writer = new(index, configuration)
14
+ writer.write
15
+ end
16
+
17
+ def write
18
+ File.open(configuration.docs_dir.join("index.html"), "w+") do |f|
19
+ f.write WurlIndex.new(index, configuration).render
20
+ end
21
+ index.examples.each do |example|
22
+ html_example = WurlExample.new(example, configuration)
23
+ FileUtils.mkdir_p(configuration.docs_dir.join(html_example.dirname))
24
+ File.open(configuration.docs_dir.join(html_example.dirname, html_example.filename), "w+") do |f|
25
+ f.write html_example.render
26
+ end
27
+ end
28
+ end
29
+ end
30
+
31
+ class WurlIndex < Mustache
32
+ def initialize(index, configuration)
33
+ @index = index
34
+ @configuration = configuration
35
+ self.template_path = configuration.template_path
36
+ end
37
+
38
+ def sections
39
+ IndexWriter.sections(examples, @configuration)
40
+ end
41
+
42
+ def url_prefix
43
+ @configuration.url_prefix
44
+ end
45
+
46
+ def examples
47
+ @index.examples.map { |example| WurlExample.new(example, @configuration) }
48
+ end
49
+ end
50
+
51
+ class WurlExample < Mustache
52
+ def initialize(example, configuration)
53
+ @example = example
54
+ @host = configuration.curl_host
55
+ self.template_path = configuration.template_path
56
+ end
57
+
58
+ def method_missing(method, *args, &block)
59
+ @example.send(method, *args, &block)
60
+ end
61
+
62
+ def respond_to?(method, include_private = false)
63
+ super || @example.respond_to?(method, include_private)
64
+ end
65
+
66
+ def dirname
67
+ resource_name.downcase.gsub(/\s+/, '_')
68
+ end
69
+
70
+ def filename
71
+ basename = description.downcase.gsub(/\s+/, '_').gsub(/[^a-z_]/, '')
72
+ "#{basename}.html"
73
+ end
74
+
75
+ def requests
76
+ super.collect do |hash|
77
+ hash[:request_headers_hash] = hash[:request_headers].collect { |k, v| {:name => k, :value => v} }
78
+ hash[:request_headers_text] = format_hash(hash[:request_headers])
79
+ hash[:request_path_no_query] = hash[:request_path].split('?').first
80
+ hash[:request_query_parameters_text] = format_hash(hash[:request_query_parameters])
81
+ hash[:request_query_parameters_hash] = hash[:request_query_parameters].collect { |k, v| {:name => k, :value => v} } if hash[:request_query_parameters].present?
82
+ hash[:response_headers_text] = format_hash(hash[:response_headers])
83
+ hash[:response_status] = hash[:response_status].to_s + " " + Rack::Utils::HTTP_STATUS_CODES[hash[:response_status]].to_s
84
+ if @host
85
+ hash[:curl] = hash[:curl].output(@host) if hash[:curl].is_a? RspecApiDocumentation::Curl
86
+ else
87
+ hash[:curl] = nil
88
+ end
89
+ hash
90
+ end
91
+ end
92
+
93
+ def url_prefix
94
+ configuration.url_prefix
95
+ end
96
+
97
+ private
98
+ def format_hash(hash = {})
99
+ return nil unless hash.present?
100
+ hash.collect do |k, v|
101
+ "#{k}: #{v}"
102
+ end.join("\n")
103
+ end
104
+ end
105
+ end
@@ -0,0 +1,250 @@
1
+ var headers = ["Accept",
2
+ "Accept-Charset",
3
+ "Accept-Encoding",
4
+ "Accept-Language",
5
+ "Authorization",
6
+ "Cache-Control",
7
+ "Connection",
8
+ "Cookie",
9
+ "Content-Length",
10
+ "Content-MD5",
11
+ "Content-Type",
12
+ "Date",
13
+ "Expect",
14
+ "From",
15
+ "Host",
16
+ "If-Match",
17
+ "If-Modified-Since",
18
+ "If-None-Match",
19
+ "If-Range",
20
+ "If-Unmodified-Since",
21
+ "Max-Forwards",
22
+ "Pragma",
23
+ "Proxy-Authorization",
24
+ "Range",
25
+ "Referer",
26
+ "TE",
27
+ "Upgrade",
28
+ "User-Agent",
29
+ "Via",
30
+ "Warning"];
31
+
32
+ function mirror(textarea, contentType, options) {
33
+ $textarea = $(textarea);
34
+ if ($textarea.val() != '') {
35
+ if(contentType.indexOf('json') >= 0) {
36
+ $textarea.val(JSON.stringify(JSON.parse($textarea.val()), undefined, 2));
37
+ options.json = true;
38
+ options.mode = 'javascript';
39
+ } else if (contentType.indexOf('javascript') >= 0) {
40
+ options.mode = 'javascript';
41
+ } else if (contentType.indexOf('xml') >= 0) {
42
+ options.mode = 'xml';
43
+ } else {
44
+ options.mode = 'htmlmixed';
45
+ }
46
+ }
47
+ return CodeMirror.fromTextArea(textarea, options);
48
+ };
49
+
50
+ function Wurl(wurlForm) {
51
+ this.$wurlForm = $(wurlForm);
52
+ var self = this;
53
+
54
+ this.requestBodyMirror = mirror(this.$wurlForm.find('.post_body textarea')[0], $('.request.content_type', this.$wurlForm).val(), {})
55
+ this.responseBodyMirror = mirror(this.$wurlForm.find('.response.body textarea')[0], $('.response.content_type', this.$wurlForm).val(), { "readOnly": true, "lineNumbers":true});
56
+
57
+ $('.give_it_a_wurl', this.$wurlForm).click(function (event) {
58
+ event.preventDefault();
59
+ self.sendWurl();
60
+ });
61
+ $('.add_header', this.$wurlForm).click(function () {
62
+ self.addInputs('header');
63
+ });
64
+
65
+ $('.add_param', this.$wurlForm).click(function () {
66
+ self.addInputs('param');
67
+ });
68
+
69
+ $('.delete_header', this.$wurlForm).live('click', function (e) {
70
+ self.deleteHeader(this);
71
+ });
72
+
73
+ $('.delete_param', this.$wurlForm).live('click', function (e) {
74
+ self.deleteParam(this);
75
+ });
76
+
77
+ $(".trash_headers", this.$wurlForm).click(function () {
78
+ self.trashHeaders();
79
+ });
80
+
81
+ $(".trash_queries", self.$wurlForm).click(function () {
82
+ self.trashQueries();
83
+ });
84
+
85
+ $('.header_pair input.value', this.$wurlForm).live('focusin', (function () {
86
+ if ($('.header_pair:last input', self.$wurlForm).val() != "") {
87
+ self.addInputs('header');
88
+ }
89
+ }));
90
+
91
+ $('.param_pair input.value', this.$wurlForm).live('focusin', (function () {
92
+ if ($('.param_pair:last input', self.$wurlForm).val() != "") {
93
+ self.addInputs('param');
94
+ }
95
+ }));
96
+
97
+ $('.url select', this.$wurlForm).change(function () {
98
+ self.updateBodyInput();
99
+ });
100
+
101
+ $(".header_pair input.key", this.$wurlForm).livequery(function () {
102
+ $(this).autocomplete({source:headers});
103
+ });
104
+
105
+ $(".clear_fields", this.$wurlForm).click(function () {
106
+ $("input[type=text], textarea", self.$wurlForm).val("");
107
+ self.trashHeaders();
108
+ self.trashQueries();
109
+ });
110
+
111
+ this.addInputs = function (type) {
112
+ var $fields = $('.' + type + '_pair', this.$wurlForm).first().clone();
113
+ $fields.children('input').val("").attr('disabled', false);
114
+ $fields.hide().appendTo(this.$wurlForm.find('.' + type + 's')).slideDown('fast');
115
+ };
116
+
117
+ this.deleteHeader = function (element) {
118
+ var $fields = $(element).closest(".header_pair");
119
+ $fields.slideUp(function () {
120
+ $fields.remove();
121
+ });
122
+ };
123
+
124
+ this.deleteParam = function (element) {
125
+ var $fields = $(element).closest(".param_pair");
126
+ $fields.slideUp(function () {
127
+ $paramFields.remove();
128
+ });
129
+ };
130
+
131
+ this.trashHeaders = function () {
132
+ $(".header_pair:visible", self.$wurlForm).each(function (i, element) {
133
+ $(element).slideUp(function () {
134
+ $(element).remove();
135
+ });
136
+ });
137
+ this.addInputs('header');
138
+ };
139
+
140
+ this.trashQueries = function () {
141
+ $(".param_pair:visible", self.$wurlForm).each(function (i, element) {
142
+ $(element).slideUp(function () {
143
+ $(element).remove();
144
+ });
145
+ });
146
+ this.addInputs('param');
147
+ };
148
+
149
+ this.updateBodyInput = function () {
150
+ var method = $('#wurl_request_method', self.$wurlForm).val();
151
+ if ($.inArray(method, ["PUT", "POST", "DELETE"]) > -1) {
152
+ $('#wurl_request_body', self.$wurlForm).attr('disabled', false).removeClass('textarea_disabled');
153
+ } else {
154
+ $('#wurl_request_body', self.$wurlForm).attr('disabled', true).addClass('textarea_disabled');
155
+ }
156
+ };
157
+ this.updateBodyInput();
158
+
159
+ this.makeBasicAuth = function () {
160
+ var user = $('#wurl_basic_auth_user', this.$wurlForm).val();
161
+ var password = $('#wurl_basic_auth_password', this.$wurlForm).val();
162
+ var token = user + ':' + password;
163
+ var hash = $.base64.encode(token);
164
+ return "Basic " + hash;
165
+ };
166
+
167
+ this.queryParams = function () {
168
+ var toReturn = [];
169
+ $(".param_pair:visible", self.$wurlForm).each(function (i, element) {
170
+ paramKey = $(element).find('input.key').val();
171
+ paramValue = $(element).find('input.value').val();
172
+ if (paramKey.length && paramValue.length) {
173
+ toReturn.push(paramKey + '=' + paramValue);
174
+ }
175
+ });
176
+ return toReturn.join("&");
177
+ };
178
+
179
+ this.getData = function () {
180
+ var method = $('#wurl_request_method', self.$wurlForm).val();
181
+ if ($.inArray(method, ["PUT", "POST", "DELETE"]) > -1) {
182
+ self.requestBodyMirror.save();
183
+ return self.requestBodyMirror.getValue();
184
+ } else {
185
+ return self.queryParams();
186
+ }
187
+ };
188
+
189
+ this.url = function () {
190
+ var url = $('#wurl_request_url', self.$wurlForm).val();
191
+ var method = $('#wurl_request_method', self.$wurlForm).val();
192
+ var params = self.queryParams();
193
+ if ($.inArray(method, ["PUT", "POST", "DELETE"]) > -1 && params.length) {
194
+ url += "?" + params;
195
+ }
196
+ return url[0] == '/' ? url : '/' + url;
197
+ };
198
+
199
+ this.sendWurl = function () {
200
+ $.ajax({
201
+ beforeSend:function (req) {
202
+ $(".header_pair:visible", self.$wurlForm).each(function (i, element) {
203
+ headerKey = $(element).find('input.key').val();
204
+ headerValue = $(element).find('input.value').val();
205
+ req.setRequestHeader(headerKey, headerValue);
206
+ });
207
+ req.setRequestHeader('Authorization', self.makeBasicAuth());
208
+ },
209
+ type:$('#wurl_request_method', self.$wurlForm).val(),
210
+ url:this.url(),
211
+ data:this.getData(),
212
+ complete:function (jqXHR) {
213
+ var $status = $('.response.status', self.$wurlForm);
214
+ $status.html(jqXHR.status + ' ' + jqXHR.statusText);
215
+
216
+ $('.response.headers', self.$wurlForm).html(jqXHR.getAllResponseHeaders());
217
+
218
+ contentType = jqXHR.getResponseHeader("content-type");
219
+ if (contentType.indexOf('json') >= 0 && jqXHR.responseText.length > 1) {
220
+ self.responseBodyMirror.setValue(JSON.stringify(JSON.parse(jqXHR.responseText), undefined, 2));
221
+ self.responseBodyMirror.setOption('mode', 'javascript');
222
+ self.responseBodyMirror.setOption('json', true);
223
+ } else if (contentType.indexOf('javascript') >= 0) {
224
+ self.responseBodyMirror.setValue(jqXHR.responseText);
225
+ self.responseBodyMirror.setOption('mode', 'javascript');
226
+ } else if (contentType.indexOf('xml') >= 0) {
227
+ self.responseBodyMirror.setValue(jqXHR.responseText);
228
+ self.responseBodyMirror.setOption('mode', 'xml');
229
+ } else {
230
+ self.responseBodyMirror.setValue(jqXHR.responseText);
231
+ self.responseBodyMirror.setOption('mode', 'htmlmixed');
232
+ }
233
+ $('.response', self.$wurlForm).effect("highlight", {}, 3000);
234
+ $('html,body').animate({ scrollTop:$('a.response_anchor', self.$wurlForm).offset().top }, { duration:'slow', easing:'swing'});
235
+ }
236
+ });
237
+ };
238
+ }
239
+
240
+ $(function () {
241
+ $('.wurl_form').each(function (index, wurlForm) {
242
+ wurl = new Wurl(wurlForm);
243
+ });
244
+
245
+ var $textAreas = $('.request.body textarea');
246
+ $textAreas.each(function(i, textarea) {
247
+ var contentType = $(textarea).parents('div.request').find('.request.content_type').val();
248
+ mirror(textarea, contentType, {"readOnly":true, "lineNumbers": true});
249
+ });
250
+ });