api_canon 0.2.3
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.
- checksums.yaml +7 -0
- data/MIT-LICENSE +20 -0
- data/README.md +52 -0
- data/Rakefile +31 -0
- data/lib/api_canon/app/controllers/api_canon_controller.rb +108 -0
- data/lib/api_canon/app/helpers/api_canon_view_helper.rb +9 -0
- data/lib/api_canon/app/views/api_canon/_api_canon.html.erb +71 -0
- data/lib/api_canon/app/views/api_canon/_form.html.erb +30 -0
- data/lib/api_canon/app/views/api_canon/_rails_2_form.html.erb +30 -0
- data/lib/api_canon/app/views/api_canon/api_canon.html.erb +1 -0
- data/lib/api_canon/app/views/application/index.html.erb +1 -0
- data/lib/api_canon/app/views/layouts/api_canon.html.erb +67 -0
- data/lib/api_canon/app.rb +1 -0
- data/lib/api_canon/document.rb +29 -0
- data/lib/api_canon/documentation_store.rb +23 -0
- data/lib/api_canon/documented_action.rb +23 -0
- data/lib/api_canon/documented_param.rb +40 -0
- data/lib/api_canon/routes.rb +12 -0
- data/lib/api_canon/version.rb +3 -0
- data/lib/api_canon.rb +54 -0
- data/lib/tasks/api_canon_tasks.rake +4 -0
- data/spec/dummy/log/test.log +0 -0
- data/spec/lib/api_canon/documented_action_spec.rb +35 -0
- data/spec/lib/api_canon/documented_param_spec.rb +54 -0
- data/spec/lib/api_canon_spec.rb +43 -0
- data/spec/spec_helper.rb +9 -0
- metadata +120 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 1fe88b5c318faea3491eed88a7679b503758363b
|
4
|
+
data.tar.gz: e07f1ce662c6c7de16941a0bc17372f2e5c5e321
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: b3ee1f26f4b745e920784701636eef94c250c2cc31a872d2eed3415300c254d48042bac7755440908373c436a05d5e0685f5215f79c40d1d251732673d74dd90
|
7
|
+
data.tar.gz: 898a5e1b441f4783c9f6d2f5104cc3031f72fe6674e643ea200063196492f7d2cd9461dbf720583902b2358a5edfc55ca8602ff0774025deebe09daab67852d6
|
data/MIT-LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright 2013 YOURNAME
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,52 @@
|
|
1
|
+
# API Canon
|
2
|
+
|
3
|
+
[](https://travis-ci.org/cwalsh/api_canon)
|
4
|
+
|
5
|
+
## Introduction
|
6
|
+
API Canon is a tool for programatically documenting Ruby on Rails APIs with example usage.
|
7
|
+
|
8
|
+
## Installation and usage
|
9
|
+
If you're using bundler, then put this in your Gemfile:
|
10
|
+
|
11
|
+
gem 'api_canon'
|
12
|
+
|
13
|
+
Then, in each controller you want to document, add the line
|
14
|
+
|
15
|
+
include ApiCanon
|
16
|
+
|
17
|
+
... which allows you to describe what all the actions in the controller are concerned about like this:
|
18
|
+
|
19
|
+
document_controller :as => 'optional_rename' do
|
20
|
+
describe "The actions here are awesome, they allow you to get a list of awesome things, and make awesome things, too!"
|
21
|
+
end
|
22
|
+
|
23
|
+
... and you can document all the actions you want like this:
|
24
|
+
|
25
|
+
document_method :index do
|
26
|
+
param :category_codes, :type => :array, :multiple => true, :example_values => Category.all(:limit => 5, :select => :code).map(&:code), :description => "Return only categories for the given category codes", :default => 'some-awesome-category-code'
|
27
|
+
end
|
28
|
+
|
29
|
+
To view the api documentation, visit the documented controller's index action with '.html' as the format.
|
30
|
+
|
31
|
+
To enable the 'test' button on the generated documentation pages, you'll need to add this to your config/routes.rb file:
|
32
|
+
|
33
|
+
ApiCanon::Routes.draw(self) # Or 'map' instead of 'self' for Rails 2
|
34
|
+
|
35
|
+
## Going forward
|
36
|
+
|
37
|
+
Right now, api_canon is changing a lot. I plan to support the following features soon:
|
38
|
+
|
39
|
+
1. Response codes - describe what you mean when you send back a 200, a 201, 403 etc.
|
40
|
+
2. Support API tokens or other authentication to allow users to edit data live, with non-GET requests.
|
41
|
+
3. Swagger API output (optional)
|
42
|
+
4. You will need to route the index action for each documented controller until such point as I provide an alternative means of getting at this documentation.
|
43
|
+
|
44
|
+
## Contributors
|
45
|
+
[Cameron Walsh](http://github.com/cwalsh)
|
46
|
+
|
47
|
+
## Contributions
|
48
|
+
1. Fork project
|
49
|
+
2. Write tests, or even code as well
|
50
|
+
3. Pull request
|
51
|
+
4. ???
|
52
|
+
5. Profit.
|
data/Rakefile
ADDED
@@ -0,0 +1,31 @@
|
|
1
|
+
#!/usr/bin/env rake
|
2
|
+
begin
|
3
|
+
require 'bundler/setup'
|
4
|
+
rescue LoadError
|
5
|
+
puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
|
6
|
+
end
|
7
|
+
begin
|
8
|
+
require 'rdoc/task'
|
9
|
+
rescue LoadError
|
10
|
+
require 'rdoc/rdoc'
|
11
|
+
require 'rake/rdoctask'
|
12
|
+
RDoc::Task = Rake::RDocTask
|
13
|
+
end
|
14
|
+
|
15
|
+
RDoc::Task.new(:rdoc) do |rdoc|
|
16
|
+
rdoc.rdoc_dir = 'rdoc'
|
17
|
+
rdoc.title = 'ApiCanon'
|
18
|
+
rdoc.options << '--line-numbers'
|
19
|
+
rdoc.rdoc_files.include('README.rdoc')
|
20
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
21
|
+
end
|
22
|
+
|
23
|
+
require "bundler/gem_tasks"
|
24
|
+
Bundler::GemHelper.install_tasks
|
25
|
+
|
26
|
+
require "rspec/core/rake_task"
|
27
|
+
|
28
|
+
RSpec::Core::RakeTask.new
|
29
|
+
|
30
|
+
task :default => :spec
|
31
|
+
task :test => :spec
|
@@ -0,0 +1,108 @@
|
|
1
|
+
module ApiCanon
|
2
|
+
class ApiCanonController < ActionController::Base
|
3
|
+
def test
|
4
|
+
response = {:methods => matching_methods, :params => sanitized(params[:doco])}
|
5
|
+
#TODO: Put routes in here, too.
|
6
|
+
matching_methods.each do |m|
|
7
|
+
response[:curl] ||= {}
|
8
|
+
response[:urls] ||= {}
|
9
|
+
if m == :get
|
10
|
+
response[:curl][m] = as_curl(api_request_url)
|
11
|
+
response[:urls][m] = api_request_url
|
12
|
+
else
|
13
|
+
response[:curl][m] = as_curl(api_request_url_for_non_get_requests, m, non_url_parameters_for_request_generation)
|
14
|
+
response[:urls][m] = api_request_url_for_non_get_requests
|
15
|
+
end
|
16
|
+
end
|
17
|
+
render :json => response
|
18
|
+
end
|
19
|
+
|
20
|
+
private
|
21
|
+
|
22
|
+
def routes
|
23
|
+
Rails.version.starts_with?('2') ? ActionController::Routing::Routes.routes : Rails.application.routes.routes
|
24
|
+
end
|
25
|
+
|
26
|
+
def matching_routes
|
27
|
+
routes.select do |r|
|
28
|
+
r.requirements[:controller] == requested_controller_path.to_s &&
|
29
|
+
r.requirements[:action] == api_document.action_name.to_s
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def as_curl(url, method=:get, params={})
|
34
|
+
case method
|
35
|
+
when :get
|
36
|
+
%{curl "#{url}"}
|
37
|
+
when :post
|
38
|
+
%{curl "#{url}" "Content-Type: application/x-www-form-urlencoded; charset=UTF-8" --data "#{params.to_query}"}
|
39
|
+
when :put
|
40
|
+
%{curl "#{url}" "Content-Type: application/x-www-form-urlencoded; charset=UTF-8" -X PUT --data "#{params.to_query}"}
|
41
|
+
when :delete
|
42
|
+
%{curl "#{url}" "Content-Type: application/x-www-form-urlencoded; charset=UTF-8" -X DELETE --data "#{params.to_query}"}
|
43
|
+
else
|
44
|
+
"PATCH, HEAD, OPTIONS, TRACE, CONNECT are not implemented"
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def matching_methods
|
49
|
+
#TODO: ApiDocument should define what methods it accepts, and fall back to route generation
|
50
|
+
if Rails.version.starts_with?('2')
|
51
|
+
matching_routes.map {|r| r.conditions[:method]}.uniq
|
52
|
+
else
|
53
|
+
matching_routes.map {|r| %w(GET PUT POST DELETE).select {|m| r.verb =~ m}}.flatten.uniq.map(&:downcase).map(&:to_sym)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
def api_document
|
58
|
+
doco = ApiCanon::DocumentationStore.fetch(requested_controller_path)
|
59
|
+
doco.documented_actions.detect {|da| da.action_name.to_s == params[:doco][:action_name] }
|
60
|
+
end
|
61
|
+
|
62
|
+
def requested_controller_path
|
63
|
+
params[:doco][:controller_path]
|
64
|
+
end
|
65
|
+
|
66
|
+
def parameters_for_request_generation
|
67
|
+
sanitized(params[:doco]).merge({:controller => "/#{requested_controller_path}", :action => api_document.action_name})
|
68
|
+
end
|
69
|
+
|
70
|
+
def non_url_parameters_for_request_generation
|
71
|
+
parameters_for_request_generation.reject {|k,v| url_param_keys.include?(k)}
|
72
|
+
end
|
73
|
+
|
74
|
+
def url_param_keys
|
75
|
+
if Rails.version.starts_with?('2')
|
76
|
+
matching_routes.inject(Set.new()) {|set,r| set += r.significant_keys.map(&:to_s) }
|
77
|
+
else
|
78
|
+
matching_routes.inject(Set.new()) {|set,r| set += r.segment_keys.map(&:to_s) }
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
def url_parameters_for_request_generation
|
83
|
+
parameters_for_request_generation.slice *url_param_keys
|
84
|
+
end
|
85
|
+
|
86
|
+
def api_request_url_for_non_get_requests
|
87
|
+
unless Rails.version.starts_with?('2')
|
88
|
+
self.class.send(:include, Rails.application.routes.url_helpers)
|
89
|
+
end
|
90
|
+
url_for url_parameters_for_request_generation
|
91
|
+
end
|
92
|
+
|
93
|
+
def api_request_url
|
94
|
+
unless Rails.version.starts_with?('2')
|
95
|
+
self.class.send(:include, Rails.application.routes.url_helpers)
|
96
|
+
end
|
97
|
+
url_for parameters_for_request_generation
|
98
|
+
end
|
99
|
+
|
100
|
+
def sanitized(doco_params)
|
101
|
+
sanitized = doco_params.dup
|
102
|
+
sanitized.delete_if {|k,v| %w(controller controller_path action controller_name action_name).include?(k.to_s) }
|
103
|
+
sanitized.each {|k,v| v.reject!(&:blank?) if v.is_a? Array}
|
104
|
+
sanitized.delete_if {|k,v| v.blank? }
|
105
|
+
sanitized
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
@@ -0,0 +1,71 @@
|
|
1
|
+
<div class='row-fluid"'>
|
2
|
+
<div class='span12'>
|
3
|
+
<%- @api_doc ||= ::ApiCanon::DocumentationStore.fetch controller.controller_path -%>
|
4
|
+
<h1><%= @api_doc.display_name %></h1>
|
5
|
+
<p class='lead'><%= @api_doc.description %></p>
|
6
|
+
<%- @api_doc.documented_actions.each do |doco| -%>
|
7
|
+
<div class='row-fluid"'>
|
8
|
+
<div class='span12'>
|
9
|
+
<h2><%= doco.action_name.to_s.titleize %></h2>
|
10
|
+
<p><%= doco.description %></p>
|
11
|
+
<% doco_prefix = "#{@api_doc.controller_name}_#{doco.action_name}" %>
|
12
|
+
<% if Rails.version.starts_with?('2') %>
|
13
|
+
<%= render :partial => 'api_canon/rails_2_form', :locals => {:doco => doco, :doco_prefix => doco_prefix} %>
|
14
|
+
<% else %>
|
15
|
+
<%= render :partial => 'api_canon/form', :locals => {:doco => doco, :doco_prefix => doco_prefix} %>
|
16
|
+
<% end %>
|
17
|
+
<div id='<%= "#{@api_doc.controller_name}_#{doco.action_name}_results" %>' class='results'>
|
18
|
+
<div class='urls'></div>
|
19
|
+
<div class='curls'></div>
|
20
|
+
<div class='response'></div>
|
21
|
+
</div>
|
22
|
+
</div>
|
23
|
+
</div>
|
24
|
+
<%- end -%>
|
25
|
+
</div>
|
26
|
+
</div>
|
27
|
+
|
28
|
+
<script type='text/javascript'>
|
29
|
+
function generate_url_and_call_it(controller_name, action_name) {
|
30
|
+
var form = jQuery('#' + controller_name + '_' + action_name + '_form');
|
31
|
+
var api_url_request = form.attr('action');
|
32
|
+
var post_request = jQuery.post(api_url_request, form.serialize());
|
33
|
+
post_request.done(function(data) {
|
34
|
+
var urls_div = jQuery('#' + controller_name + '_' + action_name + '_results div.urls')
|
35
|
+
var curls_div = jQuery('#' + controller_name + '_' + action_name + '_results div.curls')
|
36
|
+
var response_div = jQuery('#' + controller_name + '_' + action_name + '_results div.response')
|
37
|
+
//urls_div.html('<p>URLs generated: </p>');
|
38
|
+
curls_div.html('<p>Curl requests generated: </p>');
|
39
|
+
//jQuery.each(data['urls'], function(key,value) {urls_div.append(key.toUpperCase() + ': <pre>' + value + '</pre>')});
|
40
|
+
jQuery.each(data['curl'], function(key,value) {curls_div.append(key.toUpperCase() + ': <pre>' + value + '</pre>')});
|
41
|
+
jQuery.each(data['urls'], function(key,value) {
|
42
|
+
if (key == 'get') {
|
43
|
+
jQuery.get(value)
|
44
|
+
.done(function(data,textStatus,jqXHR) {
|
45
|
+
response_div.removeClass('error')
|
46
|
+
.html('<p>Response: (' + jqXHR.status + ' ' + jqXHR.statusText + ') </p><pre></pre>')
|
47
|
+
if (jqXHR.getResponseHeader('Content-Type').match(/(text|application)\/json.*/)) {
|
48
|
+
response_div.find('pre').append(JSON.stringify(eval(data), undefined, 2))
|
49
|
+
} else {
|
50
|
+
response_div.find('pre').text(jqXHR.responseText)
|
51
|
+
}
|
52
|
+
})
|
53
|
+
.fail(function(jqXHR,textStatus,errorThrown) {
|
54
|
+
response_div.addClass('error')
|
55
|
+
.html('<p>Response: (' + jqXHR.status + ' ' + jqXHR.statusText + ') </p><pre></pre>')
|
56
|
+
response_div.find('pre').text(jqXHR.responseText)
|
57
|
+
})
|
58
|
+
}
|
59
|
+
});
|
60
|
+
});
|
61
|
+
return false;
|
62
|
+
}
|
63
|
+
</script>
|
64
|
+
|
65
|
+
<% content_for :sidebar do %>
|
66
|
+
<ul class='nav nav-list'>
|
67
|
+
<% ApiCanon::DocumentationStore.instance.docos.each do |key, doco| %>
|
68
|
+
<li><%= link_to doco.display_name, url_for(:controller => doco.controller_path, :action => :index, :format => :html) %></li>
|
69
|
+
<% end %>
|
70
|
+
</ul>
|
71
|
+
<% end %>
|
@@ -0,0 +1,30 @@
|
|
1
|
+
<%= form_for :doco, :url => api_canon_test_path, :html => {:id => "#{@api_doc.controller_name}_#{doco.action_name}_form"}, :prefix => doco_prefix do |f| %>
|
2
|
+
<%= f.hidden_field :action_name, :value => doco.action_name %>
|
3
|
+
<%= f.hidden_field :controller_name, :value => @api_doc.controller_name %>
|
4
|
+
<%= f.hidden_field :controller_path, :value => @api_doc.controller_path %>
|
5
|
+
<%- if doco.params.any? -%>
|
6
|
+
<table class='table table-striped table-bordered'>
|
7
|
+
<thead>
|
8
|
+
<tr>
|
9
|
+
<th>Parameter</th>
|
10
|
+
<th>Value</th>
|
11
|
+
<th>Example Values</th>
|
12
|
+
<th>Type</th>
|
13
|
+
<th>Description</th>
|
14
|
+
</tr>
|
15
|
+
</thead>
|
16
|
+
<tbody>
|
17
|
+
<%- doco.params.each do |name, param| %>
|
18
|
+
<tr>
|
19
|
+
<td><%= name %></td>
|
20
|
+
<td><%= param.to_field(f, doco_prefix) %></td>
|
21
|
+
<td><%= param.example_values_field(f, doco_prefix) %></td>
|
22
|
+
<td><%= param.type %></td>
|
23
|
+
<td><%= param.description %></td>
|
24
|
+
</tr>
|
25
|
+
<%- end -%>
|
26
|
+
</tbody>
|
27
|
+
</table>
|
28
|
+
<%- end -%>
|
29
|
+
<%= f.submit "Test", :onclick => "generate_url_and_call_it('#{@api_doc.controller_name}', '#{doco.action_name}'); return false;", :class => 'btn' %>
|
30
|
+
<%- end -%>
|
@@ -0,0 +1,30 @@
|
|
1
|
+
<% form_for :doco, :url => api_canon_test_path, :html => {:id => "#{@api_doc.controller_name}_#{doco.action_name}_form"} do |f| %>
|
2
|
+
<%= f.hidden_field :action_name, :value => doco.action_name %>
|
3
|
+
<%= f.hidden_field :controller_name, :value => @api_doc.controller_name %>
|
4
|
+
<%= f.hidden_field :controller_path, :value => @api_doc.controller_path %>
|
5
|
+
<%- if doco.params.any? -%>
|
6
|
+
<table class='table table-striped table-bordered'>
|
7
|
+
<thead>
|
8
|
+
<tr>
|
9
|
+
<th>Parameter</th>
|
10
|
+
<th>Value</th>
|
11
|
+
<th>Example Values</th>
|
12
|
+
<th>Type</th>
|
13
|
+
<th>Description</th>
|
14
|
+
</tr>
|
15
|
+
</thead>
|
16
|
+
<tbody>
|
17
|
+
<%- doco.params.each do |name, param| %>
|
18
|
+
<tr>
|
19
|
+
<td><%= name %></td>
|
20
|
+
<td><%= param.to_field(f, doco_prefix) %></td>
|
21
|
+
<td><%= param.example_values_field(f, doco_prefix) %></td>
|
22
|
+
<td><%= param.type %></td>
|
23
|
+
<td><%= param.description %></td>
|
24
|
+
</tr>
|
25
|
+
<%- end -%>
|
26
|
+
</tbody>
|
27
|
+
</table>
|
28
|
+
<%- end -%>
|
29
|
+
<%= f.submit "Test", :onclick => "generate_url_and_call_it('#{@api_doc.controller_name}', '#{doco.action_name}'); return false;", :class => 'btn' %>
|
30
|
+
<% end %>
|
@@ -0,0 +1 @@
|
|
1
|
+
<%= render :partial => 'api_canon/api_canon' %>
|
@@ -0,0 +1 @@
|
|
1
|
+
<%= render :partial => 'api_canon/api_canon' %>
|
@@ -0,0 +1,67 @@
|
|
1
|
+
<!DOCTYPE html>
|
2
|
+
<html lang="en">
|
3
|
+
<head>
|
4
|
+
<meta charset="utf-8">
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
6
|
+
<%= yield :meta %>
|
7
|
+
<title><%= content_for?(:title) ? yield(:title) : "API Documentation" %></title>
|
8
|
+
<%= csrf_meta_tag %>
|
9
|
+
|
10
|
+
<link href="//netdna.bootstrapcdn.com/twitter-bootstrap/2.3.1/css/bootstrap-combined.min.css" rel="stylesheet">
|
11
|
+
<style>
|
12
|
+
body {
|
13
|
+
padding-top: 60px; /* 60px to make the container go all the way to the bottom of the topbar */
|
14
|
+
}
|
15
|
+
</style>
|
16
|
+
<%= stylesheet_link_tag "application", :media => "all" %>
|
17
|
+
|
18
|
+
<!-- HTML5 shim, for IE6-8 support of HTML5 elements -->
|
19
|
+
<!--[if lt IE 9]>
|
20
|
+
<script src="http://html5shiv.googlecode.com/svn/trunk/html5.js">
|
21
|
+
<![endif]-->
|
22
|
+
|
23
|
+
<!-- Fav and touch icons -->
|
24
|
+
<%# TODO: Implement these %>
|
25
|
+
<link rel="apple-touch-icon-precomposed" sizes="144x144" href="/images/ico/apple-touch-icon-144-precomposed.png">
|
26
|
+
<link rel="apple-touch-icon-precomposed" sizes="114x114" href="/images/ico/apple-touch-icon-114-precomposed.png">
|
27
|
+
<link rel="apple-touch-icon-precomposed" sizes="72x72" href="/images/ico/apple-touch-icon-72-precomposed.png">
|
28
|
+
<link rel="apple-touch-icon-precomposed" href="/images/ico/apple-touch-icon-57-precomposed.png">
|
29
|
+
<link rel="shortcut icon" href="/images/ico/favicon.png">
|
30
|
+
</head>
|
31
|
+
|
32
|
+
<body>
|
33
|
+
|
34
|
+
<div class="container-fluid">
|
35
|
+
<div class="row-fluid">
|
36
|
+
<% if content_for? :sidebar %>
|
37
|
+
<div class="span3">
|
38
|
+
<div class="well sidebar-nav">
|
39
|
+
<%= yield :sidebar %>
|
40
|
+
</div><!--/.well -->
|
41
|
+
</div><!--/span-->
|
42
|
+
<% end %>
|
43
|
+
<div class="span<%= content_for?(:sidebar) ? 8 : 11 %>">
|
44
|
+
<% if content_for?(:hero) %>
|
45
|
+
<div class="hero-unit">
|
46
|
+
<%= yield :hero %>
|
47
|
+
</div>
|
48
|
+
<% end %>
|
49
|
+
<%= yield %>
|
50
|
+
</div><!--/span-->
|
51
|
+
</div><!--/row-->
|
52
|
+
|
53
|
+
<footer>
|
54
|
+
<%= yield :footer %>
|
55
|
+
</footer>
|
56
|
+
|
57
|
+
</div>
|
58
|
+
|
59
|
+
<!-- Javascript
|
60
|
+
================================================== -->
|
61
|
+
<script src="http://code.jquery.com/jquery-1.9.1.min.js"></script>
|
62
|
+
<script src="//netdna.bootstrapcdn.com/twitter-bootstrap/2.3.1/js/bootstrap.min.js"></script>
|
63
|
+
<%= javascript_include_tag "application" %>
|
64
|
+
|
65
|
+
</body>
|
66
|
+
</html>
|
67
|
+
|
@@ -0,0 +1 @@
|
|
1
|
+
require 'api_canon/app/controllers/api_canon_controller'
|
@@ -0,0 +1,29 @@
|
|
1
|
+
module ApiCanon
|
2
|
+
class Document
|
3
|
+
attr_reader :description, :controller_path, :controller_name
|
4
|
+
attr_accessor :documented_actions
|
5
|
+
def initialize(controller_path, controller_name, opts={})
|
6
|
+
@controller_path = controller_path
|
7
|
+
@controller_name = controller_name
|
8
|
+
self.display_name = opts[:as]
|
9
|
+
@documented_actions = []
|
10
|
+
end
|
11
|
+
def describe(desc)
|
12
|
+
@description = desc
|
13
|
+
end
|
14
|
+
def display_name
|
15
|
+
@display_name || @controller_name.titleize
|
16
|
+
end
|
17
|
+
def display_name=(dn)
|
18
|
+
if dn.nil?
|
19
|
+
@display_name = nil
|
20
|
+
else
|
21
|
+
dn = dn.to_s
|
22
|
+
@display_name = (dn =~ /\A([a-z]*|[A-Z]*)\Z/ ? dn.titleize : dn)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
def add_action(documented_action)
|
26
|
+
@documented_actions << documented_action unless @documented_actions.map(&:action_name).include?(documented_action.action_name)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
module ApiCanon
|
2
|
+
# Replace this at the earliest possible opportunity
|
3
|
+
# with something that stores stuff in Redis or something
|
4
|
+
class DocumentationStore
|
5
|
+
include Singleton
|
6
|
+
def store cont_doco
|
7
|
+
@docos ||= {}
|
8
|
+
@docos[cont_doco.controller_path] = cont_doco
|
9
|
+
end
|
10
|
+
def docos
|
11
|
+
@docos ||= {}
|
12
|
+
end
|
13
|
+
def self.docos
|
14
|
+
self.instance.docos
|
15
|
+
end
|
16
|
+
def self.store cont_doco
|
17
|
+
self.instance.store cont_doco
|
18
|
+
end
|
19
|
+
def self.fetch controller_path
|
20
|
+
self.instance.docos[controller_path]
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
module ApiCanon
|
2
|
+
class DocumentedAction
|
3
|
+
attr_reader :params, :response_codes, :description, :action_name
|
4
|
+
def initialize(action_name)
|
5
|
+
@action_name = action_name
|
6
|
+
@params={}
|
7
|
+
# TODO: This should check routes to see if params[:format] is expected
|
8
|
+
@params[:format] = DocumentedParam.new :format,
|
9
|
+
:default => :json, :example_values => [:json, :xml], :type => :string,
|
10
|
+
:description => "The requested format of the response."
|
11
|
+
@response_codes={}
|
12
|
+
end
|
13
|
+
def param(param_name, options={})
|
14
|
+
@params[param_name] = DocumentedParam.new param_name, options
|
15
|
+
end
|
16
|
+
def response_code(code, options={})
|
17
|
+
@response_codes[code] = options
|
18
|
+
end
|
19
|
+
def describe(desc)
|
20
|
+
@description = desc
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
module ApiCanon
|
2
|
+
class DocumentedParam
|
3
|
+
attr_accessor :name, :values, :type, :default, :description, :example_values
|
4
|
+
attr_writer :multiple
|
5
|
+
include ActionView::Helpers
|
6
|
+
def values_for_example
|
7
|
+
example_values || values || ""
|
8
|
+
end
|
9
|
+
def multiple?
|
10
|
+
!!@multiple
|
11
|
+
end
|
12
|
+
def initialize(name, opts={})
|
13
|
+
@name = name
|
14
|
+
opts.each {|k,v| self.send("#{k}=", v) }
|
15
|
+
end
|
16
|
+
def form_values
|
17
|
+
values.presence || example_values.presence
|
18
|
+
end
|
19
|
+
def to_field(f, doco_prefix)
|
20
|
+
# TODO: This doco_prefix thing sucks. Get rid of it.
|
21
|
+
if type == :array
|
22
|
+
f.select name, form_values, {:selected => default, :include_blank => true}, {:multiple => multiple?, :class => 'input-block-level', :id => "#{doco_prefix}_#{name}"}
|
23
|
+
elsif type == :boolean
|
24
|
+
f.select name, [true,false], {:selected => default, :include_blank => true}, :class => 'input-block-level', :id => "#{doco_prefix}_#{name}"
|
25
|
+
else
|
26
|
+
f.text_field name, :value => default, :class => 'input-block-level', :id => "#{doco_prefix}_#{name}"
|
27
|
+
end
|
28
|
+
end
|
29
|
+
def example_values_field(f, doco_prefix)
|
30
|
+
if values_for_example.is_a?(Array)
|
31
|
+
if type != :array
|
32
|
+
select_tag :example_value, options_for_select([""] + values_for_example, default), :class => 'input-block-level',
|
33
|
+
:onchange => "jQuery('##{doco_prefix}_#{name}').val(this.value)", :id => "#{doco_prefix}_#{name}_example"
|
34
|
+
end
|
35
|
+
else
|
36
|
+
values_for_example
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
module ApiCanon
|
2
|
+
module Routes
|
3
|
+
def self.draw(map, options={})
|
4
|
+
route_opts = {:as => 'api_canon_test', :controller => 'api_canon/api_canon', :action => 'test'}.merge options
|
5
|
+
if Rails.version.starts_with?('2')
|
6
|
+
map.api_canon_test 'api_canon/test', route_opts
|
7
|
+
else
|
8
|
+
map.match 'api_canon/test', route_opts
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
data/lib/api_canon.rb
ADDED
@@ -0,0 +1,54 @@
|
|
1
|
+
require 'api_canon/routes'
|
2
|
+
require 'api_canon/version'
|
3
|
+
require 'api_canon/app'
|
4
|
+
require 'api_canon/document'
|
5
|
+
require 'api_canon/documented_action'
|
6
|
+
require 'api_canon/documented_param'
|
7
|
+
require 'api_canon/documentation_store'
|
8
|
+
|
9
|
+
module ApiCanon
|
10
|
+
|
11
|
+
def self.included(base)
|
12
|
+
base.extend(ClassMethods)
|
13
|
+
base.class_eval do
|
14
|
+
append_view_path File.join(File.dirname(__FILE__),'api_canon','app','views')
|
15
|
+
require 'api_canon/app/helpers/api_canon_view_helper'
|
16
|
+
helper ApiCanon::ApiCanonViewHelper
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def api_canon_docs
|
21
|
+
@api_doc = DocumentationStore.fetch controller_path
|
22
|
+
respond_to do |format|
|
23
|
+
format.html { render 'api_canon/api_canon', :layout => 'layouts/api_canon' }
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def index
|
28
|
+
if params[:format].blank? || params[:format] == 'html'
|
29
|
+
api_canon_docs
|
30
|
+
else
|
31
|
+
super
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
module ClassMethods
|
36
|
+
protected
|
37
|
+
def document_controller(opts={}, &block)
|
38
|
+
document = DocumentationStore.fetch controller_path
|
39
|
+
document ||= Document.new controller_path, controller_name, opts
|
40
|
+
document.instance_eval &block
|
41
|
+
DocumentationStore.store document
|
42
|
+
end
|
43
|
+
def document_method(method_name,&block)
|
44
|
+
document = DocumentationStore.fetch controller_path
|
45
|
+
document ||= Document.new controller_path, controller_name
|
46
|
+
documented_action = ApiCanon::DocumentedAction.new method_name
|
47
|
+
documented_action.instance_eval &block
|
48
|
+
document.add_action documented_action
|
49
|
+
DocumentationStore.store document
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
|
54
|
+
end
|
File without changes
|
@@ -0,0 +1,35 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
describe ApiCanon::DocumentedAction do
|
3
|
+
|
4
|
+
let(:document) {ApiCanon::DocumentedAction.new :index}
|
5
|
+
let(:description) {'ID is the unique identifier of the object'}
|
6
|
+
subject { document }
|
7
|
+
its(:action_name) { should == :index }
|
8
|
+
it "Should document the 'format' param by default" do
|
9
|
+
expect(document.params[:format]).to be_a ApiCanon::DocumentedParam
|
10
|
+
expect(document.params[:format].example_values). to eq [:json, :xml]
|
11
|
+
expect(document.params[:format].default).to eq :json
|
12
|
+
end
|
13
|
+
describe :param do
|
14
|
+
it "documents the named param" do
|
15
|
+
document.param :id, :default => 1, :description => description, :type => :integer
|
16
|
+
expect(document.params[:id]).to be_a ApiCanon::DocumentedParam
|
17
|
+
expect(document.params[:id].default).to eq 1
|
18
|
+
expect(document.params[:id].description).to eq description
|
19
|
+
end
|
20
|
+
end
|
21
|
+
describe :response_code do
|
22
|
+
it "describes the response codes the user can expect to see" do
|
23
|
+
document.response_code 200
|
24
|
+
pending "Need to create objects to deal with this similarly to DocumentedParam"
|
25
|
+
end
|
26
|
+
end
|
27
|
+
describe :description do
|
28
|
+
let(:description) {"This action lists a bunch of things"}
|
29
|
+
it "describes what the controller action is for" do
|
30
|
+
document.describe description
|
31
|
+
expect(document.description).to eq description
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
describe ApiCanon::DocumentedParam do
|
3
|
+
let(:example_values) { [:json, :xml] }
|
4
|
+
let(:doc_opts) do
|
5
|
+
{:type => 'string', :default => :json, :example_values => example_values}
|
6
|
+
end
|
7
|
+
let(:fake_form) { mock :form }
|
8
|
+
let(:doco_prefix) { 'foo' }
|
9
|
+
let(:documented_param) {ApiCanon::DocumentedParam.new :format, doc_opts}
|
10
|
+
subject { documented_param }
|
11
|
+
its(:name) { should eq :format }
|
12
|
+
its(:type) { should eq 'string' }
|
13
|
+
its(:default) { should eq :json }
|
14
|
+
its(:form_values) { should eq example_values }
|
15
|
+
its(:multiple?) { should eq false }
|
16
|
+
describe :to_field do
|
17
|
+
context "string-type params" do
|
18
|
+
it "Creates a text field" do
|
19
|
+
fake_form.should_receive :text_field
|
20
|
+
documented_param.to_field fake_form, doco_prefix
|
21
|
+
end
|
22
|
+
end
|
23
|
+
context "array-type params" do
|
24
|
+
before(:each) {documented_param.type = :array}
|
25
|
+
it "Creates a select field" do
|
26
|
+
fake_form.should_receive(:select).with :format, [:json, :xml], hash_including({:include_blank => true}), hash_including({:id => 'foo_format'})
|
27
|
+
documented_param.to_field fake_form, doco_prefix
|
28
|
+
end
|
29
|
+
end
|
30
|
+
context "boolean-type params" do
|
31
|
+
before(:each) {documented_param.type = :boolean}
|
32
|
+
it "Creates a select field with true and false values" do
|
33
|
+
fake_form.should_receive(:select).with :format, [true, false], hash_including({:include_blank => true}), hash_including({:id => 'foo_format'})
|
34
|
+
documented_param.to_field fake_form, doco_prefix
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
describe :example_values_field do
|
39
|
+
context "array of example values" do
|
40
|
+
it "Creates a select field" do
|
41
|
+
documented_param.should_receive(:select_tag)
|
42
|
+
documented_param.example_values_field fake_form, doco_prefix
|
43
|
+
end
|
44
|
+
it "does nothing with array-value fields" do
|
45
|
+
documented_param.type = :array
|
46
|
+
expect(documented_param.example_values_field(fake_form, doco_prefix)).to eq nil
|
47
|
+
end
|
48
|
+
it "returns the example values otherwise" do
|
49
|
+
documented_param.example_values = 'foo'
|
50
|
+
expect(documented_param.example_values_field(fake_form, doco_prefix)).to eq 'foo'
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe ApiCanon do
|
4
|
+
let(:fake_controller) do
|
5
|
+
class FakeController < ActionController::Base
|
6
|
+
include ApiCanon
|
7
|
+
end
|
8
|
+
end
|
9
|
+
describe 'including' do
|
10
|
+
it "adds class methods to the controller" do
|
11
|
+
expect(fake_controller.methods.map(&:to_s)).to include('document_method')
|
12
|
+
end
|
13
|
+
it "adds instance methods to the controller" do
|
14
|
+
expect(fake_controller.new.methods.map(&:to_s)).to include('api_canon_docs')
|
15
|
+
expect(fake_controller.new.methods.map(&:to_s)).to include('index')
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
describe "document_method" do
|
20
|
+
let(:api_document) { mock :api_document }
|
21
|
+
context "without a current controller doc" do
|
22
|
+
it "creates and stores a new ApiCanon::Document and adds the documented action" do
|
23
|
+
ApiCanon::Document.should_receive(:new).with('fake', 'fake').and_return(api_document)
|
24
|
+
ApiCanon::DocumentationStore.instance.should_receive(:store).with(api_document)
|
25
|
+
api_document.should_receive :add_action
|
26
|
+
fake_controller.send(:document_method, :index, &(Proc.new {}))
|
27
|
+
end
|
28
|
+
end
|
29
|
+
context "with a current controller doc" do
|
30
|
+
before(:each) do
|
31
|
+
fake_controller.send(:document_controller, &(Proc.new {}))
|
32
|
+
end
|
33
|
+
it "adds a documented action to the current controller doc" do
|
34
|
+
expect {
|
35
|
+
fake_controller.send(:document_method, :index, &(Proc.new {}))
|
36
|
+
}.to change {
|
37
|
+
ApiCanon::DocumentationStore.fetch('fake').documented_actions.count
|
38
|
+
}.by(1)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
end
|
data/spec/spec_helper.rb
ADDED
metadata
ADDED
@@ -0,0 +1,120 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: api_canon
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.2.3
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Cameron Walsh
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2013-05-03 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: rails
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - '>='
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: 2.3.17
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - '>='
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: 2.3.17
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: rspec
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ~>
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: 2.13.0
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ~>
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: 2.13.0
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: rake
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - '>='
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - '>='
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
description: |-
|
56
|
+
api_canon is a declarative documentation generator
|
57
|
+
for APIs. Declare the parameters and response codes,
|
58
|
+
describe them, and give some example values. api_canon
|
59
|
+
handles the rest for you.
|
60
|
+
email:
|
61
|
+
- cameron.walsh@gmail.com
|
62
|
+
executables: []
|
63
|
+
extensions: []
|
64
|
+
extra_rdoc_files: []
|
65
|
+
files:
|
66
|
+
- lib/api_canon/documented_param.rb
|
67
|
+
- lib/api_canon/version.rb
|
68
|
+
- lib/api_canon/documented_action.rb
|
69
|
+
- lib/api_canon/documentation_store.rb
|
70
|
+
- lib/api_canon/app/views/api_canon/api_canon.html.erb
|
71
|
+
- lib/api_canon/app/views/api_canon/_form.html.erb
|
72
|
+
- lib/api_canon/app/views/api_canon/_api_canon.html.erb
|
73
|
+
- lib/api_canon/app/views/api_canon/_rails_2_form.html.erb
|
74
|
+
- lib/api_canon/app/views/application/index.html.erb
|
75
|
+
- lib/api_canon/app/views/layouts/api_canon.html.erb
|
76
|
+
- lib/api_canon/app/controllers/api_canon_controller.rb
|
77
|
+
- lib/api_canon/app/helpers/api_canon_view_helper.rb
|
78
|
+
- lib/api_canon/app.rb
|
79
|
+
- lib/api_canon/document.rb
|
80
|
+
- lib/api_canon/routes.rb
|
81
|
+
- lib/tasks/api_canon_tasks.rake
|
82
|
+
- lib/api_canon.rb
|
83
|
+
- MIT-LICENSE
|
84
|
+
- Rakefile
|
85
|
+
- README.md
|
86
|
+
- spec/spec_helper.rb
|
87
|
+
- spec/lib/api_canon/documented_param_spec.rb
|
88
|
+
- spec/lib/api_canon/documented_action_spec.rb
|
89
|
+
- spec/lib/api_canon_spec.rb
|
90
|
+
- spec/dummy/log/test.log
|
91
|
+
homepage: http://github.com/cwalsh/api_canon
|
92
|
+
licenses:
|
93
|
+
- MIT
|
94
|
+
metadata: {}
|
95
|
+
post_install_message:
|
96
|
+
rdoc_options: []
|
97
|
+
require_paths:
|
98
|
+
- lib
|
99
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
100
|
+
requirements:
|
101
|
+
- - '>='
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: '0'
|
104
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
105
|
+
requirements:
|
106
|
+
- - '>='
|
107
|
+
- !ruby/object:Gem::Version
|
108
|
+
version: '0'
|
109
|
+
requirements: []
|
110
|
+
rubyforge_project:
|
111
|
+
rubygems_version: 2.0.3
|
112
|
+
signing_key:
|
113
|
+
specification_version: 4
|
114
|
+
summary: Declarative documentation generator for APIs.
|
115
|
+
test_files:
|
116
|
+
- spec/spec_helper.rb
|
117
|
+
- spec/lib/api_canon/documented_param_spec.rb
|
118
|
+
- spec/lib/api_canon/documented_action_spec.rb
|
119
|
+
- spec/lib/api_canon_spec.rb
|
120
|
+
- spec/dummy/log/test.log
|