Thank you for your interest in contributing to restclient! This document provides guidelines and best practices for contributing to this project.
- Getting Started
- Development Setup
- Project Structure
- Extending CLI & Configuration
- Code Style Guidelines
- Testing Guidelines
- Error Handling
- Documentation
- Pull Request Process
- Go 1.24.4 or later
- Make (for build automation)
# Clone the repository
git clone https://github.com/ideaspaper/restclient.git
cd restclient
# Install dependencies
go mod download
# Build the project
make build
# Run tests
make test
# Run linter
make lintrestclient/
├── main.go # Entry point (minimal code)
├── cmd/ # CLI commands (Cobra-based)
├── internal/ # Private packages (not exposed outside module)
│ ├── constants/ # Shared constants (headers, MIME types)
│ ├── filesystem/ # File system abstraction for testability
│ ├── httputil/ # HTTP utility functions
│ ├── paths/ # Path resolution utilities
│ └── stringutil/ # String manipulation helpers
├── pkg/ # Public, reusable packages
│ ├── auth/ # Authentication handlers
│ ├── client/ # HTTP client implementation
│ ├── config/ # Configuration management
│ ├── errors/ # Custom error types
│ ├── executor/ # Request execution logic
│ ├── history/ # Request history storage
│ ├── lastfile/ # Last used file tracking
│ ├── models/ # Data models (Request, Response)
│ ├── output/ # Response formatting
│ ├── parser/ # .http/.rest file parser
│ ├── postman/ # Postman import/export
│ ├── scripting/ # JavaScript scripting engine
│ ├── session/ # Session management
│ ├── tui/ # Terminal UI components
│ └── variables/ # Variable processing
└── examples/ # Example .http files
internal/: Private packages that cannot be imported outside the module. Use for implementation details.pkg/: Public, reusable packages. Use for functionality that could be used by external consumers.cmd/: CLI command definitions using Cobra framework.- Each package should have a focused, single responsibility.
- Declare the flag in
cmd/send.go(or the relevant command file) withininit()or the command builder. Follow existing naming conventions and include a helpful description. - Thread the value through the command workflow: update any helper functions (e.g.,
buildVariableProcessor,sendRequest) to accept the new option explicitly instead of reaching for globals. - Update config interactions if the flag mirrors a config knob. Make sure precedence (CLI flag > config file > default) remains clear.
- Document behavior in
README.md(usage examples) and this file if the flag affects contributor workflows. - Test coverage: add or expand table-driven tests in
cmd/send_test.go(or relevant package) to cover happy path plus edge cases (invalid values, conflicting flags).
- Default headers are centralized in
config.Config.DefaultHeaders. When adding new defaults, make them opt-in via config or flag rather than hard-coding per request. - Normalize header names using the helpers in
internal/httputilto avoid duplicates differing only by case. - If a default header introduces authentication behavior (e.g., API keys), ensure it can be disabled per-request via metadata and is covered by tests in
pkg/clientorpkg/executor.
- Add new resolver logic inside
pkg/variables/processor.go. Keep resolvers isolated (system vars vs. prompts vs. dotenv) and unit-tested via focused table tests (processor_test.go). - Fail fast when a resolver cannot satisfy a token (return a descriptive error instead of leaving
{{token}}intact). - If the resolver requires external input (filesystem, prompts), abstract it behind an interface so tests can supply fakes.
- Document the new syntax/behavior in
README.mdand reference examples inexamples/*.http.
- New behaviors that involve cancellation (e.g., long-running preprocessors) must accept
context.Contextand propagate it through to lower layers (pkg/executor,pkg/scripting). - When adding pre/post scripts, ensure both the executor and scripting tests cover interruption, success, and failure flows.
- Follow standard Go conventions and idioms
- Keep functions small and focused
- Prefer composition over inheritance
- Use dependency injection for testability
| Type | Convention | Example |
|---|---|---|
| Package names | Short, lowercase, no underscores | httputil, stringutil |
| Exported identifiers | PascalCase | HttpRequest, NewParser |
| Unexported identifiers | camelCase | parseMetadata, isComment |
| Acronyms | All caps | URL, HTTP, JSON, SSL |
| File names | Lowercase with underscores | http_parser.go, http_client.go |
| Test files | *_test.go suffix |
config_test.go |
| Mock files | *_mock.go suffix |
http_client_mock.go |
| Fuzz test files | *_fuzz_test.go suffix |
http_parser_fuzz_test.go |
| Pattern | Convention | Example |
|---|---|---|
| Constructors | New prefix |
NewHttpClient, NewParser |
| Getters | No Get prefix |
ContentType(), not GetContentType() |
| Boolean getters | Is/Has prefix |
IsValid(), HasPrefix() |
| Builder methods | With prefix |
WithResponse(), WithError() |
| Parse functions | Parse prefix |
ParseAll, ParseRequest |
- Receivers: Short, 1-2 letters (
cfor config,pfor parser,rfor request) - Loop variables:
i,jfor indices;k,vfor key-value pairs - Test cases:
ttfor test table entries,got/wantfor assertions
Organize imports in three groups, separated by blank lines:
import (
// Standard library
"context"
"fmt"
"net/http"
// External dependencies
"github.com/spf13/cobra"
"github.com/spf13/viper"
// Internal packages
"github.com/ideaspaper/restclient/internal/constants"
"github.com/ideaspaper/restclient/pkg/models"
)Group constants by category:
const (
// HTTP Headers
HeaderContentType = "Content-Type"
HeaderAuthorization = "Authorization"
)
const (
// MIME Types
MIMEApplicationJSON = "application/json"
MIMETextPlain = "text/plain"
)Use consistent struct tags for JSON and mapstructure:
type Config struct {
FollowRedirects bool `json:"followRedirect" mapstructure:"followRedirect"`
TimeoutMs int `json:"timeoutInMilliseconds" mapstructure:"timeoutInMilliseconds"`
}Use table-driven tests as the primary pattern:
func TestParseRequestLine(t *testing.T) {
tests := []struct {
name string
input string
wantMethod string
wantURL string
}{
{
name: "simple GET",
input: "GET https://api.example.com/users",
wantMethod: "GET",
wantURL: "https://api.example.com/users",
},
{
name: "POST with path",
input: "POST https://api.example.com/users/create",
wantMethod: "POST",
wantURL: "https://api.example.com/users/create",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
method, url := parseRequestLine(tt.input)
if method != tt.wantMethod {
t.Errorf("parseRequestLine() method = %v, want %v", method, tt.wantMethod)
}
if url != tt.wantURL {
t.Errorf("parseRequestLine() url = %v, want %v", url, tt.wantURL)
}
})
}
}- Subtest naming: Use descriptive names for subtests
- Error messages: Use format
"got X, want Y"or"field = X, want Y" - Test isolation: Use
t.TempDir()for temporary directories - Cleanup: Use
deferfor cleanup operations - HTTP testing: Use
httptest.NewServer()for HTTP client tests - Cancellation flows: When introducing contexts or signal handling, add table-driven tests that cover success, timeout, and explicit cancellation.
Define interfaces for testability and create mock implementations:
// Interface definition
type HTTPDoer interface {
Send(request *models.HttpRequest) (*models.HttpResponse, error)
SendWithContext(ctx context.Context, request *models.HttpRequest) (*models.HttpResponse, error)
}
// Mock implementation
type MockHTTPClient struct {
Response *models.HttpResponse
Error error
Requests []*models.HttpRequest
ResponseFunc func(req *models.HttpRequest) (*models.HttpResponse, error)
}
// Ensure interface compliance at compile time
var _ HTTPDoer = (*MockHTTPClient)(nil)
// Fluent builder methods for test setup
func (m *MockHTTPClient) WithResponse(resp *models.HttpResponse) *MockHTTPClient {
m.Response = resp
return m
}Use Go's native fuzzing for parser robustness:
func FuzzParseRequest(f *testing.F) {
// Add seed corpus
f.Add("GET https://example.com\n")
f.Add("POST https://example.com\nContent-Type: application/json\n\n{}")
f.Fuzz(func(t *testing.T, input string) {
// Parser should not panic on any input
parser := NewHttpRequestParser(input, nil, "")
_ = parser.ParseAll()
})
}# Run all tests
make test
# Run tests with coverage
make test-coverage
# Run specific package tests
go test -v ./pkg/parser/...
# Run fuzz tests
go test -fuzz=FuzzParseRequest ./pkg/parser/Use the custom error package for consistent error handling:
import "github.com/ideaspaper/restclient/pkg/errors"
// Use sentinel errors for comparison
if err != nil {
return errors.ErrNotFound
}
// Wrap errors with context
if err != nil {
return errors.Wrap(err, "failed to read config file")
}
// Use formatted wrapping
if err != nil {
return errors.Wrapf(err, "failed to parse request at line %d", lineNum)
}
// Create validation errors
if name == "$shared" {
return errors.NewValidationErrorWithValue("environment", "$shared", "cannot use reserved name")
}- Always wrap errors with context using
errors.Wrap()orerrors.Wrapf() - Use sentinel errors for errors that callers need to check with
errors.Is() - Use structured errors when additional context is needed
- Nil-safe wrapping:
errors.Wrap()returns nil if the error is nil
func LoadConfig() (*Config, error) {
data, err := os.ReadFile(path)
if err != nil {
if os.IsNotExist(err) {
return nil, errors.ErrNotFound
}
return nil, errors.Wrap(err, "failed to read config file")
}
var cfg Config
if err := json.Unmarshal(data, &cfg); err != nil {
return nil, errors.Wrap(err, "failed to parse config file")
}
return &cfg, nil
}Add a package-level comment at the top of the main file:
// Package errors provides custom error types and utilities for the REST client.
package errorsDocument exported functions starting with the function name:
// NewHttpRequestParser creates a new parser for HTTP request files.
// It accepts the file content, optional default headers, and the base directory
// for resolving relative file paths.
func NewHttpRequestParser(content string, defaultHeaders map[string]string, baseDir string) *HttpRequestParser {
// ...
}- Start documentation with the identifier name
- Be concise but descriptive
- Document exported items; unexported items are optional
- Use complete sentences
- Avoid redundant phrases like "This function..."
- Format code:
gofmt -w ./ - Run all tests:
make test - Format code: Code is auto-formatted by
gofmtandgoimports - Update documentation if adding new features
Write clear, concise commit messages:
- Use present tense ("Add feature" not "Added feature")
- Use imperative mood ("Move cursor to..." not "Moves cursor to...")
- Keep the first line under 72 characters
- Reference issues when applicable
Add GraphQL subscription support
- Implement WebSocket connection handling
- Add subscription query parsing
- Update documentation with examples
Fixes #123
Before requesting review, ensure:
- Code follows the style guidelines in this document
- All tests pass
- New code has appropriate test coverage
- Documentation is updated for new features
- No linter warnings
- Commit history is clean and logical
- Prefer small, composable packages; keep exported APIs minimal.
- Keep functions under 40 lines when possible; refactor helpers instead of nesting conditionals.
- Use context-aware variants (
FooWithContext) for operations that may block or depend on cancellation. - Handle errors explicitly; avoid ignoring returned errors unless there's documented justification.
- Add table-driven tests for new behaviors, including error cases and boundary conditions.
- Run
go test ./...before submitting pull requests. - Document exported identifiers with Go-style comments starting with the identifier name.