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:
parent
156ceba394
commit
ee70c75174
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue