vcr

package module
v0.0.0-...-3b3a803 Latest Latest
Warning

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

Go to latest
Published: Jul 17, 2026 License: BSD-3-Clause Imports: 11 Imported by: 0

README

go-ruby-vcr/vcr

vcr — go-ruby-vcr

Docs License Go Coverage

A pure-Go (no cgo) reimplementation of the deterministic core of Ruby's vcr gem — record HTTP interactions to cassettes and replay them on later runs so tests are deterministic and offline. It reproduces the cassette store, the record/replay state machine, the four record modes, the request matchers, and VCR's default YAML cassette format — without any Ruby runtime.

It is the VCR library for go-embedded-ruby (require "vcr"), but a standalone, reusable module, a sibling of the other go-ruby-* gem ports.

What it is — and isn't. Everything VCR does around the wire is deterministic and needs no interpreter, so it lives here as pure Go: the ordered list of recorded interactions, matching an incoming request against them, deciding (per record mode) whether to replay or record, and serializing the cassette to VCR's YAML schema byte-for-byte. The HTTP round-trip, the filesystem, and the wall clock are host seams: FS supplies ReadFile/WriteFile/MkdirAll (OSFS in production, an in-memory map in tests), VCR.Now supplies the recorded_at timestamp, and Cassette.Interact takes a doer func(Request) (Response, error) that performs the real request only when the cassette must record. The core opens no socket and touches no real disk of its own. The rbgo binding wires the seams to Ruby's Net::HTTP, File, and Time.

Features

Faithful port of the vcr gem's core:

  • Cassette store — an ordered list of Interaction{Request, Response, RecordedAt} with a record/replay state machine per use_cassette scope.
  • Record modesRecordOnce (:once), RecordNone (:none), RecordNewEpisodes (:new_episodes), RecordAll (:all) with VCR's exact semantics, including :once refusing to record against an existing cassette and :all re-recording from scratch.
  • Request matchingMatchMethod, MatchURI, MatchBody, MatchHeaders; the default matcher set is method + URI, and matchers are composable per cassette. Each recorded interaction is replayed once unless AllowPlaybackRepeats is set.
  • Cassette YAML — a tiny, dependency-free (de)serializer matching VCR's default schema (http_interactions:request:{method,uri,body,headers} + response:{status:{code,message},headers,body:{encoding,string}} + recorded_at), with byte-faithful round-trip.
  • Unhandled requests — an *UnhandledRequestError (VCR's UnhandledHTTPRequestError) when a request matches nothing and the mode forbids recording.
  • SeamsFS (filesystem), VCR.Now (clock), and the Interact doer (HTTP); every branch is reproducible with an in-memory FS and a fixed clock.

CGO-free, dependency-free (stdlib only), 100% test coverage, gofmt + go vet clean, and green across the six 64-bit Go targets (amd64, arm64, riscv64, loong64, ppc64le, s390x — big-endian) plus js/wasm and wasip1/wasm.

Install

go get github.com/go-ruby-vcr/vcr

Usage

package main

import (
	"fmt"

	"github.com/go-ruby-vcr/vcr"
)

func main() {
	v := vcr.New() // cassette dir "fixtures/vcr_cassettes", mode :once, os FS

	err := v.UseCassette("example", vcr.CassetteOptions{}, func(c *vcr.Cassette) error {
		req := vcr.Request{Method: "get", URI: "http://example.com/"}

		// Replays a matching recorded interaction, or records a new one by
		// calling the doer (the HTTP seam) and storing its response.
		resp, err := c.Interact(req, func(r vcr.Request) (vcr.Response, error) {
			// The rbgo binding performs the real Net::HTTP request here.
			return vcr.Response{
				Status: vcr.Status{Code: 200, Message: "OK"},
				Body:   vcr.Body{Encoding: "UTF-8", String: "hello"},
			}, nil
		})
		if err != nil {
			return err
		}
		fmt.Println(resp.Body.String)
		return nil
	})
	if err != nil {
		panic(err)
	}
}
Selecting a record mode and matchers
mode := vcr.RecordNewEpisodes
err := v.UseCassette("api", vcr.CassetteOptions{
	RecordMode: &mode,
	Matchers:   []vcr.RequestMatcher{vcr.MatchMethod, vcr.MatchURI, vcr.MatchBody},
}, func(c *vcr.Cassette) error {
	// ...
	return nil
})
Injecting the filesystem and clock (tests / hosts)
v := &vcr.VCR{
	CassetteDir: "cassettes",
	RecordMode:  vcr.RecordOnce,
	Matchers:    vcr.DefaultMatchers(),
	FS: vcr.FS{ // in-memory seam
		ReadFile:  memRead,
		WriteFile: memWrite,
		MkdirAll:  memMkdir,
	},
	Now: func() time.Time { return fixedTime },
}

Value model

gem this package
VCR.configure { |c| c.cassette_library_dir = … } &VCR{CassetteDir: …, RecordMode: …, Matchers: …}
VCR.use_cassette(name, record:) { … } (*VCR).UseCassette(name, CassetteOptions{…}, fn)
:once / :none / :new_episodes / :all RecordOnce / RecordNone / RecordNewEpisodes / RecordAll
request matchers [:method, :uri, :body, …] MatchMethod / MatchURI / MatchBody / MatchHeaders
the cassette (http_interactions: YAML) MarshalCassette / ParseCassette ([]Interaction)
an HTTPInteraction (request/response/time) Interaction{Request, Response, RecordedAt}
UnhandledHTTPRequestError *UnhandledRequestError
the intercepted Net::HTTP request the Interact doer (host seam)
the cassette file on disk the FS seam (OSFS in production)

Tests & coverage

The suite is deterministic: the cassette store is driven through an in-memory FS seam and a fixed clock, so every branch — all four record modes, every matcher, the unknown-request-in-:none error, cassette-not-found, and each filesystem-error path — is reproducible. No test opens a socket or touches a real disk, so the cross-arch qemu lanes and the Windows lane all hold coverage at 100%.

COVERPKG=$(go list ./... | paste -sd, -)
go test -race -coverpkg="$COVERPKG" -coverprofile=cover.out ./...
go tool cover -func=cover.out | tail -1   # 100.0%

WebAssembly

Being pure Go (CGO=0), this library also compiles to WebAssembly — both GOOS=js GOARCH=wasm (browser / Node.js) and GOOS=wasip1 GOARCH=wasm (WASI). CI builds both targets on every push, alongside the six 64-bit native/qemu arches.

GOOS=js     GOARCH=wasm go build ./...   # browser / Node
GOOS=wasip1 GOARCH=wasm go build ./...   # WASI (wasmtime, wasmer, wasmedge, …)

License

BSD-3-Clause — see LICENSE. Copyright the go-ruby-vcr/vcr authors.

Documentation

Overview

Package vcr is a pure-Go (CGO=0, stdlib-only) reimplementation of the Ruby gem "vcr" (https://github.com/vcr/vcr). It records HTTP interactions to "cassettes" and replays them on subsequent runs, so tests become deterministic and offline.

The library is the cassette store, request matcher, and record/replay state machine. It deliberately does NOT perform HTTP or read a wall clock itself: those are seams supplied by the caller (in particular the rbgo binding, which intercepts its bound Net::HTTP). This keeps the whole package deterministic and testable with an in-memory filesystem.

Seams

  • Filesystem: FS is a struct of func fields (ReadFile/WriteFile/MkdirAll). OSFS returns the os-backed implementation; tests inject an in-memory one.
  • Clock: VCR.Now supplies the timestamp stored as recorded_at.
  • HTTP: Cassette.Interact takes a doer func(Request) (Response, error) that performs the real request only when the cassette must record. The rbgo binding supplies a doer wired to Net::HTTP.

Record modes

Cassette format

Cassettes are serialized to VCR's default YAML schema: a top-level http_interactions sequence of {request, response, recorded_at} maps. The serializer is a small purpose-built emitter/parser (no third-party YAML dependency) that round-trips the schema byte-for-byte.

The intended Ruby surface, provided by the rbgo binding on top of this package, is:

VCR.configure { |c| c.cassette_library_dir = "..."; c.default_record_mode = :once }
VCR.use_cassette("name", record: :new_episodes) { ... }

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func MarshalCassette

func MarshalCassette(interactions []Interaction) []byte

MarshalCassette serializes interactions to VCR's default cassette YAML schema. The output round-trips through ParseCassette byte-for-byte.

func MatchBody

func MatchBody(recorded, incoming Request) bool

MatchBody matches on the request body string.

func MatchHeaders

func MatchHeaders(recorded, incoming Request) bool

MatchHeaders matches on the full set of request headers (names and their ordered values).

func MatchMethod

func MatchMethod(recorded, incoming Request) bool

MatchMethod matches on the HTTP method.

func MatchURI

func MatchURI(recorded, incoming Request) bool

MatchURI matches on the full request URI.

Types

type Body

type Body struct {
	Encoding string
	String   string
}

Body models the {encoding, string} pair VCR stores for both request and response bodies.

type Cassette

type Cassette struct {
	// Name is the cassette name as passed to UseCassette (without the .yml
	// extension added for the on-disk path).
	Name string
	// contains filtered or unexported fields
}

Cassette is an ordered list of recorded interactions plus the record/replay state machine for a single VCR.use_cassette scope. It is created by VCR.UseCassette; the block receives it and drives replay/record through Cassette.Interact (or the lower-level Cassette.Play/Cassette.Record).

func (*Cassette) CanRecord

func (c *Cassette) CanRecord() bool

CanRecord reports whether the active record mode permits recording a new interaction into this cassette.

func (*Cassette) Interact

func (c *Cassette) Interact(req Request, doer func(Request) (Response, error)) (Response, error)

Interact is the full record/replay decision for one request. It models what the rbgo binding does around a bound Net::HTTP call:

  • Unless the mode is RecordAll, it tries to replay a matching interaction.
  • Otherwise, if the mode permits recording, it invokes doer to perform the real request and records the result.
  • Otherwise it returns an *UnhandledRequestError.

doer is the HTTP seam: the caller performs the real request. It is only called when the cassette must record.

func (*Cassette) Interactions

func (c *Cassette) Interactions() []Interaction

Interactions returns a copy of the interactions the cassette currently holds (loaded from disk plus any recorded during this scope).

func (*Cassette) Mode

func (c *Cassette) Mode() RecordMode

Mode returns the cassette's record mode.

func (*Cassette) Play

func (c *Cassette) Play(req Request) (Response, bool)

Play returns the response of the first unplayed recorded interaction whose request matches req under the cassette's matchers, and true. If no interaction matches it returns the zero Response and false. Unless playback repeats are allowed, each recorded interaction is played at most once.

func (*Cassette) Record

func (c *Cassette) Record(req Request, resp Response)

Record appends a new interaction (timestamped by the clock seam) and marks the cassette dirty so it is written on eject. Newly recorded interactions are flagged as played so they are not replayed within the same scope.

type CassetteOptions

type CassetteOptions struct {
	RecordMode           *RecordMode
	Matchers             []RequestMatcher
	AllowPlaybackRepeats *bool
}

CassetteOptions overrides per-cassette settings for a single UseCassette call. Nil pointers/empty slices mean "inherit the VCR default".

type FS

type FS struct {
	ReadFile  func(name string) ([]byte, error)
	WriteFile func(name string, data []byte, perm os.FileMode) error
	MkdirAll  func(path string, perm os.FileMode) error
}

FS is the filesystem seam. Its func fields mirror the os package so tests can inject an in-memory implementation and drive every error branch deterministically. Use OSFS for the os-backed implementation.

func OSFS

func OSFS() FS

OSFS returns an FS backed by the os package.

type FormatError

type FormatError struct {
	Msg string
}

FormatError indicates that cassette bytes could not be parsed as a valid VCR cassette.

func (*FormatError) Error

func (e *FormatError) Error() string

type Interaction

type Interaction struct {
	Request    Request
	Response   Response
	RecordedAt time.Time
}

Interaction is one recorded request/response pair with the time it was recorded.

func ParseCassette

func ParseCassette(data []byte) ([]Interaction, error)

ParseCassette parses cassette bytes in VCR's YAML schema into interactions.

type RecordMode

type RecordMode int

RecordMode selects how a cassette reconciles incoming requests with what it already holds. It mirrors VCR's :once/:none/:new_episodes/:all symbols.

const (
	// RecordOnce replays recorded interactions; it records new ones only when the
	// cassette did not already exist on disk.
	RecordOnce RecordMode = iota
	// RecordNone replays only; an unmatched request is an error.
	RecordNone
	// RecordNewEpisodes replays known interactions and records new ones.
	RecordNewEpisodes
	// RecordAll never replays and always records, re-writing the cassette.
	RecordAll
)

func ParseRecordMode

func ParseRecordMode(s string) (RecordMode, error)

ParseRecordMode maps a VCR record-mode name to a RecordMode. It accepts both the bare name ("once") and the Ruby symbol spelling (":once").

func (RecordMode) String

func (m RecordMode) String() string

String returns the VCR symbol name (without the leading colon).

type Request

type Request struct {
	Method  string
	URI     string
	Body    Body
	Headers map[string][]string
}

Request is the recorded/incoming HTTP request as VCR models it. Headers maps a header name to its ordered list of values, matching the YAML schema.

type RequestMatcher

type RequestMatcher func(recorded, incoming Request) bool

RequestMatcher reports whether an incoming request should be considered a match for a previously recorded request. The first argument is the recorded request, the second is the incoming one, mirroring VCR's request matchers.

func DefaultMatchers

func DefaultMatchers() []RequestMatcher

DefaultMatchers returns VCR's default matcher set: method and URI.

type Response

type Response struct {
	Status      Status
	Headers     map[string][]string
	Body        Body
	HTTPVersion string
}

Response is the recorded HTTP response.

type Status

type Status struct {
	Code    int
	Message string
}

Status is the response status line: numeric code plus reason phrase.

type UnhandledRequestError

type UnhandledRequestError struct {
	CassetteName string
	Request      Request
	RecordMode   RecordMode
}

UnhandledRequestError is returned when an incoming request matches no recorded interaction and the active record mode does not permit recording it. It corresponds to VCR's Errors::UnhandledHTTPRequestError.

func (*UnhandledRequestError) Error

func (e *UnhandledRequestError) Error() string

type VCR

type VCR struct {
	// CassetteDir is the directory cassettes are read from and written to
	// (Ruby: cassette_library_dir).
	CassetteDir string
	// RecordMode is the default record mode for cassettes that do not override it
	// (Ruby: default_record_mode).
	RecordMode RecordMode
	// Matchers is the default matcher set; empty means DefaultMatchers.
	Matchers []RequestMatcher
	// AllowPlaybackRepeats lets a single recorded interaction be replayed more
	// than once (Ruby: allow_playback_repeats).
	AllowPlaybackRepeats bool
	// FS is the filesystem seam. Zero value is unusable; use OSFS or an injected
	// implementation.
	FS FS
	// Now is the clock seam used for recorded_at. Nil means time.Now.
	Now func() time.Time
}

VCR is the top-level configuration and cassette manager. It corresponds to Ruby's VCR module configured via VCR.configure.

func New

func New() *VCR

New returns a VCR with the conventional defaults: cassette directory "fixtures/vcr_cassettes", record mode :once, the default matchers, and the os-backed filesystem.

func (*VCR) UseCassette

func (v *VCR) UseCassette(name string, opts CassetteOptions, fn func(*Cassette) error) error

UseCassette inserts the named cassette, runs fn with it, then ejects it, writing the cassette back to disk when the scope recorded anything (or when the mode is RecordAll). It mirrors VCR.use_cassette(name, options) { ... }.

A missing cassette file is not an error (it means "no prior recordings"); any other filesystem error, and any cassette parse error, is returned. When fn returns an error the cassette is still ejected, and fn's error takes precedence over an eject error.

Jump to

Keyboard shortcuts

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