api_canon 0.2.3

Sign up to get free protection for your applications and to get access to all the features.
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,9 @@
1
+ module ApiCanon
2
+ module ApiCanonViewHelper
3
+ if Rails.version.starts_with?('2')
4
+ def content_for?(content_partial_name)
5
+ instance_variable_get("@content_for_#{content_partial_name}")
6
+ end
7
+ end
8
+ end
9
+ 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
@@ -0,0 +1,3 @@
1
+ module ApiCanon
2
+ VERSION = '0.2.3'
3
+ 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
@@ -0,0 +1,4 @@
1
+ # desc "Explaining what the task does"
2
+ # task :api_canon do
3
+ # # Task goes here
4
+ # 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
@@ -0,0 +1,9 @@
1
+ # Configure Rails Environment
2
+ ENV["RAILS_ENV"] = "test"
3
+ require 'rails'
4
+ require 'singleton'
5
+ require 'action_controller'
6
+ require 'api_canon'
7
+ # Load support files
8
+ Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each { |f| require f }
9
+
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