Files

235 lines
5.8 KiB
Go

package parser
import (
"crypto/sha1"
"encoding/hex"
"fmt"
"regexp"
"strings"
)
var (
requestLinePattern = regexp.MustCompile(`^(GET|POST|PUT|PATCH|DELETE|HEAD|OPTIONS|TRACE)\s+(\S+)(?:\s+HTTP/\d(?:\.\d)?)?\s*$`)
variablePattern = regexp.MustCompile(`\{\{\s*([A-Za-z_][A-Za-z0-9_.-]*)\s*\}\}`)
)
type RequestBlock struct {
ID string `json:"id"`
Name string `json:"name,omitempty"`
Method string `json:"method"`
URL string `json:"url"`
Headers map[string]string `json:"headers"`
Body string `json:"body"`
StartLine int `json:"startLine"`
EndLine int `json:"endLine"`
}
type ParseResult struct {
FilePath string `json:"filePath,omitempty"`
Requests []RequestBlock `json:"requests"`
Variables map[string]string `json:"variables"`
Errors []string `json:"errors"`
}
func Parse(content, filePath string) ParseResult {
normalized := strings.ReplaceAll(strings.TrimPrefix(content, "\ufeff"), "\r\n", "\n")
normalized = strings.ReplaceAll(normalized, "\r", "\n")
lines := strings.Split(normalized, "\n")
result := ParseResult{
FilePath: filePath,
Variables: map[string]string{},
Requests: []RequestBlock{},
Errors: []string{},
}
for _, segment := range splitSegments(lines) {
parseSegment(segment.lines, segment.startLine, filePath, &result)
}
return result
}
type segment struct {
lines []string
startLine int
}
func splitSegments(lines []string) []segment {
var segments []segment
current := segment{startLine: 1}
for index, line := range lines {
if strings.TrimSpace(line) == "###" {
if hasContent(current.lines) {
segments = append(segments, current)
}
current = segment{startLine: index + 2}
continue
}
current.lines = append(current.lines, line)
}
if hasContent(current.lines) {
segments = append(segments, current)
}
return segments
}
func hasContent(lines []string) bool {
for _, line := range lines {
if strings.TrimSpace(line) != "" {
return true
}
}
return false
}
func parseSegment(lines []string, startLine int, filePath string, result *ParseResult) {
name := ""
requestLineIndex := -1
for index, line := range lines {
trimmed := strings.TrimSpace(line)
if trimmed == "" {
continue
}
if strings.HasPrefix(trimmed, "@") && !strings.Contains(trimmed, " ") {
continue
}
if strings.HasPrefix(trimmed, "@") {
if key, value, ok := parseVariable(trimmed); ok {
result.Variables[key] = value
continue
}
}
if strings.HasPrefix(trimmed, "#") || strings.HasPrefix(trimmed, "//") {
if extracted := parseName(trimmed); extracted != "" {
name = extracted
}
continue
}
if requestLinePattern.MatchString(trimmed) {
requestLineIndex = index
break
}
}
if requestLineIndex == -1 {
return
}
match := requestLinePattern.FindStringSubmatch(strings.TrimSpace(lines[requestLineIndex]))
request := RequestBlock{
Name: name,
Method: match[1],
URL: match[2],
Headers: map[string]string{},
StartLine: startLine + requestLineIndex,
EndLine: startLine + len(lines) - 1,
}
bodyStart := -1
for index := requestLineIndex + 1; index < len(lines); index++ {
line := lines[index]
trimmed := strings.TrimSpace(line)
if trimmed == "" {
bodyStart = index + 1
break
}
if strings.HasPrefix(trimmed, "#") || strings.HasPrefix(trimmed, "//") {
continue
}
key, value, ok := strings.Cut(line, ":")
if !ok {
bodyStart = index
break
}
request.Headers[strings.TrimSpace(key)] = strings.TrimSpace(value)
}
if bodyStart >= 0 && bodyStart < len(lines) {
request.Body = strings.TrimSpace(strings.Join(lines[bodyStart:], "\n"))
}
request.ID = stableID(filePath, request)
result.Requests = append(result.Requests, request)
}
func parseVariable(line string) (string, string, bool) {
withoutAt := strings.TrimPrefix(line, "@")
key, value, ok := strings.Cut(withoutAt, "=")
if !ok {
return "", "", false
}
key = strings.TrimSpace(key)
if key == "" {
return "", "", false
}
return key, strings.TrimSpace(value), true
}
func parseName(line string) string {
trimmed := strings.TrimSpace(strings.TrimPrefix(strings.TrimPrefix(line, "#"), "//"))
if strings.HasPrefix(trimmed, "@name") {
return strings.TrimSpace(strings.TrimPrefix(trimmed, "@name"))
}
return ""
}
func stableID(filePath string, request RequestBlock) string {
h := sha1.New()
_, _ = h.Write([]byte(fmt.Sprintf("%s:%d:%s:%s:%s", filePath, request.StartLine, request.Name, request.Method, request.URL)))
return "req_" + hex.EncodeToString(h.Sum(nil))[:16]
}
func ResolveRequest(request RequestBlock, fileVariables map[string]string, environmentVariables map[string]string) (RequestBlock, error) {
variables := map[string]string{}
for key, value := range fileVariables {
variables[key] = value
}
for key, value := range environmentVariables {
variables[key] = value
}
replace := func(input string) (string, error) {
var missing []string
output := variablePattern.ReplaceAllStringFunc(input, func(match string) string {
name := strings.TrimSpace(strings.TrimSuffix(strings.TrimPrefix(match, "{{"), "}}"))
value, ok := variables[name]
if !ok {
missing = append(missing, name)
return match
}
return value
})
if len(missing) > 0 {
return "", fmt.Errorf("undefined variable(s): %s", strings.Join(missing, ", "))
}
return output, nil
}
resolved := request
var err error
if resolved.URL, err = replace(request.URL); err != nil {
return RequestBlock{}, err
}
if resolved.Body, err = replace(request.Body); err != nil {
return RequestBlock{}, err
}
resolved.Headers = map[string]string{}
for key, value := range request.Headers {
resolved.Headers[key], err = replace(value)
if err != nil {
return RequestBlock{}, err
}
}
return resolved, nil
}