all repos

viye @ cf8afd92aed2f4da2448c272cbbb65f19b460f5b

my shot at reimplementing xiki
5 files changed, 261 insertions(+), 0 deletions(-)
add http tool
Author: Oleksandr Smirnov olexsmir@gmail.com
Committed at: 2026-08-26 15:25:57 +0300
Authored at: 2026-08-25 22:23:52 +0300
Change ID: lyznqunxunlwzruuozlrnvnupznpqyvv
Parent: ef38ca0
A internal/plumbing/http/http.go
···
        
        1
        +package http

      
        
        2
        +

      
        
        3
        +import (

      
        
        4
        +	"context"

      
        
        5
        +	"errors"

      
        
        6
        +	"fmt"

      
        
        7
        +	"io"

      
        
        8
        +	"net/http"

      
        
        9
        +	"strings"

      
        
        10
        +	"time"

      
        
        11
        +

      
        
        12
        +	"github.com/olexsmir/viye/internal/viye"

      
        
        13
        +)

      
        
        14
        +

      
        
        15
        +// get http://localhost

      
        
        16
        +//	: Header: Value

      
        
        17
        +//	: Header2: value

      
        
        18
        +//	: {"json": "body"}

      
        
        19
        +

      
        
        20
        +const timeout = 5 * time.Second

      
        
        21
        +

      
        
        22
        +type Tool struct{}

      
        
        23
        +

      
        
        24
        +func (Tool) Name() string { return "http(get, post, put, patch, delete)" }

      
        
        25
        +func (Tool) Match(c *viye.Context) bool {

      
        
        26
        +	return (c.Cmd == "get" || c.Cmd == "post" || c.Cmd == "put" || c.Cmd == "patch" || c.Cmd == "delete")

      
        
        27
        +}

      
        
        28
        +

      
        
        29
        +func (Tool) Execute(c *viye.Context) (string, error) {

      
        
        30
        +	if len(c.Args) < 1 || !isURL(c.Args[0]) {

      
        
        31
        +		return "", errors.New("please provide an url")

      
        
        32
        +	}

      
        
        33
        +

      
        
        34
        +	headers, body, err := parseBody(viye.ParseData(c.Body))

      
        
        35
        +	if err != nil {

      
        
        36
        +		return "", fmt.Errorf("body parsing error: %v", err)

      
        
        37
        +	}

      
        
        38
        +

      
        
        39
        +	ctx, cancel := context.WithTimeout(context.Background(), timeout)

      
        
        40
        +	defer cancel()

      
        
        41
        +

      
        
        42
        +	req, err := http.NewRequestWithContext(ctx, cmdToMethod(c.Cmd), c.Args[0], strings.NewReader(body))

      
        
        43
        +	if err != nil {

      
        
        44
        +		return "", fmt.Errorf("failed to create request: %v", err)

      
        
        45
        +	}

      
        
        46
        +	for header, value := range headers {

      
        
        47
        +		req.Header.Set(header, value)

      
        
        48
        +	}

      
        
        49
        +

      
        
        50
        +	resp, err := (&http.Client{Timeout: timeout}).Do(req)

      
        
        51
        +	if err != nil {

      
        
        52
        +		return "", fmt.Errorf("failed to make a request: %v", err)

      
        
        53
        +	}

      
        
        54
        +

      
        
        55
        +	respBody, _ := io.ReadAll(resp.Body)

      
        
        56
        +	out := fmt.Sprintf("%d\t%s", resp.StatusCode, respBody)

      
        
        57
        +	return viye.FormatOutput(out), nil

      
        
        58
        +}

      
        
        59
        +

      
        
        60
        +func parseBody(inp []string) (headers map[string]string, body string, err error) {

      
        
        61
        +	if len(inp) == 0 {

      
        
        62
        +		return nil, "", nil

      
        
        63
        +	}

      
        
        64
        +	headers = make(map[string]string)

      
        
        65
        +

      
        
        66
        +	for _, line := range inp {

      
        
        67
        +		key, val, ok := strings.Cut(line, ": ")

      
        
        68
        +		if ok {

      
        
        69
        +			headers[key] = val

      
        
        70
        +		}

      
        
        71
        +	}

      
        
        72
        +

      
        
        73
        +	last := inp[len(inp)-1]

      
        
        74
        +	if _, _, ok := strings.Cut(last, ": "); !ok {

      
        
        75
        +		body = last

      
        
        76
        +	}

      
        
        77
        +

      
        
        78
        +	return headers, body, nil

      
        
        79
        +}

      
        
        80
        +

      
        
        81
        +func isURL(u string) bool {

      
        
        82
        +	return strings.HasPrefix(u, "http://") || strings.HasPrefix(u, "https://")

      
        
        83
        +}

      
        
        84
        +

      
        
        85
        +func cmdToMethod(c string) string {

      
        
        86
        +	switch c {

      
        
        87
        +	case "get":

      
        
        88
        +		return http.MethodGet

      
        
        89
        +	case "post":

      
        
        90
        +		return http.MethodPost

      
        
        91
        +	case "put":

      
        
        92
        +		return http.MethodPut

      
        
        93
        +	case "patch":

      
        
        94
        +		return http.MethodPatch

      
        
        95
        +	case "delete":

      
        
        96
        +		return http.MethodDelete

      
        
        97
        +	default:

      
        
        98
        +		panic("unreachable")

      
        
        99
        +	}

      
        
        100
        +}

      
A internal/plumbing/http/http_test.go
···
        
        1
        +package http

      
        
        2
        +

      
        
        3
        +import (

      
        
        4
        +	"io"

      
        
        5
        +	"net/http"

      
        
        6
        +	"net/http/httptest"

      
        
        7
        +	"testing"

      
        
        8
        +

      
        
        9
        +	"github.com/olexsmir/viye/internal/viye"

      
        
        10
        +

      
        
        11
        +	"olexsmir.xyz/x/is"

      
        
        12
        +)

      
        
        13
        +

      
        
        14
        +func TestMatch(t *testing.T) {

      
        
        15
        +	tests := map[string]bool{

      
        
        16
        +		"get":    true,

      
        
        17
        +		"post":   true,

      
        
        18
        +		"put":    true,

      
        
        19
        +		"patch":  true,

      
        
        20
        +		"delete": true,

      
        
        21
        +		"ls":     false,

      
        
        22
        +		"empty":  false,

      
        
        23
        +		"":       false,

      
        
        24
        +	}

      
        
        25
        +	for tinp, twant := range tests {

      
        
        26
        +		got := (Tool{}).Match(&viye.Context{Cmd: tinp})

      
        
        27
        +		is.Equal(t, twant, got)

      
        
        28
        +	}

      
        
        29
        +}

      
        
        30
        +

      
        
        31
        +func TestExecute(t *testing.T) {

      
        
        32
        +	t.Run("no args returns error", func(t *testing.T) {

      
        
        33
        +		_, err := (Tool{}).Execute(&viye.Context{Cmd: "get", Args: []string{}})

      
        
        34
        +		is.Err(t, err, "provide an url")

      
        
        35
        +	})

      
        
        36
        +

      
        
        37
        +	t.Run("invalid url returns error", func(t *testing.T) {

      
        
        38
        +		_, err := (Tool{}).Execute(&viye.Context{Cmd: "get", Args: []string{"not-a-url"}})

      
        
        39
        +		is.Err(t, err, "provide an url")

      
        
        40
        +	})

      
        
        41
        +

      
        
        42
        +	t.Run("successful GET request", func(t *testing.T) {

      
        
        43
        +		server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {

      
        
        44
        +			is.Equal(t, "GET", r.Method)

      
        
        45
        +			w.WriteHeader(http.StatusOK)

      
        
        46
        +			io.WriteString(w, "response body")

      
        
        47
        +		}))

      
        
        48
        +		defer server.Close()

      
        
        49
        +

      
        
        50
        +		got, err := (Tool{}).Execute(&viye.Context{

      
        
        51
        +			Cmd:  "get",

      
        
        52
        +			Args: []string{server.URL},

      
        
        53
        +		})

      
        
        54
        +		is.Err(t, err, nil)

      
        
        55
        +		is.Equal(t, "| 200\tresponse body\n", got)

      
        
        56
        +	})

      
        
        57
        +

      
        
        58
        +	t.Run("POST request with body", func(t *testing.T) {

      
        
        59
        +		server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {

      
        
        60
        +			is.Equal(t, "POST", r.Method)

      
        
        61
        +			body, _ := io.ReadAll(r.Body)

      
        
        62
        +			is.Equal(t, "test body", string(body))

      
        
        63
        +			w.WriteHeader(http.StatusCreated)

      
        
        64
        +			io.WriteString(w, "created")

      
        
        65
        +		}))

      
        
        66
        +		defer server.Close()

      
        
        67
        +

      
        
        68
        +		got, err := (Tool{}).Execute(&viye.Context{

      
        
        69
        +			Cmd:  "post",

      
        
        70
        +			Args: []string{server.URL},

      
        
        71
        +			Body: []string{": test body"},

      
        
        72
        +		})

      
        
        73
        +		is.Err(t, err, nil)

      
        
        74
        +		is.Equal(t, "| 201\tcreated\n", got)

      
        
        75
        +	})

      
        
        76
        +

      
        
        77
        +	t.Run("request with headers", func(t *testing.T) {

      
        
        78
        +		server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {

      
        
        79
        +			is.Equal(t, "application/json", r.Header.Get("Accept"))

      
        
        80
        +			w.WriteHeader(http.StatusOK)

      
        
        81
        +		}))

      
        
        82
        +		defer server.Close()

      
        
        83
        +

      
        
        84
        +		_, err := (Tool{}).Execute(&viye.Context{

      
        
        85
        +			Cmd:  "get",

      
        
        86
        +			Args: []string{server.URL},

      
        
        87
        +			Body: []string{": Accept: application/json"},

      
        
        88
        +		})

      
        
        89
        +		is.Err(t, err, nil)

      
        
        90
        +	})

      
        
        91
        +

      
        
        92
        +	t.Run("nonexistent server returns error", func(t *testing.T) {

      
        
        93
        +		_, err := (Tool{}).Execute(&viye.Context{

      
        
        94
        +			Cmd:  "get",

      
        
        95
        +			Args: []string{"http://localhost:1"},

      
        
        96
        +		})

      
        
        97
        +		is.Err(t, err, "failed to make a request:")

      
        
        98
        +	})

      
        
        99
        +}

      
        
        100
        +

      
        
        101
        +func TestParseBody(t *testing.T) {

      
        
        102
        +	tests := map[string]struct {

      
        
        103
        +		inp         []string

      
        
        104
        +		wantHeaders map[string]string

      
        
        105
        +		wantBody    string

      
        
        106
        +	}{

      
        
        107
        +		"empty input": {

      
        
        108
        +			inp:         []string{},

      
        
        109
        +			wantHeaders: nil,

      
        
        110
        +			wantBody:    "",

      
        
        111
        +		},

      
        
        112
        +		"headers only": {

      
        
        113
        +			inp: []string{"Header: Value", "Content-Type: no"},

      
        
        114
        +			wantHeaders: map[string]string{

      
        
        115
        +				"Header":       "Value",

      
        
        116
        +				"Content-Type": "no",

      
        
        117
        +			},

      
        
        118
        +		},

      
        
        119
        +		"headers with body": {

      
        
        120
        +			inp:      []string{"Header: Value", "Content-Type: no", "some input"},

      
        
        121
        +			wantBody: "some input",

      
        
        122
        +			wantHeaders: map[string]string{

      
        
        123
        +				"Header":       "Value",

      
        
        124
        +				"Content-Type": "no",

      
        
        125
        +			},

      
        
        126
        +		},

      
        
        127
        +		"body with colon in value": {

      
        
        128
        +			inp:         []string{"Accept: application/json", "plain text body"},

      
        
        129
        +			wantHeaders: map[string]string{"Accept": "application/json"},

      
        
        130
        +			wantBody:    "plain text body",

      
        
        131
        +		},

      
        
        132
        +	}

      
        
        133
        +	for _, tt := range tests {

      
        
        134
        +		headers, body, err := parseBody(tt.inp)

      
        
        135
        +		is.Err(t, err, nil)

      
        
        136
        +		is.Equal(t, tt.wantHeaders, headers)

      
        
        137
        +		is.Equal(t, tt.wantBody, body)

      
        
        138
        +	}

      
        
        139
        +}

      
M internal/viye/format.go
···
        33
        33
         	}

      
        34
        34
         	return strings.Join(s, "\n")

      
        35
        35
         }

      
        
        36
        +

      
        
        37
        +func ParseData(body []string) []string {

      
        
        38
        +	var res []string

      
        
        39
        +	for _, line := range body {

      
        
        40
        +		res = append(res, strings.TrimPrefix(line, ": "))

      
        
        41
        +	}

      
        
        42
        +	return res

      
        
        43
        +}

      
M internal/viye/viye_test.go
···
        48
        48
         		is.Equal(t, tt.want, gotArgs)

      
        49
        49
         	}

      
        50
        50
         }

      
        
        51
        +

      
        
        52
        +func TestContext_ParseData(t *testing.T) {

      
        
        53
        +	for _, tt := range []struct{ inp, want []string }{

      
        
        54
        +		{[]string{": line1", ": line2"}, []string{"line1", "line2"}},

      
        
        55
        +		{[]string{": line1", "line2"}, []string{"line1", "line2"}},

      
        
        56
        +		{[]string{"line1", "line2"}, []string{"line1", "line2"}},

      
        
        57
        +		{[]string{"| not data", "| again not data"}, []string{"| not data", "| again not data"}},

      
        
        58
        +	} {

      
        
        59
        +		got := ParseData(tt.inp)

      
        
        60
        +		is.Equal(t, tt.want, got)

      
        
        61
        +	}

      
        
        62
        +}

      
M main.go
···
        7
        7
         	"github.com/olexsmir/viye/internal/plumbing/config"

      
        8
        8
         	"github.com/olexsmir/viye/internal/plumbing/files"

      
        9
        9
         	"github.com/olexsmir/viye/internal/plumbing/gobin"

      
        
        10
        +	"github.com/olexsmir/viye/internal/plumbing/http"

      
        10
        11
         	"github.com/olexsmir/viye/internal/plumbing/ip"

      
        11
        12
         	"github.com/olexsmir/viye/internal/plumbing/json"

      
        12
        13
         	"github.com/olexsmir/viye/internal/plumbing/makefile"

      ···
        20
        21
         	v := viye.New()

      
        21
        22
         	v.Register(&files.Tool{})

      
        22
        23
         	v.Register(&shell.Tool{})

      
        
        24
        +	v.Register(&http.Tool{})

      
        23
        25
         	v.Register(&url.Tool{})

      
        24
        26
         	v.Register(&ip.Tool{})

      
        25
        27
         	v.Register(&gobin.Tool{})