pkg,cmd: add document generator tool

This commit is contained in:
Eric Chiang 2016-01-21 16:21:40 -08:00
parent b5c7f1978e
commit c7ed4fdd60
8 changed files with 1265 additions and 0 deletions

96
cmd/gendoc/main.go Normal file
View 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
View 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
View 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
}

View 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
View 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))
}

View 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
View 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
View 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"
}
}
}
}
}
}