138 lines
3.8 KiB
Go
138 lines
3.8 KiB
Go
package api
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"strings"
|
|
"time"
|
|
|
|
"greq/internal/storage"
|
|
)
|
|
|
|
// SavedToken is the most-recently captured bearer token.
|
|
// It is populated automatically from any response that contains a
|
|
// top-level "token", "access_token", "accessToken", "jwt", or "id_token" field.
|
|
var SavedToken string
|
|
|
|
// Response holds the parsed result of an HTTP call.
|
|
type Response struct {
|
|
StatusCode int
|
|
Status string
|
|
Body string // pretty-printed if JSON, raw otherwise
|
|
Headers http.Header
|
|
IsJSON bool
|
|
Duration time.Duration
|
|
Size int // raw body bytes
|
|
}
|
|
|
|
// ResolveVariables replaces every {{key}} placeholder in input.
|
|
//
|
|
// Resolution order:
|
|
// 1. Values from env.yaml (envs map)
|
|
// 2. The captured bearer token ({{token}} and {{access_token}})
|
|
//
|
|
// Missing keys are left as-is (e.g. "{{unknown}}" stays in the string)
|
|
// so the caller can detect unresolved variables by scanning for "{{".
|
|
// ResolveVariables replaces {{key}} placeholders in the following order:
|
|
// 1. env.yaml values (envs)
|
|
// 2. Auto-chained vars from the last response (ChainedVars, e.g. {{last_id}})
|
|
// 3. Captured bearer token ({{token}}, {{access_token}})
|
|
//
|
|
// Unknown keys are left as-is so callers can detect them by scanning for "{{".
|
|
func ResolveVariables(input string, envs map[string]string) string {
|
|
for k, v := range envs {
|
|
input = strings.ReplaceAll(input, "{{"+k+"}}", v)
|
|
}
|
|
for k, v := range ChainedVars {
|
|
input = strings.ReplaceAll(input, "{{"+k+"}}", v)
|
|
}
|
|
if SavedToken != "" {
|
|
input = strings.ReplaceAll(input, "{{token}}", SavedToken)
|
|
input = strings.ReplaceAll(input, "{{access_token}}", SavedToken)
|
|
}
|
|
return input
|
|
}
|
|
|
|
// Do executes the request described by rf with variable substitution applied.
|
|
func Do(rf storage.RequestFile, envs map[string]string) (*Response, error) {
|
|
finalURL := ResolveVariables(rf.URL, envs)
|
|
finalBody := ResolveVariables(rf.Body, envs)
|
|
|
|
req, err := http.NewRequest(rf.Method, finalURL, bytes.NewBufferString(finalBody))
|
|
if err != nil {
|
|
return nil, fmt.Errorf("build request: %w", err)
|
|
}
|
|
|
|
// Headers from request file (variables resolved)
|
|
for k, v := range rf.Headers {
|
|
req.Header.Set(k, ResolveVariables(v, envs))
|
|
}
|
|
|
|
// Auto-inject token when present and header not already set
|
|
if SavedToken != "" && req.Header.Get("Authorization") == "" {
|
|
req.Header.Set("Authorization", "Bearer "+SavedToken)
|
|
}
|
|
|
|
// Default Content-Type for non-empty bodies
|
|
if finalBody != "" && req.Header.Get("Content-Type") == "" {
|
|
req.Header.Set("Content-Type", "application/json")
|
|
}
|
|
|
|
client := &http.Client{Timeout: 15 * time.Second}
|
|
start := time.Now()
|
|
resp, err := client.Do(req)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("execute request: %w", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
raw, err := io.ReadAll(resp.Body)
|
|
elapsed := time.Since(start)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("read body: %w", err)
|
|
}
|
|
|
|
// Extract token and chain variables from the response body
|
|
extractToken(raw)
|
|
ExtractChained(raw)
|
|
|
|
// Pretty-print JSON bodies
|
|
body := string(raw)
|
|
isJSON := false
|
|
var pretty bytes.Buffer
|
|
if json.Indent(&pretty, raw, "", " ") == nil && pretty.Len() > 0 {
|
|
body = pretty.String()
|
|
isJSON = true
|
|
}
|
|
|
|
return &Response{
|
|
StatusCode: resp.StatusCode,
|
|
Status: resp.Status,
|
|
Body: body,
|
|
Headers: resp.Header,
|
|
IsJSON: isJSON,
|
|
Duration: elapsed,
|
|
Size: len(raw),
|
|
}, nil
|
|
}
|
|
|
|
// extractToken scans the top level of a JSON object for common token field names
|
|
// and stores the first match in SavedToken.
|
|
func extractToken(body []byte) {
|
|
var data map[string]interface{}
|
|
if err := json.Unmarshal(body, &data); err != nil {
|
|
return
|
|
}
|
|
for _, key := range []string{"token", "access_token", "accessToken", "jwt", "id_token"} {
|
|
if v, ok := data[key]; ok {
|
|
if s, ok := v.(string); ok && s != "" {
|
|
SavedToken = s
|
|
return
|
|
}
|
|
}
|
|
}
|
|
}
|