code to generate API documentation

This commit is contained in:
Brad Rydzewski 2015-10-06 10:19:43 -07:00
parent 25d6a8c4bc
commit 2b00d12bb6
7 changed files with 579 additions and 501 deletions

View file

@ -13,9 +13,19 @@ deps:
go get -u github.com/elazarl/go-bindata-assetfs/...
go get -u github.com/dchest/jsmin
go get -u github.com/franela/goblin
go get -u github.com/go-swagger/go-swagger
gen:
go generate $(PACKAGES)
gen: gen_static gen_template gen_migrations
gen_static:
mkdir -p static/docs_gen/api
go generate github.com/drone/drone/static
gen_template:
go generate github.com/drone/drone/template
gen_migrations:
go generate github.com/drone/drone/shared/database
build:
go build

View file

@ -0,0 +1,219 @@
// +build ignore
// This program generates api documentation from a
// swaggerfile using an amber template.
package main
import (
"crypto/md5"
"flag"
"fmt"
"io"
"log"
"os"
"github.com/eknkc/amber"
"github.com/go-swagger/go-swagger/spec"
)
var (
templ = flag.String("template", "index.amber", "")
input = flag.String("input", "swagger.json", "")
output = flag.String("output", "", "")
)
func main() {
flag.Parse()
// parses the swagger spec file
spec, err := spec.YAMLSpec(*input)
if err != nil {
log.Fatal(err)
}
swag := spec.Spec()
// create output source for file. defaults to
// stdout but may be file.
var w io.WriteCloser = os.Stdout
if *output != "" {
w, err = os.Create(*output)
if err != nil {
fmt.Fprintf(os.Stderr, "%v\n", err)
return
}
defer w.Close()
}
// we wrap the swagger file in a map, otherwise it
// won't work with our existing templates, which expect
// a map as the root parameter.
var data = map[string]interface{}{
"Swagger": normalize(swag),
}
t := amber.MustCompileFile(*templ, amber.DefaultOptions)
err = t.Execute(w, data)
if err != nil {
log.Fatal(err)
}
}
// Swagger is a simplified representation of the swagger
// document with a subset of the fields used to generate
// our API documentation.
type Swagger struct {
Tags []Tag
}
type Tag struct {
Name string
Ops []Operation
}
type Operation struct {
ID string
Method string
Path string
Desc string
Summary string
Params []Param
Results []Result
}
type Param struct {
Name string
Desc string
Type string
Example interface{}
InputTo string
IsObject bool
}
type Result struct {
Status int
Desc string
Example interface{}
IsObject bool
IsArray bool
}
// normalize is a helper function that normalizes the swagger
// file to a simpler format that makes it easier to work with
// inside the template.
func normalize(swag *spec.Swagger) Swagger {
swag_ := Swagger{}
for _, tag := range swag.Tags {
tag_ := Tag{Name: tag.Name}
// group the paths based on their tag value.
for route, path := range swag.Paths.Paths {
var ops = []*spec.Operation{
path.Get,
path.Put,
path.Post,
path.Patch,
path.Delete,
}
// flatten the operations into an array and convert
// the underlying data so that it is a bit easier to
// work with.
for _, op := range ops {
// the operation must have a tag to
// be rendered in our custom template.
if op == nil || !hasTag(tag.Name, op.Tags) {
continue
}
item := Operation{}
item.Path = route
item.Method = getMethod(op, path)
item.Desc = op.Description
item.Summary = op.Summary
item.ID = fmt.Sprintf("%x", md5.Sum([]byte(item.Path+item.Method)))
// convert the operation input parameters into
// our internal format so that it is easier to
// work with in the template.
for _, param := range op.Parameters {
param_ := Param{}
param_.Name = param.Name
param_.Desc = param.Description
param_.Type = param.Type
param_.IsObject = param.Schema != nil
param_.InputTo = param.In
if param_.IsObject {
param_.Type = param.Schema.Ref.GetPointer().String()[13:]
param_.Example = param.Schema.Example
}
item.Params = append(item.Params, param_)
}
// convert the operation response types into
// our internal format so that it is easier to
// work with in the template.
for code, resp := range op.Responses.StatusCodeResponses {
result := Result{}
result.Desc = resp.Description
result.Status = code
result.IsObject = resp.Schema != nil
if result.IsObject {
result.IsArray = resp.Schema.Items != nil
name := resp.Schema.Ref.GetPointer().String()
if len(name) != 0 {
def, _ := swag.Definitions[name[13:]]
result.Example = def.Example
}
}
if result.IsArray {
name := resp.Schema.Items.Schema.Ref.GetPointer().String()
def, _ := swag.Definitions[name[13:]]
result.Example = def.Example
}
item.Results = append(item.Results, result)
}
tag_.Ops = append(tag_.Ops, item)
}
}
swag_.Tags = append(swag_.Tags, tag_)
}
return swag_
}
// hasTag is a helper function that returns true if
// an operation has the specified tag label.
func hasTag(want string, in []string) bool {
for _, got := range in {
if got == want {
return true
}
}
return false
}
// getMethod is a helper function that returns the http
// method for the specified operation in a path.
func getMethod(op *spec.Operation, path spec.PathItem) string {
switch {
case op == path.Get:
return "GET"
case op == path.Put:
return "PUT"
case op == path.Patch:
return "PATCH"
case op == path.Post:
return "POST"
case op == path.Delete:
return "DELETE"
}
return ""
}

View file

@ -1,499 +0,0 @@
#%RAML 0.8
title: Drone API
baseUri: http://localhost:8080/api
traits:
- authenticate:
description: Some requests require authentication.
queryParameters:
access_token:
description: Access Token
type: string
example: ACCESS_TOKEN
- authorize:
responses:
401:
description: |
Access Denied. User must be authenticated.
403:
description: |
Access Forbidden. User must be a system administrator.
/repos/{owner}/{name}:
is: [ authorize, authenticate ]
uriParameters:
owner:
displayName: Owner
description: Owner of the repository
type: string
required: true
name:
displayName: Name
description: Name of the repository
type: string
required: true
get:
displayName: Find Repository
description: |
Retrieve a Repository by Name
responses:
200:
body:
application/json:
example: !include samples/repo.json
404:
description: |
Unable to find the Repository in the database
patch:
displayName: Update Repository
description: |
Update a Repository
responses:
200:
body:
application/json:
example: !include samples/repo.json
400:
description: |
Unable to update the Repository record in the database
404:
description: |
Unable to find the Repository in the database
post:
displayName: Activate Repository
description: |
Activate a Repository
responses:
200:
body:
application/json:
example: !include samples/repo.json
400:
description: |
Unable to update the Repository record in the database
403:
description: |
Unable to activate the Repository due to insufficient privileges
404:
description: |
Unable to retrieve the Repository from the remote system (ie GitHub)
409:
description: |
Unable to activate the Repository because it is already activate
500:
description: |
Unable to activate the Repository due to an internal server error.
This may indicate a problem adding hooks to the remote system (ie Github),
generating SSH deployment keys, or persisting to the database.
delete:
displayName: Delete Repository
description: |
Deletes a Repository
responses:
200:
description: |
Successfully deleted the Repository
400:
description: |
Unable to remove post-commit hooks from the remote system (ie GitHub)
404:
description: |
Unable to find the Repository in the database
500:
description: |
Unable to update the Repository record in the database
/watch:
is: [ authorize, authenticate ]
description: |
Watch the Repository
post:
responses:
200:
description: |
Successfully Watching this Repository
400:
description: |
Unable to insert the Starred record in the database
404:
description: |
Unable to find the Repository in the database
/unwatch:
is: [ authorize, authenticate ]
description: |
Unwatch the Repository
delete:
responses:
200:
description: |
Successfully Unwatched this Repository
400:
description: |
Unable to delete the Starred record in the database
404:
description: |
Unable to find the Repository in the database
#
# Builds
#
/repos/{owner}/{name}/builds:
displayName: Builds
is: [ authorize, authenticate ]
uriParameters:
owner:
displayName: Owner
description: Owner of the repository
type: string
required: true
name:
displayName: Name
description: Name of the repository
type: string
required: true
get:
displayName: List Builds
description: |
Retrieve the list of recent Builds for the Repository
responses:
200:
body:
application/json:
example: !include samples/builds.json
404:
description: |
Unable to find the Repository in the database
/{number}:
is: [ authorize, authenticate ]
uriParameters:
number:
displayName: Number
type: integer
get:
displayName: Find Build
description: |
Retrieve a specific Build by Number
responses:
200:
body:
application/json:
example: !include samples/build.json
404:
description: |
Unable to find the Build in the database
delete:
displayName: Cancel Build
description: |
Cancel an running Build by Number
responses:
200:
description: |
Successfully cancelled the Build
404:
description: |
Cannot find Build by Number
409:
description: |
Cannot cancel a Build that is already stopped
post:
displayName: Restart Build
description: |
Restart a completed Build
responses:
202:
description: |
Successfully restarted the Build
404:
description: |
Cannot find Build by Number
409:
description: |
Cannot re-start a Build that is running
#
# Build Logs
#
/repos/{owner}/{name}/logs/{number}/{job}:
displayName: Builds
is: [ authorize, authenticate ]
uriParameters:
owner:
displayName: Owner
description: Owner of the repository
type: string
required: true
name:
displayName: Name
description: Name of the repository
type: string
required: true
number:
displayName: Build Number
description: Incremental Build Number
type: integer
required: true
job:
displayName: Job Number
description: Sequential Job Number
type: integer
required: true
get:
displayName: Find Build Logs
description: |
Retrieve the Logs for a specific Build Job
responses:
200:
body:
text/plain:
404:
description: |
Cannot find Build Logs
#
# User Endpoint
#
/user:
is: [ authorize, authenticate ]
displayName: User
get:
description: |
Retrieve the currently authenticated User
responses:
200:
body:
application/json:
example: !include samples/user.json
patch:
description: |
Update the currently authenticated User
responses:
200:
description: |
Updated User
body:
application/json:
example: !include samples/user.json
400:
description: |
Bad Request. Error updating User
#
# User Repos
#
/user/repos:
is: [ authorize, authenticate ]
displayName: User Repos
get:
description: |
Retrieve the currently authenticated User's Repository list
responses:
200:
body:
application/json:
example: !include samples/repos.json
400:
description: |
Bad Request. Error retrieving Repository list
#
# User Tokens
#
/user/tokens:
is: [ authorize, authenticate ]
displayName: User Tokens
get:
description: |
Retrieve the currently authenticated User's active Token list
responses:
200:
body:
application/json:
example: !include samples/tokens.json
400:
description: |
Error retrieving the Token list
post:
description: |
Generate a new User Token
responses:
200:
body:
application/json:
example: !include samples/token.json
400:
description: |
Error when attempting to generate the JWT token
500:
description: |
Error when attempting to save the Token
/{label}:
is: [ authorize, authenticate ]
delete:
description: |
Delete a specific User Token by Label
responses:
200:
description: |
Successfully deleted the Token
400:
description: |
Error attempting to delete Token from database
404:
description: |
Unable to find Token in database
#
# Users Endpoint
#
/users:
is: [ authorize, authenticate ]
displayName: Users
get:
description: Retrieve a list of all registered Users.
displayName: List Users
responses:
200:
body:
application/json:
example: !include samples/users.json
/{login}:
is: [ authorize, authenticate ]
uriParameters:
login:
displayName: Login
description: Username in remote system
example: Octocat
type: string
required: true
get:
displayName: Find User
description: |
Retrieve a specific User by Login name
responses:
200:
body:
application/json:
schema: !include schemas/user.json
404:
description: |
Cannot find the User
post:
displayName: Enable User
description: |
Enable a new User by Login name
responses:
201:
body:
application/json:
schema: !include schemas/user.json
400:
description: |
Error inserting User into the database
patch:
displayName: Update User
description: |
Update a specific User
responses:
200:
body:
application/json:
schema: !include schemas/user.json
400:
description: |
Error updating User record in the database
delete:
displayName: Delete User
description: |
Delete a specific User
responses:
204:
description: |
Successfully deleted the User
400:
description: |
Error deleting the User from the database
403:
description: |
Cannot delete your own User account
404:
description: |
Cannot find the User
#
# Badge Endpoint
#
/badges/{owner}/{name}:
displayName: Badges
uriParameters:
owner:
displayName: Owner
description: Owner of the repository
type: string
required: true
name:
displayName: Name
description: Name of the repository
type: string
required: true
/status.svg:
description: Returns an SVG status badge for the latest Build
displayName: Status Badge
get:
queryParameters:
branch:
default: master
required: false
type: string
example: master
responses:
200:
body:
image/svg+xml:
/cc.xml:
description: Returns a CCMenu feed for the Repository
displayName: CCMenu
get:
responses:
200:
body:
application/xml:
example: !include samples/ccmenu.xml

View file

@ -0,0 +1,202 @@
.toc
list-style-type: none;
padding:0px;
margin:0px;
padding-bottom:40px;
h2
font-size:21px;
font-weight:normal;
margin-bottom:20px;
color:#2b303b;
ul
list-style-type: none;
padding:0px;
margin:0px;
margin-bottom:40px;
border-bottom: 1px solid #EEE;
padding-bottom:40px;
li
line-height:25px;
a
color: #2b303b;
text-decoration:none;
a:hover
text-decoration:underline;
[data-method]:before
content: attr(data-method);
padding:0px 10px;
line-height:18px;
min-width:70px;
font-size:11px;
text-transform:uppercase;
display: inline-block;
text-align:center;
color:#FFF;
border-radius:2px;
margin-right:20px;
[data-method="GET"]:before
background-color:#1ABC9C;
[data-method="PUT"]:before
background-color:#9B59B6;
[data-method="POST"]:before
background-color:#3498DB;
[data-method="PATCH"]:before
background-color:#E67E22;
[data-method="DELETE"]:before
background-color:#E74C3C;
[data-method]:before
background:#FFF;
border:1px solid #FFF;
[data-method="GET"]:before
color:#1ABC9C;
border-color:#1ABC9C;
[data-method="PUT"]:before
color:#9B59B6;
border-color:#9B59B6;
[data-method="POST"]:before
color:#3498DB;
border-color:#3498DB;
[data-method="PATCH"]:before
color:#E67E22;
border-color:#E67E22;
[data-method="DELETE"]:before
color:#E74C3C;
border-color:#E74C3C;
.operation
[data-method]:before
content: attr(data-method);
padding:0px 10px;
line-height:18px;
min-width:70px;
font-size:11px;
text-transform:uppercase;
display: inline-block;
text-align:center;
color:#FFF;
border-radius:2px;
margin-right:20px;
background:#FFF;
border:1px solid #FFF;
line-height: 20px;
vertical-align: top;
[data-method]:before
background:#FFF;
border:1px solid #FFF;
[data-method="GET"]:before
color:#1ABC9C;
border-color:#1ABC9C;
[data-method="PUT"]:before
color:#9B59B6;
border-color:#9B59B6;
[data-method="POST"]:before
color:#3498DB;
border-color:#3498DB;
[data-method="PATCH"]:before
color:#E67E22;
border-color:#E67E22;
[data-method="DELETE"]:before
color:#E74C3C;
border-color:#E74C3C;
.docs
margin-top:40px;
padding: 0px 50px;
padding-right:40px;
pre
margin-right: 15px;
font-size: 14px;
color: #eff1f5;
color: #2b303b;
border-radius: 2px;
background: #2b303b;
background: #ECF0F1;
white-space: pre-wrap;
word-wrap: break-word;
box-sizing: border-box;
padding: 25px 30px;
font-family: "Roboto Mono";
.operation
min-height:100vh;
padding:20px 0px;
display: flex
&> aside,
&> div
min-width:50%;
max-width:50%;
width:50%;
padding-right:40px;
h2
color:#2b303b;
font-size:21px;
h3
font-size:16px;
margin-bottom:20px;
margin-top:40px;
aside
background: rgba(43, 48, 59, 0.95);
box-sizing: border-box;
padding: 20px 0px 10px 0px;
border-radius:2px;
h4
color: #d0d4d7;
font-size:15px;
padding:20px;
padding-left:40px;
pre
background: #2b303b;
color: #d0d4d7;
margin-right:0px;
padding-left:40px;
.params
padding: 0px;
margin: 0px;
list-style-type: none;
border-top: 1px solid #f0f4f7
li
padding:15px 10px;
border-bottom:1px solid #f0f4f7;
font-size:15px
p
line-height:20px;
margin: 0 0 0 170px;
li:after
visibility: hidden;
display: block;
font-size: 0;
content: " ";
clear: both;
height: 0;
h4
float:left;
line-height:20px;
text-align:right;
padding-right:20px;
width:150px;
font-weight:bold;
font-size:15px;
small
display:block;
text-transform: uppercase;
color:#E67E22;
font-size:11px;
font-weight:normal;
margin-top:5px;

View file

@ -15,6 +15,7 @@
@import pages/repo.sass
@import pages/login.sass
@import pages/feed.sass
@import pages/docs.sass
@import header
@import search

View file

@ -289,6 +289,86 @@ body.login div.alert { position: fixed; top: 0px; left: 0px; right: 0px; line-he
.repo-row .card-header { background: #FFF; border-bottom: none; padding-right: 0px; padding-left: 0px; width: 45px; }
.toc { list-style-type: none; padding: 0px; margin: 0px; padding-bottom: 40px; }
.toc h2 { font-size: 21px; font-weight: normal; margin-bottom: 20px; color: #2b303b; }
.toc ul { list-style-type: none; padding: 0px; margin: 0px; margin-bottom: 40px; border-bottom: 1px solid #EEE; padding-bottom: 40px; }
.toc ul li { line-height: 25px; }
.toc ul li a { color: #2b303b; text-decoration: none; }
.toc ul li a:hover { text-decoration: underline; }
.toc [data-method]:before { content: attr(data-method); padding: 0px 10px; line-height: 18px; min-width: 70px; font-size: 11px; text-transform: uppercase; display: inline-block; text-align: center; color: #FFF; border-radius: 2px; margin-right: 20px; }
.toc [data-method="GET"]:before { background-color: #1ABC9C; }
.toc [data-method="PUT"]:before { background-color: #9B59B6; }
.toc [data-method="POST"]:before { background-color: #3498DB; }
.toc [data-method="PATCH"]:before { background-color: #E67E22; }
.toc [data-method="DELETE"]:before { background-color: #E74C3C; }
.toc [data-method]:before { background: #FFF; border: 1px solid #FFF; }
.toc [data-method="GET"]:before { color: #1ABC9C; border-color: #1ABC9C; }
.toc [data-method="PUT"]:before { color: #9B59B6; border-color: #9B59B6; }
.toc [data-method="POST"]:before { color: #3498DB; border-color: #3498DB; }
.toc [data-method="PATCH"]:before { color: #E67E22; border-color: #E67E22; }
.toc [data-method="DELETE"]:before { color: #E74C3C; border-color: #E74C3C; }
.operation [data-method]:before { content: attr(data-method); padding: 0px 10px; line-height: 18px; min-width: 70px; font-size: 11px; text-transform: uppercase; display: inline-block; text-align: center; color: #FFF; border-radius: 2px; margin-right: 20px; background: #FFF; border: 1px solid #FFF; line-height: 20px; vertical-align: top; }
.operation [data-method]:before { background: #FFF; border: 1px solid #FFF; }
.operation [data-method="GET"]:before { color: #1ABC9C; border-color: #1ABC9C; }
.operation [data-method="PUT"]:before { color: #9B59B6; border-color: #9B59B6; }
.operation [data-method="POST"]:before { color: #3498DB; border-color: #3498DB; }
.operation [data-method="PATCH"]:before { color: #E67E22; border-color: #E67E22; }
.operation [data-method="DELETE"]:before { color: #E74C3C; border-color: #E74C3C; }
.docs { margin-top: 40px; padding: 0px 50px; padding-right: 40px; }
.docs pre { margin-right: 15px; font-size: 14px; color: #eff1f5; color: #2b303b; border-radius: 2px; background: #2b303b; background: #ECF0F1; white-space: pre-wrap; word-wrap: break-word; box-sizing: border-box; padding: 25px 30px; font-family: "Roboto Mono"; }
.operation { min-height: 100vh; padding: 20px 0px; display: flex; }
.operation > aside, .operation > div { min-width: 50%; max-width: 50%; width: 50%; padding-right: 40px; }
.operation h2 { color: #2b303b; font-size: 21px; }
.operation h3 { font-size: 16px; margin-bottom: 20px; margin-top: 40px; }
.operation aside { background: rgba(43, 48, 59, 0.95); box-sizing: border-box; padding: 20px 0px 10px 0px; border-radius: 2px; }
.operation aside h4 { color: #d0d4d7; font-size: 15px; padding: 20px; padding-left: 40px; }
.operation aside pre { background: #2b303b; color: #d0d4d7; margin-right: 0px; padding-left: 40px; }
.operation .params { padding: 0px; margin: 0px; list-style-type: none; border-top: 1px solid #f0f4f7; }
.operation .params li { padding: 15px 10px; border-bottom: 1px solid #f0f4f7; font-size: 15px; }
.operation .params li p { line-height: 20px; margin: 0 0 0 170px; }
.operation .params li:after { visibility: hidden; display: block; font-size: 0; content: " "; clear: both; height: 0; }
.operation .params h4 { float: left; line-height: 20px; text-align: right; padding-right: 20px; width: 150px; font-weight: bold; font-size: 15px; }
.operation .params small { display: block; text-transform: uppercase; color: #E67E22; font-size: 11px; font-weight: normal; margin-top: 5px; }
.tt-open { position: absolute; top: 34px; left: 0px; z-index: 100; display: none; background: #FFF; min-width: 100%; border: 1px solid #eee; border-radius: 0px; }
.tt-selectable:hover, .tt-cursor { background: #f4f4f4; }

View file

@ -0,0 +1,65 @@
extends base
block append head
title API · Drone
block header
ol
li Documentation
ul.nav.nav-tabs
li.nav-item
a.nav-link[href="#"] Install
li.nav-item
a.nav-link[href="#"] Builds
li.nav-item
a.nav-link[href="#"] Plugins
li.nav-item
a.nav-link.active[href="#"] API Reference
block content
div.container-fluid.docs.docs-api
a[name="top"]
div.row
ul.toc
each $tag in Swagger.Tags
li
h2 #{$tag.Name}
ul
each $op in $tag.Ops
li
a[href="#"+$op.ID][data-method=$op.Method] #{$op.Path}
div.row
each $tag in Swagger.Tags
each $op in $tag.Ops
a[name=$op.ID]
div.operation
div
h2[data-method=$op.Method] #{$op.Summary}
p #{$op.Desc}
h3 Request Parameters
ul.params
each $param in $op.Params
li
h4
| #{$param.Name}
small Required
p #{$param.Desc}
h3 Response Messages
ul.params
each $result in $op.Results
li
h4
| #{$result.Status}
p #{$result.Desc}
aside
h4 Endpoint
pre #{$op.Method} #{$op.Path}
each $res in $op.Results
if $res.Example
h4 Example Response
if $res.IsArray
pre [#{$res.Example}]
else if $res.IsObject
pre #{$res.Example}