From c95742fc2d0d8cfb8356c1195b82d3bfa6135bca Mon Sep 17 00:00:00 2001 From: "Daniel G. Taylor" Date: Tue, 1 Aug 2023 16:42:48 -0700 Subject: [PATCH] feat: API edit command; apis.json schema --- cli/apiconfig.go | 51 ++++++++++++++++++- cli/apiconfig_test.go | 19 +++++++ docs/schemas/apis.json | 109 +++++++++++++++++++++++++++++++++++++++++ docs/shorthand.md | 2 +- 4 files changed, 179 insertions(+), 2 deletions(-) create mode 100644 docs/schemas/apis.json diff --git a/cli/apiconfig.go b/cli/apiconfig.go index a1c3e26..5ff2eca 100644 --- a/cli/apiconfig.go +++ b/cli/apiconfig.go @@ -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" @@ -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", @@ -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", @@ -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) } @@ -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()) +} diff --git a/cli/apiconfig_test.go b/cli/apiconfig_test.go index c76e0cb..be1b0c6 100644 --- a/cli/apiconfig_test.go +++ b/cli/apiconfig_test.go @@ -1,6 +1,7 @@ package cli import ( + "os" "testing" "github.com/stretchr/testify/assert" @@ -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) {}) + }) +} diff --git a/docs/schemas/apis.json b/docs/schemas/apis.json new file mode 100644 index 0000000..7c839af --- /dev/null +++ b/docs/schemas/apis.json @@ -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." + } + } + } + } + } + } + } + } +} diff --git a/docs/shorthand.md b/docs/shorthand.md index 925e659..a44bf34 100644 --- a/docs/shorthand.md +++ b/docs/shorthand.md @@ -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)