Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
251 changes: 251 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,54 @@ A lightweight, production-grade Go client library for the USPS Addresses 3.0 RES

**Enterprise-grade address validation and standardization for Go applications.**

## Table of Contents

- [Why go-usps?](#why-go-usps)
- [Quick Start](#quick-start)
- [Core Concepts](#core-concepts)
- [Understanding USPS Address Validation](#understanding-usps-address-validation)
- [Three Core Endpoints](#three-core-endpoints)
- [Authentication](#authentication)
- [Error Handling](#error-handling)
- [Environments](#environments)
- [Usage Examples](#usage-examples)
- [E-commerce: Validate Checkout Addresses](#e-commerce-validate-checkout-addresses)
- [Bulk Address Processing](#bulk-address-processing)
- [Auto-complete ZIP Codes](#auto-complete-zip-codes)
- [Verify Business Addresses](#verify-business-addresses)
- [Format Addresses for Mailing](#format-addresses-for-mailing)
- [Address Parsing](#address-parsing)
- [Why Use the Parser?](#why-use-the-parser)
- [Quick Example](#quick-example)
- [Integration with USPS API](#integration-with-usps-api)
- [Key Features](#key-features)
- [Common Use Cases](#common-use-cases)
- [Advanced Usage](#advanced-usage)
- [Custom Token Provider](#custom-token-provider)
- [Custom HTTP Client](#custom-http-client)
- [Retry Logic with Exponential Backoff](#retry-logic-with-exponential-backoff)
- [Circuit Breaker Pattern](#circuit-breaker-pattern)
- [Request Middleware](#request-middleware)
- [Manual OAuth Management](#manual-oauth-management)
- [Testing with Mock Responses](#testing-with-mock-responses)
- [Advanced Topics](#advanced-topics)
- [Distributed Systems Considerations](#distributed-systems-considerations)
- [Caching Strategies](#caching-strategies)
- [Rate Limiting](#rate-limiting)
- [Observability](#observability)
- [Health Checks](#health-checks)
- [Production Checklist](#production-checklist)
- [API Reference](#api-reference)
- [Response Fields](#response-fields)
- [Configuration Options](#configuration-options)
- [Error Types](#error-types)
- [Additional Resources](#additional-resources)
- [Development](#development)
- [Requirements](#requirements)
- [API Documentation](#api-documentation)
- [License](#license)
- [Contributing](#contributing)

## Why go-usps?

- **🎯 Complete Coverage** - All USPS Addresses 3.0 and OAuth 2.0 endpoints
Expand Down Expand Up @@ -403,6 +451,209 @@ func FormatMailingLabel(address *models.AddressRequest) (string, error) {

---

## Address Parsing

The `parser` package provides intelligent parsing of free-form address strings into structured
`AddressRequest` objects. This is essential for handling user input that doesn't follow a strict format.

### Why Use the Parser?

- **Handle free-form input** - Parse addresses from a single text field
- **Smart tokenization** - Automatically identifies address components
- **USPS standardization** - Applies official USPS abbreviations and formatting
- **Validation feedback** - Provides diagnostics for missing or incorrect components
- **Zero dependencies** - Pure Go implementation using only the standard library

### Quick Example

```go
import (
"context"
"fmt"
"log"

"github.com/my-eq/go-usps"
"github.com/my-eq/go-usps/parser"
)

func main() {
// Parse a free-form address string
input := "123 North Main Street Apartment 4B, New York, NY 10001-1234"
parsed, diagnostics := parser.Parse(input)

// Check for issues
for _, d := range diagnostics {
fmt.Printf("%s: %s\n", d.Severity, d.Message)
}

// Convert to AddressRequest
req := parsed.ToAddressRequest()

fmt.Printf("Street: %s\n", req.StreetAddress) // "123 N MAIN ST"
fmt.Printf("Secondary: %s\n", req.SecondaryAddress) // "APT 4B"
fmt.Printf("City: %s\n", req.City) // "NEW YORK"
fmt.Printf("State: %s\n", req.State) // "NY"
fmt.Printf("ZIP: %s\n", req.ZIPCode) // "10001"
fmt.Printf("ZIP+4: %s\n", req.ZIPPlus4) // "1234"
}
```

### Integration with USPS API

Combine parsing with USPS validation for the complete workflow:

```go
import (
"context"
"fmt"
"log"

"github.com/my-eq/go-usps"
"github.com/my-eq/go-usps/parser"
)

func ValidateFreeFormAddress(userInput string) error {
// Step 1: Parse the free-form input
parsed, diagnostics := parser.Parse(userInput)

// Step 2: Check for critical errors
for _, d := range diagnostics {
if d.Severity == parser.SeverityError {
return fmt.Errorf("parse error: %s - %s", d.Message, d.Remediation)
}
}

// Step 3: Convert to AddressRequest
req := parsed.ToAddressRequest()

// Step 4: Validate with USPS API
client := usps.NewClientWithOAuth("client-id", "client-secret")
resp, err := client.GetAddress(context.Background(), req)
if err != nil {
return fmt.Errorf("validation failed: %v", err)
}

fmt.Printf("Validated address: %s, %s, %s %s\n",
resp.Address.StreetAddress,
resp.Address.City,
resp.Address.State,
resp.Address.ZIPCode)

return nil
}
```

### Key Features

**Intelligent Component Recognition:**

```go
// Handles directionals
parser.Parse("123 North Main St, New York, NY 10001")
// → Street: "123 N MAIN ST"

// Handles secondary units
parser.Parse("456 Oak Ave Apt 4B, Boston, MA 02101")
// → Street: "456 OAK AVE", Secondary: "APT 4B"

// Handles ZIP+4
parser.Parse("789 Elm Blvd, Chicago, IL 60601-1234")
// → ZIP: "60601", ZIP+4: "1234"
```

**Automatic Standardization:**

The parser applies USPS Publication 28 standards automatically:

| Input | Standardized |
|-----------------|--------------|
| Street | ST |
| Avenue | AVE |
| Boulevard | BLVD |
| North | N |
| Apartment | APT |
| Suite | STE |

**Diagnostics and Validation:**

```go
parsed, diagnostics := parser.Parse("123 Main St, New York")

for _, d := range diagnostics {
fmt.Printf("%s: %s\n", d.Severity, d.Message)
if d.Remediation != "" {
fmt.Printf(" Fix: %s\n", d.Remediation)
}
}

// Output:
// Error: Missing required state code
// Fix: Add a 2-letter state code (e.g., NY, CA, TX)
// Warning: Missing ZIP code
// Fix: Add a 5-digit ZIP code for better address validation
```

### Common Use Cases

**Single-field address input:**

```go
// User enters complete address in one field
userInput := "123 Main St Apt 4, New York, NY 10001"
parsed, _ := parser.Parse(userInput)
req := parsed.ToAddressRequest()

// Now ready for USPS validation
resp, err := client.GetAddress(ctx, req)
```

**Form auto-fill:**

```go
// Parse as user types, fill individual fields
parsed, _ := parser.Parse(userInput)

// Use ToAddressRequest() for proper formatting
req := parsed.ToAddressRequest()

streetField.SetText(req.StreetAddress)
secondaryField.SetText(req.SecondaryAddress)
cityField.SetText(req.City)
stateField.SetText(req.State)
zipField.SetText(req.ZIPCode)
```

**Import from CSV or external data:**

```go
// Parse addresses from external sources
for _, row := range csvData {
parsed, diagnostics := parser.Parse(row.AddressColumn)

// Check for errors
hasErrors := false
for _, d := range diagnostics {
if d.Severity == parser.SeverityError {
hasErrors = true
break
}
}

if hasErrors {
log.Printf("Skipping invalid address: %s", row.AddressColumn)
continue
}

req := parsed.ToAddressRequest()
// Validate with USPS...
}
```

For complete parser documentation, see the
[parser package README](https://github.com/my-eq/go-usps/blob/main/parser/README.md).

---

## Advanced Usage

### Custom Token Provider
Expand Down
4 changes: 3 additions & 1 deletion parser/parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,9 @@ func New() *Parser {
}

// Parse parses a free-form address string into a structured ParsedAddress.
// Returns the parsed address and any diagnostics (warnings or errors).
// It tokenizes the input, applies USPS standardization rules, and validates
// the address components. Returns the parsed address and any diagnostics
// (warnings or errors) encountered during parsing.
func Parse(input string) (*ParsedAddress, []Diagnostic) {
p := New()
return p.Parse(input)
Expand Down
3 changes: 3 additions & 0 deletions parser/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,9 @@ type ParsedAddress struct {
}

// ToAddressRequest converts a ParsedAddress to a models.AddressRequest.
// The method combines parsed components into the format required by the USPS API,
// including building the street address from house number, directionals, street name,
// and suffix, and the secondary address from unit designator and number.
func (p *ParsedAddress) ToAddressRequest() *models.AddressRequest {
req := &models.AddressRequest{
State: p.State,
Expand Down
Loading