api_canon 0.2.3
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
+
[![Build Status](https://travis-ci.org/cwalsh/api_canon.png?branch=master)](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
|