adapters

package module
v0.0.11 Latest Latest
Warning

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

Go to latest
Published: Nov 30, 2025 License: MIT Imports: 8 Imported by: 0

README

Station Manager: adapter package

A high-performance, thread-safe Go library for struct-to-struct field adaptation with intelligent handling of AdditionalData fields, pluggable converters, and validation hooks.

Features

  • Direct Field Copying using name or JSON tag match
  • Field Conversion via scoped converter functions (global, destination-type, (src,dst) pair)
  • Validation Hooks (same scoping precedence as converters) for enforcing invariants post-conversion
  • Smart AdditionalData marshal/unmarshal (opt-in/opt-out flags)
  • Copy-on-Write atomic registries (no locks on fast path)
  • Metadata caching & optional warming (zero allocations per field after warmup)
  • Builder API for ergonomic configuration
  • High test coverage (85%+) & concurrency tests

Installation

go get github.com/Station-Manager/adapters

API

  • Core: Into(dst, src) error copies from src struct to dst struct. Both must be pointers to structs.
  • Generics helpers:
    • Copy[T any](a *Adapter, dst *T, src any) error
    • AdaptTo[T any](a *Adapter, src any) (*T, error)
    • Make[T any](a *Adapter, src any) (T, error)
  • Registration:
    • Converters: RegisterConverter, RegisterConverterFor, RegisterConverterForPair
    • Validators: RegisterValidator, RegisterValidatorFor, RegisterValidatorForPair
    • Batch: Batch(func(*RegistryBatch)) to group registrations
Tags
  • Prefer json:"name" tags for field name matching.
  • adapter:"ignore" skips a field.
  • adapter:"additional" marks a field of type null.JSON or sqlboiler/types.JSON as AdditionalData.
AdditionalData semantics
  • Direct fields win by default (PreferFields). Switch to PreferAdditionalData via WithOverwritePolicy.
  • Case-insensitive key matching is opt-in: WithCaseInsensitiveAdditionalData(true).
  • Control marshaling/unmarshaling with WithDisableMarshalAdditionalData and WithDisableUnmarshalAdditionalData.

Performance

  • Metadata is cached; call WarmMetadata(samples...) to prebuild for hot types.
  • Case-insensitive matching uses precomputed lowercase maps to avoid per-key scans.
  • AdditionalData map is lazily allocated when needed to reduce allocations.

See PROFILING.md for profiling commands and tuning notes.

Quick Start

package main

import (
    "fmt"
    "github.com/Station-Manager/adapters"
)

// Basic usage: destination-first Into(dst, src)
type ExampleSrc struct{ Name string }
type ExampleDst struct{ Name string }

func main() {
    adapter := adapters.New()
    src := ExampleSrc{Name: "Alice"}
    var dst ExampleDst
    if err := adapter.Into(&dst, &src); err != nil {
        panic(err)
    }
    fmt.Println(dst.Name)
}
Generics helpers
package main

import (
    "fmt"
    "github.com/Station-Manager/adapters"
)

// Examples showing package-level generic helpers: AdaptTo, Copy, Make
type Src struct{ Name string }
type Dest struct{ Name string }

func exampleAdaptTo() {
    a := adapters.New()
    s := Src{Name: "Bob"}
    out, err := adapters.AdaptTo[Dest](a, &s)
    if err != nil {
        panic(err)
    }
    fmt.Println("AdaptTo ->", out.Name)
}

func exampleCopy() {
    a := adapters.New()
    s := Src{Name: "Claire"}
    var d Dest
    if err := adapters.Copy[Dest](a, &d, &s); err != nil {
        panic(err)
    }
    fmt.Println("Copy ->", d.Name)
}

func exampleMake() {
    a := adapters.New()
    s := Src{Name: "Dan"}
    v, err := adapters.Make[Dest](a, &s)
    if err != nil {
        panic(err)
    }
    fmt.Println("Make ->", v.Name)
}

func main() {
    exampleAdaptTo()
    exampleCopy()
    exampleMake()
}
Batch registration
adapter.Batch(func(r *adapters.RegistryBatch){
    r.GlobalConverter("Temperature", conv)
    r.GlobalValidator("Name", notEmpty)
})
AdditionalData field rename
type D struct {
    Extras null.JSON `adapter:"additional" json:"extras"`
}
Converters
adapter.RegisterConverter("Temperature", func(v any) (any, error) {
    c := v.(float64)
    return (c*9/5)+32, nil // Celsius -> Fahrenheit
})
Validators

Validators run after setting a field (and after any converter). Return an error to abort adaptation.

adapter.RegisterValidator("Name", func(v any) error {
    if len(v.(string)) == 0 { return errors.New("name required") }
    return nil
})
Builder API
ad := adapters.NewBuilder().
    WithOptions(adapters.WithCaseInsensitiveAdditionalData(true)).
    AddConverter("Name", adapters.MapString(strings.ToUpper)).
    AddValidator("Name", func(v any) error { if v.(string)=="" { return errors.New("empty") }; return nil }).
    Build()
AdditionalData Controls

Options:

  • WithDisableUnmarshalAdditionalData(true) skip source AdditionalData expansion
  • WithDisableMarshalAdditionalData(true) skip marshaling remaining fields into destination AdditionalData
  • WithIncludeZeroValues(true) include zero values when marshaling
  • WithCaseInsensitiveAdditionalData(true) case-insensitive key matching
  • WithOverwritePolicy(PreferAdditionalData) allow AdditionalData to overwrite direct fields
JSON Tag Precedence

Field matching order:

  1. Exact field name match
  2. JSON tag name match
  3. (If case-insensitive option on) case-insensitive variations

Adapter-specific struct tags (adapter:"ignore") are minimal and only used to ignore fields. Prefer JSON tags for naming.

Validation + Conversion Precedence

For both converters and validators: pair > destination-type > global.

Opting Out of AdditionalData

Use the disable options (above). If disabled, no JSON marshal/unmarshal occurs.

Metadata Warmup
adapter.WarmMetadata(ExampleSrc{}, ExampleDst{})

Pre-builds metadata to reduce first-call latency in hot paths.

Migration (vNext API change)

Previous API used Adapt(dst, src) with source first in some call sites. The new API standardizes on Into(&dst, &src) (destination first) for consistency with common Go patterns (io.Reader/io.Writer style). To migrate:

  1. Replace adapter.Adapt(a, b) with adapter.Into(b, a).
  2. Update any generic wrappers to use Copy/AdaptTo/Make helpers.
  3. Remove any direct field reflection; prefer batch registrations if adding many converters.

BuildPlan Cache

A build-plan cache accelerates repeated adaptations between the same (src,dst) type pair. Each plan stores:

  • Field index paths
  • Pre-resolved converter & validator functions (respecting precedence: pair > dst > global)
  • AdditionalData presence and indices
  • Adapter generation stamp (invalidates automatically when registries change)

This reduces per-field map lookups and dynamic converter resolution. Benchmarks (Intel i3-10100F) improvements:

  • BasicFieldCopy: ~1450ns -> ~508ns
  • WithConverter: ~1710ns -> ~564ns
  • LargeStruct: ~5140ns -> ~1560ns
  • Concurrent: ~432ns -> ~167ns

No public API change was required; plans are transparent. For latency-sensitive services, pre-warm metadata and plans early:

ad := adapters.New()
ad.WarmMetadata(ExampleSrc{}, ExampleDst{}) // metadata
// first Into call will create plan; optionally perform a single dry run during startup
_ = ad.Into(&ExampleDst{}, &ExampleSrc{})

A future helper WarmPlans(pairs...) could be added if needed.

Performance (updated)

  • Metadata & plan caches avoid repeated reflection and map lookups.
  • Lowercase maps eliminate O(n) scans for case-insensitive AdditionalData.
  • Lazy AdditionalData map & marshal skip allocations when not needed.
  • Copy-on-write registries keep the hot path lock-free.

Error Handling

Adapt returns an error if:

  • src or dst nil
  • Arguments not pointers to structs
  • Converter returns error
  • Validator returns error
  • AdditionalData contains invalid JSON

Concurrency

Registries use atomic pointer swaps with copy-on-write maps; Adapt performs only reads (no locks). Registering converters/validators is safe concurrently with adaptations.

Performance

Fast path operations avoid reflection map lookups by cached metadata.

License

See LICENSE.

Documentation

Overview

Package adapters provides struct-to-struct adaptation with field conversion and AdditionalData handling.

The Adapter type manages field conversions and performs struct-to-struct adaptation with special handling for AdditionalData fields of type null.JSON or sqlboiler/types.JSON.

Basic Usage

adapter := adapters.New()
err := adapter.Into(&destStruct, &sourceStruct)

Adaptation Rules

The Into method follows these rules in order:

  1. Copy fields with the same name and type directly
  2. Copy and convert fields with the same name using registered converters
  3. Marshal remaining source fields to dst.AdditionalData (null.JSON or types.JSON), if present
  4. Unmarshal src.AdditionalData (null.JSON or types.JSON) to populate dst fields

Field Converters

Register custom converters for specific field names:

adapter.RegisterConverter("Temperature", func(src interface{}) (interface{}, error) {
    temp := src.(float64)
    return int(temp), nil
})

Ignoring Fields

Fields can be excluded from adaptation using struct tags:

type User struct {
    Name     string
    Password string `adapter:"ignore"`  // Not copied or marshaled
    Email    string
    Token    string `adapter:"-"`       // Alternative syntax
}

Ignored fields are:

  • Not copied between source and destination structs
  • Not marshaled to AdditionalData
  • Not unmarshaled from AdditionalData

AdditionalData

The AdditionalData field (type null.JSON or types.JSON) has special handling:

  • Fields present in source but not in destination are marshaled to dst.AdditionalData
  • Fields in src.AdditionalData that match dst field names are unmarshaled to dst
  • Direct field copying takes precedence over AdditionalData unmarshaling by default

Embedded Structs

Embedded struct fields (including pointer-to-struct) are flattened and treated as if they were defined directly in the parent struct.

Thread Safety

The Adapter is safe for concurrent use. Multiple goroutines can call Into and register converters/validators concurrently. Internals use copy-on-write registries and cached plans.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func AdaptTo added in v0.0.7

func AdaptTo[T any](a *Adapter, src any) (*T, error)

func Copy added in v0.0.7

func Copy[T any](a *Adapter, dst *T, src any) error

func Make added in v0.0.7

func Make[T any](a *Adapter, src any) (T, error)

Types

type Adapter

type Adapter struct {
	// contains filtered or unexported fields
}

Adapter performs struct adaptation with optional converters & AdditionalData handling. See README for usage and option guidelines.

func New

func New() *Adapter

New creates an Adapter with default options.

func NewWithOptions added in v0.0.7

func NewWithOptions(opts ...Option) *Adapter

NewWithOptions creates a new Adapter with provided options.

func (*Adapter) Batch added in v0.0.7

func (a *Adapter) Batch(apply func(*RegistryBatch))

func (*Adapter) Into added in v0.0.7

func (a *Adapter) Into(dst, src interface{}) error

Into performs adaptation from src -> dst; dst,src order for ergonomics

func (*Adapter) RegisterConverter

func (a *Adapter) RegisterConverter(fieldName string, fn ConverterFunc)

RegisterConverter adds a global field converter (applies to any src/dst containing fieldName).

func (*Adapter) RegisterConverterFor added in v0.0.7

func (a *Adapter) RegisterConverterFor(dstType any, fieldName string, fn ConverterFunc)

RegisterConverterFor scope: destination type + fieldName.

func (*Adapter) RegisterConverterForPair added in v0.0.7

func (a *Adapter) RegisterConverterForPair(srcType, dstType any, fieldName string, fn ConverterFunc)

RegisterConverterForPair scope: (srcType,dstType)+fieldName highest precedence.

func (*Adapter) RegisterValidator added in v0.0.7

func (a *Adapter) RegisterValidator(fieldName string, fn ValidatorFunc)

RegisterValidator adds a global validator for a field name.

func (*Adapter) RegisterValidatorFor added in v0.0.7

func (a *Adapter) RegisterValidatorFor(dstType any, fieldName string, fn ValidatorFunc)

RegisterValidatorFor adds a validator scoped to a destination type.

func (*Adapter) RegisterValidatorForPair added in v0.0.7

func (a *Adapter) RegisterValidatorForPair(srcType, dstType any, fieldName string, fn ValidatorFunc)

RegisterValidatorForPair adds a validator scoped to (srcType,dstType) for a field name.

func (*Adapter) WarmMetadata added in v0.0.7

func (a *Adapter) WarmMetadata(examples ...any)

WarmMetadata pre-builds metadata for provided example values or types.

type Builder added in v0.0.7

type Builder struct {
	// contains filtered or unexported fields
}

Builder provides a fluent API to construct an Adapter with options, converters and validators pre-registered.

func NewBuilder added in v0.0.7

func NewBuilder() *Builder

NewBuilder creates a new builder.

func (*Builder) AddConverter added in v0.0.7

func (b *Builder) AddConverter(field string, fn ConverterFunc) *Builder

AddConverter registers a global converter by field name.

func (*Builder) AddConverterFor added in v0.0.7

func (b *Builder) AddConverterFor(dst any, field string, fn ConverterFunc) *Builder

AddConverterFor registers a converter for a destination type and field name.

func (*Builder) AddConverterForPair added in v0.0.7

func (b *Builder) AddConverterForPair(src, dst any, field string, fn ConverterFunc) *Builder

AddConverterForPair registers a converter for a (src,dst) pair and field name.

func (*Builder) AddValidator added in v0.0.7

func (b *Builder) AddValidator(field string, fn ValidatorFunc) *Builder

AddValidator registers a global validator by field name.

func (*Builder) AddValidatorFor added in v0.0.7

func (b *Builder) AddValidatorFor(dst any, field string, fn ValidatorFunc) *Builder

AddValidatorFor registers a validator for a destination type and field name.

func (*Builder) AddValidatorForPair added in v0.0.7

func (b *Builder) AddValidatorForPair(src, dst any, field string, fn ValidatorFunc) *Builder

AddValidatorForPair registers a validator for a (src,dst) pair and field name.

func (*Builder) Build added in v0.0.7

func (b *Builder) Build() *Adapter

Build constructs an Adapter using a single registry swap for converters and validators.

func (*Builder) WithOptions added in v0.0.7

func (b *Builder) WithOptions(opts ...Option) *Builder

WithOptions appends adapter options to the builder.

type ConverterFunc

type ConverterFunc func(src interface{}) (interface{}, error)

ConverterFunc is a function that converts a source field value to a destination field value. It is registered by field name and applies to any source/destination struct pair.

func ComposeConverters added in v0.0.7

func ComposeConverters(fns ...ConverterFunc) ConverterFunc

Composition helpers ComposeConverters chains multiple ConverterFunc instances left-to-right. If any converter returns an error it aborts. Nil output propagates immediately.

func MapString added in v0.0.7

func MapString(f func(string) string) ConverterFunc

MapString returns a ConverterFunc applying f when src is a string; otherwise returns src unchanged.

type Option added in v0.0.7

type Option func(*Options)

func WithCaseInsensitiveAdditionalData added in v0.0.7

func WithCaseInsensitiveAdditionalData(v bool) Option

func WithDisableMarshalAdditionalData added in v0.0.7

func WithDisableMarshalAdditionalData(v bool) Option

func WithDisableUnmarshalAdditionalData added in v0.0.7

func WithDisableUnmarshalAdditionalData(v bool) Option

func WithIncludeZeroValues added in v0.0.7

func WithIncludeZeroValues(v bool) Option

func WithOverwritePolicy added in v0.0.7

func WithOverwritePolicy(p OverwritePolicy) Option

type Options added in v0.0.7

type Options struct {
	IncludeZeroValues              bool            // when true, include zero-valued fields in marshaled AdditionalData
	CaseInsensitiveAdditionalData  bool            // when true, AdditionalData keys are matched case-insensitively
	OverwritePolicy                OverwritePolicy // controls if AdditionalData overwrites direct fields
	DisableMarshalAdditionalData   bool            // when true, do not marshal remaining fields into destination AdditionalData
	DisableUnmarshalAdditionalData bool            // when true, ignore source AdditionalData
}

type OverwritePolicy added in v0.0.7

type OverwritePolicy int

OverwritePolicy controls how AdditionalData values interact with already-set fields

const (
	PreferFields         OverwritePolicy = iota // default: do not overwrite fields set from direct mapping
	PreferAdditionalData                        // overwrite fields with values from AdditionalData if present
)

type RegistryBatch added in v0.0.7

type RegistryBatch struct {
	// contains filtered or unexported fields
}

Batch registration to reduce COW churn

func (*RegistryBatch) ConverterFor added in v0.0.7

func (b *RegistryBatch) ConverterFor(dst any, field string, fn ConverterFunc)

func (*RegistryBatch) ConverterForPair added in v0.0.7

func (b *RegistryBatch) ConverterForPair(src, dst any, field string, fn ConverterFunc)

func (*RegistryBatch) GlobalConverter added in v0.0.7

func (b *RegistryBatch) GlobalConverter(field string, fn ConverterFunc)

RegistryBatch helpers

func (*RegistryBatch) GlobalValidator added in v0.0.7

func (b *RegistryBatch) GlobalValidator(field string, fn ValidatorFunc)

func (*RegistryBatch) ValidatorFor added in v0.0.7

func (b *RegistryBatch) ValidatorFor(dst any, field string, fn ValidatorFunc)

func (*RegistryBatch) ValidatorForPair added in v0.0.7

func (b *RegistryBatch) ValidatorForPair(src, dst any, field string, fn ValidatorFunc)

type ValidatorFunc added in v0.0.7

type ValidatorFunc func(value interface{}) error

ValidatorFunc validates a field value after conversion and assignment candidate.

Directories

Path Synopsis

Jump to

Keyboard shortcuts

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