delta

package module
v1.0.0 Latest Latest
Warning

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

Go to latest
Published: Jun 15, 2026 License: MIT Imports: 6 Imported by: 0

README

delta-go

A generic, reflection-based HTTP PATCH engine for Go structs.

go get github.com/VishalDalwadi/delta-go

The contract

Patch payload Behaviour
Key present with a value Field is updated
Key absent Field is left as-is
Key present, value null Field is reset to its zero value

The original object is never mutated. Apply deep-clones it, applies changes to the clone, and returns both as pointers — Original is the same pointer you passed in, Object is a fresh allocation.


Basic usage

import delta "github.com/VishalDalwadi/delta-go"

// 1. Parse the PATCH body
p, err := delta.NewPatch[User](r.Body, /* opts */)
if err != nil {
    http.Error(w, err.Error(), http.StatusBadRequest)
    return
}

// Or from an io.Reader directly
p, err := delta.NewPatchFromReader[User](r.Body)

// 2. Fetch your existing object
user, err := db.GetUser(id)

// 3. Apply
result, err := p.Apply(&user)
if err != nil {
    http.Error(w, err.Error(), http.StatusBadRequest)
    return
}

// result.Original      — same pointer as &user, never mutated
// result.Object        — *User, patched clone, ready to persist
// result.UpdatedFields — json tag names of fields that actually changed
// result.Diff()        — []FieldDiff with old/new values per changed field

Acting on changes

UpdatedFields lists only fields whose value actually changed (same value sent → not listed). Diff() gives you old and new side by side — computed lazily from Original and Object.

result, err := p.Apply(&existing)

// Inspect what changed
for _, d := range result.Diff() {
    switch d.Field {
    case "email":
        sendVerificationEmail(d.OldValue.(string), d.NewValue.(string))
    case "groups":
        auditGroupChange(d.OldValue.([]string), d.NewValue.([]string))
    }
}

// Or just loop updated field names if you only need the new values
for _, field := range result.UpdatedFields {
    switch field {
    case "is_admin":
        notifySecurityTeam(result.Object)
    }
}

db.SaveUser(result.Object)

Field restrictions

Restrict which fields may be patched at the call site.

// Include — only these fields may be patched
result, err := p.Apply(&user, delta.IncludeFields("display_name", "email"))

// Exclude — block specific fields
result, err := p.Apply(&user, delta.ExcludeFields("id", "created_at"))

// Both — effective = include ∩ complement(exclude)
result, err := p.Apply(&user,
    delta.IncludeFields("email", "display_name", "groups"),
    delta.ExcludeFields("groups"),
)
// effective: email, display_name only

Any field in the patch that is not in the effective set returns an error. Unknown fields not present on the struct are silently ignored — this preserves backward compatibility across schema changes.


Nested structs

Nested structs are patched with the same partial semantics, recursively. Only the keys sent inside the nested object are touched.

{ "address": { "city": "Reno" } }
// address.city → "Reno"
// address.street, address.zip → untouched

Map and slice operations

By default, maps and slices are full-replaced when their key is present. Enable operations for finer control:

p, err := delta.NewPatch[User](data, delta.WithOperations())

Operations are validated at construction time — sending a slice operation on a map field (or vice versa) returns an error.

Map operations
{ "tags": { "$set":   { "env": "prod" } } }
{ "tags": { "$merge": { "env": "prod", "tier": "gold" } } }
{ "tags": { "$unset": ["old_key"] } }
{ "tags": { "$clear": true } }
Operation Behaviour
$set Set a single key (or a small number of keys)
$merge Merge an object in — adds new keys, updates existing ones
$unset Remove the listed keys
$clear Wipe the entire map — takes priority over other ops
(plain object) Full replace
Slice operations
{ "groups": { "$add":    ["admin"] } }
{ "groups": { "$merge":  ["admin"] } }
{ "groups": { "$remove": ["viewer"] } }
{ "groups": { "$clear":  true } }
Operation Behaviour
$add Append elements — duplicates allowed
$merge Append only if not already present (dedup)
$remove Remove matching elements
$clear Empty the slice — takes priority over other ops
(plain array) Full replace

API reference

Constructors
// From []byte
func NewPatch[T any](data []byte, opts ...PatchOption) (*Patch[T], error)

// From io.Reader
func NewPatchFromReader[T any](r io.Reader, opts ...PatchOption) (*Patch[T], error)
PatchOption
// Enable $set/$unset/$merge/$clear for maps and $add/$remove/$merge/$clear for slices.
// Validates operations at construction time.
func WithOperations() PatchOption
Apply
func (p *Patch[T]) Apply(target *T, opts ...ApplyOption) (ApplyResult[T], error)
ApplyOption
// Restrict patchable fields to this list.
func IncludeFields(fields ...string) ApplyOption

// Block these fields from being patched.
func ExcludeFields(fields ...string) ApplyOption
ApplyResult
type ApplyResult[T any] struct {
    Original      *T       // same pointer passed to Apply — never mutated
    Object        *T       // freshly allocated patched clone
    UpdatedFields []string // json tag names of fields that actually changed
}

// Diff returns old/new values for each field in UpdatedFields.
func (r ApplyResult[T]) Diff() []FieldDiff
FieldDiff
type FieldDiff struct {
    Field    string
    OldValue any
    NewValue any
}

Design notes

  • No mutation — original deep-cloned via JSON round-trip before any field is touched
  • Pointer identityOriginal is the same pointer you passed; Object is a new allocation
  • No-op detection — fields only appear in UpdatedFields if the value genuinely changed
  • Silent unknown fields — forward/backward compatible across API schema evolution
  • Early validation — operation type mismatches caught at NewPatch, not at Apply
  • Operations opt-in — ship with plain replace everywhere, enable per-endpoint as needed
  • Policy at the call siteIncludeFields/ExcludeFields per handler, per role

Requirements

Go 1.21+ (generics)


License

MIT

Documentation

Overview

Package delta provides a generic, reflection-based HTTP PATCH engine for Go structs.

It supports partial updates (key present = update, key absent = leave, null = reset), field include/exclude lists, deep cloning with pointer identity on the original, change tracking, diffing, and opt-in operation wrappers for fine-grained map and slice control.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

This section is empty.

Types

type ApplyOption

type ApplyOption func(*options)

ApplyOption configures a single Apply call.

func ExcludeFields

func ExcludeFields(fields ...string) ApplyOption

ExcludeFields blocks these fields from being patched (by json tag name). Combined with IncludeFields: effective = include ∩ complement(exclude).

func IncludeFields

func IncludeFields(fields ...string) ApplyOption

IncludeFields restricts the patch to only these fields (by json tag name). Combined with ExcludeFields: effective = include ∩ complement(exclude).

type ApplyResult

type ApplyResult[T any] struct {
	// Original is the same pointer passed to Apply — never mutated.
	Original *T
	// Object is a freshly allocated deep-clone of Original with changes applied.
	Object *T
	// UpdatedFields lists json tag names of fields whose value actually changed.
	UpdatedFields []string
}

ApplyResult is returned by Patch.Apply.

func (ApplyResult[T]) Diff

func (r ApplyResult[T]) Diff() []FieldDiff

Diff returns a flat list of old/new values for each field in UpdatedFields. Reflects into Original and Object — both must be non-nil.

type FieldDiff

type FieldDiff struct {
	Field    string
	OldValue any
	NewValue any
}

FieldDiff holds the old and new value of a single changed field.

type Patch

type Patch[T any] struct {
	// contains filtered or unexported fields
}

Patch is the entry point for applying a PATCH payload to a value of type T. Constructed via NewPatch or NewPatchFromReader.

func NewPatch

func NewPatch[T any](data []byte, opts ...PatchOption) (*Patch[T], error)

NewPatch parses a JSON PATCH body and constructs a Patch[T]. Returns an error if the body is not a valid JSON object, or if WithOperations is set and the payload contains invalid operation shapes.

func NewPatchFromReader

func NewPatchFromReader[T any](r io.Reader, opts ...PatchOption) (*Patch[T], error)

NewPatchFromReader parses a JSON PATCH body from an io.Reader and constructs a Patch[T].

func (*Patch[T]) Apply

func (p *Patch[T]) Apply(target *T, opts ...ApplyOption) (ApplyResult[T], error)

Apply deep-clones target, applies the patch, and returns the result. target is stored as Original (same pointer, never mutated). Object is a freshly allocated clone with changes applied.

type PatchOption

type PatchOption func(*patchConfig)

PatchOption configures patch construction.

func WithOperations

func WithOperations() PatchOption

WithOperations enables $set / $unset / $merge / $clear for maps and $add / $remove / $merge / $clear for slices. When enabled, the patch is validated at construction time — invalid operations for a given field type return an error from NewPatch.

Jump to

Keyboard shortcuts

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