diff --git a/cmd/cmsh/main.go b/cmd/cmsh/main.go index cc5d919..bfcf4cd 100644 --- a/cmd/cmsh/main.go +++ b/cmd/cmsh/main.go @@ -3,8 +3,9 @@ package main import ( "context" "fmt" - "github.com/chzyer/readline" "log" + + "github.com/chzyer/readline" "ucl.lmika.dev/repl" "ucl.lmika.dev/ucl" "ucl.lmika.dev/ucl/builtins" @@ -27,6 +28,7 @@ func main() { ucl.WithModule(builtins.Lists()), ucl.WithModule(builtins.Time()), ucl.WithModule(builtins.Fns()), + ucl.WithModule(builtins.URLs()), ) ctx := context.Background() diff --git a/ucl/builtins/urls.go b/ucl/builtins/urls.go new file mode 100644 index 0000000..f21f391 --- /dev/null +++ b/ucl/builtins/urls.go @@ -0,0 +1,67 @@ +package builtins + +import ( + "context" + "errors" + "fmt" + "io" + "net/http" + "strings" + + "ucl.lmika.dev/ucl" +) + +func URLs() ucl.Module { + return ucl.Module{ + Name: "urls", + Builtins: map[string]ucl.BuiltinHandler{ + "fetch": urlsFetch, + }, + } +} + +func urlsFetch(ctx context.Context, args ucl.CallArgs) (any, error) { + var url string + + if err := args.Bind(&url); err != nil { + return nil, err + } + + if strings.HasPrefix(url, "http:") || strings.HasPrefix(url, "https:") { + return urlFetchHTTP(ctx, url, args) + } + + return nil, errors.New("unsupported URL scheme") +} + +func urlFetchHTTP(ctx context.Context, url string, args ucl.CallArgs) (any, error) { + req, err := http.NewRequestWithContext(ctx, "GET", url, nil) + if err != nil { + return nil, err + } + + resp, err := http.DefaultClient.Do(req) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + if resp.StatusCode != 200 { + return nil, fmt.Errorf("non-200 status code: %v", resp.StatusCode) + } + + // Do content negotiation + contentType := resp.Header.Get("Content-Type") + switch { + case strings.HasPrefix(contentType, "text/"): + bts, err := io.ReadAll(resp.Body) + if err != nil { + return nil, err + } + + // TODO: honour encoding + return string(bts), nil + } + + return nil, errors.New("unsupported content type: " + contentType) +} diff --git a/ucl/builtins/urls_test.go b/ucl/builtins/urls_test.go new file mode 100644 index 0000000..47d46a9 --- /dev/null +++ b/ucl/builtins/urls_test.go @@ -0,0 +1,37 @@ +package builtins_test + +import ( + "context" + "testing" + + "github.com/stretchr/testify/assert" + "ucl.lmika.dev/ucl" + "ucl.lmika.dev/ucl/builtins" +) + +func TestURLs_Fetch_http(t *testing.T) { + tests := []struct { + desc string + eval string + want any + wantErr bool + }{ + {desc: "fetch 1", eval: `in (urls:fetch "https://www.example.com") "Example Domain"`, want: true}, + } + + for _, tt := range tests { + t.Run(tt.desc, func(t *testing.T) { + inst := ucl.New( + ucl.WithModule(builtins.Strs()), + ucl.WithModule(builtins.URLs()), + ) + res, err := inst.EvalString(context.Background(), tt.eval) + if tt.wantErr { + assert.Error(t, err) + } else { + assert.NoError(t, err) + assert.Equal(t, tt.want, res) + } + }) + } +}