235 lines
5.8 KiB
Go
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
|
|
}
|