spec2go

module
v1.1.0 Latest Latest
Warning

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

Go to latest
Published: Mar 22, 2026 License: MIT

README ΒΆ

πŸ“‹ spec2go

A Go implementation of the Specification Pattern for composable, reusable business rules.

Stop scattering validation logic across your codebase. spec2go lets you define small, testable business rules as specifications and combine them into policies β€” making your domain logic explicit, reusable, and easy to reason about.

CI codecov Go Reference Go

πŸ“‘ Table of Contents

πŸ” Overview

spec2go provides a clean, type-safe way to define and evaluate business rules using Go generics. Instead of scattering validation logic throughout your codebase, you define atomic specifications that can be composed into policies.

// Define failure reasons as typed constants
type Reason string

const (
    Underage        Reason = "UNDERAGE"
    EmailNotVerified Reason = "EMAIL_NOT_VERIFIED"
)

// Define specifications
isAdult := spec.New("IsAdult", func(u User) bool { return u.Age >= 18 }, Underage)
hasVerifiedEmail := spec.New("HasVerifiedEmail", func(u User) bool { return u.EmailVerified }, EmailNotVerified)

// Build a policy
registrationPolicy := spec.NewPolicy[User, Reason]().
    With(isAdult).
    With(hasVerifiedEmail)

// Evaluate
result := registrationPolicy.EvaluateFailFast(user)

if result.AllPassed() {
    // proceed
} else {
    // handle result.FailureReasons()
}

✨ Features

  • 🧩 Composable β€” Build complex rules from simple, reusable specifications
  • πŸ”’ Type-safe β€” Failure reasons use Go generics (R comparable), not bare strings
  • ⚑ Two evaluation modes β€” EvaluateFailFast (stops on first failure) or EvaluateAll (collects all failures)
  • πŸ”— Logical operators β€” AllOf, AnyOf, AnyOfAll, Not for combining specifications
  • 🐹 Idiomatic Go β€” Generics, interfaces, package-level functions, fmt.Stringer
  • 🚫 Zero dependencies β€” Pure Go standard library only

πŸ“¦ Installation

go get github.com/caik/spec2go
import "github.com/caik/spec2go/pkg/spec"

πŸ“š Core Concepts

πŸ“Œ Specification

A single, atomic condition that evaluates a context and returns pass/fail with a reason.

Simple spec via New:

minimumAge := spec.New("MinimumAge",
    func(a LoanApplication) bool { return a.ApplicantAge >= 18 },
    ReasonApplicantTooYoung,
)

Custom struct spec (for complex logic or multiple failure reasons):

type DocumentCheck struct {
    spec.NamedSpec[Claim, ClaimReason]
}

func (s DocumentCheck) Evaluate(ctx Claim) spec.SpecificationResult[ClaimReason] {
    var missing []ClaimReason
	
    if !ctx.HasIDDocument { 
		missing = append(missing, MissingID) 
	}
    
	if !ctx.HasProofOfLoss { 
		missing = append(missing, MissingProofOfLoss) 
	}
	
    if len(missing) == 0 {
        return spec.Pass[ClaimReason](s.Name())
    }
	
    return spec.Fail(s.Name(), missing...)
}
πŸ“œ Policy

An ordered collection of specifications evaluated as a unit:

loanPolicy := spec.NewPolicy[LoanApplication, Reason]().
    With(minimumAge).
    With(maximumAge).
    With(creditCheck)

// Fail-fast: stop at first failure (best for performance)
result := loanPolicy.EvaluateFailFast(application)

// Evaluate all: collect every failure (best for showing all errors to the user)
result := loanPolicy.EvaluateAll(application)
πŸ”— Composites

Combine specifications with logical operators:

// AND β€” all must pass (always evaluates all)
fullyVerified := spec.AllOf("FullyVerified", emailVerified, phoneVerified)

// OR β€” at least one must pass (short-circuits on first pass)
hasPayment := spec.AnyOf("HasPayment", hasCreditCard, hasBankAccount)

// OR β€” at least one must pass (evaluates all, no short-circuit)
hasPayment := spec.AnyOfAll("HasPayment", hasCreditCard, hasBankAccount)

// NOT β€” inverts the result
notBlocked := spec.Not("NotBlocked", ReasonBlocked, isBlockedCountry)

Composites can be nested arbitrarily and produce human-readable expressions:

fmt.Println(loanPolicy.String())
// (MinimumAge AND (GoodCreditScore OR (SufficientIncome AND IsEmployed)))
πŸ’‘ Failure Reasons

Since Go has no enums, define failure reasons as typed constants β€” any comparable type works:

// Typed string constants (recommended β€” prints meaningfully without extra code)
type LoanReason string

const (
    AgeTooYoung        LoanReason = "AGE_TOO_YOUNG"
    InsufficientIncome LoanReason = "INSUFFICIENT_INCOME"
    PoorCreditScore    LoanReason = "POOR_CREDIT_SCORE"
)

// Typed int with iota (more memory-efficient for large sets)
type LoanReason int

const (
    AgeTooYoung LoanReason = iota
    InsufficientIncome
    PoorCreditScore
)

🎯 Examples

The examples/ directory contains complete working examples:

# Loan eligibility β€” basic specs + AnyOf/AllOf nesting
go run ./examples/loan/

# E-commerce order validation β€” custom struct spec + Not
go run ./examples/ecommerce/

# Feature access control β€” multiple policies + dynamic spec factories
go run ./examples/accesscontrol/

πŸ› οΈ Building

# Build all packages
go build ./...

# Run all tests
go test ./...

# Run tests with race detector and coverage
go test ./... -race -coverprofile=coverage.out

# View coverage report
go tool cover -html=coverage.out

🀝 Contributing

Contributions are welcome! Please see CONTRIBUTING.md for guidelines.

βš–οΈ License

License

Released 2026 by Carlos Henrique Severino

Directories ΒΆ

Path Synopsis
examples
accesscontrol command
ecommerce command
loan command
pkg

Jump to

Keyboard shortcuts

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