generate an api.json file from 'yard' API docs

Add 'swagger' API metadata generator from Yard. This metadata
can be used to generate a Canvas API wrapper library, or to benefit
from the swagger doc ecosystem.

See https://github.com/wordnik/swagger-core/wiki

Test Plan:
- Call 'rake doc:api API_YML=1' from the command line
- Check that api.yml file is created in current directory

fixes CNVS-7311

Change-Id: I9c5f756192794e5ea767c4db745a777cd63c3942
Reviewed-on: https://gerrit.instructure.com/23079
Tested-by: Jenkins <jenkins@instructure.com>
Reviewed-by: Nathan Mills <nathanm@instructure.com>
Product-Review: Duane Johnson <duane@instructure.com>
QA-Review: Duane Johnson <duane@instructure.com>
This commit is contained in:
Duane Johnson 2013-08-15 13:13:40 -06:00
parent 156ceba394
commit ee70c75174
8 changed files with 365 additions and 0 deletions

View File

@ -0,0 +1,74 @@
require 'hash_view'
class ArgumentView < HashView
attr_reader :line
def initialize(line)
@line = line.gsub(/\s+/m, " ")
@name, remaining = @line.scan(/^([^\s]+)(.*)$/).first
@type, @desc = parse_type_desc(remaining)
# debugger
@name.strip! if @name
@type = format(@type.strip.gsub('[', '').gsub(']', '')) if @type
@desc.strip! if @desc
end
# Atrocious use of regex to parse out type signatures such as:
# "[[Integer], Optional] The IDs of the override's target students."
def parse_type_desc(str)
parts = str.strip.
gsub(/\]\s+,/, '],'). # turn "] ," into "],"
gsub(/[^,] /){ |s| s[0] == ']' ? s[0] + '|||' : s }. # put "|||" between type and desc
split('|||')
if parts.size == 1
[nil, parts.first]
else
parts
end
end
def name
format(@name)
end
def desc
format(@desc)
end
def type_parts
(@type || '').split(/\s*[,\|]\s*/).
map{ |t| t.force_encoding('UTF-8') }
end
def types
type_parts.reject{ |t| %w(optional required).include?(t.downcase) }
end
def optional?
type_parts.map{ |t| t.downcase }.include?('optional')
end
def required?
!!optional?
end
def to_swagger
{
"paramType" => "path",
"name" => name,
"description" => desc,
"type" => types.first,
# "format" => "",
"required" => required?,
}
end
def to_hash
{
"name" => name,
"desc" => desc,
"types" => types,
"optional" => optional?,
}
end
end

View File

@ -0,0 +1,33 @@
require 'hash_view'
require 'method_view'
class ControllerView < HashView
def initialize(controller)
@controller = controller
end
def name
format(@controller.name)
end
def raw_methods
@controller.children.select do |method|
method.tags.find do |tag|
tag.tag_name.downcase == "api"
end
end
end
def methods
raw_methods.map do |method|
MethodView.new(method)
end
end
def to_hash
{
"name" => name,
"methods" => methods.map{ |m| m.to_hash },
}
end
end

View File

@ -0,0 +1,10 @@
class HashView
def to_hash
{}
end
protected
def format(str)
str.to_s.force_encoding('UTF-8') if str
end
end

View File

@ -0,0 +1,101 @@
require 'hash_view'
require 'argument_view'
require 'route_view'
require 'return_view'
class MethodView < HashView
def initialize(method)
@method = method
end
def name
format(@method.name)
end
def api_tag
@api_tag ||= select_tags("api").first
end
def summary
if api_tag
format(api_tag.text)
end
end
def nickname
summary.downcase.gsub(/\s+/, '_')
end
def desc
format(@method.docstring)
end
def raw_arguments
select_tags("argument")
end
def arguments
raw_arguments.map do |tag|
ArgumentView.new(tag.text)
end
end
def return_tag
select_tags("returns").first
end
def returns
if return_tag
ReturnView.new(return_tag.text)
else
ReturnViewNull.new
end
end
def route
@route ||= RouteView.new(@method)
end
def parameters
arguments.map do |arg|
arg.to_swagger
end
end
def operation
{
"httpMethod" => route.verb,
"nickname" => nickname,
"responseClass" => returns.to_swagger,
"parameters" => parameters,
"summary" => summary,
"notes" => desc
}
end
def to_swagger
{
"path" => route.api_path,
"description" => desc,
"operations" => [operation]
}
end
def to_hash
{
"name" => name,
"summary" => summary,
"desc" => desc,
"arguments" => arguments.map{ |a| a.to_hash },
"returns" => returns.to_hash,
"route" => route.to_hash,
}
end
protected
def select_tags(tag_name)
@method.tags.select do |tag|
tag.tag_name.downcase == tag_name
end
end
end

View File

@ -0,0 +1,46 @@
require 'hash_view'
class ReturnViewNull < HashView
def array?; false; end
def type; nil; end
def to_hash
{
"array" => array?,
"type" => format(type),
}
end
def to_swagger
nil
end
end
class ReturnView < ReturnViewNull
def initialize(line)
if line
@line = line.gsub(/\s+/m, " ").strip
end
end
def array?
if @line
@line.include?('[') && @line.include?(']')
else
false
end
end
def type
if @line
@line.gsub('[', '').gsub(']', '')
else
nil
end
end
def to_swagger
type
end
end

View File

@ -0,0 +1,51 @@
require 'hash_view'
class RouteView < HashView
def initialize(yard_method_object)
@object = yard_method_object
@controller = @object.parent.path.underscore.sub("_controller", '')
@action = @object.path.sub(/^.*#/, '').sub(/_with_.*$/, '')
end
def route
@route ||= begin
routes = ApiRouteSet::V1.api_methods_for_controller_and_action(@controller, @action)
# Choose shortest route (preferrably without .json suffix)
routes.sort_by { |r| r.segments.join.size }.first
end
end
def route_name
ActionController::Routing::Routes.named_routes.routes.index(route).to_s.sub("api_v1_", "")
end
def file_path
filepath = "app/controllers/#{@controller}_controller.rb"
filepath = nil unless File.file?(File.join(Rails.root, filepath))
filepath
end
def api_path
path = route.segments.inject("") { |str,s| str << s.to_s }
path.chop! if path.length > 1
path
end
def verb
route.conditions[:method].to_s.upcase
end
def reqs
route.requirements
end
def to_hash
{
"verb" => verb,
"api_path" => api_path,
"reqs" => reqs,
"name" => route_name,
"file_path" => file_path,
}
end
end

View File

@ -0,0 +1,28 @@
# encoding: utf-8
$:.unshift(File.dirname(__FILE__))
require 'controller_view'
def init
apis = []
controllers = run_verifier(options[:objects])
controllers.each do |controller|
ControllerView.new(controller).methods.each do |method|
apis << method.to_swagger
end
end
resource_listing = {
"apiVersion" => "1.0",
"swaggerVersion" => "1.2",
"basePath" => "http://canvas.instructure.com/api/v1",
# "resourcePath": "/pet"
"apis" => apis,
# "models": models,
}
filename = "api.json"
puts "Writing API data to #{filename}"
File.open(filename, "w") do |file|
file.puts JSON.pretty_generate(resource_listing)
end
end

View File

@ -39,6 +39,28 @@ namespace :doc do
"See #{DOC_DIR}/index.html"
end
namespace(:api) do
# Produces api.json file as output (in the current dir). The api.json file is
# an API description in JSON format that follows the 'swagger' API description
# standard.
YARD::Rake::YardocTask.new(:swagger) do |t|
t.before = proc { FileUtils.rm_rf(API_DOC_DIR) }
t.files = %w[
app/controllers/*.rb
vendor/plugins/*/app/controllers/*.rb
vendor/plugins/*/lib/*.rb
]
t.options = %W[
-e lib/api_routes.rb
-p doc
-t api
-o #{API_DOC_DIR}
]
t.options << '-f' << 'text'
end
end
end
rescue LoadError