Files
greq/internal/api/client.go
T
Mester Gábor 2dd6519168 Initial commit
2026-03-19 07:12:03 +01:00

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
}
}
}
}