5 files changed,
261 insertions(+),
0 deletions(-)
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/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{})