Skip to content

Commit

Permalink
feat: API edit command; apis.json schema
Browse files Browse the repository at this point in the history
  • Loading branch information
danielgtaylor committed Aug 1, 2023
1 parent 3a541b4 commit c95742f
Show file tree
Hide file tree
Showing 4 changed files with 179 additions and 2 deletions.
51 changes: 50 additions & 1 deletion cli/apiconfig.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,13 @@ import (
"errors"
"fmt"
"os"
"os/exec"
"path"
"path/filepath"
"sort"
"strings"

"github.com/google/shlex"
"github.com/spf13/cobra"
"github.com/spf13/viper"
"golang.org/x/exp/maps"
Expand Down Expand Up @@ -101,6 +104,12 @@ func initAPIConfig() {
panic(err)
}

if apis.GetString("$schema") == "" {
// Attempt to update the config to add the schema for docs/validation.
apis.Set("$schema", "https://rest.sh/schemas/apis.json")
apis.WriteConfig()
}

// Register api init sub-command to register the API.
apiCommand = &cobra.Command{
GroupID: "generic",
Expand Down Expand Up @@ -152,6 +161,14 @@ func initAPIConfig() {
Run: askInitAPIDefault,
})

apiCommand.AddCommand(&cobra.Command{
Use: "edit",
Short: "Edit APIs configuration",
Long: "Edit the APIs configuration in your default editor.",
Args: cobra.NoArgs,
Run: func(cmd *cobra.Command, args []string) { editAPIs(os.Exit) },
})

apiCommand.AddCommand(&cobra.Command{
Use: "show short-name",
Short: "Show API config",
Expand Down Expand Up @@ -188,7 +205,14 @@ func initAPIConfig() {

// Register API sub-commands
configs = apiConfigs{}
if err := apis.Unmarshal(&configs); err != nil {
tmp := viper.New()
for k, v := range apis.AllSettings() {
if k == "$schema" {
continue
}
tmp.Set(k, v)
}
if err := tmp.Unmarshal(&configs); err != nil {
panic(err)
}

Expand Down Expand Up @@ -247,3 +271,28 @@ func findAPI(uri string) (string, *APIConfig) {

return "", nil
}

func editAPIs(exitFunc func(int)) {
editor := getEditor()
if editor == "" {
fmt.Fprintln(os.Stderr, `Please set the VISUAL or EDITOR environment variable with your preferred editor. Examples:
export VISUAL="code --wait"
export EDITOR="vim"`)
exitFunc(1)
return
}

parts, err := shlex.Split(editor)
panicOnErr(err)
name := parts[0]
args := append(parts[1:], path.Join(
getConfigDir(viper.GetString("app-name")), "apis.json",
))

c := exec.Command(name, args...)
c.Stdin = os.Stdin
c.Stdout = os.Stdout
c.Stderr = os.Stderr
panicOnErr(c.Run())
}
19 changes: 19 additions & 0 deletions cli/apiconfig_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package cli

import (
"os"
"testing"

"github.com/stretchr/testify/assert"
Expand All @@ -22,3 +23,21 @@ func TestAPIShow(t *testing.T) {
captured := runNoReset("api show test")
assert.Equal(t, captured, "\x1b[38;5;247m{\x1b[0m\n \x1b[38;5;74m\"base\"\x1b[0m\x1b[38;5;247m:\x1b[0m \x1b[38;5;150m\"https://api.example.com\"\x1b[0m\n\x1b[38;5;247m}\x1b[0m\n")
}

func TestEditAPIsMissingEditor(t *testing.T) {
os.Setenv("EDITOR", "")
os.Setenv("VISUAL", "")
exited := false
editAPIs(func(code int) {
exited = true
})
assert.True(t, exited)
}

func TestEditBadCommand(t *testing.T) {
os.Setenv("EDITOR", "bad-command")
os.Setenv("VISUAL", "")
assert.Panics(t, func() {
editAPIs(func(code int) {})
})
}
109 changes: 109 additions & 0 deletions docs/schemas/apis.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
{
"$schema": "http://json-schema.org/draft-07/schema",
"type": "object",
"properties": {
"$schema": {
"type": "string",
"format": "uri",
"description": "The URL of the JSON Schema that describes the structure and semantics of the remainder of the document"
}
},
"additionalProperties": {
"type": "object",
"required": ["base"],
"properties": {
"base": {
"type": "string",
"format": "uri-reference",
"description": "The base URL of the API. This is used to try and fetch the OpenAPI spec as well as resolve relative references. If the base contains a path, OpenAPI operations are assumed relative to that base path."
},
"spec_files": {
"type": "array",
"description": "The local filename or remote URL of the OpenAPI spec file(s) to load for this API if autodetection cannot be used. If multiple files are specified, their operations will be merged together.",
"items": {
"type": "string"
}
},
"profiles": {
"type": "object",
"description": "A map of profile names (e.g. 'default') to profile information that can include headers, query params, auth, and custom TLS settings. A default profile is required.",
"required": ["default"],
"additionalProperties": {
"type": "object",
"properties": {
"base": {
"type": "string",
"format": "uri",
"description": "Override the base URL of the API"
},
"headers": {
"type": "object",
"description": "Header names and values to send on each request.",
"additionalProperties": {
"type": "string"
}
},
"query": {
"type": "object",
"description": "Query parameters to send on each request.",
"additionalProperties": {
"type": "string"
}
},
"auth": {
"type": "object",
"description": "Authentication & authorization setting for this API profile.",
"required": ["name"],
"properties": {
"name": {
"description": "Authentication & authorization scheme name.",
"anyOf": [
{
"enum": [
"oauth-client-credentials",
"oauth-authorization-code",
"external-tool"
]
},
{
"type": "string"
}
]
},
"params": {
"type": "object",
"description": "Auth parameter names and values to send as additional values in the auth request. These are specific to each auth scheme name and implementation, and include things like the OAuth2 authorize / token URLs, client ID / secret, audience, etc. See https://rest.sh/#/configuration?id=api-auth.",
"additionalProperties": {
"type": "string"
}
}
}
},
"tls": {
"type": "object",
"description": "Custom TLS (HTTPS) certificate verification settings.",
"properties": {
"insecure": {
"type": "boolean",
"description": "If true, do not verify TLS certificates when making requests to this API."
},
"cert": {
"type": "string",
"description": "The local filename of a TLS certificate."
},
"key": {
"type": "string",
"description": "The local filename of a TLS private key."
},
"ca_cert": {
"type": "string",
"description": "The local filename of a TLS certificate authority."
}
}
}
}
}
}
}
}
}
2 changes: 1 addition & 1 deletion docs/shorthand.md
Original file line number Diff line number Diff line change
Expand Up @@ -282,7 +282,7 @@ $ j 'twitter: "@user"'

### Patch (partial update)

Partial updates are supported on existing data, which can be used to implement HTTP `PATCH`, templating, and other similar features. The suggested content type for HTTP `PATCH` is `application/shorthand-patch`. This feature combines the best of both:
Partial updates are supported on existing data, which can be used to implement HTTP `PATCH`, templating, and other similar features. The suggested content type for HTTP `PATCH` is `application/merge-patch+shorthand`. This feature combines the best of both:

- [JSON Merge Patch](https://datatracker.ietf.org/doc/html/rfc7386)
- [JSON Patch](https://www.rfc-editor.org/rfc/rfc6902)
Expand Down

0 comments on commit c95742f

Please sign in to comment.