From 5a801591861de3c370bdac1a489cf2ea326c4458 Mon Sep 17 00:00:00 2001 From: "Daniel G. Taylor" Date: Wed, 2 Aug 2023 10:08:12 -0700 Subject: [PATCH] feat: operations base path config option --- cli/api.go | 8 ++- cli/apiconfig.go | 11 +-- docs/configuration.md | 27 +++++++ docs/schemas/apis.json | 160 +++++++++++++++++++++++++++++++++++------ 4 files changed, 179 insertions(+), 27 deletions(-) diff --git a/cli/api.go b/cli/api.go index 963804f..28fa0dd 100644 --- a/cli/api.go +++ b/cli/api.go @@ -266,7 +266,13 @@ func Load(entrypoint string, root *cobra.Command) (API, error) { if l.Detect(resp) { resp.Body = io.NopCloser(bytes.NewReader(body)) - api, err := load(root, *uri, *resolved, resp, name, l) + // Override the operation base path if requested, otherwise + // default to the API entrypoint. + opsBase := uri + if config.OperationBase != "" { + opsBase = uri.ResolveReference(&url.URL{Path: config.OperationBase}) + } + api, err := load(root, *opsBase, *resolved, resp, name, l) if err == nil { cacheAPI(name, &api) } diff --git a/cli/apiconfig.go b/cli/apiconfig.go index 5ff2eca..30cfae4 100644 --- a/cli/apiconfig.go +++ b/cli/apiconfig.go @@ -44,11 +44,12 @@ type APIProfile struct { // APIConfig describes per-API configuration options like the base URI and // auth scheme, if any. type APIConfig struct { - name string - Base string `json:"base" yaml:"base"` - SpecFiles []string `json:"spec_files,omitempty" yaml:"spec_files,omitempty" mapstructure:"spec_files,omitempty"` - Profiles map[string]*APIProfile `json:"profiles,omitempty" yaml:"profiles,omitempty" mapstructure:",omitempty"` - TLS *TLSConfig `json:"tls,omitempty" yaml:"tls,omitempty" mapstructure:",omitempty"` + name string + Base string `json:"base" yaml:"base"` + OperationBase string `json:"operation_base,omitempty" yaml:"operation_base,omitempty" mapstructure:"operation_base,omitempty"` + SpecFiles []string `json:"spec_files,omitempty" yaml:"spec_files,omitempty" mapstructure:"spec_files,omitempty"` + Profiles map[string]*APIProfile `json:"profiles,omitempty" yaml:"profiles,omitempty" mapstructure:",omitempty"` + TLS *TLSConfig `json:"tls,omitempty" yaml:"tls,omitempty" mapstructure:",omitempty"` } // Save the API configuration to disk. diff --git a/docs/configuration.md b/docs/configuration.md index 882e67a..6a982c7 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -131,6 +131,16 @@ $ restish api sync $NAME ?> This is usually not necessary, as Restish will update the API description every 24 hours. Use this if you want to force an update sooner! +### Editing All APIs + +You can edit all APIs at once in your editor of choice via: + +```bash +$ restish api edit +``` + +You will need to have `EDITOR` or `VISUAL` environment variables set to which editor you want to use, e.g. `export VISUAL='code --wait'` for VSCode. + ### Persistent headers & query parameters Follow the prompts to add or edit persistent headers or query parameters. These are values that get sent with **every request** when using that profile. @@ -353,3 +363,20 @@ In this case you can download the spec files to your machine and link to them (o ``` !> If more than one file path is specified, then the loaded APIs are merged in the order specified. You will get operations from both APIs, but there can only be a single API title or description so the first encountered non-zero value is used. + +### Operation Base Path + +Most of the time when an API is served at some sub-path like `https://example.com/my-api` the operation paths should be treated as relative to that sub-path, that is an operation `/foo` would result in a request to `https://example.com/my-api/foo`. Sometimes that is not the behavior you want, for example the OpenAPI operations may already contain the full path including the sub-path. + +The `operation_base` parameter can be used to change this behavior. It defaults to the API base path, but can be changed to any URL reference and will be resolved against the base path. For example, to make an operation use `/my-op` rather than `/my-api/v2-beta1/my-op` as its URL path: + +```json +{ + "my-api-beta": { + "base": "https://example.com/my-api/v2-beta1", + "operation_base": "/" + } +} +``` + +?> This is an advanced feature which is not needed in most cases. diff --git a/docs/schemas/apis.json b/docs/schemas/apis.json index 7c839af..122c8a9 100644 --- a/docs/schemas/apis.json +++ b/docs/schemas/apis.json @@ -17,6 +17,11 @@ "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." }, + "operation_base": { + "type": "string", + "format": "uri-reference", + "description": "Overrides the base URL path of API operations. This can be used to treat the OpenAPI paths as absolute even when an API is served from a subpath on the server, or make other modifications to support additional use-cases. If unset, this matches the base URL 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.", @@ -51,33 +56,146 @@ } }, "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" - ] + "oneOf": [ + { + "type": "object", + "description": "Authentication & authorization setting for this API profile.", + "additionalProperties": false, + "required": ["name", "params"], + "properties": { + "name": { + "const": "http-basic", + "description": "Auth scheme name." + }, + "params": { + "type": "object", + "description": "Parameters for the auth scheme. For http-basic, this is the username and password to send with each request.", + "additionalProperties": false, + "required": ["username", "password"], + "properties": { + "username": { + "type": "string", + "description": "The username to send with each request." + }, + "password": { + "type": "string", + "description": "The password to send with each request." + } + } + } + } + }, + { + "type": "object", + "description": "Authentication & authorization setting for this API profile.", + "additionalProperties": false, + "required": ["name", "params"], + "properties": { + "name": { + "const": "oauth-client-credentials", + "description": "Auth scheme name." + }, + "params": { + "type": "object", + "description": "Parameters for the auth scheme. For oauth-client-credentials, this is at least the client ID, client secret, and token URL.", + "required": ["client_id", "client_secret", "token_url"], + "properties": { + "audience": { + "type": "string", + "description": "Audience restricts which APIs will accept the generated auth token." + }, + "client_id": { + "type": "string", + "description": "The client ID to send with each request." + }, + "client_secret": { + "type": "string", + "description": "The client secret to send with each request." + }, + "scopes": { + "type": "string", + "description": "A space-separated list of scopes to request, enabling access to certain resources & actions in the API." + }, + "token_url": { + "type": "string", + "description": "The URL to request an auth token from." + } + } + } + } + }, + { + "type": "object", + "description": "Authentication & authorization setting for this API profile.", + "additionalProperties": false, + "required": ["name", "params"], + "properties": { + "name": { + "const": "oauth-authorization-code", + "description": "Auth scheme name." }, - { - "type": "string" + "params": { + "type": "object", + "description": "Parameters for the auth scheme. For oauth-authorization-code, this is at least the client ID, authorize URL, and token URL.", + "required": ["client_id", "authorize_url", "token_url"], + "properties": { + "audience": { + "type": "string", + "description": "Audience restricts which APIs will accept the generated auth token." + }, + "client_id": { + "type": "string", + "description": "The client ID to send with each request." + }, + "client_secret": { + "type": "string", + "description": "The client secret (if any) to send with each request." + }, + "scopes": { + "type": "string", + "description": "A space-separated list of scopes to request, enabling access to certain resources & actions in the API." + }, + "token_url": { + "type": "string", + "description": "The URL to request an auth token from." + }, + "authorize_url": { + "type": "string", + "description": "The URL to initiate a web login flow." + } + } } - ] + } }, - "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" + "description": "Authentication & authorization setting for this API profile.", + "additionalProperties": false, + "required": ["name", "params"], + "properties": { + "name": { + "const": "external-tool", + "description": "Auth scheme name." + }, + "params": { + "type": "object", + "description": "Parameters for the auth scheme. For external-tool, this is the commandline to run to get the auth token. The commandline must output an object with 'uri' and 'headers' properties.", + "required": ["commandline"], + "properties": { + "commandline": { + "type": "string", + "description": "The commandline to run to get the auth token. The commandline must output an object with 'uri' and 'headers' properties." + }, + "omitbody": { + "type": "string", + "description": "If true, do not send the request body to the command on stdin, just the 'method', 'uri', and 'headers' as a JSON object.", + "enum": ["true", "false"] + } + } + } } } - } + ] }, "tls": { "type": "object",