From c7ed4fdd60736db631badd21223b258fd0ad5cad Mon Sep 17 00:00:00 2001 From: Eric Chiang Date: Thu, 21 Jan 2016 16:21:40 -0800 Subject: [PATCH 1/2] pkg,cmd: add document generator tool --- cmd/gendoc/main.go | 96 +++++++++ pkg/gendoc/gendoc.go | 68 +++++++ pkg/gendoc/googleapi.go | 143 ++++++++++++++ pkg/gendoc/googleapi_test.go | 333 ++++++++++++++++++++++++++++++++ pkg/gendoc/markdown.go | 159 +++++++++++++++ pkg/gendoc/markdown_test.go | 68 +++++++ pkg/gendoc/testdata/admin.json | 102 ++++++++++ pkg/gendoc/testdata/worker.json | 296 ++++++++++++++++++++++++++++ 8 files changed, 1265 insertions(+) create mode 100644 cmd/gendoc/main.go create mode 100644 pkg/gendoc/gendoc.go create mode 100644 pkg/gendoc/googleapi.go create mode 100644 pkg/gendoc/googleapi_test.go create mode 100644 pkg/gendoc/markdown.go create mode 100644 pkg/gendoc/markdown_test.go create mode 100644 pkg/gendoc/testdata/admin.json create mode 100644 pkg/gendoc/testdata/worker.json diff --git a/cmd/gendoc/main.go b/cmd/gendoc/main.go new file mode 100644 index 00000000..ff51229c --- /dev/null +++ b/cmd/gendoc/main.go @@ -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 +} diff --git a/pkg/gendoc/gendoc.go b/pkg/gendoc/gendoc.go new file mode 100644 index 00000000..b205f49f --- /dev/null +++ b/pkg/gendoc/gendoc.go @@ -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 +} diff --git a/pkg/gendoc/googleapi.go b/pkg/gendoc/googleapi.go new file mode 100644 index 00000000..58ea732b --- /dev/null +++ b/pkg/gendoc/googleapi.go @@ -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 +} diff --git a/pkg/gendoc/googleapi_test.go b/pkg/gendoc/googleapi_test.go new file mode 100644 index 00000000..a8b75b73 --- /dev/null +++ b/pkg/gendoc/googleapi_test.go @@ -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) + } + } +} diff --git a/pkg/gendoc/markdown.go b/pkg/gendoc/markdown.go new file mode 100644 index 00000000..752b57ca --- /dev/null +++ b/pkg/gendoc/markdown.go @@ -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)) +} diff --git a/pkg/gendoc/markdown_test.go b/pkg/gendoc/markdown_test.go new file mode 100644 index 00000000..975af93d --- /dev/null +++ b/pkg/gendoc/markdown_test.go @@ -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) + } + } +} diff --git a/pkg/gendoc/testdata/admin.json b/pkg/gendoc/testdata/admin.json new file mode 100644 index 00000000..447b54f4 --- /dev/null +++ b/pkg/gendoc/testdata/admin.json @@ -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" + } + } + } + } + } +} diff --git a/pkg/gendoc/testdata/worker.json b/pkg/gendoc/testdata/worker.json new file mode 100644 index 00000000..7d3570f9 --- /dev/null +++ b/pkg/gendoc/testdata/worker.json @@ -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" + } + } + } + } + } +} From e6963f078a1bfb5c018bf3a329e09b3538962e1d Mon Sep 17 00:00:00 2001 From: Eric Chiang Date: Mon, 1 Feb 2016 16:09:39 -0800 Subject: [PATCH 2/2] schema: regenerate schemas with markdown documentation --- build | 1 + schema/adminschema/README.md | 107 ++++++++++++ schema/adminschema/v1-json.go | 3 +- schema/generator | 12 +- schema/workerschema/README.md | 305 +++++++++++++++++++++++++++++++++ schema/workerschema/v1-gen.go | 3 +- schema/workerschema/v1-json.go | 2 +- 7 files changed, 428 insertions(+), 5 deletions(-) create mode 100644 schema/adminschema/README.md create mode 100644 schema/workerschema/README.md diff --git a/build b/build index eec8fd48..1831bee2 100755 --- a/build +++ b/build @@ -15,3 +15,4 @@ go build -o bin/dexctl -ldflags="$LD_FLAGS" github.com/coreos/dex/cmd/dexctl go build -o bin/dex-overlord -ldflags="$LD_FLAGS" github.com/coreos/dex/cmd/dex-overlord go build -o bin/example-app github.com/coreos/dex/examples/app go build -o bin/example-cli github.com/coreos/dex/examples/cli +go build -o bin/gendoc github.com/coreos/dex/cmd/gendoc diff --git a/schema/adminschema/README.md b/schema/adminschema/README.md new file mode 100644 index 00000000..cdb7bb89 --- /dev/null +++ b/schema/adminschema/README.md @@ -0,0 +1,107 @@ + +# Dex Admin API + +The Dex Admin API. + +__Version:__ v1 + +## Models + + +### Admin + + + +``` +{ + email: string, + id: string, + password: string +} +``` + +### State + + + +``` +{ + AdminUserCreated: boolean +} +``` + + +## Paths + + +### POST /admin + +> __Summary__ + +> Create Admin + +> __Description__ + +> Create a new admin user. + + +> __Parameters__ + +> |Name|Located in|Description|Required|Type| +|:-----|:-----|:-----|:-----|:-----| +| | body | | Yes | [Admin](#admin) | + + +> __Responses__ + +> |Code|Description|Type| +|:-----|:-----|:-----| +| 200 | | [Admin](#admin) | +| default | Unexpected error | | + + +### GET /admin/{id} + +> __Summary__ + +> Get Admin + +> __Description__ + +> Retrieve information about an admin user. + + +> __Parameters__ + +> |Name|Located in|Description|Required|Type| +|:-----|:-----|:-----|:-----|:-----| +| id | path | | Yes | string | + + +> __Responses__ + +> |Code|Description|Type| +|:-----|:-----|:-----| +| 200 | | [Admin](#admin) | +| default | Unexpected error | | + + +### GET /state + +> __Summary__ + +> Get State + +> __Description__ + +> Get the state of the Dex DB + + +> __Responses__ + +> |Code|Description|Type| +|:-----|:-----|:-----| +| 200 | | [State](#state) | +| default | Unexpected error | | + + diff --git a/schema/adminschema/v1-json.go b/schema/adminschema/v1-json.go index 8110bff7..67a8d158 100644 --- a/schema/adminschema/v1-json.go +++ b/schema/adminschema/v1-json.go @@ -1,5 +1,4 @@ package adminschema - // // This file is automatically generated by schema/generator // @@ -105,4 +104,4 @@ const DiscoveryJSON = `{ } } } -` +` \ No newline at end of file diff --git a/schema/generator b/schema/generator index ffb974c9..54effc73 100755 --- a/schema/generator +++ b/schema/generator @@ -11,17 +11,28 @@ if [ $SCHEMA = "worker" ]; then IN="schema/workerschema/v1.json" OUT="schema/workerschema/v1-json.go" GEN="schema/workerschema/v1-gen.go" + DOC="schema/workerschema/README.md" GOPKG="workerschema" elif [ $SCHEMA = 'admin' ]; then IN="schema/adminschema/v1.json" OUT="schema/adminschema/v1-json.go" GEN="schema/adminschema/v1-gen.go" + DOC="schema/adminschema/README.md" GOPKG="adminschema" else echo "Usage: generator [worker|admin]" exit 1 fi +GENDOC=bin/gendoc + +if [ ! -f $GENDOC ]; then + echo "gendoc command line tool not found. please run build script at the top level of this repo" + exit 1 +fi + +$GENDOC --f $IN --o $DOC + # See schema/generator_import.go for instructions on updating the dependency PKG="google.golang.org/api/google-api-go-generator" @@ -52,5 +63,4 @@ GOPATH=${PWD}/gopath ./bin/google-api-go-generator \ -output "${GEN}" # Finally, fix the import in the bindings to refer to the vendored google-api package -sed -i '' -e "s%google.golang.org%github.com/coreos/dex/Godeps/_workspace/src/google.golang.org%" "${GEN}" goimports -w ${GEN} diff --git a/schema/workerschema/README.md b/schema/workerschema/README.md new file mode 100644 index 00000000..8ff8c31b --- /dev/null +++ b/schema/workerschema/README.md @@ -0,0 +1,305 @@ + +# Dex API + +The Dex REST API + +__Version:__ v1 + +## Models + + +### Client + + + +``` +{ + id: string, + redirectURIs: [ + string + ] +} +``` + +### ClientPage + + + +``` +{ + clients: [ + Client + ], + nextPageToken: string +} +``` + +### ClientWithSecret + + + +``` +{ + id: string, + redirectURIs: [ + string + ], + secret: string +} +``` + +### Error + + + +``` +{ + error: string, + error_description: string +} +``` + +### User + + + +``` +{ + admin: boolean, + createdAt: string, + disabled: boolean, + displayName: string, + email: string, + emailVerified: boolean, + id: string +} +``` + +### UserCreateRequest + + + +``` +{ + redirectURL: string, + user: User +} +``` + +### UserCreateResponse + + + +``` +{ + emailSent: boolean, + resetPasswordLink: string, + user: User +} +``` + +### UserDisableRequest + + + +``` +{ + disable: boolean // If true, disable this user, if false, enable them. No error is signaled if the user state doesn't change. +} +``` + +### UserDisableResponse + + + +``` +{ + ok: boolean +} +``` + +### UserResponse + + + +``` +{ + user: User +} +``` + +### UsersResponse + + + +``` +{ + nextPageToken: string, + users: [ + User + ] +} +``` + + +## Paths + + +### GET /clients + +> __Summary__ + +> List Clients + +> __Description__ + +> Retrieve a page of Client objects. + + +> __Parameters__ + +> |Name|Located in|Description|Required|Type| +|:-----|:-----|:-----|:-----|:-----| +| nextPageToken | query | | No | string | + + +> __Responses__ + +> |Code|Description|Type| +|:-----|:-----|:-----| +| 200 | | [ClientPage](#clientpage) | +| default | Unexpected error | | + + +### POST /clients + +> __Summary__ + +> Create Clients + +> __Description__ + +> Register a new Client. + + +> __Parameters__ + +> |Name|Located in|Description|Required|Type| +|:-----|:-----|:-----|:-----|:-----| +| | body | | Yes | [Client](#client) | + + +> __Responses__ + +> |Code|Description|Type| +|:-----|:-----|:-----| +| 200 | | [ClientWithSecret](#clientwithsecret) | +| default | Unexpected error | | + + +### GET /users + +> __Summary__ + +> List Users + +> __Description__ + +> Retrieve a page of User objects. + + +> __Parameters__ + +> |Name|Located in|Description|Required|Type| +|:-----|:-----|:-----|:-----|:-----| +| nextPageToken | query | | No | string | +| maxResults | query | | No | integer | + + +> __Responses__ + +> |Code|Description|Type| +|:-----|:-----|:-----| +| 200 | | [UsersResponse](#usersresponse) | +| default | Unexpected error | | + + +### POST /users + +> __Summary__ + +> Create Users + +> __Description__ + +> Create a new User. + + +> __Parameters__ + +> |Name|Located in|Description|Required|Type| +|:-----|:-----|:-----|:-----|:-----| +| | body | | Yes | [UserCreateRequest](#usercreaterequest) | + + +> __Responses__ + +> |Code|Description|Type| +|:-----|:-----|:-----| +| 200 | | [UserCreateResponse](#usercreateresponse) | +| default | Unexpected error | | + + +### GET /users/{id} + +> __Summary__ + +> Get Users + +> __Description__ + +> Get a single User object by id. + + +> __Parameters__ + +> |Name|Located in|Description|Required|Type| +|:-----|:-----|:-----|:-----|:-----| +| id | path | | Yes | string | + + +> __Responses__ + +> |Code|Description|Type| +|:-----|:-----|:-----| +| 200 | | [UserResponse](#userresponse) | +| default | Unexpected error | | + + +### POST /users/{id}/disable + +> __Summary__ + +> Disable Users + +> __Description__ + +> Enable or disable a user. + + +> __Parameters__ + +> |Name|Located in|Description|Required|Type| +|:-----|:-----|:-----|:-----|:-----| +| id | path | | Yes | string | +| | body | | Yes | [UserDisableRequest](#userdisablerequest) | + + +> __Responses__ + +> |Code|Description|Type| +|:-----|:-----|:-----| +| 200 | | [UserDisableResponse](#userdisableresponse) | +| default | Unexpected error | | + + diff --git a/schema/workerschema/v1-gen.go b/schema/workerschema/v1-gen.go index 950c9278..7b9aa784 100644 --- a/schema/workerschema/v1-gen.go +++ b/schema/workerschema/v1-gen.go @@ -137,7 +137,8 @@ type UserCreateResponseUser struct { } type UserDisableRequest struct { - // Disable: If true, disable this user, if false, enable them + // Disable: If true, disable this user, if false, enable them. No error + // is signaled if the user state doesn't change. Disable bool `json:"disable,omitempty"` } diff --git a/schema/workerschema/v1-json.go b/schema/workerschema/v1-json.go index 5e6c576d..6c408dc5 100644 --- a/schema/workerschema/v1-json.go +++ b/schema/workerschema/v1-json.go @@ -176,7 +176,7 @@ const DiscoveryJSON = `{ "properties": { "disable": { "type": "boolean", - "description": "If true, disable this user, if false, enable them" + "description": "If true, disable this user, if false, enable them. No error is signaled if the user state doesn't change." } } },