Skip to content

Commit

Permalink
Merge pull request #200 from danielgtaylor/freedesktop-config-loc
Browse files Browse the repository at this point in the history
Adhere to freedesktop conventions (instead of ~/.restish) fixes #98
  • Loading branch information
danielgtaylor authored Jun 23, 2023
2 parents 8202815 + acfc510 commit 3a541b4
Show file tree
Hide file tree
Showing 9 changed files with 112 additions and 41 deletions.
6 changes: 3 additions & 3 deletions cli/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import (
"net/http"
"net/url"
"os"
"path"
"path/filepath"
"strings"
"time"

Expand Down Expand Up @@ -99,7 +99,7 @@ func cacheAPI(name string, api *API) {
if err != nil {
LogError("Could not marshal API cache %s", err)
}
filename := path.Join(getCacheDir(), name+".cbor")
filename := filepath.Join(getCacheDir(), name+".cbor")
if err := os.WriteFile(filename, b, 0o600); err != nil {
LogError("Could not write API cache %s", err)
}
Expand Down Expand Up @@ -131,7 +131,7 @@ func Load(entrypoint string, root *cobra.Command) (API, error) {
expires := Cache.GetTime(name + ".expires")
if !viper.GetBool("rsh-no-cache") && !expires.IsZero() && expires.After(time.Now()) {
var cached API
filename := path.Join(getCacheDir(), name+".cbor")
filename := filepath.Join(getCacheDir(), name+".cbor")
if data, err := os.ReadFile(filename); err == nil {
if err := cbor.Unmarshal(data, &cached); err == nil {
if cached.RestishVersion == root.Version {
Expand Down
4 changes: 2 additions & 2 deletions cli/apiconfig.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import (
"errors"
"fmt"
"os"
"path"
"path/filepath"
"sort"
"strings"

Expand Down Expand Up @@ -89,7 +89,7 @@ func initAPIConfig() {

// Write a blank cache if no file is already there. Later you can use
// configs.SaveConfig() to write new values.
filename := path.Join(viper.GetString("config-directory"), "apis.json")
filename := filepath.Join(viper.GetString("config-directory"), "apis.json")
if _, err := os.Stat(filename); os.IsNotExist(err) {
if err := os.WriteFile(filename, []byte("{}"), 0600); err != nil {
panic(err)
Expand Down
65 changes: 48 additions & 17 deletions cli/cli.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,8 @@ import (
"net/http"
"net/url"
"os"
"path"
"path/filepath"
"regexp"
"runtime"
"runtime/debug"
"strings"
"time"
Expand Down Expand Up @@ -572,23 +570,50 @@ Not after (expires): %s (%s)
}

func userHomeDir() string {
if runtime.GOOS == "windows" {
home := os.Getenv("HOMEDRIVE") + os.Getenv("HOMEPATH")
if home == "" {
home = os.Getenv("USERPROFILE")
}
return home
home, err := os.UserHomeDir()
if err != nil {
panic("HOME directoy is not defined")
}
return os.Getenv("HOME")
return home
}

func getConfigDir(appName string) string {
configDirEnv := strings.ToUpper(appName) + "_CONFIG_DIR"

configDir := os.Getenv(configDirEnv)

if configDir == "" {
configDir = path.Join(userHomeDir(), "."+appName)
// Create new config directory
configBase, _ := os.UserConfigDir()
configDir = filepath.Join(configBase, appName)

// Check for legacy config dir
legacyConfigDir := filepath.Join(viper.GetString("home-directory"), "."+appName)
if _, err := os.Stat(legacyConfigDir); err == nil {
// Only migrate if the new config dir doesn't exist, so that this
// is a one-time operation. There are edge cases where configs could
// get lost if we migrate every time (e.g. running an old version
// that creates an empty ~/.restish/apis.json).
if _, err := os.Stat(configDir); err != nil {
// Define files to migrate
for _, filename := range []string{
"config.json",
"apis.json",
"cache.json",
} {
oldPath := filepath.Join(legacyConfigDir, filename)
newDir := configDir
if filename == "cache.json" {
newDir = getCacheDir()
}
if _, err := os.Stat(oldPath); err == nil {
os.MkdirAll(newDir, 0700)
os.Rename(oldPath, filepath.Join(newDir, filename))
}
}
// Everything else is a cache that can be regenerated
os.RemoveAll(legacyConfigDir)
}
}
}
return configDir
}
Expand All @@ -600,23 +625,30 @@ func getCacheDir() string {
cacheDir := os.Getenv(cacheDirEnv)

if cacheDir == "" {
cacheDir = path.Join(userHomeDir(), "."+appName)
cache, _ := os.UserCacheDir()
cacheDir = filepath.Join(cache, appName)
}
return cacheDir
}

func initConfig(appName, envPrefix string) {
viper.Set("app-name", appName)

// One-time setup to ensure the path exists so we can write files into it
// later as needed.
home := userHomeDir()
viper.Set("home-directory", home)

configDir := getConfigDir(appName)
if err := os.MkdirAll(configDir, 0700); err != nil {
panic(err)
}

// Load configuration from file(s) if provided.
viper.SetConfigName("config")
viper.AddConfigPath("/etc/" + appName + "/")
viper.AddConfigPath("$HOME/." + appName + "/")
viper.AddConfigPath(filepath.Join("/etc/", appName))
viper.AddConfigPath(filepath.Join(viper.GetString("home-directory"), "."+appName))
viper.AddConfigPath(configDir)
viper.ReadInConfig()

// Load configuration from the environment if provided. Flags below get
Expand All @@ -626,7 +658,6 @@ func initConfig(appName, envPrefix string) {
viper.AutomaticEnv()

// Save a few things that will be useful elsewhere.
viper.Set("app-name", appName)
viper.Set("config-directory", configDir)
viper.SetDefault("server-index", 0)
}
Expand All @@ -644,13 +675,13 @@ func initCache(appName string) {

// Write a blank cache if no file is already there. Later you can use
// cli.Cache.SaveConfig() to write new values.
filename := path.Join(cacheDir, "cache.json")
filename := filepath.Join(cacheDir, "cache.json")
if _, err := os.Stat(filename); os.IsNotExist(err) {
if err := os.WriteFile(filename, []byte("{}"), 0600); err != nil {
panic(err)
}
}

viper.Set("cache-dir", cacheDir)
Cache.ReadInConfig()
}

Expand Down
4 changes: 2 additions & 2 deletions cli/cli_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ package cli
import (
"net/http"
"os"
"path"
"path/filepath"
"strings"
"testing"
"time"
Expand Down Expand Up @@ -271,7 +271,7 @@ func TestAPISync(t *testing.T) {

func TestDuplicateAPIBase(t *testing.T) {
defer func() {
os.Remove(path.Join(userHomeDir(), ".test", "apis.json"))
os.Remove(filepath.Join(getConfigDir("test"), "apis.json"))
reset(false)
}()
reset(false)
Expand Down
10 changes: 5 additions & 5 deletions cli/interactive_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import (
"net/http"
"net/url"
"os"
"path"
"path/filepath"
"testing"

"gopkg.in/h2non/gock.v1"
Expand Down Expand Up @@ -36,8 +36,8 @@ func (a *mockAsker) askSelect(message string, options []string, def interface{},

func TestInteractive(t *testing.T) {
// Remove existing config if present...
os.Remove(path.Join(userHomeDir(), ".test", "apis.json"))
os.Remove(path.Join(userHomeDir(), ".test", "cache.json"))
os.Remove(filepath.Join(getConfigDir("test"), "apis.json"))
os.Remove(filepath.Join(getConfigDir("test"), "cache.json"))

reset(false)

Expand Down Expand Up @@ -99,8 +99,8 @@ func (l *testLoader) Load(entrypoint, spec url.URL, resp *http.Response) (API, e

func TestInteractiveAutoConfig(t *testing.T) {
// Remove existing config if present...
os.Remove(path.Join(userHomeDir(), ".test", "apis.json"))
os.Remove(path.Join(userHomeDir(), ".test", "cache.json"))
os.Remove(filepath.Join(getConfigDir("test"), "apis.json"))
os.Remove(filepath.Join(getConfigDir("test"), "cache.json"))

reset(false)
AddLoader(&testLoader{
Expand Down
4 changes: 2 additions & 2 deletions cli/transport.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import (
"fmt"
"io"
"net/http"
"path"
"path/filepath"
"strings"
"time"

Expand Down Expand Up @@ -44,7 +44,7 @@ func shouldCache(resp *http.Response) bool {

// CachedTransport returns an HTTP transport with caching abilities.
func CachedTransport() *httpcache.Transport {
t := httpcache.NewTransport(diskcache.New(path.Join(getCacheDir(), "responses")))
t := httpcache.NewTransport(diskcache.New(filepath.Join(getCacheDir(), "responses")))
t.MarkCachedResponses = false
return t
}
Expand Down
34 changes: 26 additions & 8 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,17 @@ Global configuration affects all commands and can be set in one of three ways, g

1. Command line arguments
2. Environment variables
3. Configuration files (`/etc/restish/config.json` or `~/.restish/config.json`)
3. Configuration files

Configuration file locations are operating-system dependent:

| OS | Path |
| ------- | --------------------------------------------------- |
| Mac | `~/Library/Application Support/restish/config.json` |
| Windows | `%AppData%\restish\config.json` |
| Linux | `~/.config/restish/config.json` |

You can quickly determine which is being used via `restish localhost -v 2>&1 | grep config-directory`.

The global options in addition to `--help` and `--version` are:

Expand Down Expand Up @@ -46,8 +56,8 @@ $ RSH_VERBOSE=1 RSH_PROFILE=testing restish api.rest.sh/images
```

```bash
# Configuration file
$ echo '{"rsh-verbose": true, "rsh-profile": "testing"}' > ~/.restish/config.json
# Configuration file (Linux example)
$ echo '{"rsh-verbose": true, "rsh-profile": "testing"}' > ~/.config/restish/config.json
$ restish api.rest.sh/images
```

Expand All @@ -63,10 +73,18 @@ Adding or editing an API is possible via an interactive terminal UI:
$ restish api configure $NAME [$BASE_URI]
```

You should see something like the following, which enables you to create and edit profiles, headers, query parameters, and auth, eventually saving the data to `~/.restish/apis.json`:
You should see something like the following, which enables you to create and edit profiles, headers, query parameters, and auth:

<img alt="Screen Shot" src="https://user-images.githubusercontent.com/106826/83099522-79dd3200-a062-11ea-8a78-b03a2fecf030.png">

Eventually the data is saved to one of the following:

| OS | Path |
| ------- | ------------------------------------------------- |
| Mac | `~/Library/Application Support/restish/apis.json` |
| Windows | `%AppData%\restish\apis.json` |
| Linux | `~/.config/restish/apis.json` |

If the API offers autoconfiguration data (e.g. through the [`x-cli-config` OpenAPI extension](/openapi.md#AutoConfiguration)) then you may be prompted for other values and some settings may already be configured for you.

Once an API is configured, you can start using it by using its short name. For example, given an API named `example`:
Expand Down Expand Up @@ -300,13 +318,13 @@ Two parameters are accepted for this authentication method:
The serialized body will be supplied in the following form to the
helper commandline:

``` json
```json
{
"method": "GET",
"uri": "http://...",
"headers": {
"content-type": [""],
// …
"content-type": [""]
// …
},
"body": ""
}
Expand All @@ -323,7 +341,7 @@ parameters only will be considered:

Sometimes an API won't provide a way to fetch its spec document, or a third-party will provide a spec for an existing public API, for example GitHub or Stripe.

In this case you can download the spec files to your machine and link to them (or provide a URL) in the API configuration. Use the `spec_files` array configuration directive for this in `~/.restish/apis.json`:
In this case you can download the spec files to your machine and link to them (or provide a URL) in the API configuration. Use the `spec_files` array configuration directive for this in the [`apis.json` file](#/configuration?id=adding-an-api):

```json
{
Expand Down
18 changes: 17 additions & 1 deletion docs/guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -218,7 +218,23 @@ $ restish api configure $SHORT_NAME $URL
$ restish api configure example https://api.rest.sh
```

What exactly does this do? It sets up the API short name `example` to point to `https://api.rest.sh` in a configuration file (usually `~/.restish/apis.json`) and finds [https://api.rest.sh/openapi.json](https://api.rest.sh/openapi.json) in order to discover available API operations, documentation, parameters, schemas, etc. You can see the available operations via:
What exactly does this do? It sets up the API short name `example` to point to `https://api.rest.sh` in a configuration file:

| OS | Path |
| ------- | ------------------------------------------------- |
| Mac | `~/Library/Application Support/restish/apis.json` |
| Windows | `%AppData%\restish\apis.json` |
| Linux | `~/.config/restish/apis.json` |

If an OAuth2 flow had been configured, the resulting auth token and (optional) refresh token will be cached in another file:

| OS | Path |
| ------- | ------------------------------------- |
| Mac | `~/Library/Caches/restish/cache.json` |
| Windows | `%LocalAppData%\restish\cache.json` |
| Linux | `~/.cache/restish/cache.json` |

Restish finds [https://api.rest.sh/openapi.json](https://api.rest.sh/openapi.json) in order to discover available API operations, documentation, parameters, schemas, etc. You can see the available operations via:

```bash
# If an OpenAPI or other API description document was found, this will show
Expand Down
8 changes: 7 additions & 1 deletion docs/output.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,13 @@ graph LR

## Caching

By default, Restish will cache responses with appropriate [RFC 7234](https://tools.ietf.org/html/rfc7234) caching headers set. When fetching API service descriptions, a 24-hour cache is used if _no cache headers_ are sent by the API. This is to prevent hammering the API each time the CLI is run. The cached responses are stored in `~/.restish/responses`.
By default, Restish will cache responses with appropriate [RFC 7234](https://tools.ietf.org/html/rfc7234) caching headers set. When fetching API service descriptions, a 24-hour cache is used if _no cache headers_ are sent by the API. This is to prevent hammering the API each time the CLI is run. The cached responses are stored in one of the following operating-system dependent locations:

| OS | Path |
| ------- | ------------------------------------ |
| Mac | `~/Library/Caches/restish/responses` |
| Windows | `%LocalAppData%\restish\responses` |
| Linux | `~/.cache/restish/responses` |

The easiest way to tell if a cached response has been used is to look at the `Date` header, which will not change from request to request if a cached response is returned.

Expand Down

0 comments on commit 3a541b4

Please sign in to comment.