From 7a16b2a51c0e2bb99307d645a1bdeb96393bf05c Mon Sep 17 00:00:00 2001 From: Dean Jackson Date: Sat, 30 Jun 2018 22:38:30 +0200 Subject: [PATCH] Extract Alfred's variable-related methods into Config --- _examples/settings/info.plist | 8 +- _examples/settings/main.go | 8 +- alfred.go | 242 +++------------ alfred_config.go | 185 ------------ alfred_test.go | 69 +---- config.go | 333 +++++++++++++++++++++ alfred_bind.go => config_bind.go | 60 ++-- alfred_bind_test.go => config_bind_test.go | 87 ++++-- alfred_config_test.go => config_test.go | 144 ++++----- doc.go | 10 +- env.go | 42 ++- testutils_test.go | 38 +-- workflow.go | 31 +- workflow_options.go | 15 - workflow_paths.go | 4 +- workflow_test.go | 8 - 16 files changed, 624 insertions(+), 660 deletions(-) delete mode 100644 alfred_config.go create mode 100644 config.go rename alfred_bind.go => config_bind.go (89%) rename alfred_bind_test.go => config_bind_test.go (89%) rename alfred_config_test.go => config_test.go (72%) diff --git a/_examples/settings/info.plist b/_examples/settings/info.plist index f3f8af6..6c5176f 100644 --- a/_examples/settings/info.plist +++ b/_examples/settings/info.plist @@ -354,13 +354,13 @@ variables={allvars} variables API_KEY - + 3dwrrupbujdjcznddbva6fuj3rmzbd HOSTNAME - localhost + www.example.com PORT - + 1234 USERNAME - dave + jimmy_the_spliff variablesdontexport diff --git a/_examples/settings/main.go b/_examples/settings/main.go index a5ce334..b8a0bbb 100644 --- a/_examples/settings/main.go +++ b/_examples/settings/main.go @@ -12,13 +12,13 @@ Workflow settings demonstrates binding a struct to Alfred's settings. The workflow's settings are stored in info.plist/the workflow's configuration sheet in Alfred Preferences. -These are imported into the Server struct using Alfred.To(). +These are imported into the Server struct using Config.To(). The Script Filter displays these settings, and you can select one to change its value. If you enter a new value, this is saved to info.plist/the configuration -sheet via Alfred.SetConfig(), and the workflow is run again by calling +sheet via Config.Set(), and the workflow is run again by calling its "settings" External Trigger via Alfred.RunTrigger(). */ package main @@ -60,7 +60,7 @@ func runSet(key, value string) { log.Printf("saving %#v to %s ...", value, key) - if err := wf.Alfred.SetConfig(key, value, false).Do(); err != nil { + if err := wf.Config.Set(key, value, false).Do(); err != nil { wf.FatalError(err) } @@ -118,7 +118,7 @@ func run() { } // Update config from environment variables - if err := wf.Alfred.To(srv); err != nil { + if err := wf.Config.To(srv); err != nil { panic(err) } diff --git a/alfred.go b/alfred.go index 73eba0a..4c48482 100644 --- a/alfred.go +++ b/alfred.go @@ -9,89 +9,30 @@ package aw import ( - "errors" "fmt" "path/filepath" - "strings" "github.com/deanishe/awgo/util" ) -// Environment variables containing workflow and Alfred info. -// -// Read the values with os.Getenv(EnvVarName) or via Alfred: -// -// // Returns a string -// Alfred.Get(EnvVarName) -// // Parse string into a bool -// Alfred.GetBool(EnvVarDebug) -// -const ( - // Workflow info assigned in Alfred Preferences - EnvVarName = "alfred_workflow_name" // Name of workflow - EnvVarBundleID = "alfred_workflow_bundleid" // Bundle ID - EnvVarVersion = "alfred_workflow_version" // Workflow version - - EnvVarUID = "alfred_workflow_uid" // Random UID assigned by Alfred - - // Workflow storage directories - EnvVarCacheDir = "alfred_workflow_cache" // For temporary data - EnvVarDataDir = "alfred_workflow_data" // For permanent data - - // Set to 1 when Alfred's debugger is open - EnvVarDebug = "alfred_debug" - - // Theme info. Colours are in rgba format, e.g. "rgba(255,255,255,1.0)" - EnvVarTheme = "alfred_theme" // ID of user's selected theme - EnvVarThemeBG = "alfred_theme_background" // Background colour - EnvVarThemeSelectionBG = "alfred_theme_selection_background" // BG colour of selected item - - // Alfred info - EnvVarAlfredVersion = "alfred_version" // Alfred's version number - EnvVarAlfredBuild = "alfred_version_build" // Alfred's build number - EnvVarPreferences = "alfred_preferences" // Path to "Alfred.alfredpreferences" file - // Machine-specific hash. Machine preferences are stored in - // Alfred.alfredpreferences/local/ - EnvVarLocalhash = "alfred_preferences_localhash" -) - /* -Alfred is a wrapper for Alfred's AppleScript API. With the API, you can -open Alfred in various modes and manipulate persistent workflow variables, -i.e. the values saved in info.plist. +Alfred wraps Alfred's AppleScript API, allowing you to open Alfred in +various modes or call External Triggers. -Because calling Alfred is slow, the API uses a "Doer" interface, where -commands are collected and all sent together when Alfred.Do() is called: + a := NewAlfred() // Open Alfred - a := NewAlfred() - if err := a.Search("").Do(); err != nil { + if err := a.Search(""); err != nil { // handle error } // Browse /Applications - a = NewAlfred() - if err := a.Browse("/Applications").Do(); err != nil { + if err := a.Browse("/Applications"); err != nil { // handle error } - - // Set multiple configuration values - a = NewAlfred() - - a.SetConfig("USERNAME", "dave") - a.SetConfig("CLEARANCE", "highest") - a.SetConfig("PASSWORD", "hunter2") - - if err := a.Do(); err != nil { - // handle error - } - */ type Alfred struct { Env - bundleID string - scripts []string - err error } // NewAlfred creates a new Alfred from the environment. @@ -100,11 +41,7 @@ type Alfred struct { // is initialised from that instead of the system environment. func NewAlfred(env ...Env) *Alfred { - var ( - a *Alfred - bid string - e Env - ) + var e Env if len(env) > 0 { e = env[0] @@ -112,82 +49,39 @@ func NewAlfred(env ...Env) *Alfred { e = sysEnv{} } - if s, ok := e.Lookup("alfred_workflow_bundleid"); ok { - bid = s - } - - a = &Alfred{ - Env: e, - bundleID: bid, - scripts: []string{}, - err: nil, - } + return &Alfred{e} +} - return a +// Search runs Alfred with the given query. Use an empty query to just open Alfred. +func (a *Alfred) Search(query string) error { + _, err := util.RunJS(fmt.Sprintf(scriptSearch, util.QuoteJS(query))) + return err } -// Do calls Alfred and runs the accumulated actions. -// -// If an error was encountered while preparing any commands, it will be -// returned here. It also returns an error if there are no commands to run, -// or if the call to Alfred fails. -// -// Succeed or fail, any accumulated scripts and errors are cleared when Do() -// is called. -func (a *Alfred) Do() error { +// Browse tells Alfred to open path in navigation mode. +func (a *Alfred) Browse(path string) error { var err error - if a.err != nil { - // reset - err, a.err = a.err, nil - a.scripts = []string{} - + if path, err = filepath.Abs(path); err != nil { return err } - if len(a.scripts) == 0 { - return errors.New("no commands to run") - } - - script := strings.Join(a.scripts, "\n") - // reset - a.scripts = []string{} - - // log.Printf("-----------\n%s\n------------", script) - - _, err = util.RunJS(script) - + _, err = util.RunJS(fmt.Sprintf(scriptBrowse, util.QuoteJS(path))) return err } -// Search runs Alfred with the given query. Use an empty query to just open Alfred. -func (a *Alfred) Search(query string) *Alfred { - return a.addScript(scriptSearch, query) -} - -// Browse tells Alfred to open path in navigation mode. -func (a *Alfred) Browse(path string) *Alfred { - - path, err := filepath.Abs(path) - if err != nil { - a.err = err - return a - } - - return a.addScript(scriptBrowse, path) -} - // SetTheme tells Alfred to use the specified theme. -func (a *Alfred) SetTheme(name string) *Alfred { - return a.addScript(scriptSetTheme, name) +func (a *Alfred) SetTheme(name string) error { + _, err := util.RunJS(fmt.Sprintf(scriptSetTheme, util.QuoteJS(name))) + return err } // Action tells Alfred to show File Actions for path(s). -func (a *Alfred) Action(path ...string) *Alfred { +func (a *Alfred) Action(path ...string) error { if len(path) == 0 { - return a + return nil } var paths []string @@ -196,18 +90,14 @@ func (a *Alfred) Action(path ...string) *Alfred { p, err := filepath.Abs(p) if err != nil { - a.err = fmt.Errorf("[action] couldn't make path absolute (%s): %v", p, err) - continue + return fmt.Errorf("[action] couldn't make path absolute (%s): %v", p, err) } paths = append(paths, p) } - script := fmt.Sprintf(scriptAction, util.QuoteJS(paths)) - - a.scripts = append(a.scripts, script) - - return a + _, err := util.RunJS(fmt.Sprintf(scriptAction, util.QuoteJS(paths))) + return err } // RunTrigger runs an External Trigger in the given workflow. Query may be empty. @@ -215,89 +105,25 @@ func (a *Alfred) Action(path ...string) *Alfred { // It accepts one optional bundleID argument, which is the bundle ID of the // workflow whose trigger should be run. // If not specified, it defaults to the current workflow's. -func (a *Alfred) RunTrigger(name, query string, bundleID ...string) *Alfred { - - bid := a.getBundleID(bundleID...) - opts := map[string]interface{}{ - "inWorkflow": bid, - } - - if query != "" { - opts["withArgument"] = query - } +func (a *Alfred) RunTrigger(name, query string, bundleID ...string) error { - return a.addScriptOpts(scriptTrigger, name, opts) -} - -// SetConfig saves a workflow variable to info.plist. -// -// It accepts one optional bundleID argument, which is the bundle ID of the -// workflow whose configuration should be changed. -// If not specified, it defaults to the current workflow's. -func (a *Alfred) SetConfig(key, value string, export bool, bundleID ...string) *Alfred { - - bid := a.getBundleID(bundleID...) - opts := map[string]interface{}{ - "toValue": value, - "inWorkflow": bid, - "exportable": export, - } - - return a.addScriptOpts(scriptSetConfig, key, opts) -} - -// setMulti is an internal wrapper around SetConfig and do. It implements -// the internal bindDest interface to make testing easier. -func (a *Alfred) setMulti(variables map[string]string, export bool) error { - - for k, v := range variables { - a.SetConfig(k, v, export) + var bid string + if len(bundleID) > 0 { + bid = bundleID[0] + } else { + bid, _ = a.Lookup(EnvVarBundleID) } - return a.Do() -} - -// RemoveConfig removes a workflow variable from info.plist. -// -// It accepts one optional bundleID argument, which is the bundle ID of the -// workflow whose configuration should be changed. -// If not specified, it defaults to the current workflow's. -func (a *Alfred) RemoveConfig(key string, bundleID ...string) *Alfred { - - bid := a.getBundleID(bundleID...) opts := map[string]interface{}{ "inWorkflow": bid, } - return a.addScriptOpts(scriptRmConfig, key, opts) -} - -// Add a JavaScript that takes a single argument. -func (a *Alfred) addScript(script, arg string) *Alfred { - - script = fmt.Sprintf(script, util.QuoteJS(arg)) - a.scripts = append(a.scripts, script) - - return a -} - -// Run a JavaScript that takes two arguments, a string and an object. -func (a *Alfred) addScriptOpts(script, name string, opts map[string]interface{}) *Alfred { - - script = fmt.Sprintf(script, util.QuoteJS(name), util.QuoteJS(opts)) - a.scripts = append(a.scripts, script) - - return a -} - -// Extract bundle ID from argument, Alfred.BundleID or environment (via BundleID()). -func (a *Alfred) getBundleID(bundleID ...string) string { - - if len(bundleID) > 0 { - return bundleID[0] + if query != "" { + opts["withArgument"] = query } - return a.bundleID + _, err := util.RunJS(fmt.Sprintf(scriptTrigger, util.QuoteJS(name), util.QuoteJS(opts))) + return err } // JXA scripts to call Alfred diff --git a/alfred_config.go b/alfred_config.go deleted file mode 100644 index 415691f..0000000 --- a/alfred_config.go +++ /dev/null @@ -1,185 +0,0 @@ -// -// Copyright (c) 2017 Dean Jackson -// -// MIT Licence. See http://opensource.org/licenses/MIT -// -// Created on 2017-08-13 -// - -package aw - -import ( - "fmt" - "strconv" - "strings" - "time" -) - -// Get returns the value for envvar "key". -// It accepts one optional "fallback" argument. If no envvar is set, returns -// fallback or an empty string. -// -// If a variable is set, but empty, its value is used. -func (a *Alfred) Get(key string, fallback ...string) string { - - var fb string - - if len(fallback) > 0 { - fb = fallback[0] - } - s, ok := a.Lookup(key) - if !ok { - return fb - } - return s -} - -// GetString is a synonym for Get. -func (a *Alfred) GetString(key string, fallback ...string) string { - return a.Get(key, fallback...) -} - -// GetInt returns the value for envvar "key" as an int. -// It accepts one optional "fallback" argument. If no envvar is set, returns -// fallback or 0. -// -// Values are parsed with strconv.ParseInt(). If strconv.ParseInt() fails, -// tries to parse the number with strconv.ParseFloat() and truncate it to an -// int. -func (a *Alfred) GetInt(key string, fallback ...int) int { - - var fb int - - if len(fallback) > 0 { - fb = fallback[0] - } - s, ok := a.Lookup(key) - if !ok { - return fb - } - - i, err := parseInt(s) - if err != nil { - return fb - } - - return int(i) -} - -// GetFloat returns the value for envvar "key" as a float. -// It accepts one optional "fallback" argument. If no envvar is set, returns -// fallback or 0.0. -// -// Values are parsed with strconv.ParseFloat(). -func (a *Alfred) GetFloat(key string, fallback ...float64) float64 { - - var fb float64 - - if len(fallback) > 0 { - fb = fallback[0] - } - s, ok := a.Lookup(key) - if !ok { - return fb - } - - n, err := strconv.ParseFloat(s, 64) - if err != nil { - return fb - } - - return n -} - -// GetDuration returns the value for envvar "key" as a time.Duration. -// It accepts one optional "fallback" argument. If no envvar is set, returns -// fallback or 0. -// -// Values are parsed with time.ParseDuration(). -func (a *Alfred) GetDuration(key string, fallback ...time.Duration) time.Duration { - - var fb time.Duration - - if len(fallback) > 0 { - fb = fallback[0] - } - s, ok := a.Lookup(key) - if !ok { - return fb - } - - d, err := time.ParseDuration(s) - if err != nil { - return fb - } - - return d -} - -// GetBool returns the value for envvar "key" as a boolean. -// It accepts one optional "fallback" argument. If no envvar is set, returns -// fallback or false. -// -// Values are parsed with strconv.ParseBool(). -func (a *Alfred) GetBool(key string, fallback ...bool) bool { - - var fb bool - - if len(fallback) > 0 { - fb = fallback[0] - } - s, ok := a.Lookup(key) - if !ok { - return fb - } - - b, err := strconv.ParseBool(s) - if err != nil { - return fb - } - - return b -} - -// Check that minimum required values are set. -func validateAlfred(a *Alfred) error { - - var ( - issues []string - required = map[string]string{ - EnvVarBundleID: a.Get(EnvVarBundleID), - EnvVarCacheDir: a.Get(EnvVarCacheDir), - EnvVarDataDir: a.Get(EnvVarDataDir), - } - ) - - for k, v := range required { - if v == "" { - issues = append(issues, k+" is not set") - } - } - - if issues != nil { - return fmt.Errorf("Invalid Workflow environment: %s", strings.Join(issues, ", ")) - } - - return nil -} - -// parse an int, falling back to parsing it as a float -func parseInt(s string) (int, error) { - i, err := strconv.ParseInt(s, 10, 32) - if err == nil { - return int(i), nil - } - - // Try to parse as float, then convert - n, err := strconv.ParseFloat(s, 64) - if err != nil { - return 0, fmt.Errorf("invalid int: %v", s) - } - return int(n), nil -} - -// Convert interface{} to a string. -func stringify(v interface{}) string { return fmt.Sprintf("%v", v) } diff --git a/alfred_test.go b/alfred_test.go index e3b8a66..1f12d05 100644 --- a/alfred_test.go +++ b/alfred_test.go @@ -19,8 +19,6 @@ var ( testAction = false testBrowse = false testTrigger = false - testSetConf = false - testRmConf = false testSetTheme = false ) @@ -30,11 +28,11 @@ func TestAlfred(t *testing.T) { if testSearch { - if err := a.Search("").Do(); err != nil { + if err := a.Search(""); err != nil { t.Error(err) } - if err := a.Search("awgo alfred").Do(); err != nil { + if err := a.Search("awgo alfred"); err != nil { t.Error(err) } } @@ -43,83 +41,28 @@ func TestAlfred(t *testing.T) { h := os.ExpandEnv("$HOME") - if err := a.Action(h+"/Desktop", ".").Do(); err != nil { + if err := a.Action(h+"/Desktop", "."); err != nil { t.Error(err) } } if testBrowse { - if err := a.Browse(".").Do(); err != nil { + if err := a.Browse("."); err != nil { t.Error(err) } } if testTrigger { - if err := a.RunTrigger("test", "AwGo, yo!").Do(); err != nil { - t.Error(err) - } - } - - if testSetConf { - - if err := a.SetConfig("AWGO_TEST_UNITTEST", "AwGo, yo!", true).Do(); err != nil { - t.Error(err) - } - - many := map[string]string{ - "MANY_0": "VALUE_0", - "MANY_1": "VALUE_1", - "MANY_2": "VALUE_2", - "MANY_3": "VALUE_3", - "MANY_4": "VALUE_4", - "MANY_5": "VALUE_5", - "MANY_6": "VALUE_6", - "MANY_7": "VALUE_7", - "MANY_8": "VALUE_8", - "MANY_9": "VALUE_9", - } - - a := NewAlfred() - for k, v := range many { - a.SetConfig(k, v, true) - } - if err := a.Do(); err != nil { - t.Error(err) - } - } - - if testRmConf { - - keys := []string{ - "AWGO_TEST_BOOL", "AWGO_TEST_DURATION", "AWGO_TEST_EMPTY", - "AWGO_TEST_FLOAT", "AWGO_TEST_INT", "AWGO_TEST_NAME", - "AWGO_TEST_QUOTED", "AWGO_TEST_UNITTEST", - "BENCH_0", "BENCH_1", "BENCH_10", "BENCH_11", - "BENCH_12", "BENCH_13", "BENCH_14", "BENCH_15", - "BENCH_16", "BENCH_17", "BENCH_18", "BENCH_19", - "BENCH_2", "BENCH_20", "BENCH_21", "BENCH_22", - "BENCH_23", "BENCH_24", "BENCH_3", "BENCH_4", - "BENCH_5", "BENCH_6", "BENCH_7", "BENCH_8", - "BENCH_9", "MANY_0", "MANY_1", "MANY_2", - "MANY_3", "MANY_4", "MANY_5", "MANY_6", - "MANY_7", "MANY_8", "MANY_9", - } - a := NewAlfred() - - for _, k := range keys { - a.RemoveConfig(k) - } - - if err := a.Do(); err != nil { + if err := a.RunTrigger("test", "AwGo, yo!"); err != nil { t.Error(err) } } if testSetTheme { - if err := a.SetTheme("Alfred Notepad").Do(); err != nil { + if err := a.SetTheme("Alfred Notepad"); err != nil { t.Error(err) } } diff --git a/config.go b/config.go new file mode 100644 index 0000000..194c04b --- /dev/null +++ b/config.go @@ -0,0 +1,333 @@ +// +// Copyright (c) 2018 Dean Jackson +// +// MIT Licence. See http://opensource.org/licenses/MIT +// +// Created on 2018-06-30 +// + +package aw + +import ( + "errors" + "fmt" + "strconv" + "strings" + "time" + + "github.com/deanishe/awgo/util" +) + +// Environment variables containing workflow and Alfred info. +// +// Read the values with os.Getenv(EnvVarName) or via Alfred: +// +// // Returns a string +// Alfred.Get(EnvVarName) +// // Parse string into a bool +// Alfred.GetBool(EnvVarDebug) +// +const ( + // Workflow info assigned in Alfred Preferences + EnvVarName = "alfred_workflow_name" // Name of workflow + EnvVarBundleID = "alfred_workflow_bundleid" // Bundle ID + EnvVarVersion = "alfred_workflow_version" // Workflow version + + EnvVarUID = "alfred_workflow_uid" // Random UID assigned by Alfred + + // Workflow storage directories + EnvVarCacheDir = "alfred_workflow_cache" // For temporary data + EnvVarDataDir = "alfred_workflow_data" // For permanent data + + // Set to 1 when Alfred's debugger is open + EnvVarDebug = "alfred_debug" + + // Theme info. Colours are in rgba format, e.g. "rgba(255,255,255,1.0)" + EnvVarTheme = "alfred_theme" // ID of user's selected theme + EnvVarThemeBG = "alfred_theme_background" // Background colour + EnvVarThemeSelectionBG = "alfred_theme_selection_background" // BG colour of selected item + + // Alfred info + EnvVarAlfredVersion = "alfred_version" // Alfred's version number + EnvVarAlfredBuild = "alfred_version_build" // Alfred's build number + EnvVarPreferences = "alfred_preferences" // Path to "Alfred.alfredpreferences" file + // Machine-specific hash. Machine preferences are stored in + // Alfred.alfredpreferences/local/ + EnvVarLocalhash = "alfred_preferences_localhash" +) + +// Config loads workflow settings from Alfred's environment variables. +// +// The Get* methods read a variable from the environment, converting it to +// the desired type, and the Set() method saves a variable to info.plist. +// +// NOTE: Because calling Alfred via AppleScript is very slow (~0.2s/call), +// Config users a "Doer" API for setting variables, whereby calls are collected +// and all executed at once when Config.Do() is called: +// +// cfg := NewConfig() +// if err := cfg.Set("key1", "value1").Set("key2", "value2").Do(); err != nil { +// // handle error +// } +// +// Finally, you can use Config.To() to populate a struct from environment +// variables, and Config.From() to read a struct's fields and save them +// to info.plist. +type Config struct { + Env + scripts []string + err error +} + +// NewConfig creates a new Config from the environment. +// +// It accepts one optional Env argument. If an Env is passed, Config +// is initialised from that instead of the system environment. +func NewConfig(env ...Env) *Config { + + var e Env + if len(env) > 0 { + e = env[0] + } else { + e = sysEnv{} + } + return &Config{e, []string{}, nil} +} + +// Get returns the value for envvar "key". +// It accepts one optional "fallback" argument. If no envvar is set, returns +// fallback or an empty string. +// +// If a variable is set, but empty, its value is used. +func (cfg *Config) Get(key string, fallback ...string) string { + + var fb string + + if len(fallback) > 0 { + fb = fallback[0] + } + s, ok := cfg.Lookup(key) + if !ok { + return fb + } + return s +} + +// GetString is a synonym for Get. +func (cfg *Config) GetString(key string, fallback ...string) string { + return cfg.Get(key, fallback...) +} + +// GetInt returns the value for envvar "key" as an int. +// It accepts one optional "fallback" argument. If no envvar is set, returns +// fallback or 0. +// +// Values are parsed with strconv.ParseInt(). If strconv.ParseInt() fails, +// tries to parse the number with strconv.ParseFloat() and truncate it to an +// int. +func (cfg *Config) GetInt(key string, fallback ...int) int { + + var fb int + + if len(fallback) > 0 { + fb = fallback[0] + } + s, ok := cfg.Lookup(key) + if !ok { + return fb + } + + i, err := parseInt(s) + if err != nil { + return fb + } + + return int(i) +} + +// GetFloat returns the value for envvar "key" as a float. +// It accepts one optional "fallback" argument. If no envvar is set, returns +// fallback or 0.0. +// +// Values are parsed with strconv.ParseFloat(). +func (cfg *Config) GetFloat(key string, fallback ...float64) float64 { + + var fb float64 + + if len(fallback) > 0 { + fb = fallback[0] + } + s, ok := cfg.Lookup(key) + if !ok { + return fb + } + + n, err := strconv.ParseFloat(s, 64) + if err != nil { + return fb + } + + return n +} + +// GetDuration returns the value for envvar "key" as a time.Duration. +// It accepts one optional "fallback" argument. If no envvar is set, returns +// fallback or 0. +// +// Values are parsed with time.ParseDuration(). +func (cfg *Config) GetDuration(key string, fallback ...time.Duration) time.Duration { + + var fb time.Duration + + if len(fallback) > 0 { + fb = fallback[0] + } + s, ok := cfg.Lookup(key) + if !ok { + return fb + } + + d, err := time.ParseDuration(s) + if err != nil { + return fb + } + + return d +} + +// GetBool returns the value for envvar "key" as a boolean. +// It accepts one optional "fallback" argument. If no envvar is set, returns +// fallback or false. +// +// Values are parsed with strconv.ParseBool(). +func (cfg *Config) GetBool(key string, fallback ...bool) bool { + + var fb bool + + if len(fallback) > 0 { + fb = fallback[0] + } + s, ok := cfg.Lookup(key) + if !ok { + return fb + } + + b, err := strconv.ParseBool(s) + if err != nil { + return fb + } + + return b +} + +// Set saves a workflow variable to info.plist. +// +// It accepts one optional bundleID argument, which is the bundle ID of the +// workflow whose configuration should be changed. +// If not specified, it defaults to the current workflow's. +func (cfg *Config) Set(key, value string, export bool, bundleID ...string) *Config { + + bid := cfg.getBundleID(bundleID...) + opts := map[string]interface{}{ + "toValue": value, + "inWorkflow": bid, + "exportable": export, + } + + return cfg.addScriptOpts(scriptSetConfig, key, opts) +} + +// Unset removes a workflow variable from info.plist. +// +// It accepts one optional bundleID argument, which is the bundle ID of the +// workflow whose configuration should be changed. +// If not specified, it defaults to the current workflow's. +func (cfg *Config) Unset(key string, bundleID ...string) *Config { + + bid := cfg.getBundleID(bundleID...) + opts := map[string]interface{}{ + "inWorkflow": bid, + } + + return cfg.addScriptOpts(scriptRmConfig, key, opts) +} + +// Do calls Alfred and runs the accumulated actions. +// +// If an error was encountered while preparing any commands, it will be +// returned here. It also returns an error if there are no commands to run, +// or if the call to Alfred fails. +// +// Succeed or fail, any accumulated scripts and errors are cleared when Do() +// is called. +func (cfg *Config) Do() error { + + var err error + + if cfg.err != nil { + // reset + err, cfg.err = cfg.err, nil + cfg.scripts = []string{} + + return err + } + + if len(cfg.scripts) == 0 { + return errors.New("no commands to run") + } + + script := strings.Join(cfg.scripts, "\n") + // reset + cfg.scripts = []string{} + + _, err = util.RunJS(script) + + return err +} + +// Extract bundle ID from argument or default. +func (cfg *Config) getBundleID(bundleID ...string) string { + + if len(bundleID) > 0 { + return bundleID[0] + } + + bid, _ := cfg.Lookup(EnvVarBundleID) + return bid +} + +// Add a JavaScript that takes a single argument. +func (cfg *Config) addScript(script, arg string) *Config { + + script = fmt.Sprintf(script, util.QuoteJS(arg)) + cfg.scripts = append(cfg.scripts, script) + + return cfg +} + +// Run a JavaScript that takes two arguments, a string and an object. +func (cfg *Config) addScriptOpts(script, name string, opts map[string]interface{}) *Config { + + script = fmt.Sprintf(script, util.QuoteJS(name), util.QuoteJS(opts)) + cfg.scripts = append(cfg.scripts, script) + + return cfg +} + +// parse an int, falling back to parsing it as a float +func parseInt(s string) (int, error) { + i, err := strconv.ParseInt(s, 10, 32) + if err == nil { + return int(i), nil + } + + // Try to parse as float, then convert + n, err := strconv.ParseFloat(s, 64) + if err != nil { + return 0, fmt.Errorf("invalid int: %v", s) + } + return int(n), nil +} + +// Convert interface{} to a string. +func stringify(v interface{}) string { return fmt.Sprintf("%v", v) } diff --git a/alfred_bind.go b/config_bind.go similarity index 89% rename from alfred_bind.go rename to config_bind.go index 2707fcb..fd424f5 100644 --- a/alfred_bind.go +++ b/config_bind.go @@ -3,7 +3,7 @@ // // MIT Licence. See http://opensource.org/licenses/MIT // -// Created on 2018-02-12 +// Created on 2018-06-30 // package aw @@ -19,7 +19,7 @@ import ( ) // To populates (tagged) struct v with values from the environment. -func (a *Alfred) To(v interface{}) error { +func (cfg *Config) To(v interface{}) error { binds, err := extract(v) if err != nil { @@ -27,7 +27,7 @@ func (a *Alfred) To(v interface{}) error { } for _, bind := range binds { - if err := bind.Import(a); err != nil { + if err := bind.Import(cfg); err != nil { return err } } @@ -39,18 +39,18 @@ func (a *Alfred) To(v interface{}) error { // // All supported and unignored fields are saved, although empty variables // (i.e. "") are not overwritten with Go zero values, e.g. "0" or "false". -func (a *Alfred) From(v interface{}) error { +func (cfg *Config) From(v interface{}) error { - variables, err := a.bindVars(v) + variables, err := cfg.bindVars(v) if err != nil { return err } - return a.setMulti(variables, false) + return cfg.setMulti(variables, false) } // extract binding values as {ENVVAR: value} map. -func (a *Alfred) bindVars(v interface{}) (map[string]string, error) { +func (cfg *Config) bindVars(v interface{}) (map[string]string, error) { variables := map[string]string{} @@ -60,7 +60,7 @@ func (a *Alfred) bindVars(v interface{}) (map[string]string, error) { } for _, bind := range binds { - if k, v, ok := bind.GetVar(a); ok { + if k, v, ok := bind.GetVar(cfg); ok { variables[k] = v } } @@ -68,6 +68,16 @@ func (a *Alfred) bindVars(v interface{}) (map[string]string, error) { return variables, nil } +// setMulti batches the saving of multiple variables. +func (cfg *Config) setMulti(variables map[string]string, export bool) error { + + for k, v := range variables { + cfg.Set(k, v, export) + } + + return cfg.Do() +} + // binding links an environment variable to the field of a struct. type binding struct { Name string @@ -86,7 +96,7 @@ type bindSource interface { type bindDest interface { GetString(key string, fallback ...string) string - // SetConfig(key, value string, export bool, bundleID ...string) *Alfred + // SetConfig(key, value string, export bool, bundleID ...string) *Config setMulti(variables map[string]string, export bool) error } @@ -140,38 +150,6 @@ func (bind *binding) GetVar(dst bindDest) (key, value string, ok bool) { return } -/* -// Export populates dst from target struct. -func (bind *binding) Export(dst bindDest) error { - - rv := reflect.Indirect(reflect.ValueOf(bind.Target)) - - if bind.FieldNum > rv.NumField() { - return fmt.Errorf("invalid FieldNum (%d) for %s (%v)", bind.FieldNum, bind.Name, rv) - } - - var ( - value = rv.Field(bind.FieldNum) - s = fmt.Sprintf("%v", value) - cur = dst.GetString(bind.EnvVar) - curZero = isZeroString(cur, value.Kind()) - newZero = isZeroValue(value) - ) - - // Don't pull zero-value fields into empty variables. - if curZero && newZero { - // log.Printf("[bind] %s: both empty", field.Name) - return nil - } - - if err := dst.SetConfig(bind.EnvVar, s, false).Do(); err != nil { - return err - } - - return nil -} -*/ - func (bind *binding) setValue(rv *reflect.Value, src bindSource) error { switch bind.Kind { diff --git a/alfred_bind_test.go b/config_bind_test.go similarity index 89% rename from alfred_bind_test.go rename to config_bind_test.go index ec27677..bd728ee 100644 --- a/alfred_bind_test.go +++ b/config_bind_test.go @@ -3,7 +3,7 @@ // // MIT Licence. See http://opensource.org/licenses/MIT // -// Created on 2018-02-12 +// Created on 2018-06-30 // package aw @@ -11,6 +11,7 @@ package aw import ( "fmt" "log" + "os" "reflect" "testing" "time" @@ -43,7 +44,7 @@ var ( // Test bindDest implementation that captures saves. type testDest struct { - Alf *Alfred + Cfg *Config Saves map[string]string } @@ -56,7 +57,7 @@ func (dst *testDest) setMulti(variables map[string]string, export bool) error { return nil } func (dst *testDest) GetString(key string, fallback ...string) string { - return dst.Alf.GetString(key, fallback...) + return dst.Cfg.GetString(key, fallback...) } // Verify checks that dst.Saves has the same content as saves. @@ -90,9 +91,9 @@ func (dst *testDest) Verify(saves map[string]string) error { } // Returns a test implementation of Env -func bindTestEnv() mapEnv { +func bindTestEnv() MapEnv { - return mapEnv{ + return MapEnv{ "ID": "not empty", "HOST": testHostname, "ONLINE": fmt.Sprintf("%v", testOnline), @@ -107,7 +108,7 @@ func bindTestEnv() mapEnv { // TestExtract verifies extraction of struct fields and tags. func TestExtract(t *testing.T) { - a := NewAlfred() + cfg := NewConfig() th := &testHost{} data := map[string]string{ "Hostname": "HOST", @@ -154,20 +155,20 @@ func TestExtract(t *testing.T) { } // Fail to load fields for _, bind := range binds { - if err := bind.Import(a); err == nil { + if err := bind.Import(cfg); err == nil { t.Errorf("Accepted bad binding (%s)", bind.Name) } } } -// TestAlfredTo verifies that a struct is correctly populated from an Alfred. -func TestAlfredTo(t *testing.T) { +// TestConfigTo verifies that a struct is correctly populated from a Config. +func TestConfigTo(t *testing.T) { h := &testHost{} e := bindTestEnv() - a := NewAlfred(e) + cfg := NewConfig(e) - if err := a.To(h); err != nil { + if err := cfg.To(h); err != nil { t.Fatal(err) } @@ -205,10 +206,10 @@ func TestAlfredTo(t *testing.T) { } -// TestAlfredFrom verifies that a bindDest is correctly populated from a (tagged) struct. -func TestAlfredFrom(t *testing.T) { +// TestConfigFrom verifies that a bindDest is correctly populated from a (tagged) struct. +func TestConfigFrom(t *testing.T) { - e := mapEnv{ + e := MapEnv{ "ID": "", "HOST": "", "ONLINE": "true", // must be set: "" is the same as false @@ -219,12 +220,12 @@ func TestAlfredFrom(t *testing.T) { "PING_AVERAGE": "0.0", // zero value } - a := NewAlfred(e) + cfg := NewConfig(e) th := &testHost{} - // Check Alfred is set up correctly + // Check Config is set up correctly for k, v := range e { - s := a.Get(k) + s := cfg.Get(k) if s != v { t.Errorf("Bad %s. Expected=%v, Got=%v", k, v, s) } @@ -258,9 +259,9 @@ func TestAlfredFrom(t *testing.T) { // Exports v into a testDest and verifies it against x. testBind := func(v interface{}, x map[string]string) { - dst := &testDest{a, map[string]string{}} + dst := &testDest{cfg, map[string]string{}} - variables, err := a.bindVars(v) + variables, err := cfg.bindVars(v) if err != nil { t.Fatal(err) } @@ -326,6 +327,54 @@ func TestVarName(t *testing.T) { } } +// Populate a struct from workflow/environment variables. See EnvVarForField +// for information on how fields are mapped to environment variables if +// no variable name is specified using an `env:"name"` tag. +func ExampleConfig_To() { + + // Set some test values + os.Setenv("USERNAME", "dave") + os.Setenv("API_KEY", "hunter2") + os.Setenv("INTERVAL", "5m") + os.Setenv("FORCE", "1") + + // Program settings to load from env + type Settings struct { + Username string + APIKey string + UpdateInterval time.Duration `env:"INTERVAL"` + Force bool + } + + var ( + s = &Settings{} + cfg = NewConfig() + ) + + // Populate Settings from workflow/environment variables + if err := cfg.To(s); err != nil { + panic(err) + } + + fmt.Println(s.Username) + fmt.Println(s.APIKey) + fmt.Println(s.UpdateInterval) + fmt.Println(s.Force) + + // Output: + // dave + // hunter2 + // 5m0s + // true + + unsetEnv( + "USERNAME", + "API_KEY", + "INTERVAL", + "FORCE", + ) +} + // Rules for generating an environment variable name from a struct field name. func ExampleEnvVarForField() { // Single-case words are upper-cased diff --git a/alfred_config_test.go b/config_test.go similarity index 72% rename from alfred_config_test.go rename to config_test.go index 196aff1..3afcc49 100644 --- a/alfred_config_test.go +++ b/config_test.go @@ -1,9 +1,9 @@ // -// Copyright (c) 2017 Dean Jackson +// Copyright (c) 2018 Dean Jackson // // MIT Licence. See http://opensource.org/licenses/MIT // -// Created on 2017-08-13 +// Created on 2018-06-30 // package aw @@ -15,8 +15,8 @@ import ( "time" ) -// TestAlfredEnv verifies that Alfred holds the expected values. -func TestAlfredEnv(t *testing.T) { +// TestConfigEnv verifies that Config holds the expected values. +func TestConfigEnv(t *testing.T) { data := []struct { name, x, key string @@ -25,8 +25,8 @@ func TestAlfredEnv(t *testing.T) { {"Name", tName, EnvVarName}, {"BundleID", tBundleID, EnvVarBundleID}, {"UID", tUID, EnvVarUID}, - {"AlfredVersion", tAlfredVersion, EnvVarAlfredVersion}, - {"AlfredBuild", tAlfredBuild, EnvVarAlfredBuild}, + {"ConfigVersion", tAlfredVersion, EnvVarAlfredVersion}, + {"ConfigBuild", tAlfredBuild, EnvVarAlfredBuild}, {"Theme", tTheme, EnvVarTheme}, {"ThemeBackground", tThemeBackground, EnvVarThemeBG}, {"ThemeSelectionBackground", tThemeSelectionBackground, EnvVarThemeSelectionBG}, @@ -36,15 +36,15 @@ func TestAlfredEnv(t *testing.T) { {"CacheDir", tDataDir, EnvVarDataDir}, } - ctx := NewAlfred(testEnv) + cfg := NewConfig(testEnv) - v := ctx.GetBool(EnvVarDebug) + v := cfg.GetBool(EnvVarDebug) if v != tDebug { t.Errorf("bad Debug. Expected=%v, Got=%v", tDebug, v) } for _, td := range data { - s := ctx.Get(td.key) + s := cfg.Get(td.key) if s != td.x { t.Errorf("Bad %s. Expected=%v, Got=%v", td.name, td.x, s) } @@ -52,7 +52,7 @@ func TestAlfredEnv(t *testing.T) { } func TestGet(t *testing.T) { - env := mapEnv{ + env := MapEnv{ "key": "value", "key2": "value2", "empty": "", @@ -76,11 +76,11 @@ func TestGet(t *testing.T) { {"key3", []string{"bob"}, "bob"}, } - e := NewAlfred(env) + cfg := NewConfig(env) // Verify env is the same for k, x := range env { - v := e.Get(k) + v := cfg.Get(k) if v != x { t.Errorf("Bad '%s'. Expected=%v, Got=%v", k, x, v) } @@ -88,7 +88,7 @@ func TestGet(t *testing.T) { // Test Get for _, td := range data { - v := e.Get(td.key, td.fb...) + v := cfg.Get(td.key, td.fb...) if v != td.out { t.Errorf("Bad '%s'. Expected=%v, Got=%v", td.key, td.out, v) } @@ -97,7 +97,7 @@ func TestGet(t *testing.T) { } func TestGetInt(t *testing.T) { - env := mapEnv{ + env := MapEnv{ "one": "1", "two": "2", "zero": "0", @@ -130,10 +130,10 @@ func TestGetInt(t *testing.T) { {"float", []int{5}, 3}, } - e := NewAlfred(env) + cfg := NewConfig(env) // Test GetInt for _, td := range data { - v := e.GetInt(td.key, td.fb...) + v := cfg.GetInt(td.key, td.fb...) if v != td.out { t.Errorf("Bad '%s'. Expected=%v, Got=%v", td.key, td.out, v) } @@ -142,7 +142,7 @@ func TestGetInt(t *testing.T) { } func TestGetFloat(t *testing.T) { - env := mapEnv{ + env := MapEnv{ "one.three": "1.3", "two": "2.0", "zero": "0", @@ -171,10 +171,10 @@ func TestGetFloat(t *testing.T) { {"word", []float64{5.0}, 5.0}, } - e := NewAlfred(env) + cfg := NewConfig(env) // Test GetFloat for _, td := range data { - v := e.GetFloat(td.key, td.fb...) + v := cfg.GetFloat(td.key, td.fb...) if v != td.out { t.Errorf("Bad '%s'. Expected=%v, Got=%v", td.key, td.out, v) } @@ -183,7 +183,7 @@ func TestGetFloat(t *testing.T) { } func TestGetDuration(t *testing.T) { - env := mapEnv{ + env := MapEnv{ "5mins": "5m", "1hour": "1h", "zero": "0", @@ -213,11 +213,11 @@ func TestGetDuration(t *testing.T) { {"word", []time.Duration{time.Second * 5}, time.Second * 5}, } - e := NewAlfred(env) + cfg := NewConfig(env) // Test GetDuration for _, td := range data { - v := e.GetDuration(td.key, td.fb...) + v := cfg.GetDuration(td.key, td.fb...) if v != td.out { t.Errorf("Bad '%s'. Expected=%v, Got=%v", td.key, td.out, v) } @@ -226,7 +226,7 @@ func TestGetDuration(t *testing.T) { } func TestGetBool(t *testing.T) { - env := mapEnv{ + env := MapEnv{ "empty": "", "t": "t", "f": "f", @@ -260,11 +260,11 @@ func TestGetBool(t *testing.T) { {"word", []bool{true}, true}, } - e := NewAlfred(env) + cfg := NewConfig(env) // Test GetBool for _, td := range data { - v := e.GetBool(td.key, td.fb...) + v := cfg.GetBool(td.key, td.fb...) if v != td.out { t.Errorf("Bad '%s'. Expected=%v, Got=%v", td.key, td.out, v) } @@ -297,21 +297,21 @@ func TestStringify(t *testing.T) { } } -// Basic usage of Alfred.Get. Returns an empty string if variable is unset. -func ExampleAlfred_Get() { +// Basic usage of Config.Get. Returns an empty string if variable is unset. +func ExampleConfig_Get() { // Set some test variables os.Setenv("TEST_NAME", "Bob Smith") os.Setenv("TEST_ADDRESS", "7, Dreary Lane") - // New Alfred from environment - a := NewAlfred() + // New Config from environment + cfg := NewConfig() - fmt.Println(a.Get("TEST_NAME")) - fmt.Println(a.Get("TEST_ADDRESS")) - fmt.Println(a.Get("TEST_NONEXISTENT")) // unset variable + fmt.Println(cfg.Get("TEST_NAME")) + fmt.Println(cfg.Get("TEST_ADDRESS")) + fmt.Println(cfg.Get("TEST_NONEXISTENT")) // unset variable // GetString is a synonym - fmt.Println(a.GetString("TEST_NAME")) + fmt.Println(cfg.GetString("TEST_NAME")) // Output: // Bob Smith @@ -323,19 +323,19 @@ func ExampleAlfred_Get() { } // The fallback value is returned if the variable is unset. -func ExampleAlfred_Get_fallback() { +func ExampleConfig_Get_fallback() { // Set some test variables os.Setenv("TEST_NAME", "Bob Smith") os.Setenv("TEST_ADDRESS", "7, Dreary Lane") os.Setenv("TEST_EMAIL", "") - // New Alfred from environment - a := NewAlfred() + // New Config from environment + cfg := NewConfig() - fmt.Println(a.Get("TEST_NAME", "default name")) // fallback ignored - fmt.Println(a.Get("TEST_ADDRESS", "default address")) // fallback ignored - fmt.Println(a.Get("TEST_EMAIL", "test@example.com")) // fallback ignored (var is empty, not unset) - fmt.Println(a.Get("TEST_NONEXISTENT", "hi there!")) // unset variable + fmt.Println(cfg.Get("TEST_NAME", "default name")) // fallback ignored + fmt.Println(cfg.Get("TEST_ADDRESS", "default address")) // fallback ignored + fmt.Println(cfg.Get("TEST_EMAIL", "test@example.com")) // fallback ignored (var is empty, not unset) + fmt.Println(cfg.Get("TEST_NONEXISTENT", "hi there!")) // unset variable // Output: // Bob Smith @@ -347,18 +347,18 @@ func ExampleAlfred_Get_fallback() { } // Getting int values with and without fallbacks. -func ExampleAlfred_GetInt() { +func ExampleConfig_GetInt() { // Set some test variables os.Setenv("PORT", "3000") os.Setenv("PING_INTERVAL", "") - // New Alfred from environment - a := NewAlfred() + // New Config from environment + cfg := NewConfig() - fmt.Println(a.GetInt("PORT")) - fmt.Println(a.GetInt("PORT", 5000)) // fallback is ignored - fmt.Println(a.GetInt("PING_INTERVAL")) // returns zero value - fmt.Println(a.GetInt("PING_INTERVAL", 60)) // returns fallback + fmt.Println(cfg.GetInt("PORT")) + fmt.Println(cfg.GetInt("PORT", 5000)) // fallback is ignored + fmt.Println(cfg.GetInt("PING_INTERVAL")) // returns zero value + fmt.Println(cfg.GetInt("PING_INTERVAL", 60)) // returns fallback // Output: // 3000 // 3000 @@ -369,17 +369,17 @@ func ExampleAlfred_GetInt() { } // Strings are parsed to floats using strconv.ParseFloat(). -func ExampleAlfred_GetFloat() { +func ExampleConfig_GetFloat() { // Set some test variables os.Setenv("TOTAL_SCORE", "172.3") os.Setenv("AVERAGE_SCORE", "7.54") - // New Alfred from environment - a := NewAlfred() + // New Config from environment + cfg := NewConfig() - fmt.Printf("%0.2f\n", a.GetFloat("TOTAL_SCORE")) - fmt.Printf("%0.1f\n", a.GetFloat("AVERAGE_SCORE")) - fmt.Println(a.GetFloat("NON_EXISTENT_SCORE", 120.5)) + fmt.Printf("%0.2f\n", cfg.GetFloat("TOTAL_SCORE")) + fmt.Printf("%0.1f\n", cfg.GetFloat("AVERAGE_SCORE")) + fmt.Println(cfg.GetFloat("NON_EXISTENT_SCORE", 120.5)) // Output: // 172.30 // 7.5 @@ -389,24 +389,24 @@ func ExampleAlfred_GetFloat() { } // Durations are parsed using time.ParseDuration. -func ExampleAlfred_GetDuration() { +func ExampleConfig_GetDuration() { // Set some test variables os.Setenv("DURATION_NAP", "20m") os.Setenv("DURATION_EGG", "5m") os.Setenv("DURATION_BIG_EGG", "") os.Setenv("DURATION_MATCH", "1.5h") - // New Alfred from environment - a := NewAlfred() + // New Config from environment + cfg := NewConfig() // returns time.Duration - fmt.Println(a.GetDuration("DURATION_NAP")) - fmt.Println(a.GetDuration("DURATION_EGG") * 2) + fmt.Println(cfg.GetDuration("DURATION_NAP")) + fmt.Println(cfg.GetDuration("DURATION_EGG") * 2) // fallback with unset variable - fmt.Println(a.GetDuration("DURATION_POWERNAP", time.Minute*45)) + fmt.Println(cfg.GetDuration("DURATION_POWERNAP", time.Minute*45)) // or an empty one - fmt.Println(a.GetDuration("DURATION_BIG_EGG", time.Minute*10)) - fmt.Println(a.GetDuration("DURATION_MATCH").Minutes()) + fmt.Println(cfg.GetDuration("DURATION_BIG_EGG", time.Minute*10)) + fmt.Println(cfg.GetDuration("DURATION_MATCH").Minutes()) // Output: // 20m0s @@ -424,7 +424,7 @@ func ExampleAlfred_GetDuration() { } // Strings are parsed using strconv.ParseBool(). -func ExampleAlfred_GetBool() { +func ExampleConfig_GetBool() { // Set some test variables os.Setenv("LIKE_PEAS", "t") @@ -435,20 +435,20 @@ func ExampleAlfred_GetBool() { os.Setenv("LIKE_BVB", "false") os.Setenv("LIKE_BAYERN", "FALSE") - // New Alfred from environment - a := NewAlfred() + // New Config from environment + cfg := NewConfig() // strconv.ParseBool() supports many formats - fmt.Println(a.GetBool("LIKE_PEAS")) - fmt.Println(a.GetBool("LIKE_CARROTS")) - fmt.Println(a.GetBool("LIKE_BEANS")) - fmt.Println(a.GetBool("LIKE_LIVER")) - fmt.Println(a.GetBool("LIKE_TOMATOES")) - fmt.Println(a.GetBool("LIKE_BVB")) - fmt.Println(a.GetBool("LIKE_BAYERN")) + fmt.Println(cfg.GetBool("LIKE_PEAS")) + fmt.Println(cfg.GetBool("LIKE_CARROTS")) + fmt.Println(cfg.GetBool("LIKE_BEANS")) + fmt.Println(cfg.GetBool("LIKE_LIVER")) + fmt.Println(cfg.GetBool("LIKE_TOMATOES")) + fmt.Println(cfg.GetBool("LIKE_BVB")) + fmt.Println(cfg.GetBool("LIKE_BAYERN")) // Fallback - fmt.Println(a.GetBool("LIKE_BEER", true)) + fmt.Println(cfg.GetBool("LIKE_BEER", true)) // Output: // true diff --git a/doc.go b/doc.go index 6b971aa..dcfb043 100644 --- a/doc.go +++ b/doc.go @@ -14,8 +14,9 @@ https://www.alfredapp.com/ It provides APIs for interacting with Alfred (e.g. Script Filter feedback) and the workflow environment (variables, caches, settings). -NOTE: AwGo is currently in development. The API *will* change as I learn to -write idiomatic Go, and should not be considered stable until v1.0. +NOTE: AwGo is currently in development. The API *will* change and should +not be considered stable until v1.0. Until then, vendoring AwGo (e.g. +with dep or vgo) is strongly recommended. Links @@ -28,6 +29,10 @@ Issues: https://github.com/deanishe/awgo/issues Licence: https://github.com/deanishe/awgo/blob/master/LICENCE +Be sure to also check out the _examples/ subdirectory, which contains +some simple, but complete, workflows that demonstrate the features +of AwGo and useful workflow idioms. + Features @@ -41,6 +46,7 @@ The main features are: - Simple, but powerful, API for caching/saving workflow data. - Run scripts and script code. - Call Alfred's AppleScript API from Go. + - Read and write workflow settings from info.plist. - Workflow update API with built-in support for GitHub releases. - Pre-configured logging for easier debugging, with a rotated log file. - Catches panics, logs stack trace and shows user an error message. diff --git a/env.go b/env.go index d962d18..321cc92 100644 --- a/env.go +++ b/env.go @@ -8,7 +8,11 @@ package aw -import "os" +import ( + "fmt" + "os" + "strings" +) // Env is the datasource for configuration lookups. // @@ -32,8 +36,44 @@ type Env interface { Lookup(key string) (string, bool) } +// MapEnv is a testing helper that makes it simple to convert a map[string]string +// to an Env. +type MapEnv map[string]string + +// Lookup implements Env. It returns values from the map. +func (env MapEnv) Lookup(key string) (string, bool) { + s, ok := env[key] + return s, ok +} + // sysEnv implements Env based on the real environment. type sysEnv struct{} // Lookup wraps os.LookupEnv(). func (e sysEnv) Lookup(key string) (string, bool) { return os.LookupEnv(key) } + +// Check that minimum required values are set. +func validateEnv(env Env) error { + + var ( + issues []string + required = []string{ + EnvVarBundleID, + EnvVarCacheDir, + EnvVarDataDir, + } + ) + + for _, k := range required { + v, ok := env.Lookup(k) + if !ok || v == "" { + issues = append(issues, k+" is not set") + } + } + + if issues != nil { + return fmt.Errorf("Invalid Workflow environment: %s", strings.Join(issues, ", ")) + } + + return nil +} diff --git a/testutils_test.go b/testutils_test.go index 8c05149..2da6ed1 100644 --- a/testutils_test.go +++ b/testutils_test.go @@ -16,14 +16,6 @@ import ( "testing" ) -// mapEnv is a string: string mapping that implements Env. -type mapEnv map[string]string - -func (env mapEnv) Lookup(key string) (string, bool) { - s, ok := env[key] - return s, ok -} - var ( tVersion = "0.14" tName = "AwGo" @@ -40,7 +32,7 @@ var ( tCacheDir = os.ExpandEnv("$HOME/Library/Caches/com.runningwithcrayons.Alfred-3/Workflow Data/net.deanishe.awgo") tDataDir = os.ExpandEnv("$HOME/Library/Application Support/Alfred 3/Workflow Data/net.deanishe.awgo") - testEnv = mapEnv{ + testEnv = MapEnv{ EnvVarVersion: tVersion, EnvVarName: tName, EnvVarBundleID: tBundleID, @@ -100,8 +92,8 @@ var ( ) // Call function with a test environment. -func withTestEnv(fun func(e mapEnv)) { - e := mapEnv{ +func withTestEnv(fun func(e MapEnv)) { + e := MapEnv{ EnvVarVersion: tVersion, EnvVarName: tName, EnvVarBundleID: tBundleID, @@ -124,7 +116,7 @@ func withTestEnv(fun func(e mapEnv)) { // Call function in a test workflow environment. func withTestWf(fun func(wf *Workflow)) { - withTestEnv(func(e mapEnv) { + withTestEnv(func(e MapEnv) { var ( curdir, dir string @@ -216,17 +208,17 @@ func TestWithTestWf(t *testing.T) { {"Name", tName, wf.Name()}, {"BundleID", tBundleID, wf.BundleID()}, - {"Ctx.UID", tUID, wf.Alfred.Get(EnvVarUID)}, - {"Ctx.AlfredVersion", tAlfredVersion, wf.Alfred.Get(EnvVarAlfredVersion)}, - {"Ctx.AlfredBuild", tAlfredBuild, wf.Alfred.Get(EnvVarAlfredBuild)}, - {"Ctx.Theme", tTheme, wf.Alfred.Get(EnvVarTheme)}, - {"Ctx.ThemeBackground", tThemeBackground, wf.Alfred.Get(EnvVarThemeBG)}, - {"Ctx.ThemeSelectionBackground", tThemeSelectionBackground, - wf.Alfred.Get(EnvVarThemeSelectionBG)}, - {"Ctx.Preferences", tPreferences, wf.Alfred.Get(EnvVarPreferences)}, - {"Ctx.Localhash", tLocalhash, wf.Alfred.Get(EnvVarLocalhash)}, - {"Ctx.CacheDir", cd, wf.Alfred.Get(EnvVarCacheDir)}, - {"Ctx.DataDir", dd, wf.Alfred.Get(EnvVarDataDir)}, + {"Config.UID", tUID, wf.Config.Get(EnvVarUID)}, + {"Config.AlfredVersion", tAlfredVersion, wf.Config.Get(EnvVarAlfredVersion)}, + {"Config.AlfredBuild", tAlfredBuild, wf.Config.Get(EnvVarAlfredBuild)}, + {"Config.Theme", tTheme, wf.Config.Get(EnvVarTheme)}, + {"Config.ThemeBackground", tThemeBackground, wf.Config.Get(EnvVarThemeBG)}, + {"Config.ThemeSelectionBackground", tThemeSelectionBackground, + wf.Config.Get(EnvVarThemeSelectionBG)}, + {"Config.Preferences", tPreferences, wf.Config.Get(EnvVarPreferences)}, + {"Config.Localhash", tLocalhash, wf.Config.Get(EnvVarLocalhash)}, + {"Config.CacheDir", cd, wf.Config.Get(EnvVarCacheDir)}, + {"Config.DataDir", dd, wf.Config.Get(EnvVarDataDir)}, } if wf.Debug() != tDebug { diff --git a/workflow.go b/workflow.go index 1061604..6b03485 100644 --- a/workflow.go +++ b/workflow.go @@ -71,13 +71,11 @@ func init() { // See the _examples/ subdirectory for some full examples of workflows. type Workflow struct { sync.WaitGroup - // The response that will be sent to Alfred. Workflow provides - // convenience wrapper methods, so you don't normally have to - // interact with this directly. - Feedback *Feedback + // Interface to workflow's settings. + // Reads workflow variables by type and saves new values to info.plist. + Config *Config - // Interface to Alfred. - // Access workflow variables by type and save settings to info.plist. + // Call Alfred's AppleScript functions. Alfred *Alfred // Cache is a Cache pointing to the workflow's cache directory. @@ -88,6 +86,11 @@ type Workflow struct { // persist until the user closes Alfred or runs a different workflow. Session *Session + // The response that will be sent to Alfred. Workflow provides + // convenience wrapper methods, so you don't normally have to + // interact with this directly. + Feedback *Feedback + // Updater fetches updates for the workflow. Updater Updater @@ -135,7 +138,12 @@ func NewFromEnv(env Env, opts ...Option) *Workflow { env = sysEnv{} } + if err := validateEnv(env); err != nil { + panic(err) + } + wf := &Workflow{ + Config: NewConfig(env), Alfred: NewAlfred(env), Feedback: &Feedback{}, logPrefix: DefaultLogPrefix, @@ -148,9 +156,6 @@ func NewFromEnv(env Env, opts ...Option) *Workflow { wf.MagicActions = defaultMagicActions(wf) wf.Configure(opts...) - if err := validateAlfred(wf.Alfred); err != nil { - panic(err) - } wf.Cache = NewCache(wf.CacheDir()) wf.Data = NewCache(wf.DataDir()) @@ -226,7 +231,7 @@ func (wf *Workflow) initializeLogging() { // setup sheet in Alfred Preferences. func (wf *Workflow) BundleID() string { - s := wf.Alfred.Get(EnvVarBundleID) + s := wf.Config.Get(EnvVarBundleID) if s == "" { wf.Fatal("No bundle ID set. You *must* set a bundle ID to use AwGo.") } @@ -235,11 +240,11 @@ func (wf *Workflow) BundleID() string { // Name returns the workflow's name as specified in the workflow's main // setup sheet in Alfred Preferences. -func (wf *Workflow) Name() string { return wf.Alfred.Get(EnvVarName) } +func (wf *Workflow) Name() string { return wf.Config.Get(EnvVarName) } // Version returns the workflow's version set in the workflow's configuration // sheet in Alfred Preferences. -func (wf *Workflow) Version() string { return wf.Alfred.Get(EnvVarVersion) } +func (wf *Workflow) Version() string { return wf.Config.Get(EnvVarVersion) } // SessionID returns the session ID for this run of the workflow. // This is used internally for session-scoped caching. @@ -265,7 +270,7 @@ func (wf *Workflow) SessionID() string { } // Debug returns true if Alfred's debugger is open. -func (wf *Workflow) Debug() bool { return wf.Alfred.GetBool(EnvVarDebug) } +func (wf *Workflow) Debug() bool { return wf.Config.GetBool(EnvVarDebug) } // Args returns command-line arguments passed to the program. // It intercepts "magic args" and runs the corresponding actions, terminating diff --git a/workflow_options.go b/workflow_options.go index 266640c..a6bf4b5 100644 --- a/workflow_options.go +++ b/workflow_options.go @@ -196,18 +196,3 @@ func RemoveMagic(actions ...MagicAction) Option { return AddMagic(actions...) } } - -// customEnv provides an alternative Env to load settings from. -// -// It should be passed to New() as the first option, as Workflow -// is initialised based on the settings provided by the Env. -func customEnv(e Env) Option { - - return func(wf *Workflow) Option { - - prev := wf.Alfred - wf.Alfred = NewAlfred(e) - - return customEnv(prev) - } -} diff --git a/workflow_paths.go b/workflow_paths.go index 8622ff8..11d66b5 100644 --- a/workflow_paths.go +++ b/workflow_paths.go @@ -34,7 +34,7 @@ func (wf *Workflow) Dir() string { func (wf *Workflow) CacheDir() string { if wf.cacheDir == "" { - wf.cacheDir = wf.Alfred.Get(EnvVarCacheDir) + wf.cacheDir = wf.Config.Get(EnvVarCacheDir) } return wf.cacheDir @@ -54,7 +54,7 @@ func (wf *Workflow) ClearCache() error { func (wf *Workflow) DataDir() string { if wf.dataDir == "" { - wf.dataDir = wf.Alfred.Get(EnvVarDataDir) + wf.dataDir = wf.Config.Get(EnvVarDataDir) } return wf.dataDir diff --git a/workflow_test.go b/workflow_test.go index 71604e7..4deb58c 100644 --- a/workflow_test.go +++ b/workflow_test.go @@ -79,14 +79,6 @@ func TestOptions(t *testing.T) { RemoveMagic(logMA{}), func(wf *Workflow) bool { return wf.MagicActions.actions["log"] == nil }, "Remove Magic"}, - { - customEnv(mapEnv{ - "alfred_workflow_bundleid": "fakeid", - "alfred_workflow_cache": os.Getenv("alfred_workflow_cache"), - "alfred_workflow_data": os.Getenv("alfred_workflow_data"), - }), - func(wf *Workflow) bool { return wf.BundleID() == "fakeid" }, - "CustomEnv"}, } for _, td := range data {