rspec_api_documentation 0.5.2 → 0.6.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 (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
+ });