Skip to content

Commit 3dade7c

Browse files
authored
Merge pull request #19 from mutablelogic/v1
Updated command and added weatherapi
2 parents c0189e1 + f14ba4b commit 3dade7c

File tree

9 files changed

+336
-31
lines changed

9 files changed

+336
-31
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ There are also some example clients which use this library:
2222
* [NewsAPI client](https://github.com/mutablelogic/go-client/tree/main/pkg/newsapi)
2323
* [Ollama API client](https://github.com/mutablelogic/go-client/tree/main/pkg/ollama) for locally-hosted LLM's
2424
* [OpenAI API client](https://github.com/mutablelogic/go-client/tree/main/pkg/openai) for OpenAI LLM's
25+
* [WeatherAPI client](https://github.com/mutablelogic/go-client/tree/main/pkg/weatherapi)
2526

2627
Aiming to have compatibility with go version 1.21 and above.
2728

cmd/api/cmd.go

Lines changed: 33 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,32 @@ type Fn struct {
2929
///////////////////////////////////////////////////////////////////////////////
3030
// PUBLIC METHODS
3131

32-
func (c *Cmd) Get(args []string) (*Fn, []string, error) {
32+
func (c *Cmd) Get(name string) *Fn {
33+
for _, fn := range c.Fn {
34+
if fn.Name == name {
35+
return &fn
36+
}
37+
}
38+
return nil
39+
}
40+
41+
func (fn *Fn) CheckArgs(args []string) error {
42+
// Check number of arguments
43+
if fn.MinArgs != 0 && uint(len(args)) < fn.MinArgs {
44+
return fmt.Errorf("not enough arguments for %q (expected >= %d)", fn.Name, fn.MinArgs)
45+
}
46+
if fn.MaxArgs != 0 && uint(len(args)) > fn.MaxArgs {
47+
return fmt.Errorf("too many arguments for %q (expected <= %d)", fn.Name, fn.MaxArgs)
48+
}
49+
return nil
50+
}
51+
52+
/*
53+
if fn == nil {
54+
return nil, fmt.Errorf("unknown command %q", name)
55+
}
56+
57+
return c.getFn(name), nil
3358
// Get the command function
3459
var fn *Fn
3560
var nargs uint
@@ -47,25 +72,18 @@ func (c *Cmd) Get(args []string) (*Fn, []string, error) {
4772
}
4873
4974
// Check number of arguments
75+
name := fn.Name
76+
if name == "" {
77+
name = c.Name
78+
}
5079
if fn.MinArgs != 0 && nargs < fn.MinArgs {
51-
return nil, nil, fmt.Errorf("not enough arguments for %q", fn.Name)
80+
return nil, nil, fmt.Errorf("not enough arguments for %q", name)
5281
}
5382
if fn.MaxArgs != 0 && nargs > fn.MaxArgs {
54-
return nil, nil, fmt.Errorf("too many arguments for %q", fn.Name)
83+
return nil, nil, fmt.Errorf("too many arguments for %q", name)
5584
}
5685
5786
// Return the command
5887
return fn, out, nil
5988
}
60-
61-
///////////////////////////////////////////////////////////////////////////////
62-
// PRIVATE METHODS
63-
64-
func (c *Cmd) getFn(name string) *Fn {
65-
for _, fn := range c.Fn {
66-
if fn.Name == name {
67-
return &fn
68-
}
69-
}
70-
return nil
71-
}
89+
*/

cmd/api/flags.go

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ type Flags struct {
2323
cmds []Cmd
2424
cmd *Cmd
2525
root string
26+
fn string
2627
args []string
2728
names map[string]*Value
2829
}
@@ -92,15 +93,17 @@ func (flags *Flags) Parse(args []string) (*Fn, []string, error) {
9293
}
9394
}
9495

95-
// If the name of the command is the same as the name of the application
9696
if cmd := flags.getCommandSet(flags.Name()); cmd != nil {
97+
// If the name of the command is the same as the name of the application
9798
flags.cmd = cmd
9899
flags.root = cmd.Name
100+
flags.fn = ""
99101
flags.args = flags.Args()
100102
} else if flags.NArg() > 0 {
101103
if cmd := flags.getCommandSet(flags.Arg(0)); cmd != nil {
102104
flags.cmd = cmd
103105
flags.root = strings.Join([]string{flags.Name(), cmd.Name}, " ")
106+
flags.fn = flags.Arg(1)
104107
flags.args = flags.Args()[1:]
105108
}
106109
}
@@ -135,15 +138,18 @@ func (flags *Flags) Parse(args []string) (*Fn, []string, error) {
135138
}
136139

137140
// Set the function to call
138-
fn, args, err := flags.cmd.Get(flags.args)
139-
if err != nil {
140-
fmt.Fprintf(os.Stderr, "%v: %v\n", flags.cmd.Name, err)
141-
return nil, nil, err
142-
} else if fn == nil {
143-
fmt.Fprintln(os.Stderr, "Unknown command, try -help")
141+
fn := flags.cmd.Get(flags.fn)
142+
if fn == nil {
143+
fmt.Fprintf(os.Stderr, "Unknown command %q, try -help\n", flags.fn)
144144
return nil, nil, ErrHelp
145145
}
146146

147+
// Check the number of arguments
148+
if err := fn.CheckArgs(flags.args); err != nil {
149+
fmt.Fprintln(os.Stderr, err)
150+
return nil, nil, err
151+
}
152+
147153
// Return success
148154
return fn, args, nil
149155
}

cmd/api/install.go

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,18 @@ import (
77
"path/filepath"
88
)
99

10-
func install(path, name string, flags *Flags) error {
10+
func install(flags *Flags) error {
1111
var info os.FileInfo
12+
var path string
1213

13-
// stat the api binary
14-
if v, err := os.Stat(filepath.Join(path, name)); err != nil {
14+
exec, err := os.Executable()
15+
if err != nil {
16+
return err
17+
} else if v, err := os.Stat(exec); err != nil {
1518
return err
1619
} else {
1720
info = v
21+
path = filepath.Dir(exec)
1822
}
1923

2024
// check for an existing symlink
@@ -31,7 +35,7 @@ func install(path, name string, flags *Flags) error {
3135
result = errors.Join(result, fmt.Errorf("failed to install %q: %w", cmd.Name, err))
3236
}
3337
// Make symlink
34-
if err := os.Symlink(filepath.Join(path, name), filepath.Join(path, cmd.Name)); err != nil {
38+
if err := os.Symlink(exec, filepath.Join(path, cmd.Name)); err != nil {
3539
result = errors.Join(result, fmt.Errorf("failed to install %q: %w", cmd.Name, err))
3640
}
3741
}

cmd/api/main.go

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,23 +14,22 @@ import (
1414
)
1515

1616
func main() {
17-
name := path.Base(os.Args[0])
18-
path := path.Dir(os.Args[0])
19-
flags := NewFlags(name)
17+
flags := NewFlags(path.Base(os.Args[0]))
2018

2119
// Register commands
2220
ipifyRegister(flags)
2321
bwRegister(flags)
24-
newsapiRegister(flags)
2522
anthropicRegister(flags)
23+
newsapiRegister(flags)
24+
weatherapiRegister(flags)
2625

2726
// Parse command line and return function to run
2827
fn, args, err := flags.Parse(os.Args[1:])
2928
if errors.Is(err, ErrHelp) {
3029
os.Exit(0)
3130
}
3231
if errors.Is(err, ErrInstall) {
33-
if err := install(path, name, flags); err != nil {
32+
if err := install(flags); err != nil {
3433
fmt.Fprintln(os.Stderr, err)
3534
os.Exit(-2)
3635
}

cmd/api/weatherapi.go

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
package main
2+
3+
import (
4+
"context"
5+
"fmt"
6+
7+
"github.com/djthorpe/go-tablewriter"
8+
"github.com/mutablelogic/go-client"
9+
"github.com/mutablelogic/go-client/pkg/weatherapi"
10+
)
11+
12+
///////////////////////////////////////////////////////////////////////////////
13+
// GLOBALS
14+
15+
var (
16+
weatherapiName = "weatherapi"
17+
weatherapiClient *weatherapi.Client
18+
)
19+
20+
///////////////////////////////////////////////////////////////////////////////
21+
// LIFECYCLE
22+
23+
func weatherapiRegister(flags *Flags) {
24+
// Register flags required
25+
flags.String(newsapiName, "weatherapi-key", "${WEATHERAPI_KEY}", "API Key")
26+
27+
flags.Register(Cmd{
28+
Name: weatherapiName,
29+
Description: "Obtain weather information from https://www.weatherapi.com/",
30+
Parse: weatherapiParse,
31+
Fn: []Fn{
32+
{Call: weatherapiCurrent, Description: "Return current weather, given city, zip code, IP address or lat,long", MaxArgs: 1},
33+
},
34+
})
35+
}
36+
37+
func weatherapiParse(flags *Flags, opts ...client.ClientOpt) error {
38+
apiKey := flags.GetString("weatherapi-key")
39+
if apiKey == "" {
40+
return fmt.Errorf("missing -weatherapi-key flag")
41+
}
42+
if client, err := weatherapi.New(apiKey, opts...); err != nil {
43+
return err
44+
} else {
45+
weatherapiClient = client
46+
}
47+
48+
// Return success
49+
return nil
50+
}
51+
52+
///////////////////////////////////////////////////////////////////////////////
53+
// METHODS
54+
55+
func weatherapiCurrent(_ context.Context, w *tablewriter.Writer, args []string) error {
56+
var q string
57+
if len(args) == 1 {
58+
q = args[0]
59+
} else {
60+
q = "auto:ip"
61+
}
62+
63+
// Request -> Response
64+
weather, err := weatherapiClient.Current(q)
65+
if err != nil {
66+
return err
67+
}
68+
69+
// Write table
70+
w.Write(weather.Location)
71+
w.Write(weather.Current)
72+
73+
// Return success
74+
return nil
75+
}

pkg/weatherapi/client.go

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
/*
2+
weatherapi implements an API client for WeatherAPI (https://www.weatherapi.com/docs/)
3+
*/
4+
package weatherapi
5+
6+
import (
7+
// Packages
8+
"net/url"
9+
10+
"github.com/mutablelogic/go-client"
11+
12+
// Namespace imports
13+
. "github.com/djthorpe/go-errors"
14+
)
15+
16+
///////////////////////////////////////////////////////////////////////////////
17+
// TYPES
18+
19+
type Client struct {
20+
*client.Client
21+
key string
22+
}
23+
24+
///////////////////////////////////////////////////////////////////////////////
25+
// GLOBALS
26+
27+
const (
28+
endPoint = "https://api.weatherapi.com/v1"
29+
)
30+
31+
///////////////////////////////////////////////////////////////////////////////
32+
// LIFECYCLE
33+
34+
// Create a new client
35+
func New(ApiKey string, opts ...client.ClientOpt) (*Client, error) {
36+
// Check for missing API key
37+
if ApiKey == "" {
38+
return nil, ErrBadParameter.With("missing API key")
39+
}
40+
// Create client
41+
opts = append(opts, client.OptEndpoint(endPoint))
42+
client, err := client.New(opts...)
43+
if err != nil {
44+
return nil, err
45+
}
46+
47+
// Return the client
48+
return &Client{
49+
Client: client,
50+
key: ApiKey,
51+
}, nil
52+
}
53+
54+
///////////////////////////////////////////////////////////////////////////////
55+
// PUBLIC METHODS
56+
57+
// Current weather
58+
func (c *Client) Current(q string) (Weather, error) {
59+
var response Weather
60+
61+
// Set defaults
62+
response.Query = q
63+
64+
// Set query parameters
65+
query := url.Values{}
66+
query.Set("key", c.key)
67+
query.Set("q", q)
68+
69+
// Request -> Response
70+
if err := c.Do(nil, &response, client.OptPath("current.json"), client.OptQuery(query)); err != nil {
71+
return Weather{}, err
72+
} else {
73+
return response, nil
74+
}
75+
}

pkg/weatherapi/client_test.go

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
package weatherapi_test
2+
3+
import (
4+
"os"
5+
"testing"
6+
7+
// Packages
8+
opts "github.com/mutablelogic/go-client"
9+
weatherapi "github.com/mutablelogic/go-client/pkg/weatherapi"
10+
assert "github.com/stretchr/testify/assert"
11+
)
12+
13+
func Test_client_001(t *testing.T) {
14+
assert := assert.New(t)
15+
client, err := weatherapi.New(GetApiKey(t), opts.OptTrace(os.Stderr, true))
16+
assert.NoError(err)
17+
assert.NotNil(client)
18+
t.Log(client)
19+
}
20+
21+
func Test_client_002(t *testing.T) {
22+
assert := assert.New(t)
23+
client, err := weatherapi.New(GetApiKey(t), opts.OptTrace(os.Stderr, true))
24+
assert.NoError(err)
25+
26+
weather, err := client.Current("Berlin, Germany")
27+
if !assert.NoError(err) {
28+
t.SkipNow()
29+
}
30+
t.Log(weather)
31+
}
32+
33+
///////////////////////////////////////////////////////////////////////////////
34+
// ENVIRONMENT
35+
36+
func GetApiKey(t *testing.T) string {
37+
key := os.Getenv("WEATHERAPI_KEY")
38+
if key == "" {
39+
t.Skip("WEATHERAPI_KEY not set")
40+
t.SkipNow()
41+
}
42+
return key
43+
}

0 commit comments

Comments
 (0)