diff --git a/README.md b/README.md index 55ae387..dcf01b7 100644 --- a/README.md +++ b/README.md @@ -85,9 +85,44 @@ kulala-fmt --check --verbose file1.http file2.rest http/*.http - Checks if the file is formatted and valid - Removes extraneous newlines - Makes sure document variables are at the top of the file -- Lowercases all headers -- Puts all metadata right after the headers +- Lowercases all headers (when HTTP/2 or HTTP/3) else it will uppercase the first letter +- Puts all metadata right before the request line - Ensures all comments are using `#` and not `//` +- Ensures all comments are at the top of the request + +So a perfect request would look like this: + +```http +@variables1=value1 + +# This is a comment +# This is another comment +# @someother metatag +# @name REQUEST_NAME_ONE +GET http://localhost:8080/api/v1/health HTTP/1.1 +Content-Type: application/json + +{ + "key": "value" +} +``` + +or this: + +```http +@variables1=value1 + +# This is a comment +# This is another comment +# @someother metatag +# @name REQUEST_NAME_ONE +GET http://localhost:8080/api/v1/health HTTP/2 +content-type: application/json + +{ + "key": "value" +} +``` If run on all files it also warns when it finds both `.env` and `http-client.env.json` files in the same directory, because that might cause unexpected behavior. diff --git a/go.mod b/go.mod index b2a20e3..4752b6c 100644 --- a/go.mod +++ b/go.mod @@ -21,4 +21,5 @@ require ( github.com/spf13/pflag v1.0.5 // indirect golang.org/x/exp v0.0.0-20231006140011-7918f672742d // indirect golang.org/x/sys v0.20.0 // indirect + golang.org/x/text v0.17.0 // indirect ) diff --git a/go.sum b/go.sum index be8f88d..13c8cba 100644 --- a/go.sum +++ b/go.sum @@ -40,6 +40,8 @@ golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQz golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y= golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.17.0 h1:XtiM5bkSOt+ewxlOE/aE/AKEHibwj/6gvWMl9Rsh0Qc= +golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/internal/parser/parser.go b/internal/parser/parser.go index e76b386..93e2e9e 100644 --- a/internal/parser/parser.go +++ b/internal/parser/parser.go @@ -2,19 +2,32 @@ package parser import ( "os" + "regexp" "strings" "github.com/charmbracelet/log" "github.com/mistweaverco/kulala-fmt/internal/config" + "golang.org/x/text/cases" + "golang.org/x/text/language" ) +type Header struct { + Name string + Value string +} + +type Metadata struct { + Name string + Value string +} + type Section struct { Comments []string Method string URL string Version string - Headers []string - Metadata []string + Headers []Header + Metadata []Metadata Body string } @@ -24,6 +37,33 @@ type Document struct { Valid bool } +var caser = cases.Title(language.Und) +var metaDataRegex = regexp.MustCompile("^# @") + +func parseHTTPLine(line string) (method string, url string, version string) { + method = "" + url = "" + version = "" + + pattern := `^(GET|POST|PUT|DELETE|PATCH|HEAD|OPTIONS)\s+(\S+)\s*(.*?)(HTTP/\d+\.?\d?)?$` + + re := regexp.MustCompile(pattern) + + matches := re.FindStringSubmatch(line) + + if len(matches) > 0 { + method = matches[1] + url = matches[2] + if matches[4] != "" { + version = matches[4] + } else { + version = "HTTP/1.1" // Default to HTTP/1.1 if not provided + } + } + + return method, url, version +} + func isRequestLine(line string) bool { return strings.HasPrefix(line, "GET") || strings.HasPrefix(line, "POST") || strings.HasPrefix(line, "PUT") || strings.HasPrefix(line, "DELETE") } @@ -37,8 +77,8 @@ func parseSection(section string, document *Document) Section { Method: "", URL: "", Version: "", - Headers: []string{}, - Metadata: []string{}, + Headers: []Header{}, + Metadata: []Metadata{}, Body: "", } lines := strings.Split(section, "\n") @@ -53,7 +93,13 @@ func parseSection(section string, document *Document) Section { document.Variables = append(document.Variables, line) continue } else if strings.HasPrefix(line, "# @") { - parsedSection.Metadata = append(parsedSection.Metadata, line) + metadata := strings.Split(metaDataRegex.ReplaceAllString(line, ""), " ") + metaDataName := metadata[0] + metaDataValue := strings.Join(metadata[1:], " ") + parsedSection.Metadata = append(parsedSection.Metadata, Metadata{ + Name: metaDataName, + Value: metaDataValue, + }) continue } else if strings.HasPrefix(line, "#") || strings.HasPrefix(line, "//") { if strings.HasPrefix(line, "//") { @@ -64,22 +110,27 @@ func parseSection(section string, document *Document) Section { parsedSection.Comments = append(parsedSection.Comments, line) continue } else if isRequestLine(line) { - splits := strings.Split(line, " ") - parsedSection.Method = splits[0] - parsedSection.URL = splits[1] - if len(splits) > 2 { - parsedSection.Version = splits[2] - } + reqMethod, reqURL, reqVersion := parseHTTPLine(line) + parsedSection.Method = reqMethod + parsedSection.URL = reqURL + parsedSection.Version = reqVersion in_request = false in_header = true continue } else if in_header { if strings.Contains(line, ":") { + httpVersion := parsedSection.Version line = strings.Trim(line, " ") splits := strings.Split(line, ":") - splits[0] = strings.ToLower(splits[0]) - line = strings.Join(splits, ":") - parsedSection.Headers = append(parsedSection.Headers, line) + headerName := strings.ToLower(splits[0]) + headerValue := strings.Join(splits[1:], ":") + if httpVersion != "HTTP/2" && httpVersion != "HTTP/3" { + headerName = caser.String(headerName) + } + parsedSection.Headers = append(parsedSection.Headers, Header{ + Name: headerName, + Value: headerValue, + }) } } else if in_body { parsedSection.Body += line @@ -117,14 +168,14 @@ func documentToString(document Document) string { documentString += comment + "\n" } for _, metadata := range section.Metadata { - if strings.HasPrefix(metadata, "# @name ") { + if metadata.Name == "name" { continue } - documentString += metadata + "\n" + documentString += "# @" + metadata.Name + " " + metadata.Value + "\n" } for _, metadata := range section.Metadata { - if strings.HasPrefix(metadata, "# @name ") { - documentString += metadata + "\n" + if metadata.Name == "name" { + documentString += "# @" + metadata.Name + " " + metadata.Value + "\n" } } documentString += section.Method + " " + section.URL @@ -133,7 +184,7 @@ func documentToString(document Document) string { } documentString += "\n" for _, header := range section.Headers { - documentString += header + "\n" + documentString += header.Name + ": " + header.Value + "\n" } if section.Body != "" { documentString += "\n" + section.Body + "\n"