restclient

package module
v0.0.9 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 30, 2025 License: MIT Imports: 29 Imported by: 0

README

Go REST Client Library

A Go library for executing HTTP requests from .http files and validating responses. Write once, use everywhere - for both manual testing and automated E2E tests.

Why This Library?

Problem: I wanted to use the same .http files for both manual testing (JetBrains HTTP Client, VS Code REST Client) and automated E2E testing in Go. No existing library offered full compatibility with both environments.

Solution: This library parses .http files exactly like popular IDE extensions, enabling seamless workflow between manual and automated testing.

Key Features

  • Full JetBrains/VS Code compatibility - Same .http syntax, variables, and behaviors
  • Variable substitution - Custom variables, environment variables, system variables ({{$guid}}, {{$randomInt}}, etc.)
  • Response validation - Compare responses against .hresp files with placeholders ({{$any}}, {{$regexp}}, {{$anyGuid}})
  • Multiple requests per file - Separated by ###
  • E2E testing ready - Perfect for automated integration tests

Installation

go get github.com/bmcszk/go-restclient

Quick Start

1. Create a .http file
@baseUrl = https://api.example.com
@userId = 123

### Get user profile
GET {{baseUrl}}/users/{{userId}}
Authorization: Bearer {{authToken}}
X-Request-ID: {{$guid}}

### Create new user  
POST {{baseUrl}}/users
Content-Type: application/json

{
  "id": "{{$randomInt 1000 9999}}",
  "name": "Test User",
  "createdAt": "{{$timestamp}}"
}
2. Execute in Go
package main

import (
    "context"
    "log"
    "github.com/bmcszk/go-restclient"
)

func main() {
    client, err := restclient.NewClient(
        restclient.WithVars(map[string]interface{}{
            "authToken": "your-token-here",
        }),
    )
    if err != nil {
        log.Fatal(err)
    }

    responses, err := client.ExecuteFile(context.Background(), "requests.http")
    if err != nil {
        log.Fatal(err)
    }

    for i, resp := range responses {
        if resp.Error != nil {
            log.Printf("Request %d failed: %v", i+1, resp.Error)
        } else {
            log.Printf("Request %d: %d %s", i+1, resp.StatusCode, resp.Status)
        }
    }
}

Variable Types

Custom Variables
@baseUrl = https://api.example.com
@userId = 123

GET {{baseUrl}}/users/{{userId}}
System Variables
  • {{$guid}} - UUID (e.g., 123e4567-e89b-12d3-a456-426614174000)
  • {{$randomInt}} or {{$randomInt 1 100}} - Random integer
  • {{$timestamp}} - Unix timestamp
  • {{$datetime}} or {{$datetime "2006-01-02"}} - Current datetime
  • {{$processEnv VAR_NAME}} - Environment variable
  • {{$dotenv VAR_NAME}} - From .env file
JetBrains Faker Variables
  • {{$randomFirstName}}, {{$randomLastName}}
  • {{$randomPhoneNumber}}, {{$randomStreetAddress}}
  • {{$randomUrl}}, {{$randomUserAgent}}
Programmatic Variables (highest precedence)
client, err := restclient.NewClient(
    restclient.WithVars(map[string]interface{}{
        "userId": "override-value",
        "authToken": "secret-token",
    }),
)

Response Validation

Create .hresp files to validate responses:

responses.hresp:

HTTP/1.1 200 OK
Content-Type: application/json

{
  "id": "{{$anyGuid}}",
  "name": "{{$any}}",
  "createdAt": "{{$anyTimestamp}}"
}

###

HTTP/1.1 201 Created
Content-Type: application/json

{
  "id": "{{$regexp `\d{4}`}}",
  "status": "created"
}

Validate in Go:

err := client.ValidateResponses("responses.hresp", responses...)
if err != nil {
    log.Fatal("Validation failed:", err)
}
Validation Placeholders
  • {{$any}} - Matches any text
  • {{$regexp pattern}} - Regex pattern (in backticks)
  • {{$anyGuid}} - UUID format
  • {{$anyTimestamp}} - Unix timestamp
  • {{$anyDatetime 'format'}} - Datetime (rfc1123, iso8601, or custom)

Client Options

client, err := restclient.NewClient(
    restclient.WithBaseURL("https://api.example.com"),
    restclient.WithDefaultHeader("X-API-Key", "secret"),
    restclient.WithHTTPClient(customHTTPClient),
    restclient.WithVars(variables),
)

Compatible Syntax

Works with files created for:

📚 Complete HTTP Syntax Reference - Comprehensive documentation of all supported HTTP request syntax, variables, and features.

Use Cases

Manual Testing

Use your favorite IDE extension to test APIs during development.

Automated E2E Testing
func TestUserAPI(t *testing.T) {
    client, _ := restclient.NewClient(
        restclient.WithBaseURL(testServer.URL),
    )
    
    responses, err := client.ExecuteFile(context.Background(), "user_tests.http")
    require.NoError(t, err)
    
    err = client.ValidateResponses("user_expected.hresp", responses...)
    require.NoError(t, err)
}
CI/CD Integration
go test ./tests/e2e/... # Runs tests using .http files

Development

Prerequisites
  • Go 1.21+
Commands
make check          # Run all checks (lint, test, build)
make test-unit      # Run unit tests only
go test .           # Quick test
Test Coverage

Current coverage: 78.4% (187 tests passing)

License

MIT License

Documentation

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

This section is empty.

Types

type Client

type Client struct {
	BaseURL        string
	DefaultHeaders http.Header
	// contains filtered or unexported fields
}

Client is the main struct for interacting with the REST client library. It holds configuration like the HTTP client, base URL, default headers, and programmatic variables for substitution.

func NewClient

func NewClient(options ...ClientOption) (*Client, error)

NewClient creates a new instance of the REST client. Options for customization (e.g., timeout, custom transport) can be added later.

func (*Client) ExecuteFile

func (c *Client) ExecuteFile(ctx context.Context, requestFilePath string) ([]*Response, error)

ExecuteFile parses a request file (.http, .rest), executes all requests found, and returns their responses. It returns an error if the file cannot be parsed or no requests are found. Individual request execution errors are stored within each Response object.

Variable Substitution Workflow: 1. File Parsing (`parseRequestFile`):

  • Loads .env file from the request file's directory.
  • Generates request-scoped system variables (e.g., `{{$uuid}}`) once for the entire file parsing pass.
  • Resolves `@variable = value` definitions. The `value` itself can contain placeholders, which are resolved using: Client programmatic vars > request-scoped system vars > OS env vars > .env vars.

2. Request Execution (within `ExecuteFile` loop for each request):

  • Re-generates request-scoped system variables (e.g., `{{$uuid}}`) *once per individual request* to ensure uniqueness if needed across multiple requests in the same file, but consistency within a single request.
  • For each part of the request (URL, headers, body): a. `resolveVariablesInText` is called. For {{variableName}} placeholders (where 'variableName' does not start with '$'), the precedence is: Client programmatic vars > file-scoped `@vars` (rcRequest.ActiveVariables) > Environment vars (parsedFile.EnvironmentVariables) > Global vars (parsedFile.GlobalVariables) > OS env vars > .env vars > fallback. System variables like {{$uuid}} are resolved from the request-scoped map if the placeholder is {{$systemVarName}}. It resolves simple system variables like `{{$uuid}}` from the request-scoped map. It leaves dynamic system variables (e.g., `{{$dotenv NAME}}`) untouched for the next step. b. `substituteDynamicSystemVariables` is called: This handles system variables requiring arguments (e.g., `{{$dotenv NAME}}`, `{{$processEnv NAME}}`, `{{$randomInt MIN MAX}}`).

Programmatic variables for substitution can be set on the Client using `WithVars()`.

func (*Client) ValidateResponses

func (c *Client) ValidateResponses(responseFilePath string, actualResponses ...*Response) error

ValidateResponses compares actual HTTP responses against a set of expected responses parsed from the specified .hresp file. It leverages the client's configuration for variable substitution. The `actualResponses` parameter is variadic, allowing zero or more responses to be passed.

As a method on the `Client`, it uses `c.programmaticVars` for programmatic variables and the client instance `c` itself for resolving system variables (e.g., {{$uuid}}) within the .hresp content. Variables can also be defined in the .hresp file using `@name = value` syntax. The precedence for variable resolution is detailed in `hresp_vars.go:resolveAndSubstitute`.

It returns a consolidated error (multierror) if any discrepancies are found (e.g., status mismatch, header mismatch, body mismatch, or count mismatch between actual and expected responses), or nil if all validations pass. Errors during file reading, @define extraction, variable substitution, or .hresp parsing are also returned.

type ClientOption

type ClientOption func(*Client) error

ClientOption is a functional option for configuring the Client.

func WithBaseURL

func WithBaseURL(baseURL string) ClientOption

WithBaseURL sets a base URL for the client.

func WithDefaultHeader

func WithDefaultHeader(key, value string) ClientOption

WithDefaultHeader adds a default header to be sent with every request.

func WithDefaultHeaders

func WithDefaultHeaders(headers http.Header) ClientOption

WithDefaultHeaders adds multiple default headers.

func WithEnvironment

func WithEnvironment(name string) ClientOption

WithEnvironment sets the name of the environment to be used from http-client.env.json.

func WithHTTPClient

func WithHTTPClient(hc *http.Client) ClientOption

WithHTTPClient allows providing a custom http.Client.

func WithVars

func WithVars(vars map[string]any) ClientOption

WithVars sets programmatic variables for the client instance. These variables can be used in .http and .hresp files. Programmatic variables have the highest precedence during substitution, overriding file-defined variables, environment variables, and .env variables. If called multiple times, the provided vars are merged with existing ones, with new values for existing keys overwriting old ones.

type ExpectedResponse

type ExpectedResponse struct {
	StatusCode *int
	Status     *string
	Headers    http.Header // For header presence/value checks
	Body       *string     // Expected body content (exact match or regex)
}

ExpectedResponse defines what an actual response should be compared against. This might be loaded from a file (e.g., request_name.expected.json or .http). Or it could be defined programmatically.

type ParsedFile

type ParsedFile struct {
	// FilePath is the absolute path to the parsed .rest or .http file.
	FilePath string
	// Requests is a list of all HTTP requests defined in the file.
	Requests []*Request
	// EnvironmentVariables are key-value pairs loaded from an associated environment file (e.g., http-client.env.json).
	// These are used as a base for variable substitution.
	EnvironmentVariables map[string]string
	// GlobalVariables are key-value pairs accumulated during the execution of
	// requests in this file (or imported files).
	// These are set by `client.global.set()` in response handler scripts and are available to subsequent requests.
	GlobalVariables map[string]string
	// FileVariables are key-value pairs defined directly within the .http file using the `@name = value` syntax.
	// Their scope is the current file, and they are resolved at parse time.
	FileVariables map[string]string
}

ParsedFile represents all content parsed from a single .rest or .http file. It holds multiple requests, environment context, and global variables accumulated during execution.

type Request

type Request struct {
	// Name is an optional identifier for the request.
	// Parsed from "### Request Name" or "// @name Request Name" or "# @name Request Name".
	Name         string
	Method       string
	RawURLString string   // The raw URL string as read from the file, before variable substitution
	URL          *url.URL // Parsed URL, potentially after variable substitution
	HTTPVersion  string   // e.g., "HTTP/1.1"
	Headers      http.Header
	Body         io.Reader // For streaming body content after processing
	// Store the raw body string as read from the file, before variable substitution
	RawBody string
	GetBody func() (io.ReadCloser, error) // For http.Request.GetBody compatibility

	// ActiveVariables are variables resolved at the time of request execution,
	// sourced from environment, global scope (from previous scripts), and pre-request scripts.
	ActiveVariables map[string]string

	// PreRequestScript contains details of the JavaScript to be run before this request.
	PreRequestScript *Script
	// ResponseHandlerScript contains details of the JavaScript to be run after this request.
	ResponseHandlerScript *Script

	// FilePath is the absolute path to the .rest or .http file this request was parsed from.
	// Used for context, resolving relative paths for imports, script files, etc.
	FilePath string
	// LineNumber is the starting line number of this request definition in the source file.
	LineNumber int

	// Request Settings Directives (JetBrains compatibility)
	// NoRedirect indicates that this request should not follow redirects (from @no-redirect directive)
	NoRedirect bool
	// NoCookieJar indicates that this request should not use the cookie jar (from @no-cookie-jar directive)
	NoCookieJar bool
	// Timeout specifies a custom timeout for this request (from @timeout directive)
	Timeout time.Duration

	// External file body configuration
	// ExternalFilePath stores the path for external file body references (< ./path/to/file or <@ ./path/to/file)
	ExternalFilePath string
	// ExternalFileEncoding specifies the encoding for external file reading (e.g., "latin1", "utf-8")
	ExternalFileEncoding string
	// ExternalFileWithVariables indicates if the external file should have variable substitution applied (<@ syntax)
	ExternalFileWithVariables bool
}

Request represents a parsed HTTP request from a .rest file.

type RequestLineResult

type RequestLineResult int

RequestLineResult represents the result of parsing a request line

const (
	// RequestLineContinues indicates the request line was processed normally
	RequestLineContinues RequestLineResult = iota
	// RequestLineFinalizedBySeparator indicates the request was finalized due to a same-line separator
	RequestLineFinalizedBySeparator
)

type ResolveOptions

type ResolveOptions struct {
	FallbackToOriginal bool // If true, an unresolved placeholder {{var}} becomes "{{var}}"
	FallbackToEmpty    bool // If true, an unresolved placeholder {{var}} becomes "" (empty string)
}

ResolveOptions controls the behavior of variable substitution. If both FallbackToOriginal and FallbackToEmpty are false, and a variable is not found, an error or specific handling might occur (though current implementation defaults to empty string if not original).

type Response

type Response struct {
	Request        *Request // The original request that led to this response
	Status         string   // e.g., "200 OK"
	StatusCode     int      // e.g., 200
	Proto          string   // e.g., "HTTP/1.1"
	Headers        http.Header
	Body           []byte        // Raw response body
	BodyString     string        // Response body as a string (convenience)
	Duration       time.Duration // Time taken for the request-response cycle
	Size           int64         // Response size in bytes (Content-Length or actual)
	IsTLS          bool          // True if the connection was over TLS
	TLSVersion     string        // e.g., "TLS 1.3" (if IsTLS is true)
	TLSCipherSuite string        // e.g., "TLS_AES_128_GCM_SHA256" (if IsTLS is true)
	Error          error         // Error encountered during request execution or response processing
}

Response captures the details of an HTTP response received from a server.

type Script

type Script struct {
	Path       string // Path to an external .js file, if applicable.
	Content    string // Inline script content, if applicable.
	IsExternal bool   // True if the script is from an external file.
}

Script represents a JavaScript script, either inline or from an external file. It's used for pre-request and response handler scripts.

Directories

Path Synopsis

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL