├── examples └── base │ ├── go.mod │ └── main.go ├── tui ├── consts.go └── progress.go ├── rc.toml ├── utils.go ├── main.go ├── magefile.go ├── .golangci.yaml ├── raw_config.go ├── go.mod ├── request.go ├── go.sum └── config.go /examples/base/go.mod: -------------------------------------------------------------------------------- 1 | module base 2 | 3 | go 1.23.3 4 | -------------------------------------------------------------------------------- /tui/consts.go: -------------------------------------------------------------------------------- 1 | package tui 2 | 3 | const ( 4 | MAX_PROGRESS = 100 5 | MAIN_COLOR = "#FF7CCB" 6 | SECOND_COLOR = "#FDFF8C" 7 | ) 8 | -------------------------------------------------------------------------------- /rc.toml: -------------------------------------------------------------------------------- 1 | [settings] 2 | base_url = "http://localhost:7900" 3 | output = "./output/" 4 | load_env = ".env" 5 | include = ["./hc/"] 6 | 7 | [req.default] 8 | path = "/test" 9 | method = "POST" 10 | body = """ 11 | { 12 | "data": 123, 13 | "token": "secret" 14 | } 15 | """ 16 | -------------------------------------------------------------------------------- /utils.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | type Optional[T any] struct { 4 | Defined bool 5 | value T 6 | } 7 | 8 | func (o Optional[T]) IsDefined() bool { 9 | return o.Defined 10 | } 11 | 12 | func (o Optional[T]) GetValue() (T, bool) { 13 | return o.value, o.Defined 14 | } 15 | 16 | func (o *Optional[T]) SetValue(value T) { 17 | o.Defined = true 18 | o.value = value 19 | } 20 | 21 | func NewOptional[T any](value T) Optional[T] { 22 | return Optional[T]{Defined: true, value: value} 23 | } 24 | 25 | func NewOptionalEmpty[T any]() Optional[T] { 26 | return Optional[T]{Defined: false} 27 | } 28 | -------------------------------------------------------------------------------- /examples/base/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "log" 7 | "net/http" 8 | ) 9 | 10 | func main() { 11 | http.HandleFunc("POST /test", func(w http.ResponseWriter, r *http.Request) { 12 | var json_body map[string]interface{} 13 | err := json.NewDecoder(r.Body).Decode(&json_body) 14 | if err != nil { 15 | http.Error(w, err.Error(), http.StatusBadRequest) 16 | return 17 | } 18 | fmt.Println(json_body) 19 | 20 | return_data := map[string]interface{}{ 21 | "status": "ok", 22 | } 23 | json.NewEncoder(w).Encode(return_data) 24 | }) 25 | log.Println("Server started on port 7900") 26 | http.ListenAndServe(":7900", nil) 27 | } 28 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "os" 6 | 7 | "github.com/urfave/cli/v2" 8 | ) 9 | 10 | func main() { 11 | app := &cli.App{ 12 | Name: "hc", 13 | Usage: "TUI based HTTP Client", 14 | Version: "v0.1.0", 15 | Commands: []*cli.Command{ 16 | { 17 | Name: "init", 18 | Usage: "Creates new HC project", 19 | Action: func(c *cli.Context) error { 20 | // arg := c.Args().Slice() 21 | return nil 22 | }, 23 | }, 24 | }, 25 | Action: func(c *cli.Context) error { 26 | HandleRequest(c.Args().Slice()) 27 | return nil 28 | }, 29 | } 30 | 31 | err := app.Run(os.Args) 32 | if err != nil { 33 | log.Fatal(err) 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /tui/progress.go: -------------------------------------------------------------------------------- 1 | package tui 2 | 3 | import ( 4 | "github.com/charmbracelet/bubbles/progress" 5 | tea "github.com/charmbracelet/bubbletea" 6 | ) 7 | 8 | type progressState struct { 9 | progress progress.Model 10 | Current int 11 | LoadingText string 12 | FinishText string 13 | } 14 | 15 | func (p progressState) Init() tea.Cmd { 16 | return nil 17 | } 18 | 19 | func (p progressState) View() 20 | 21 | type ProgressElement struct { 22 | state *progressState 23 | program *tea.Program 24 | } 25 | 26 | func NewProgressElement(loadingText, finishText string) ProgressElement { 27 | return ProgressElement{ 28 | state: &progressState{ 29 | Current: 0, 30 | LoadingText: loadingText, 31 | FinishText: finishText, 32 | progress: progress.New(progress.WithScaledGradient(MAIN_COLOR, SECOND_COLOR), 33 | progress.WithWidth(40)), 34 | }, 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /magefile.go: -------------------------------------------------------------------------------- 1 | //go:build mage 2 | 3 | package main 4 | 5 | import ( 6 | "fmt" 7 | "os/exec" 8 | ) 9 | 10 | // Build compiles the project. 11 | func Build() error { 12 | if err := Lint(); err != nil { 13 | return err 14 | } 15 | fmt.Println("Building the project...") 16 | return exec.Command("go", "build", "./...").Run() 17 | } 18 | 19 | // Test runs the tests. 20 | func Test() error { 21 | fmt.Println("Running tests...") 22 | return exec.Command("go", "test", "./...").Run() 23 | } 24 | 25 | func Lint() error { 26 | fmt.Println("Linting the project...") 27 | cmd := exec.Command("golangci-lint", "run") 28 | 29 | output, err := cmd.CombinedOutput() 30 | fmt.Println(string(output)) 31 | 32 | return err 33 | } 34 | 35 | func Move() error { 36 | if err := Build(); err != nil { 37 | return err 38 | } 39 | return exec.Command("sudo", "mv", "./hc", "/usr/local/bin/hc").Run() 40 | } 41 | -------------------------------------------------------------------------------- /.golangci.yaml: -------------------------------------------------------------------------------- 1 | linters: 2 | enable: 3 | - revive 4 | 5 | linters-settings: 6 | revive: 7 | max-open-files: 2048 8 | ignore-generated-header: true 9 | severity: error 10 | rules: 11 | - name: argument-limit 12 | arguments: [3] 13 | - name: deep-exit 14 | - name: defer 15 | - name: early-return 16 | arguments: ["preserveScope"] 17 | - name: empty-block 18 | - name: enforce-map-style 19 | arguments: ["make"] 20 | - name: filename-format 21 | arguments: ["^[_a-z][_a-z0-9]*\\.go$"] 22 | - name: function-result-limit 23 | arguments: [2] 24 | - name: import-shadowing 25 | - name: max-control-nesting 26 | arguments: [3] 27 | - name: nested-structs 28 | - name: range 29 | - name: struct-tag 30 | - name: unchecked-type-assertion 31 | - name: unreachable-code 32 | - name: use-any 33 | -------------------------------------------------------------------------------- /raw_config.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/BurntSushi/toml" 5 | ) 6 | 7 | type RawSettings struct { 8 | BaseUrl *string `toml:"base_url"` 9 | Output *string `toml:"output"` 10 | LoadEnv *string `toml:"load_env"` 11 | Include *[]string `toml:"include"` 12 | } 13 | 14 | type RawRequestConfig struct { 15 | Args []string `toml:"args"` 16 | Method *string `toml:"method"` 17 | Url *string `toml:"url"` 18 | Path *string `toml:"path"` 19 | Select []string `toml:"select"` 20 | Headers []string `toml:"headers"` 21 | Body *string `toml:"body"` 22 | BodyType *string `toml:"body_type"` 23 | Extend *string `toml:"extend"` 24 | } 25 | 26 | type RawConfig struct { 27 | Settings *RawSettings `toml:"settings"` 28 | Requests map[string]RawRequestConfig `toml:"req"` 29 | } 30 | 31 | func ParseRawConfig(path string) (RawConfig, error) { 32 | var config RawConfig 33 | if _, err := toml.DecodeFile(path, &config); err != nil { 34 | return config, err 35 | } 36 | return config, nil 37 | } 38 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module rc 2 | 3 | go 1.23.3 4 | 5 | require github.com/urfave/cli/v2 v2.27.5 // direct 6 | 7 | require ( 8 | github.com/BurntSushi/toml v1.4.0 // direct 9 | github.com/cpuguy83/go-md2man/v2 v2.0.5 // indirect 10 | github.com/russross/blackfriday/v2 v2.1.0 // indirect 11 | github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect 12 | ) 13 | 14 | require ( 15 | github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect 16 | github.com/charmbracelet/bubbles v0.20.0 // indirect 17 | github.com/charmbracelet/bubbletea v1.2.4 // indirect 18 | github.com/charmbracelet/harmonica v0.2.0 // indirect 19 | github.com/charmbracelet/lipgloss v1.0.0 // indirect 20 | github.com/charmbracelet/x/ansi v0.4.5 // indirect 21 | github.com/charmbracelet/x/term v0.2.1 // indirect 22 | github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect 23 | github.com/lucasb-eyer/go-colorful v1.2.0 // indirect 24 | github.com/mattn/go-isatty v0.0.20 // indirect 25 | github.com/mattn/go-localereader v0.0.1 // indirect 26 | github.com/mattn/go-runewidth v0.0.16 // indirect 27 | github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect 28 | github.com/muesli/cancelreader v0.2.2 // indirect 29 | github.com/muesli/termenv v0.15.2 // indirect 30 | github.com/pelletier/go-toml v1.9.5 // indirect 31 | github.com/pelletier/go-toml/v2 v2.2.3 // indirect 32 | github.com/rivo/uniseg v0.4.7 // indirect 33 | golang.org/x/sync v0.9.0 // indirect 34 | golang.org/x/sys v0.27.0 // indirect 35 | golang.org/x/text v0.3.8 // indirect 36 | ) 37 | -------------------------------------------------------------------------------- /request.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "io" 7 | "log" 8 | "net/http" 9 | "strings" 10 | ) 11 | 12 | func HandleRequest(args []string) { 13 | // read config and parse 14 | rawConfig, err := ParseRawConfig("rc.toml") 15 | if err != nil { 16 | log.Fatal(err) 17 | } 18 | config, err := NewConfigFromRaw(rawConfig) 19 | if err != nil { 20 | log.Fatal(err) 21 | } 22 | 23 | // parse args 24 | if len(args) != 1 { 25 | log.Fatal("Invalid number of arguments. Should be 1: rc ") 26 | } 27 | request_name := args[0] 28 | 29 | // do request 30 | request_config, ok := config.Requests[request_name] 31 | if !ok { 32 | log.Fatalf("Unknown request: %s", request_name) 33 | } 34 | resp, err := DoRequest(&request_config) 35 | if err != nil { 36 | log.Fatal(err) 37 | } 38 | err = HandleResponse(&request_config, resp) 39 | if err != nil { 40 | log.Fatal(err) 41 | } 42 | } 43 | 44 | // makes the http request, main function where all logics are 45 | func DoRequest(config *RequestConfig) (*http.Response, error) { 46 | // create body 47 | var bodyReader io.Reader 48 | body, ok := config.Body.GetValue() 49 | if ok { 50 | bodyReader = strings.NewReader(string(body)) 51 | } else { 52 | bodyReader = nil 53 | } 54 | 55 | // create request 56 | req, err := http.NewRequest(config.Method.String(), config.Url.String(), bodyReader) 57 | if err != nil { 58 | return nil, err 59 | } 60 | for _, value := range config.Headers { 61 | req.Header.Add(value.Key, value.Value) 62 | } 63 | 64 | // create client 65 | client := &http.Client{} 66 | 67 | // do request 68 | resp, err := client.Do(req) 69 | if err != nil { 70 | return nil, err 71 | } 72 | return resp, nil 73 | } 74 | 75 | func HandleResponse(config *RequestConfig, resp *http.Response) error { 76 | switch config.BodyType { 77 | case JSON: 78 | var json_body map[string]any 79 | data, err := io.ReadAll(resp.Body) 80 | if err != nil { 81 | return err 82 | } 83 | if err := json.Unmarshal(data, &json_body); err != nil { 84 | return err 85 | } 86 | for key, value := range json_body { 87 | fmt.Printf("%s: %v\n", key, value) 88 | } 89 | return nil 90 | case TEXT: 91 | return fmt.Errorf("not implemented") 92 | } 93 | return fmt.Errorf("invalid body format %s", config.BodyType) 94 | } 95 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/BurntSushi/toml v1.4.0 h1:kuoIxZQy2WRRk1pttg9asf+WVv6tWQuBNVmK8+nqPr0= 2 | github.com/BurntSushi/toml v1.4.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= 3 | github.com/alecthomas/kong v1.5.1 h1:9quB93P2aNGXf5C1kWNei85vjBgITNJQA4dSwJQGCOY= 4 | github.com/alecthomas/kong v1.5.1/go.mod h1:p2vqieVMeTAnaC83txKtXe8FLke2X07aruPWXyMPQrU= 5 | github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= 6 | github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= 7 | github.com/charmbracelet/bubbles v0.20.0 h1:jSZu6qD8cRQ6k9OMfR1WlM+ruM8fkPWkHvQWD9LIutE= 8 | github.com/charmbracelet/bubbles v0.20.0/go.mod h1:39slydyswPy+uVOHZ5x/GjwVAFkCsV8IIVy+4MhzwwU= 9 | github.com/charmbracelet/bubbletea v1.2.4 h1:KN8aCViA0eps9SCOThb2/XPIlea3ANJLUkv3KnQRNCE= 10 | github.com/charmbracelet/bubbletea v1.2.4/go.mod h1:Qr6fVQw+wX7JkWWkVyXYk/ZUQ92a6XNekLXa3rR18MM= 11 | github.com/charmbracelet/harmonica v0.2.0 h1:8NxJWRWg/bzKqqEaaeFNipOu77YR5t8aSwG4pgaUBiQ= 12 | github.com/charmbracelet/harmonica v0.2.0/go.mod h1:KSri/1RMQOZLbw7AHqgcBycp8pgJnQMYYT8QZRqZ1Ao= 13 | github.com/charmbracelet/lipgloss v1.0.0 h1:O7VkGDvqEdGi93X+DeqsQ7PKHDgtQfF8j8/O2qFMQNg= 14 | github.com/charmbracelet/lipgloss v1.0.0/go.mod h1:U5fy9Z+C38obMs+T+tJqst9VGzlOYGj4ri9reL3qUlo= 15 | github.com/charmbracelet/x/ansi v0.4.5 h1:LqK4vwBNaXw2AyGIICa5/29Sbdq58GbGdFngSexTdRM= 16 | github.com/charmbracelet/x/ansi v0.4.5/go.mod h1:dk73KoMTT5AX5BsX0KrqhsTqAnhZZoCBjs7dGWp4Ktw= 17 | github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ= 18 | github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg= 19 | github.com/cpuguy83/go-md2man/v2 v2.0.5 h1:ZtcqGrnekaHpVLArFSe4HK5DoKx1T0rq2DwVB0alcyc= 20 | github.com/cpuguy83/go-md2man/v2 v2.0.5/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= 21 | github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4= 22 | github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM= 23 | github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= 24 | github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= 25 | github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= 26 | github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 27 | github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4= 28 | github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88= 29 | github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U= 30 | github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= 31 | github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= 32 | github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= 33 | github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI= 34 | github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo= 35 | github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA= 36 | github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo= 37 | github.com/muesli/termenv v0.15.2 h1:GohcuySI0QmI3wN8Ok9PtKGkgkFIk7y6Vpb5PvrY+Wo= 38 | github.com/muesli/termenv v0.15.2/go.mod h1:Epx+iuz8sNs7mNKhxzH4fWXGNpZwUaJKRS1noLXviQ8= 39 | github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8= 40 | github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= 41 | github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M= 42 | github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc= 43 | github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= 44 | github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= 45 | github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= 46 | github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= 47 | github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 48 | github.com/urfave/cli/v2 v2.27.5 h1:WoHEJLdsXr6dDWoJgMq/CboDmyY/8HMMH1fTECbih+w= 49 | github.com/urfave/cli/v2 v2.27.5/go.mod h1:3Sevf16NykTbInEnD0yKkjDAeZDS0A6bzhBH5hrMvTQ= 50 | github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 h1:gEOO8jv9F4OT7lGCjxCBTO/36wtF6j2nSip77qHd4x4= 51 | github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM= 52 | golang.org/x/sync v0.9.0 h1:fEo0HyrW1GIgZdpbhCRO0PkJajUS5H9IFUztCgEo2jQ= 53 | golang.org/x/sync v0.9.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= 54 | golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 55 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 56 | golang.org/x/sys v0.27.0 h1:wBqf8DvsY9Y/2P8gAfPDEYNuS30J4lPHJxXSb/nJZ+s= 57 | golang.org/x/sys v0.27.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 58 | golang.org/x/text v0.3.8 h1:nAL+RVCQ9uMn3vJZbV+MRnydTJFPf8qqY42YiA6MrqY= 59 | golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= 60 | -------------------------------------------------------------------------------- /config.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "net/url" 6 | "strings" 7 | ) 8 | 9 | // type of http/rest request method 10 | type RestMethod string 11 | 12 | func (m RestMethod) String() string { 13 | return string(m) 14 | } 15 | 16 | const ( 17 | GET RestMethod = "GET" 18 | POST RestMethod = "POST" 19 | PUT RestMethod = "PUT" 20 | PATCH RestMethod = "PATCH" 21 | DELETE RestMethod = "DELETE" 22 | ) 23 | 24 | func ParseRestMethod(s string) (RestMethod, error) { 25 | switch s { 26 | case "GET": 27 | return GET, nil 28 | case "POST": 29 | return POST, nil 30 | case "PUT": 31 | return PUT, nil 32 | case "PATCH": 33 | return PATCH, nil 34 | case "DELETE": 35 | return DELETE, nil 36 | case "get": 37 | return GET, nil 38 | case "post": 39 | return POST, nil 40 | case "put": 41 | return PUT, nil 42 | case "patch": 43 | return PATCH, nil 44 | case "delete": 45 | return DELETE, nil 46 | default: 47 | return "", fmt.Errorf("Unknown method: %s. Only GET, POST, PUT, PATCH and DELETE are supported", s) 48 | } 49 | } 50 | 51 | // type of argument 52 | type ConfigArgType string 53 | 54 | const ( 55 | STRING ConfigArgType = "string" 56 | INT ConfigArgType = "int" 57 | FLOAT ConfigArgType = "float" 58 | BOOL ConfigArgType = "bool" 59 | ) 60 | 61 | func ParseConfigArgType(s string) (ConfigArgType, error) { 62 | switch s { 63 | case "string": 64 | return STRING, nil 65 | case "int": 66 | return INT, nil 67 | case "float": 68 | return FLOAT, nil 69 | case "bool": 70 | return BOOL, nil 71 | default: 72 | return "", fmt.Errorf("Unknown argument type: %s. Only string, int, float and bool are supported", s) 73 | } 74 | } 75 | 76 | // type of body type 77 | type BodyType string 78 | 79 | const ( 80 | TEXT BodyType = "text" 81 | JSON BodyType = "json" 82 | ) 83 | 84 | func ParseBodyType(s string) (BodyType, error) { 85 | switch s { 86 | case "text": 87 | return TEXT, nil 88 | case "json": 89 | return JSON, nil 90 | default: 91 | return "", fmt.Errorf("Unknown body type: %s. Only text and json are supported", s) 92 | } 93 | } 94 | 95 | // Selector to show result 96 | type Selector interface { 97 | Run(headers map[string]string, body string, bodyType BodyType) string 98 | Kind() string 99 | } 100 | 101 | func CreateSelector(showString string) (Selector, error) { 102 | parts := strings.Split(showString, ":") 103 | switch parts[0] { 104 | case "header": 105 | if len(parts) != 2 { 106 | return nil, fmt.Errorf("Invalid header selector: %s. Format should be header.", showString) 107 | } 108 | return HeaderSelector{Key: parts[1]}, nil 109 | case "cookie": 110 | if len(parts) != 2 { 111 | return nil, fmt.Errorf("Invalid cookie selector: %s. Format should be cookie.", showString) 112 | } 113 | return CookieSelector{Key: parts[1]}, nil 114 | case "body": 115 | if len(parts) == 1 { 116 | return BodySelector{}, nil 117 | } 118 | depth := strings.Split(parts[1], ".") 119 | return BodySelector{Depth: depth}, nil 120 | } 121 | return nil, fmt.Errorf("Unknown selector: %s", showString) 122 | } 123 | 124 | // to select header 125 | type HeaderSelector struct { 126 | Key string 127 | } 128 | 129 | func (hs HeaderSelector) Run(headers map[string]string, body string, bodyType BodyType) string { 130 | return headers[hs.Key] 131 | } 132 | 133 | func (hs HeaderSelector) Kind() string { 134 | return "header" 135 | } 136 | 137 | // to select body 138 | type BodySelector struct { 139 | Depth []string 140 | } 141 | 142 | func (bs BodySelector) Run(headers map[string]string, body string, bodyType BodyType) string { 143 | return body 144 | } 145 | 146 | func (bs BodySelector) Kind() string { 147 | return "body" 148 | } 149 | 150 | // cookie selector 151 | type CookieSelector struct { 152 | Key string 153 | } 154 | 155 | func (cs CookieSelector) Run(headers map[string]string, body string, bodyType BodyType) string { 156 | return headers[cs.Key] 157 | } 158 | 159 | func (cs CookieSelector) Kind() string { 160 | return "cookie" 161 | } 162 | 163 | // Main config 164 | type Config struct { 165 | Settings Optional[GlobalSettings] 166 | Requests map[string]RequestConfig 167 | } 168 | 169 | type GlobalSettings struct { 170 | BaseUrl url.URL 171 | Output string 172 | EnvVars map[string]string 173 | Include []string 174 | } 175 | 176 | type RequestConfig struct { 177 | Args []Argument 178 | Method RestMethod 179 | Url url.URL 180 | Select []Selector 181 | Headers []Header 182 | Body Optional[[]byte] 183 | BodyType BodyType 184 | } 185 | 186 | type Argument struct { 187 | Key string 188 | ArgType ConfigArgType 189 | } 190 | 191 | type Header struct { 192 | Key string 193 | Value string 194 | } 195 | 196 | func NewConfigFromRaw(raw RawConfig) (Config, error) { 197 | // parses global settings 198 | opt_gs := NewOptionalEmpty[GlobalSettings]() 199 | if raw_gs := raw.Settings; raw_gs != nil { 200 | gs := GlobalSettings{} 201 | if base_url := raw_gs.BaseUrl; base_url != nil { 202 | bu, err := url.Parse(*base_url) 203 | if err != nil { 204 | return Config{}, err 205 | } 206 | gs.BaseUrl = *bu 207 | } 208 | if output := raw_gs.Output; output != nil { 209 | gs.Output = *output 210 | } 211 | if load_env := raw_gs.LoadEnv; load_env != nil { 212 | gs.EnvVars = make(map[string]string) 213 | } 214 | if include := raw_gs.Include; include != nil { 215 | gs.Include = *include 216 | } 217 | 218 | opt_gs.SetValue(gs) 219 | } 220 | // parses config requests 221 | requests := make(map[string]RequestConfig) 222 | for name, raw_req := range raw.Requests { 223 | raw_req, err := FillExtend(raw_req, raw.Requests) 224 | if err != nil { 225 | return Config{}, err 226 | } 227 | rc := RequestConfig{} 228 | 229 | // parses arguments with their types 230 | for _, arg := range raw_req.Args { 231 | parts := strings.Split(arg, ":") 232 | // if no type is specified, default to string 233 | if len(parts) == 1 { 234 | rc.Args = append(rc.Args, Argument{Key: parts[0], ArgType: "string"}) 235 | } else if len(parts) == 2 { 236 | // else if the type is specified 237 | arg_type, err := ParseConfigArgType(parts[1]) 238 | if err != nil { 239 | return Config{}, err 240 | } 241 | rc.Args = append(rc.Args, Argument{Key: parts[0], ArgType: arg_type}) 242 | } else { 243 | return Config{}, fmt.Errorf("Invalid argument: %s. Format should be key:type or key", arg) 244 | } 245 | } 246 | 247 | // parses rest method 248 | if method := raw_req.Method; method != nil { 249 | m, err := ParseRestMethod(*method) 250 | if err != nil { 251 | return Config{}, err 252 | } 253 | rc.Method = m 254 | } else { 255 | rc.Method = GET 256 | } 257 | 258 | // parses url and path 259 | if path := raw_req.Path; path != nil { 260 | 261 | // if global base url is not defined, returns error 262 | if gs, ok := opt_gs.GetValue(); ok { 263 | rc.Url = gs.BaseUrl 264 | rc.Url.Path = *path 265 | } else { 266 | return Config{}, fmt.Errorf("Url is not defined in global settings, so path can't be used") 267 | } 268 | 269 | // if url is defined, path is ignored 270 | if raw_url := raw_req.Url; raw_url != nil { 271 | u, err := url.Parse(*raw_url) 272 | if err != nil { 273 | return Config{}, err 274 | } 275 | rc.Url = *u 276 | } 277 | } 278 | 279 | // parses headers 280 | for _, header := range raw_req.Headers { 281 | parts := strings.Split(header, ":") 282 | if len(parts) != 2 { 283 | return Config{}, fmt.Errorf("Header must be of format key:value") 284 | } 285 | rc.Headers = append(rc.Headers, Header{Key: parts[0], Value: parts[1]}) 286 | } 287 | 288 | // parses body type 289 | if body_type := raw_req.BodyType; body_type != nil { 290 | bt, err := ParseBodyType(*body_type) 291 | if err != nil { 292 | return Config{}, err 293 | } 294 | rc.BodyType = bt 295 | } else { 296 | rc.BodyType = JSON 297 | } 298 | 299 | // parses body 300 | if body := raw_req.Body; body != nil { 301 | rc.Body = NewOptional([]byte(*body)) 302 | } else { 303 | rc.Body = NewOptionalEmpty[[]byte]() 304 | } 305 | 306 | // parses selector 307 | for _, raw_sel := range raw_req.Select { 308 | selector, err := CreateSelector(raw_sel) 309 | if err != nil { 310 | return Config{}, err 311 | } 312 | rc.Select = append(rc.Select, selector) 313 | } 314 | 315 | requests[name] = rc 316 | } 317 | return Config{Settings: opt_gs, Requests: requests}, nil 318 | } 319 | 320 | // Fills extended request 321 | func FillExtend(raw_req RawRequestConfig, all_reqs map[string]RawRequestConfig) (RawRequestConfig, error) { 322 | if extend := raw_req.Extend; extend != nil { 323 | target, found := all_reqs[*extend] 324 | if !found { 325 | return RawRequestConfig{}, fmt.Errorf("Can't find extended request: %s", *extend) 326 | } 327 | if raw_req.Url == nil { 328 | raw_req.Url = target.Url 329 | } 330 | if raw_req.Path == nil { 331 | raw_req.Path = target.Path 332 | } 333 | if raw_req.Method == nil { 334 | raw_req.Method = target.Method 335 | } 336 | if raw_req.Body == nil { 337 | raw_req.Body = target.Body 338 | } 339 | raw_req.Select = append(raw_req.Select, target.Select...) 340 | raw_req.Headers = append(raw_req.Headers, target.Headers...) 341 | raw_req.Args = append(raw_req.Args, target.Args...) 342 | } 343 | return raw_req, nil 344 | } 345 | 346 | func (c Config) String() string { 347 | baseString := "" 348 | if gs, ok := c.Settings.GetValue(); ok { 349 | baseString += "Settings:\n" 350 | baseString += fmt.Sprintf(" BaseUrl: %s\n", gs.BaseUrl.String()) 351 | baseString += fmt.Sprintf(" Output: %s\n", gs.Output) 352 | baseString += fmt.Sprintf(" EnvVars: %s\n", gs.EnvVars) 353 | baseString += fmt.Sprintf(" Include: %s\n", gs.Include) 354 | baseString += "\n" 355 | } 356 | for name, req := range c.Requests { 357 | baseString += fmt.Sprintf("Request: %s\n", name) 358 | baseString += fmt.Sprintf(" Args: %s\n", req.Args) 359 | baseString += fmt.Sprintf(" Method: %s\n", req.Method) 360 | baseString += fmt.Sprintf(" Url: %s\n", req.Url.String()) 361 | baseString += fmt.Sprintf(" Select: %s\n", req.Select) 362 | baseString += fmt.Sprintf(" Headers: %s\n", req.Headers) 363 | if body, ok := req.Body.GetValue(); ok { 364 | baseString += fmt.Sprintf(" Body: %s\n", body) 365 | } 366 | baseString += "\n" 367 | } 368 | return baseString 369 | } 370 | --------------------------------------------------------------------------------