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 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