sumtype

package module
v1.0.2 Latest Latest
Warning

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

Go to latest
Published: Oct 20, 2025 License: MIT Imports: 5 Imported by: 0

README

sumtype

A Go package that provides casting capabilities between sum type projections using unsafe pointers.

For more information, see my article describing this technique: https://medium.com/@jeffreymrichter/a-novel-approach-to-sum-types-in-go-e777790954cf

Overview

This package provides a Caster type that allows casting between a JSON-serializable struct and its variants. All variants must have the same memory layout, with the first field being a non-exported xxxCaster field of type sumtype.Caster[Json].

Features

  • Type-safe casting between sum type projections
  • JSON marshaling/unmarshaling support
  • String representation with pretty-printed JSON
  • Zero out non-relevant fields for specific variants

Usage

package main

import "github.com/JeffreyRichter/sumtype"

// Define your JSON struct and variants here
// See the test files for examples

Installation

go get github.com/JeffreyRichter/sumtype

Requirements

  • Go 1.25 or later

License

MIT License - see LICENSE file for details

Documentation

Overview

Example
package main

import (
	"encoding/json/jsontext"
	"encoding/json/v2"
	"fmt"

	"github.com/JeffreyRichter/sumtype"
)

// Article describing this technique:
// https://medium.com/@jeffreymrichter/a-novel-approach-to-sum-types-in-go-e777790954cf

// ********** THE CODE BELOW SHOWS HOW TO USE A SUM TYPE ********** //

// ptr returns a pointer to the given value.
// Delete this function and use 'new' when using Go 1.26.
func ptr[T any](v T) *T { return &v }

// jsonFromWebService shows how to build an array of Shape objects of various kinds.
func jsonFromWebService() []byte {
	shapes := []*Shape{
		(&CircleShape{
			Kind:   ptr(CircleShapeKind),
			Color:  ptr("red"),
			Radius: ptr(1),
		}).Shape(),

		(&RectangleShape{
			Kind:   ptr(RectangleShapeKind),
			Color:  ptr("green"),
			Width:  ptr(15),
			Height: ptr(15),
		}).Shape(),

		(&RectangleShape{
			Kind:   ptr(RectangleShapeKind),
			Color:  ptr("blue"),
			Width:  ptr(5),
			Height: ptr(5),
		}).Shape(),
	}

	// Marshal the array to JSON
	incomingJson, _ := json.Marshal(shapes, jsontext.WithIndent("  "))
	fmt.Println(string(incomingJson) + "\n----------") // Show what the JSON looks like
	return ([]byte)(incomingJson)                      // Return JSON to caller/client
}

func main() {
	// Simulate getting a JSON array of shape objects from a Web Service
	incomingJson := jsonFromWebService()

	// Unmarshal JSON to an array of Shape objects
	var shapes []*Shape
	_ = json.Unmarshal(incomingJson, &shapes)

	// Process the shape objects
	for _, s := range shapes {
		switch *s.Kind {
		case CircleShapeKind: // "circle"
			c := s.Circle()                          // Cast *Shape to *CircleShape
			c.Color, c.Radius = ptr("white"), ptr(2) // Change circle fields

		case RectangleShapeKind: // "rectangle"
			r := s.Rectangle()                 // Cast *Shape to *RectangleShape
			r.Height = ptr(min(*r.Height, 10)) // Forbid a Height > 10
			r.Width = ptr(min(*r.Width, 10))   // Forbid a Width > 10

			if *r.Height < 10 && *r.Width < 10 { // Contrived example for demo
				// Demo: Show how to convert from current Kind to another Kind
				c := r.SetCircle()                      // Convert *RectangleShape to *CircleShape
				c.Color, c.Radius = ptr("blue"), ptr(5) // Change circle fields
			}

		default:
			// This can happen if the Web Service returns a new kind (perhaps
			// in a new version) that this client code never knew about.
			fmt.Printf("Unrecognized shape kind: %s\n", *s.Kind)
		}
	}

	// Marshal the modified array of Shape objects back to JSON
	outgoingJson, _ := json.Marshal(shapes, jsontext.WithIndent("  "))
	fmt.Println(string(outgoingJson)) // Show what the JSON looks like
	// Not shown: Send outgoingJson back to Web Service

}

// ********** THE CODE BELOW SHOWS HOW TO DEFINE A SUM TYPE ********** //

// At app initialization, panic if any of shape's projection structs don't match
var _ = sumtype.Caster[shape]{}.ValidateStructFields(true, Shape{}, CircleShape{}, RectangleShape{})

const (
	// CircleShapeKind is the kind for circle shapes
	CircleShapeKind ShapeKind = "circle"

	// RectangleShapeKind is the kind for rectangle shapes
	RectangleShapeKind ShapeKind = "rectangle"
)

type (
	// ShapeKind is the discriminator indicating which type of Shape
	ShapeKind string

	// shape is package-private and used for (un)marshaling (all data fields are public).
	shape struct {
		// shapeCaster MUST be 1st field, unexported & embedded for method "inheritance"
		shapeCaster

		// Color is the color of the shape (shared by all shapes)
		Color *string `json:"color,omitempty"`

		// Kind is the discriminator indicating which type of Shape (shared by all shapes)
		Kind *ShapeKind `json:"kind,omitempty"`

		// Radius is the radius of a circle shape
		Radius *int `json:"radius,omitempty"`

		// Width is the width of a rectangle shape
		Width *int `json:"width,omitempty"`

		// Height is the height of a rectangle shape
		Height *int `json:"height,omitempty"`
	}

	// Shape is public and exposes fields common to all shape kinds
	Shape struct {
		// shapeCaster MUST be 1st field, unexported & embedded for method "inheritance"
		shapeCaster

		// Color is the color of the shape (shared by all shapes)
		Color *string

		// Kind is the discriminator indicating which type of Shape (shared by all shapes)
		Kind *ShapeKind

		// radius is the radius of a circle shape
		_ *int

		// width is the width of a rectangle shape
		_ *int

		// height is the height of a rectangle shape
		_ *int
	}

	// CircleShape is public and exposes fields related to a circle kind.
	CircleShape struct {
		// shapeCaster MUST be 1st field, unexported & embedded for method "inheritance"
		shapeCaster

		// Color is the color of the shape (shared by all shapes)
		Color *string

		// Kind is the discriminator indicating which type of Shape (shared by all shapes)
		Kind *ShapeKind

		// Radius is the radius of a circle shape
		Radius *int

		// width is the width of a rectangle shape
		_ *int

		// height is the height of a rectangle shape
		_ *int
	}

	// RectangleShape is public and exposes fields related to a rectangle kind.
	RectangleShape struct {
		// shapeCaster MUST be 1st field, unexported & embedded for method "inheritance"
		shapeCaster

		// Color is the color of the shape (shared by all shapes)
		Color *string

		// Kind is the discriminator indicating which type of Shape (shared by all shapes)
		Kind *ShapeKind

		// radius is the radius of a circle shape
		_ *int

		// Width is the width of a rectangle shape
		Width *int

		// Height is the height of a rectangle shape
		Height *int
	}

	// shapeCaster provides methods to cast between *shape and its variants. The 1st field of shape
	// and all its variants is an unexported shapeCaster whose underlying type is sumtype.Caster[shape].
	// NOTE: This also hides sumtypes.Caster's MarshalJSON/UnmarshalJSON/String methods so they
	// cannot be called directly on shape variants.
	shapeCaster sumtype.Caster[shape]
)

// RULES: String & MarshalJSON require by-val receiver, UnmarshalJSON requires by-ref receiver

// String returns a readable JSON representation of the shape
func (s Shape) String() string { return (&s).caster().String() }

// MarshalJSON marshals the shape to JSON
func (s Shape) MarshalJSON() ([]byte, error) { return (&s).caster().MarshalJSON() }

// UnmarshalJSON unmarshals JSON data to the shape
func (s *Shape) UnmarshalJSON(data []byte) error { return s.caster().UnmarshalJSON(data) }

// String returns a readable JSON representation of the shape
func (s CircleShape) String() string { return (&s).caster().String() }

// MarshalJSON marshals the CircleShape to JSON
func (s CircleShape) MarshalJSON() ([]byte, error) { return (&s).caster().MarshalJSON() }

// UnmarshalJSON unmarshals JSON data to the CircleShape
func (s *CircleShape) UnmarshalJSON(data []byte) error { return s.caster().UnmarshalJSON(data) }

// String returns a readable JSON representation of the shape
func (s RectangleShape) String() string { return (&s).caster().String() }

// MarshalJSON marshals the RectangleShape to JSON
func (s RectangleShape) MarshalJSON() ([]byte, error) { return (&s).caster().MarshalJSON() }

// UnmarshalJSON unmarshals JSON data to the RectangleShape
func (s *RectangleShape) UnmarshalJSON(data []byte) error { return s.caster().UnmarshalJSON(data) }

// RULES: Methods that cast a pointer from 1 type to another, require by-ref receiver (XxxCaster methods).

// caster returns shapeCaster's underlyting sumtype.Caster to access its helper methods.
func (c *shapeCaster) caster() *sumtype.Caster[shape] { return (*sumtype.Caster[shape])(c) }

// json casts the pointer *c to *shape, the JSONable type (ALL JSON fields are public).
func (c *shapeCaster) json() *shape { return c.caster().Json() }

// ensureKind ensures that the current shape kind matches the specified kind; it panics if not.
func (c *shapeCaster) ensureKind(kind ShapeKind) {
	if c.json().Kind == nil {
		panic(fmt.Sprintf("can't cast shape from Kind=nil to Kind=%s", kind))
	}
	if *c.json().Kind != kind {
		panic(fmt.Sprintf("can't cast shape from Kind=%v to Kind=%s", *c.json().Kind, kind))
	}
}

// Shape casts a *XxxShape to the common *Shape
func (c *shapeCaster) Shape() *Shape { return sumtype.Cast[Shape](c.caster()) }

// Circle casts any *XxxShape to a *CircleShape; it panics if Kind != CircleShapeKind.
func (c *shapeCaster) Circle() *CircleShape {
	c.ensureKind(CircleShapeKind)
	return sumtype.Cast[CircleShape](c.caster())
}

// Rectangle casts any *XxxShape to a *RectangleShape; it panics if Kind != RectangleShapeKind.
func (c *shapeCaster) Rectangle() *RectangleShape {
	c.ensureKind(RectangleShapeKind)
	return sumtype.Cast[RectangleShape](c.caster())
}

// SetCircle casts any *XxxShape to a *CircleShape
func (c *shapeCaster) SetCircle() *CircleShape {
	s := c.Shape()
	*s.Kind = CircleShapeKind
	c.caster().ZeroNonKindFields(s)
	return s.Circle()
}

// SetRectangle casts any *XxxShape to a *RectangleShape
func (c *shapeCaster) SetRectangle() *RectangleShape {
	s := c.Shape()
	*s.Kind = RectangleShapeKind
	c.caster().ZeroNonKindFields(s)
	return s.Rectangle()
}

// String returns a readable JSON representation of the shape
func (c *shapeCaster) String() string {
	j, _ := json.Marshal(c.json(), jsontext.WithIndent("  "))
	return string(j)
}
Output:

[
  {
    "color": "red",
    "kind": "circle",
    "radius": 1
  },
  {
    "color": "green",
    "kind": "rectangle",
    "width": 15,
    "height": 15
  },
  {
    "color": "blue",
    "kind": "rectangle",
    "width": 5,
    "height": 5
  }
]
----------
[
  {
    "color": "white",
    "kind": "circle",
    "radius": 2
  },
  {
    "color": "green",
    "kind": "rectangle",
    "width": 10,
    "height": 10
  },
  {
    "color": "blue",
    "kind": "circle",
    "radius": 5
  }
]

Index

Examples

Constants

This section is empty.

Variables

This section is empty.

Functions

func Cast

func Cast[To any, From any](caster *Caster[From]) *To

Cast casts From a caster To another sum type projection type.

Types

type Caster

type Caster[Json any] struct{}

Caster provides methods to cast between a sum type *Json and its variants (which all have the same fields as Json). The 1st field of Json and all its variants must be a non-exported xxxCaster type whose underlying type is sumtype.Caster[Json]. All methods that cast a pointer from one projection type to another, require by-ref receivers.

func (*Caster[Json]) Json

func (c *Caster[Json]) Json() *Json

Json casts c to *Json where Json is the JSONable struct (ALL JSON fields are exported).

func (*Caster[Json]) MarshalJSON

func (c *Caster[Json]) MarshalJSON() ([]byte, error)

MarshalJSON marshals the json struct instance to JSON

func (*Caster[Json]) String

func (c *Caster[Json]) String() string

String returns a readable JSON representation of the Json struct instance

func (*Caster[Json]) UnmarshalJSON

func (c *Caster[Json]) UnmarshalJSON(data []byte) error

UnmarshalJSON unmarshals JSON data to the Json struct instance

func (Caster[Json]) ValidateStructFields

func (c Caster[Json]) ValidateStructFields(panicOnError bool, structs ...any) error

ValidateStructFields ensures that Json and all the specific projection types have struct fields in the same order and same type. If panicOnError is true, ValidateStructFields panics if there is an error, otherwise it returns the error (or nil if no error).

func (*Caster[Json]) ZeroNonKindFields

func (c *Caster[Json]) ZeroNonKindFields(ptrToKindStruct any)

ZeroNonKindFields sets all fields not relevant to "Kind" to their zero value