pkg,cmd: add document generator tool
This commit is contained in:
parent
b5c7f1978e
commit
c7ed4fdd60
8 changed files with 1265 additions and 0 deletions
96
cmd/gendoc/main.go
Normal file
96
cmd/gendoc/main.go
Normal file
|
@ -0,0 +1,96 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
|
||||
"github.com/coreos/dex/pkg/gendoc"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var cmd = &cobra.Command{
|
||||
Use: "gendoc",
|
||||
Short: "Generate documentation from REST specifications.",
|
||||
Long: `A tool to generate documentation for dex's REST APIs.`,
|
||||
RunE: gen,
|
||||
}
|
||||
|
||||
var (
|
||||
infile string
|
||||
outfile string
|
||||
readFlavor string
|
||||
writeFlavor string
|
||||
)
|
||||
|
||||
func init() {
|
||||
cmd.PersistentFlags().StringVar(&infile, "f", "", "File to read from. If ommitted read from stdin.")
|
||||
cmd.PersistentFlags().StringVar(&outfile, "o", "", "File to write to. If ommitted write to stdout.")
|
||||
cmd.PersistentFlags().StringVar(&readFlavor, "r", "googleapi", "Flavor of REST spec to read. Currently only supports 'googleapi'.")
|
||||
cmd.PersistentFlags().StringVar(&writeFlavor, "w", "markdown", "Flavor of documentation. Currently only supports 'markdown'.")
|
||||
|
||||
}
|
||||
|
||||
func main() {
|
||||
if err := cmd.Execute(); err != nil {
|
||||
fmt.Fprintln(os.Stderr, err)
|
||||
os.Exit(2)
|
||||
}
|
||||
}
|
||||
|
||||
func gen(cmd *cobra.Command, args []string) error {
|
||||
var (
|
||||
in io.Reader
|
||||
out io.Writer
|
||||
decode func(io.Reader) (gendoc.Document, error)
|
||||
encode func(gendoc.Document) ([]byte, error)
|
||||
)
|
||||
|
||||
switch readFlavor {
|
||||
case "googleapi":
|
||||
decode = gendoc.ParseGoogleAPI
|
||||
default:
|
||||
return fmt.Errorf("unsupported read flavor %q", readFlavor)
|
||||
}
|
||||
|
||||
switch writeFlavor {
|
||||
case "markdown":
|
||||
encode = gendoc.Document.MarshalMarkdown
|
||||
default:
|
||||
return fmt.Errorf("unsupported write flavor %q", writeFlavor)
|
||||
}
|
||||
|
||||
if infile == "" {
|
||||
in = os.Stdin
|
||||
} else {
|
||||
f, err := os.OpenFile(infile, os.O_RDONLY, 0644)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
in = f
|
||||
}
|
||||
|
||||
if outfile == "" {
|
||||
out = os.Stdout
|
||||
} else {
|
||||
f, err := os.OpenFile(outfile, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
out = f
|
||||
}
|
||||
|
||||
doc, err := decode(in)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to decode input: %v", err)
|
||||
}
|
||||
data, err := encode(doc)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to encode document: %v", err)
|
||||
}
|
||||
|
||||
_, err = out.Write(data)
|
||||
return err
|
||||
}
|
68
pkg/gendoc/gendoc.go
Normal file
68
pkg/gendoc/gendoc.go
Normal file
|
@ -0,0 +1,68 @@
|
|||
package gendoc
|
||||
|
||||
type Document struct {
|
||||
Title string
|
||||
Description string
|
||||
Version string
|
||||
Paths []Path
|
||||
Models []Schema
|
||||
}
|
||||
|
||||
type Path struct {
|
||||
Method string
|
||||
Path string
|
||||
Summary string
|
||||
Description string
|
||||
Parameters []Parameter
|
||||
Responses []Response
|
||||
}
|
||||
|
||||
type byPath []Path
|
||||
|
||||
func (p byPath) Len() int { return len(p) }
|
||||
func (p byPath) Swap(i, j int) { p[i], p[j] = p[j], p[i] }
|
||||
func (p byPath) Less(i, j int) bool {
|
||||
if p[i].Path == p[j].Path {
|
||||
return p[i].Method < p[j].Method
|
||||
}
|
||||
return p[i].Path < p[j].Path
|
||||
}
|
||||
|
||||
type Parameter struct {
|
||||
Name string
|
||||
LocatedIn string
|
||||
Description string
|
||||
Required bool
|
||||
Type string
|
||||
}
|
||||
|
||||
const (
|
||||
TypeArray = "array"
|
||||
TypeBool = "boolean"
|
||||
TypeFloat = "float"
|
||||
TypeInt = "integer"
|
||||
TypeObject = "object"
|
||||
TypeString = "string"
|
||||
)
|
||||
|
||||
type Schema struct {
|
||||
Name string
|
||||
Type string
|
||||
Description string
|
||||
Children []Schema
|
||||
Ref string
|
||||
}
|
||||
|
||||
type byName []Schema
|
||||
|
||||
func (n byName) Len() int { return len(n) }
|
||||
func (n byName) Swap(i, j int) { n[i], n[j] = n[j], n[i] }
|
||||
func (n byName) Less(i, j int) bool { return n[i].Name < n[j].Name }
|
||||
|
||||
const CodeDefault = 0
|
||||
|
||||
type Response struct {
|
||||
Code int // 0 means "Default"
|
||||
Description string
|
||||
Type string
|
||||
}
|
143
pkg/gendoc/googleapi.go
Normal file
143
pkg/gendoc/googleapi.go
Normal file
|
@ -0,0 +1,143 @@
|
|||
// Package gendoc generates documentation for REST APIs.
|
||||
package gendoc
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io"
|
||||
"path"
|
||||
"sort"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func ParseGoogleAPI(r io.Reader) (Document, error) {
|
||||
var d doc
|
||||
if err := json.NewDecoder(r).Decode(&d); err != nil {
|
||||
return Document{}, err
|
||||
}
|
||||
return d.toDocument(), nil
|
||||
}
|
||||
|
||||
// doc represents a Google API specification document. It is NOT intended to encompass all
|
||||
// options provided by the spec, only the minimal fields needed to convert dex's API
|
||||
// definitions into documentation.
|
||||
type doc struct {
|
||||
Name string `json:"name"`
|
||||
Version string `json:"version"`
|
||||
Title string `json:"title"`
|
||||
Description string `json:"description"`
|
||||
DocumentationLink string `json:"documentationLink"`
|
||||
Protocol string `json:"protocol"`
|
||||
BasePath string `json:"basePath"`
|
||||
Schemas map[string]schema `json:"schemas"`
|
||||
Resources map[string]methods `json:"resources"`
|
||||
}
|
||||
|
||||
type methods struct {
|
||||
Methods map[string]resource `json:"methods"`
|
||||
}
|
||||
|
||||
type param struct {
|
||||
Type string `json:"type"`
|
||||
Required bool `json:"required"`
|
||||
Location string `json:"location"`
|
||||
}
|
||||
|
||||
type schema struct {
|
||||
ID string `json:"id"`
|
||||
Type string `json:"type"`
|
||||
Description string `json:"description"`
|
||||
Items *schema `json:"items"`
|
||||
Format string `json:"format"`
|
||||
Properties map[string]schema
|
||||
Ref string `json:"$ref"`
|
||||
}
|
||||
|
||||
type resource struct {
|
||||
Description string `json:"description"`
|
||||
Method string `json:"httpMethod"`
|
||||
Path string `json:"path"`
|
||||
Parameters map[string]param `json:"parameters"`
|
||||
Request *ref `json:"request"`
|
||||
Response *ref `json:"response"`
|
||||
}
|
||||
|
||||
type ref struct {
|
||||
Ref string `json:"$ref"`
|
||||
}
|
||||
|
||||
func (d doc) toDocument() Document {
|
||||
gDoc := Document{
|
||||
Title: d.Title,
|
||||
Description: d.Description,
|
||||
Version: d.Version,
|
||||
}
|
||||
for name, s := range d.Schemas {
|
||||
s.ID = name
|
||||
gDoc.Models = append(gDoc.Models, s.toSchema())
|
||||
}
|
||||
|
||||
for object, methods := range d.Resources {
|
||||
for action, r := range methods.Methods {
|
||||
gDoc.Paths = append(gDoc.Paths, r.toPath(d, object, action))
|
||||
}
|
||||
}
|
||||
|
||||
sort.Sort(byPath(gDoc.Paths))
|
||||
sort.Sort(byName(gDoc.Models))
|
||||
return gDoc
|
||||
}
|
||||
|
||||
func (s schema) toSchema() Schema {
|
||||
sch := Schema{
|
||||
Name: s.ID,
|
||||
Type: s.Type,
|
||||
Description: s.Description,
|
||||
Ref: s.Ref,
|
||||
}
|
||||
for name, prop := range s.Properties {
|
||||
c := prop.toSchema()
|
||||
c.Name = name
|
||||
sch.Children = append(sch.Children, c)
|
||||
}
|
||||
if s.Items != nil {
|
||||
sch.Children = []Schema{s.Items.toSchema()}
|
||||
}
|
||||
sort.Sort(byName(sch.Children))
|
||||
return sch
|
||||
}
|
||||
|
||||
func (r resource) toPath(d doc, object, action string) Path {
|
||||
p := Path{
|
||||
Method: r.Method,
|
||||
Path: path.Join("/", r.Path),
|
||||
Summary: strings.TrimSpace(action + " " + object),
|
||||
Description: r.Description,
|
||||
}
|
||||
for name, param := range r.Parameters {
|
||||
p.Parameters = append(p.Parameters, Parameter{
|
||||
Name: name,
|
||||
LocatedIn: param.Location,
|
||||
Required: param.Required,
|
||||
Type: param.Type,
|
||||
})
|
||||
}
|
||||
if r.Request != nil {
|
||||
ref := r.Request.Ref
|
||||
p.Parameters = append(p.Parameters, Parameter{
|
||||
LocatedIn: "body",
|
||||
Required: true,
|
||||
Type: ref,
|
||||
})
|
||||
}
|
||||
if r.Response != nil {
|
||||
p.Responses = append(p.Responses, Response{
|
||||
Code: 200,
|
||||
Type: r.Response.Ref,
|
||||
})
|
||||
}
|
||||
p.Responses = append(p.Responses, Response{
|
||||
Code: CodeDefault,
|
||||
Description: "Unexpected error",
|
||||
})
|
||||
return p
|
||||
}
|
333
pkg/gendoc/googleapi_test.go
Normal file
333
pkg/gendoc/googleapi_test.go
Normal file
|
@ -0,0 +1,333 @@
|
|||
package gendoc
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io/ioutil"
|
||||
"testing"
|
||||
|
||||
"github.com/kylelemons/godebug/pretty"
|
||||
)
|
||||
|
||||
func TestToSchema(t *testing.T) {
|
||||
tests := []struct {
|
||||
s schema
|
||||
want Schema
|
||||
}{
|
||||
{
|
||||
s: schema{
|
||||
ID: "UsersResponse",
|
||||
Type: "object",
|
||||
Properties: map[string]schema{
|
||||
"users": {
|
||||
Type: "array",
|
||||
Items: &schema{
|
||||
Ref: "User",
|
||||
},
|
||||
},
|
||||
"nextPageToken": {Type: "string"},
|
||||
},
|
||||
},
|
||||
want: Schema{
|
||||
Name: "UsersResponse",
|
||||
Type: "object",
|
||||
Children: []Schema{
|
||||
{
|
||||
Name: "nextPageToken",
|
||||
Type: "string",
|
||||
},
|
||||
{
|
||||
Name: "users",
|
||||
Type: "array",
|
||||
Children: []Schema{
|
||||
{
|
||||
Ref: "User",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
s: schema{
|
||||
ID: "UserCreateResponse",
|
||||
Type: "object",
|
||||
Properties: map[string]schema{
|
||||
"user": {
|
||||
Type: "object",
|
||||
Ref: "User",
|
||||
},
|
||||
"resetPasswordLink": {Type: "string"},
|
||||
"emailSent": {Type: "boolean"},
|
||||
},
|
||||
},
|
||||
want: Schema{
|
||||
Name: "UserCreateResponse",
|
||||
Type: "object",
|
||||
Children: []Schema{
|
||||
{
|
||||
Name: "emailSent",
|
||||
Type: "boolean",
|
||||
},
|
||||
{
|
||||
Name: "resetPasswordLink",
|
||||
Type: "string",
|
||||
},
|
||||
{
|
||||
Name: "user",
|
||||
Type: "object",
|
||||
Ref: "User",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for i, tt := range tests {
|
||||
got := tt.s.toSchema()
|
||||
if diff := pretty.Compare(got, tt.want); diff != "" {
|
||||
t.Errorf("case %d: got != want: %s", i, diff)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestUnmarsal(t *testing.T) {
|
||||
tests := []struct {
|
||||
file string
|
||||
want doc
|
||||
}{
|
||||
{
|
||||
file: "testdata/admin.json",
|
||||
want: doc{
|
||||
Name: "adminschema",
|
||||
Version: "v1",
|
||||
Title: "Dex Admin API",
|
||||
Description: "The Dex Admin API.",
|
||||
DocumentationLink: "http://github.com/coreos/dex",
|
||||
Protocol: "rest",
|
||||
BasePath: "/api/v1/",
|
||||
Schemas: map[string]schema{
|
||||
"Admin": schema{
|
||||
ID: "Admin",
|
||||
Type: "object",
|
||||
Description: "Admin represents an admin user within the database",
|
||||
Properties: map[string]schema{
|
||||
"id": {Type: "string"},
|
||||
"email": {Type: "string"},
|
||||
"password": {Type: "string"},
|
||||
},
|
||||
},
|
||||
"State": schema{
|
||||
ID: "State",
|
||||
Type: "object",
|
||||
Description: "Admin represents dex data within.",
|
||||
Properties: map[string]schema{
|
||||
"AdminUserCreated": {Type: "boolean"},
|
||||
},
|
||||
},
|
||||
},
|
||||
Resources: map[string]methods{
|
||||
"Admin": methods{
|
||||
Methods: map[string]resource{
|
||||
"Get": resource{
|
||||
Description: "Retrieve information about an admin user.",
|
||||
Method: "GET",
|
||||
Path: "admin/{id}",
|
||||
Parameters: map[string]param{
|
||||
"id": param{
|
||||
Type: "string",
|
||||
Required: true,
|
||||
Location: "path",
|
||||
},
|
||||
},
|
||||
Response: &ref{"Admin"},
|
||||
},
|
||||
"Create": resource{
|
||||
Description: "Create a new admin user.",
|
||||
Method: "POST",
|
||||
Path: "admin",
|
||||
Request: &ref{"Admin"},
|
||||
Response: &ref{"Admin"},
|
||||
},
|
||||
},
|
||||
},
|
||||
"State": methods{
|
||||
Methods: map[string]resource{
|
||||
"Get": resource{
|
||||
Description: "Get the state of the Dex DB",
|
||||
Method: "GET",
|
||||
Path: "state",
|
||||
Response: &ref{"State"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
data, err := ioutil.ReadFile(tt.file)
|
||||
if err != nil {
|
||||
t.Errorf("case %q: read file failed %v", tt.file, err)
|
||||
continue
|
||||
}
|
||||
var d doc
|
||||
if err := json.Unmarshal(data, &d); err != nil {
|
||||
t.Errorf("case %q: failed to unmarshal %v", tt.file, err)
|
||||
continue
|
||||
}
|
||||
if diff := pretty.Compare(d, tt.want); diff != "" {
|
||||
t.Errorf("case %q: got did not match want: %s", tt.file, diff)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestUnmarshalSchema(t *testing.T) {
|
||||
tests := []struct {
|
||||
file string
|
||||
want map[string]schema
|
||||
}{
|
||||
{
|
||||
file: "testdata/worker.json",
|
||||
want: map[string]schema{
|
||||
"Error": schema{
|
||||
ID: "Error",
|
||||
Type: "object",
|
||||
Properties: map[string]schema{
|
||||
"error": {Type: "string"},
|
||||
"error_description": {Type: "string"},
|
||||
},
|
||||
},
|
||||
"Client": schema{
|
||||
ID: "Client",
|
||||
Type: "object",
|
||||
Properties: map[string]schema{
|
||||
"id": {Type: "string"},
|
||||
"redirectURIs": {
|
||||
Type: "array",
|
||||
Items: &schema{Type: "string"},
|
||||
},
|
||||
},
|
||||
},
|
||||
"ClientWithSecret": schema{
|
||||
ID: "Client",
|
||||
Type: "object",
|
||||
Properties: map[string]schema{
|
||||
"id": {Type: "string"},
|
||||
"secret": {Type: "string"},
|
||||
"redirectURIs": {
|
||||
Type: "array",
|
||||
Items: &schema{Type: "string"},
|
||||
},
|
||||
},
|
||||
},
|
||||
"ClientPage": schema{
|
||||
ID: "ClientPage",
|
||||
Type: "object",
|
||||
Properties: map[string]schema{
|
||||
"clients": {
|
||||
Type: "array",
|
||||
Items: &schema{
|
||||
Ref: "Client",
|
||||
},
|
||||
},
|
||||
"nextPageToken": {
|
||||
Type: "string",
|
||||
},
|
||||
},
|
||||
},
|
||||
"User": schema{
|
||||
ID: "User",
|
||||
Type: "object",
|
||||
Properties: map[string]schema{
|
||||
"id": {Type: "string"},
|
||||
"email": {Type: "string"},
|
||||
"displayName": {Type: "string"},
|
||||
"emailVerified": {Type: "boolean"},
|
||||
"admin": {Type: "boolean"},
|
||||
"disabled": {Type: "boolean"},
|
||||
"createdAt": {
|
||||
Type: "string",
|
||||
Format: "date-time",
|
||||
},
|
||||
},
|
||||
},
|
||||
"UserResponse": schema{
|
||||
ID: "UserResponse",
|
||||
Type: "object",
|
||||
Properties: map[string]schema{
|
||||
"user": {Ref: "User"},
|
||||
},
|
||||
},
|
||||
"UsersResponse": schema{
|
||||
ID: "UsersResponse",
|
||||
Type: "object",
|
||||
Properties: map[string]schema{
|
||||
"users": {
|
||||
Type: "array",
|
||||
Items: &schema{
|
||||
Ref: "User",
|
||||
},
|
||||
},
|
||||
"nextPageToken": {Type: "string"},
|
||||
},
|
||||
},
|
||||
"UserCreateRequest": schema{
|
||||
ID: "UserCreateRequest",
|
||||
Type: "object",
|
||||
Properties: map[string]schema{
|
||||
"user": {
|
||||
Ref: "User",
|
||||
},
|
||||
"redirectURL": {Type: "string", Format: "url"},
|
||||
},
|
||||
},
|
||||
"UserCreateResponse": schema{
|
||||
ID: "UserCreateResponse",
|
||||
Type: "object",
|
||||
Properties: map[string]schema{
|
||||
"user": {
|
||||
Type: "object",
|
||||
Ref: "User",
|
||||
},
|
||||
"resetPasswordLink": {Type: "string"},
|
||||
"emailSent": {Type: "boolean"},
|
||||
},
|
||||
},
|
||||
"UserDisableRequest": schema{
|
||||
ID: "UserDisableRequest",
|
||||
Type: "object",
|
||||
Properties: map[string]schema{
|
||||
"disable": {
|
||||
Type: "boolean",
|
||||
Description: "If true, disable this user, if false, enable them. No error is signaled if the user state doesn't change.",
|
||||
},
|
||||
},
|
||||
},
|
||||
"UserDisableResponse": schema{
|
||||
ID: "UserDisableResponse",
|
||||
Type: "object",
|
||||
Properties: map[string]schema{
|
||||
"ok": {Type: "boolean"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
data, err := ioutil.ReadFile(tt.file)
|
||||
if err != nil {
|
||||
t.Errorf("case %q: read file failed %v", tt.file, err)
|
||||
continue
|
||||
}
|
||||
var d doc
|
||||
if err := json.Unmarshal(data, &d); err != nil {
|
||||
t.Errorf("case %q: failed to unmarshal %v", tt.file, err)
|
||||
continue
|
||||
}
|
||||
if diff := pretty.Compare(d.Schemas, tt.want); diff != "" {
|
||||
t.Errorf("case %q: got did not match want: %s", tt.file, diff)
|
||||
}
|
||||
}
|
||||
}
|
159
pkg/gendoc/markdown.go
Normal file
159
pkg/gendoc/markdown.go
Normal file
|
@ -0,0 +1,159 @@
|
|||
package gendoc
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"text/template"
|
||||
"unicode"
|
||||
)
|
||||
|
||||
var funcs = template.FuncMap{
|
||||
"renderJSON": func(i interface{}) string {
|
||||
if s, ok := i.(Schema); ok {
|
||||
return s.toJSON()
|
||||
}
|
||||
return ""
|
||||
},
|
||||
"toLink": toLink,
|
||||
"toCodeStr": func(code int) string {
|
||||
if code == CodeDefault {
|
||||
return "default"
|
||||
}
|
||||
return strconv.Itoa(code)
|
||||
},
|
||||
}
|
||||
|
||||
var markdownTmpl = template.Must(template.New("md").Funcs(funcs).Parse(`
|
||||
# {{ .Title }}
|
||||
|
||||
{{ .Description }}
|
||||
|
||||
__Version:__ {{ .Version }}
|
||||
|
||||
## Models
|
||||
|
||||
{{ range $i, $model := .Models }}
|
||||
### {{ $model.Name }}
|
||||
|
||||
{{ $model.Description }}
|
||||
|
||||
{{ $model | renderJSON }}
|
||||
{{ end }}
|
||||
|
||||
## Paths
|
||||
|
||||
{{ range $i, $path := .Paths }}
|
||||
### {{ $path.Method }} {{ $path.Path }}
|
||||
|
||||
> __Summary__
|
||||
|
||||
> {{ $path.Summary }}
|
||||
|
||||
> __Description__
|
||||
|
||||
> {{ $path.Description }}
|
||||
|
||||
{{ if $path.Parameters }}
|
||||
> __Parameters__
|
||||
|
||||
> |Name|Located in|Description|Required|Type|
|
||||
|:-----|:-----|:-----|:-----|:-----|
|
||||
{{ range $i, $p := $path.Parameters }}| {{ $p.Name }} | {{ $p.LocatedIn }} | {{ $p.Description }} | {{ if $p.Required }}Yes{{ else }}No{{ end }} | {{ $p.Type | toLink }} |
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
> __Responses__
|
||||
|
||||
> |Code|Description|Type|
|
||||
|:-----|:-----|:-----|
|
||||
{{ range $i, $r := $path.Responses }}| {{ $r.Code | toCodeStr }} | {{ $r.Description }} | {{ $r.Type | toLink }} |
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
`))
|
||||
|
||||
func (doc Document) MarshalMarkdown() ([]byte, error) {
|
||||
var b bytes.Buffer
|
||||
if err := markdownTmpl.Execute(&b, doc); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return b.Bytes(), nil
|
||||
}
|
||||
|
||||
func (m Schema) toJSON() string {
|
||||
var b bytes.Buffer
|
||||
b.WriteString("```\n")
|
||||
m.writeJSON(&b, "", true)
|
||||
b.WriteString("\n```")
|
||||
return b.String()
|
||||
}
|
||||
|
||||
var indentStr = " "
|
||||
|
||||
func (m Schema) writeJSON(b *bytes.Buffer, indent string, first bool) {
|
||||
if m.Ref != "" {
|
||||
b.WriteString(m.Ref)
|
||||
return
|
||||
}
|
||||
if first {
|
||||
b.WriteString(indent)
|
||||
}
|
||||
|
||||
switch m.Type {
|
||||
case TypeArray:
|
||||
b.WriteString("[")
|
||||
for i, c := range m.Children {
|
||||
if i > 0 {
|
||||
b.WriteString(",")
|
||||
}
|
||||
b.WriteString("\n")
|
||||
b.WriteString(indent + indentStr)
|
||||
c.writeJSON(b, indent+indentStr, false)
|
||||
}
|
||||
b.WriteString("\n" + indent + "]")
|
||||
case TypeObject:
|
||||
b.WriteString("{")
|
||||
for i, c := range m.Children {
|
||||
if i > 0 {
|
||||
b.WriteString(",")
|
||||
}
|
||||
b.WriteString("\n")
|
||||
b.WriteString(indent + indentStr + c.Name + ": ")
|
||||
c.writeJSON(b, indent+indentStr, false)
|
||||
}
|
||||
b.WriteString("\n" + indent + "}")
|
||||
case TypeBool, TypeFloat, TypeInt, TypeString:
|
||||
b.WriteString(m.Type)
|
||||
if m.Description != "" {
|
||||
b.WriteString(" // ")
|
||||
b.WriteString(m.Description)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func toAnchor(s string) string {
|
||||
var b bytes.Buffer
|
||||
r := strings.NewReader(s)
|
||||
for {
|
||||
r, _, err := r.ReadRune()
|
||||
if err != nil {
|
||||
return b.String()
|
||||
}
|
||||
switch {
|
||||
case r == ' ':
|
||||
b.WriteRune('-')
|
||||
case 'a' <= r && r <= 'z':
|
||||
b.WriteRune(r)
|
||||
case 'A' <= r && r <= 'Z':
|
||||
b.WriteRune(unicode.ToLower(r))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func toLink(s string) string {
|
||||
switch s {
|
||||
case "string", "boolean", "integer", "":
|
||||
return s
|
||||
}
|
||||
return fmt.Sprintf("[%s](#%s)", s, toAnchor(s))
|
||||
}
|
68
pkg/gendoc/markdown_test.go
Normal file
68
pkg/gendoc/markdown_test.go
Normal file
|
@ -0,0 +1,68 @@
|
|||
package gendoc
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/kylelemons/godebug/diff"
|
||||
)
|
||||
|
||||
func TestToAnchor(t *testing.T) {
|
||||
tests := []struct {
|
||||
s string
|
||||
want string
|
||||
}{
|
||||
{"foo", "foo"},
|
||||
{"foo bar", "foo-bar"},
|
||||
{"POST /foo/{id}", "post-fooid"},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
if got := toAnchor(tt.s); got != tt.want {
|
||||
t.Errorf("toAnchor(%q): want=%q, got=%q", tt.s, tt.want, got)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestToJSON(t *testing.T) {
|
||||
tests := []struct {
|
||||
s Schema
|
||||
want string
|
||||
}{
|
||||
{
|
||||
s: Schema{
|
||||
Name: "UsersResponse",
|
||||
Type: "object",
|
||||
Children: []Schema{
|
||||
{
|
||||
Name: "nextPageToken",
|
||||
Type: "string",
|
||||
},
|
||||
{
|
||||
Name: "users",
|
||||
Type: "array",
|
||||
Children: []Schema{
|
||||
{
|
||||
Ref: "User",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
want: "```" + `
|
||||
{
|
||||
nextPageToken: string,
|
||||
users: [
|
||||
User
|
||||
]
|
||||
}
|
||||
` + "```",
|
||||
},
|
||||
}
|
||||
|
||||
for i, tt := range tests {
|
||||
got := tt.s.toJSON()
|
||||
if d := diff.Diff(got, tt.want); d != "" {
|
||||
t.Errorf("case %d: want != got: %s", i, d)
|
||||
}
|
||||
}
|
||||
}
|
102
pkg/gendoc/testdata/admin.json
vendored
Normal file
102
pkg/gendoc/testdata/admin.json
vendored
Normal file
|
@ -0,0 +1,102 @@
|
|||
{
|
||||
"kind": "discovery#restDescription",
|
||||
"discoveryVersion": "v1",
|
||||
"id": "dex:v1",
|
||||
"name": "adminschema",
|
||||
"version": "v1",
|
||||
"title": "Dex Admin API",
|
||||
"description": "The Dex Admin API.",
|
||||
"documentationLink": "http://github.com/coreos/dex",
|
||||
"protocol": "rest",
|
||||
"icons": {
|
||||
"x16": "",
|
||||
"x32": ""
|
||||
},
|
||||
"labels": [],
|
||||
"baseUrl": "$ENDPOINT/api/v1/",
|
||||
"basePath": "/api/v1/",
|
||||
"rootUrl": "$ENDPOINT/",
|
||||
"servicePath": "api/v1/",
|
||||
"batchPath": "batch",
|
||||
"parameters": {},
|
||||
"auth": {},
|
||||
"schemas": {
|
||||
"Admin": {
|
||||
"id": "Admin",
|
||||
"type": "object",
|
||||
"description": "Admin represents an admin user within the database",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"email": {
|
||||
"type": "string"
|
||||
},
|
||||
"password": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"State": {
|
||||
"id": "State",
|
||||
"type": "object",
|
||||
"description": "Admin represents dex data within.",
|
||||
"properties": {
|
||||
"AdminUserCreated": {
|
||||
"type": "boolean"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"resources": {
|
||||
"Admin": {
|
||||
"methods": {
|
||||
"Get": {
|
||||
"id": "dex.admin.Admin.Get",
|
||||
"description": "Retrieve information about an admin user.",
|
||||
"httpMethod": "GET",
|
||||
"path": "admin/{id}",
|
||||
"parameters": {
|
||||
"id": {
|
||||
"type": "string",
|
||||
"required": true,
|
||||
"location": "path"
|
||||
}
|
||||
},
|
||||
"parameterOrder": [
|
||||
"id"
|
||||
],
|
||||
"response": {
|
||||
"$ref": "Admin"
|
||||
}
|
||||
|
||||
},
|
||||
"Create": {
|
||||
"id": "dex.admin.Admin.Create",
|
||||
"description": "Create a new admin user.",
|
||||
"httpMethod": "POST",
|
||||
"path": "admin",
|
||||
"request": {
|
||||
"$ref": "Admin"
|
||||
},
|
||||
"response": {
|
||||
"$ref": "Admin"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"State": {
|
||||
"methods": {
|
||||
"Get": {
|
||||
"id": "dex.admin.State.Get",
|
||||
"description": "Get the state of the Dex DB",
|
||||
"httpMethod": "GET",
|
||||
"path": "state",
|
||||
"response": {
|
||||
"$ref": "State"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
296
pkg/gendoc/testdata/worker.json
vendored
Normal file
296
pkg/gendoc/testdata/worker.json
vendored
Normal file
|
@ -0,0 +1,296 @@
|
|||
{
|
||||
"kind": "discovery#restDescription",
|
||||
"discoveryVersion": "v1",
|
||||
"id": "dex:v1",
|
||||
"name": "workerschema",
|
||||
"version": "v1",
|
||||
"title": "Dex API",
|
||||
"description": "The Dex REST API",
|
||||
"documentationLink": "http://github.com/coreos/dex",
|
||||
"protocol": "rest",
|
||||
"icons": {
|
||||
"x16": "",
|
||||
"x32": ""
|
||||
},
|
||||
"labels": [],
|
||||
"baseUrl": "$ENDPOINT/api/v1/",
|
||||
"basePath": "/api/v1/",
|
||||
"rootUrl": "$ENDPOINT/",
|
||||
"servicePath": "api/v1/",
|
||||
"batchPath": "batch",
|
||||
"parameters": {},
|
||||
"auth": {},
|
||||
"schemas": {
|
||||
"Error": {
|
||||
"id": "Error",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"error": {
|
||||
"type": "string"
|
||||
},
|
||||
"error_description": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"Client": {
|
||||
"id": "Client",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"redirectURIs": {
|
||||
"required": true,
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"ClientWithSecret": {
|
||||
"id": "Client",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"secret": {
|
||||
"type": "string"
|
||||
},
|
||||
"redirectURIs": {
|
||||
"required": true,
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"ClientPage": {
|
||||
"id": "ClientPage",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"clients": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "Client"
|
||||
}
|
||||
},
|
||||
"nextPageToken": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"User": {
|
||||
"id": "User",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"email": {
|
||||
"type": "string"
|
||||
},
|
||||
"displayName": {
|
||||
"type": "string"
|
||||
},
|
||||
"emailVerified": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"admin": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"disabled": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"createdAt": {
|
||||
"type": "string",
|
||||
"format": "date-time"
|
||||
}
|
||||
}
|
||||
},
|
||||
"UserResponse": {
|
||||
"id": "UserResponse",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"user": {
|
||||
"$ref": "User"
|
||||
}
|
||||
}
|
||||
},
|
||||
"UsersResponse": {
|
||||
"id": "UsersResponse",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"users": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "User"
|
||||
}
|
||||
},
|
||||
"nextPageToken": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"UserCreateRequest": {
|
||||
"id": "UserCreateRequest",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"user": {
|
||||
"$ref": "User"
|
||||
},
|
||||
"redirectURL": {
|
||||
"type": "string",
|
||||
"format": "url"
|
||||
}
|
||||
}
|
||||
},
|
||||
"UserCreateResponse": {
|
||||
"id": "UserCreateResponse",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"user": {
|
||||
"type": "object",
|
||||
"$ref": "User"
|
||||
},
|
||||
"resetPasswordLink": {
|
||||
"type": "string"
|
||||
},
|
||||
"emailSent": {
|
||||
"type": "boolean"
|
||||
}
|
||||
}
|
||||
},
|
||||
"UserDisableRequest": {
|
||||
"id": "UserDisableRequest",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"disable": {
|
||||
"type": "boolean",
|
||||
"description": "If true, disable this user, if false, enable them. No error is signaled if the user state doesn't change."
|
||||
}
|
||||
}
|
||||
},
|
||||
"UserDisableResponse": {
|
||||
"id": "UserDisableResponse",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"ok": {
|
||||
"type": "boolean"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"resources": {
|
||||
"Clients": {
|
||||
"methods": {
|
||||
"List": {
|
||||
"id": "dex.Client.List",
|
||||
"description": "Retrieve a page of Client objects.",
|
||||
"httpMethod": "GET",
|
||||
"path": "clients",
|
||||
"parameters": {
|
||||
"nextPageToken": {
|
||||
"type": "string",
|
||||
"location": "query"
|
||||
}
|
||||
},
|
||||
"response": {
|
||||
"$ref": "ClientPage"
|
||||
}
|
||||
},
|
||||
"Create": {
|
||||
"id": "dex.Client.Create",
|
||||
"description": "Register a new Client.",
|
||||
"httpMethod": "POST",
|
||||
"path": "clients",
|
||||
"request": {
|
||||
"$ref": "Client"
|
||||
},
|
||||
"response": {
|
||||
"$ref": "ClientWithSecret"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Users": {
|
||||
"methods": {
|
||||
"List": {
|
||||
"id": "dex.User.List",
|
||||
"description": "Retrieve a page of User objects.",
|
||||
"httpMethod": "GET",
|
||||
"path": "users",
|
||||
"parameters": {
|
||||
"nextPageToken": {
|
||||
"type": "string",
|
||||
"location": "query"
|
||||
},
|
||||
"maxResults": {
|
||||
"type": "integer",
|
||||
"location": "query"
|
||||
}
|
||||
},
|
||||
"response": {
|
||||
"$ref": "UsersResponse"
|
||||
}
|
||||
},
|
||||
"Get": {
|
||||
"id": "dex.User.Get",
|
||||
"description": "Get a single User object by id.",
|
||||
"httpMethod": "GET",
|
||||
"path": "users/{id}",
|
||||
"parameters": {
|
||||
"id": {
|
||||
"type": "string",
|
||||
"required": true,
|
||||
"location": "path"
|
||||
}
|
||||
},
|
||||
"parameterOrder": [
|
||||
"id"
|
||||
],
|
||||
"response": {
|
||||
"$ref": "UserResponse"
|
||||
}
|
||||
},
|
||||
"Create": {
|
||||
"id": "dex.User.Create",
|
||||
"description": "Create a new User.",
|
||||
"httpMethod": "POST",
|
||||
"path": "users",
|
||||
"request": {
|
||||
"$ref": "UserCreateRequest"
|
||||
},
|
||||
"response": {
|
||||
"$ref": "UserCreateResponse"
|
||||
}
|
||||
},
|
||||
"Disable": {
|
||||
"id": "dex.User.Disable",
|
||||
"description": "Enable or disable a user.",
|
||||
"httpMethod": "POST",
|
||||
"path": "users/{id}/disable",
|
||||
"parameters": {
|
||||
"id": {
|
||||
"type": "string",
|
||||
"required": true,
|
||||
"location": "path"
|
||||
}
|
||||
},
|
||||
"parameterOrder": [
|
||||
"id"
|
||||
],
|
||||
"request": {
|
||||
"$ref": "UserDisableRequest"
|
||||
},
|
||||
"response": {
|
||||
"$ref": "UserDisableResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
Reference in a new issue