Initial commit
This commit is contained in:
@@ -0,0 +1,137 @@
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user