morph

package module
v0.0.0-...-b05fda7 Latest Latest
Warning

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

Go to latest
Published: Jun 28, 2023 License: MIT Imports: 12 Imported by: 0

README

Morph

Go Reference

Morph

Morph is a Go code generator that makes it easy to map between Go structs, automatically derive new Go structs, compare structs, and transform functions in different ways.

Use cases for Morph

  • "Compile-time reflection" for Go.
  • Serialisation and interoperability with other systems e.g. JSON, XML, SQL, and binary.
  • Separation between data modeling layers.
  • Functional programming.
  • Graphics programming.
  • Statistics e.g. histograms and buckets.

Why use Morph?

  • Reduce time spent maintaining boilerplate.
  • Fewer opportunities for bugs caused by typos in copy & paste code.

Morph features

  • Fully programmable (in native Go).
  • Full support for generics.
  • No runtime reflection.
  • No struct field tags to learn or cause clutter.
  • Elegant and composable building blocks.
  • Big library of done-for-you mappings and helpers:
  • Zero external dependencies!

Status

  • Morph core is feature complete ✓
  • broken refactor for more general purpose custom extensibility ⬅
  • Documentation is in progress ⬅
  • Needs more features in subpackages
  • Morph core needs tidying and better error handling

Quick Examples

These are all covered in more detail in the tutorials which follow this section.

Mapping between structs

Take the source code for an example struct, Apple, which uses custom-made weight and price packages.

type Apple struct {
    Picked    time.Time
    LastEaten time.Time
    Weight    weight.Weight
    Price     price.Price
}

Let's say we want a new struct, similar to this, but where every field is an integer, to make it easier to serialise and interoperate with other systems.

We're going to call it an Orange.

Morph can quickly let us generate:

// Orange is like an [Apple], but represented with ints.
type Orange struct {
    Picked    int64 // time in seconds since Unix epoch
    LastEaten int64 // time in seconds since Unix epoch
    Weight    int64 // weight in grams
    Price     int64 // price in pence
}

// AppleToOrange converts an [Apple] to an [Orange].
func AppleToOrange(from Apple) Orange {
    return Orange{
        Picked:    from.Picked.UTC().Unix(),
        LastEaten: from.LastEaten.UTC().Unix(),
        Weight:    from.Weight.Grams(),
        Price:     from.Price.Pence(),
    }
}

// OrangeToApple converts an [Orange] to an [Apple].
func OrangeToApple(from Orange) Apple {
    return Apple{
        Picked:    time.Unix(from.Picked, 0).UTC(),
        LastEaten: time.Unix(from.LastEaten, 0).UTC(),
        Weight:    weight.FromGrams(from.Weight),
        Price:     price.FromPence(from.Price),
    }
}

In fact, we can quickly generate any additional assignment, comparison, or inspection functions we desire.

From sorting:

// Fresher returns true if Orange "o" was picked more recently than Orange
// "target". This can be used as an ordering to sort oranges by freshness or 
// in order of those most in danger of going bad.
func (o Orange) FresherThan(target Orange) bool { /* ... */ }

To serialization:

// MarshalJSON converts an Orange to a JSON representation. It omits empty 
// fields, converts all field names to lowercase, and means we don't 
// have to pollute our struct type definition with tags.
func (o Orange) MarshalJSON() ([]byte, error) { /* ... */ }
Deep copy and deep equals without reflection

Let's take the code for a recursive tree type:

type Tree struct {
    Value string
    Time time.Time
    Children []Tree
}

Morph can generate custom copy, equals, ordering, deep copy, deep equals, and deep ordering functions -- all without using runtime reflection:

// TreesEqual returns true if two [Tree] values are equal, with the Value
// string compared in a case-insensitive manner.
func TreesEqual(a Tree, b Tree) bool {
    return strings.EqualFold(a.Value, b.Value) &&
        a.Time.Equals(b.Time) &&
        slices.EqualFunc(a.Children, b.Children, TreesEqual)
}

// Copy returns a copy of the [Tree] t.
func (t *Tree) Copy() Tree {
    // for a supplied slice application operator, `Map`.
    return Tree{
        Value: t.Value,
        Time: t.Time,
        Children: append([]Tree(nil), Map(Tree.Copy, t.Children))
    }
}

// TreesLessThan returns true if the first [Tree] is less than the second.
func TreesLessThan(a Tree, b Tree) {
    // for a supplied slice comparison operator, `LessThanFunc`.
    return (a.Value < b.Value) ||
        b.Time.After(a) ||
        LessThanFunc(a.Children, b.Children, TreesLessThan)
}

This can also be extended to support collections that contain cycles.

Wrapping a function in different ways

Go supports higher-order functions and generic types. This means we can take an ordinary function, like func (a A, b B) (A, error) for any type A and B, and write a generic higher-order function that takes this function as an input and returns a different function as a result.

However, Go is limited in that it can't do this for arbitrary functions of any number of input arguments and any number of return values. A developer has to create a separate function for every combination of input and output counts.

For example, there's no way to write a higher order function in native Go that automatically applies context.TODO to every possible function that takes a context.Context for its first argument.

We can use Morph to create and compose various automatic transformations of functions, thereby increasing our expressive power for functional programming in Go.

Let's take the source code for an example function, Divide:

// Divide returns a divided by b. If b is zero, returns an error.
func Divide(a, b float64) (float64, error) { /* ... */ }

Morph can quickly let us generate functions that wrap Divide but take a bunch of different forms (implementations elided for the sake of readability).

// Halver divides x by two.
func Halver(x float64) float64 { /* ... */ }

// Divider constructs a function that partially applies [Divide] with a
// constant divisor, returning a new function that divides by the given
// divisor. It panics if the divisor is zero.
func Divider(divisor float64) func (a float64) float64 { /* ... */ }

// DividePromise returns a callback function (a promise) to call Divide(a, b) 
// when called. The division is not performed until the returned promise is
// called.
func DividePromise(x, b float64) func () (float64, error) { /* ... */ }

// DivideResult returns the result of Divide(a,b) collected into a Result 
// sum type.
func DivideResult(a, b float64) result.R[float64] { /* ... */ }

Tutorials

Structs

This section is to be read in order, as it incrementally introduces more advanced Morph features for manipulating structs.

  1. Apples To Oranges: mapping between Go structs with Morph.
  2. Deep copy and deep equals without runtime reflection.
  3. Automatically generate XML or JSON struct tags.
  4. Mapping to column-orientated data types.
Functions

This section is to be read in order, as it incrementally introduces more advanced Morph features for manipulating functions.

  • Wrapping and transforming Go functions with Morph.
  • Creating custom function wrappers.
  • Wrapping and transforming higher-order functions.
General
  • Repeatable Morph code generation with go generate.

Security Model

WARNING: It is assumed that all inputs are trusted. DO NOT accept arbitrary input from untrusted sources under any circumstances, as this will parse and generate arbitrary code.

Documentation

Overview

Package morph is a Go code generator that generates code to map between structs and manipulate the form of functions.

All types should be considered read-only & immutable except where otherwise specified.

Need help? Ask on morph's GitHub issue tracker or check out the tutorials in the README. Also, paid commercial support and training is available via Open Source at Tawesoft.

Security Model

WARNING: It is assumed that all inputs are trusted. DO NOT accept arbitrary input from untrusted sources under any circumstances, as this will parse and generate arbitrary code.

Index

Examples

Constants

View Source
const (
	FieldExpressionTypeVoid  = "void"
	FieldExpressionTypeBool  = "bool"
	FieldExpressionTypeValue = "value"
)

Variables

This section is empty.

Functions

This section is empty.

Types

type ArgRewriter

type ArgRewriter struct {
	Capture   []Variable
	Formatter string
}

ArgRewriter either describes how an outer function rewrites its inputs before passing them to a wrapped inner function, or how an outer function rewrites the outputs of a wrapped inner function before returning them from the outer function.

It does this through capturing arguments into temporary variables, and formatting a new list of arguments or return values.

This is a low-level implementation for advanced use. The functions in morph/funcwrappers.go provide nicer APIs for many use cases.

A WrappedFunction uses two ArgRewriters in the following sequence, in pseudocode:

func outerfunc(args) (returns) { // step 0, a call to outerfunc
    inputs... = Input ArgRewriter captured args // step 1
    results... = innerfunc(Input ArgWriter formats inputs) // step 2
    outputs = Output ArgRewriter captured results // step 3
    return Output ArgRewriter formats outputs // step 4
}

At each step, an ArgRewriter can retrieve the results of a previous step (and only the immediately previous step) by "$" token notation, accessing a named argument by "$name", and an argument by index by "$n" for some decimal n. At each step, a WrappedFunction uses an ArgRewriter to transform the result of the previous values.

In step 1, the results retrieved by "$" token notation are the input arguments to the outerfunc. In step 2, they are the captured arguments from step 1. In step 3, they are the return values from the innerfunc call. In step 4, they are the captured arguments from step 3.

Capture is a list of Field elements which describe how to temporarily store inputs to or outputs of invocations of the inner function. Only the Name, Type, and Value fields on each capture are used, and each is optional.

If a Name is given, a subsequent step can refer to the result by "$name", not just by index "$n".

The Value of the capture is a code expression of how it stores an input or output. In most cases, this will be simply correspond to the value of a matching input or output, unchanged. Here, inputs and named outputs can be referred to using "$" token notation by name e.g. "$name", or by index e.g. "$0". A value need not refer to any previous step, e.g. can be a literal like "2", or some function call like "time.Now()". For a single-valued result that is used only once, it may be better to do this in Formatter.

A capture with an empty string for a Value at position i in the capture slice captures the argument at position i in the previous step. Likewise, a capture with an empty Type at position i in the capture slice has the type of the argument at position i in the previous step.

The capture arg's Type is a comma-separated list of types that the Value produces. If the type list has more than one entry, e.g. "int, error" then the captured value captures a tuple result type instead of a single value. Each field in the tuple can be accessed by index by adding ".N" for any decimal N to a "$" replacement token e.g. "$name.0" or "$0.3".

All inputs and outputs must be accounted for. This can be achieved by capturing a value, or part of a tuple value, to a capture field with a type of "_". For a tuple value, each discarded element of the tuple must be explicitly discarded, for example with a capture type specified like "int, _, _".

Formatter describes how the captured arguments or results are rewritten as the input arguments to the inner function, or as the outer function's return arguments.

For example, given some function `Foo(x float64) float64`, you can imagine some outer wrapping function using an ArgRewriters that converts it into `func(x float64) (float64, float64) => math.Modf(Foo(x))`, with the following ArgRewriter values:

Input: ArgRewriter{
    Capture: []Field{{Name: "x", Type: "float64", Value: "$x"},},
    Formatter: "$0",
}

Output: ArgRewriter{
    Capture: []Field{
        {Type: "float64, float64", Value: "math.Modf($x)"},
    },
    Formatter: "$0.0, $0.1",
}

type Argument

type Argument struct {
	Name string
	Type string
}

Argument represents a named and typed argument e.g. a type constraint or an argument to a function.

type BuiltinFieldExpression

type BuiltinFieldExpression string

BuiltinFieldExpression is a FieldExpression-like value with a constant builtin FieldExpressionType.

type Field

type Field struct {
	Name    string
	Type    string
	Tag     string
	Comment string

	// For fields appearing in structs that have been mapped only...
	Reverse FieldMapper

	Converter BuiltinFieldExpression // x=y;  See [StructConverter].
	Comparer  BuiltinFieldExpression // x==y; See [Struct.Comparer].
	Copier    BuiltinFieldExpression // x=x;  See [Struct.Copier].
	Orderer   BuiltinFieldExpression // x<y;  See [Struct.Orderer].
	Zeroer    BuiltinFieldExpression // x=0;  See [Struct.Zeroer].
	Truther   BuiltinFieldExpression // x!=0; See [Struct.Truther].

	// Custom are field expressions indexed by the FieldExpressionType's Name.
	// If set, they define a custom operation on the field.
	Custom []FieldExpression
}

Field represents a named and typed field in a struct, with optional struct tags, comments, and various "expressions" describing operations on fields of that type.

Fields should be considered readonly, except inside a FieldMapper.

The Tag, if any, does not include surrounding quote marks, and the Comment, if any, does not have a trailing new line or comment characters such as "//", a starting "/*", or an ending "*/".

A field appearing in a struct may also define a reverse function, which is a FieldMapper that performs the opposite conversion of a FieldMapper applied to the field. It is not necessary that the result of applying a reverse function is itself reversible. Not all mappings are reversible, so this may be set to nil.

For example, if a FieldMapper applies a transformation with

Converter == "$dest.$ = $src.$ * 2"

Then the reverse function should apply a transformation with

Converter == "$dest.$ = $src.$ / 2"

When creating a reverse function, care should be taken to not overwrite a field's existing reverse function. This can be achieved by composing the two functions with the Compose function in the fieldmappers sub package, for example:

myNewField.Reverse = fieldmappers.Compose(newfunc, myOldField.Reverse))

(this is safe even when myOldField.Reverse is nil).

func (*Field) AppendComments

func (f *Field) AppendComments(comments ...string)

AppendComments appends a comment to the field's existing comment string (if any), joined with a newline separator.

Note that this modifies the field in-place, so should be done on a copy where appropriate.

func (*Field) AppendTags

func (f *Field) AppendTags(tags ...string)

AppendTags appends a tag to the field's existing tags (if any), joined with a space separator, as is the convention.

Note that this modifies the field in-place, so should be done on a copy where appropriate.

Each tag in the tags list to be appended should be a single key:value pair.

If a tag in the tags list to be appended is already present in the original struct tag string, it is not appended.

If any tags do not have the conventional format, the value returned is unspecified.

func (Field) Copy

func (f Field) Copy() Field

Copy returns a (deep) copy of a Field, ensuring that slices aren't aliased.

func (Field) GetCustomExpression

func (f Field) GetCustomExpression(name string) *FieldExpression

GetCustomExpression retrieves a named custom field expression from a field's slice of custom expressions, or nil if not found.

func (*Field) Rewrite

func (f *Field) Rewrite(input Field)

Rewrite performs the special '$' replacement of a field's Name and Type described by FieldMapper.

Note that this modifies the field in-place, so should be done on a copy where appropriate.

func (*Field) SetCustomExpression

func (f *Field) SetCustomExpression(expression FieldExpression)

SetCustomExpression adds a custom field expression to a field's slice of custom expressions or, if one with that name already exists, replaces that expression.

type FieldExpression

type FieldExpression struct {
	Type    *FieldExpressionType
	Pattern string // e.g. "$dest.$ = append([]$src.$.$type(nil), $src.$)"
}

FieldExpression is an instruction describing how to assign, compare, or inspect, or perform some other operation on a field or fields.

Most users will just call [Struct.Converter], Struct.Comparer, etc. and do not need to use this unless using Morph to make their own custom function generators.

A field expression's Type defines what the expression does e.g. compare, convert, etc.

An expression may apply to either a single field on one struct value, or on two matching fields on two struct values, depending on the Type.

Note one important limitation: expressions of the same Type overwrite each other and do not nest. Two field mappings are incompatible if they touch the same expressions on the same fields. When making multiple incompatible mappings, you must create intermediate temporary struct types.

A field expression's Pattern defines how a field expression of that Type is applied to a specific field or fields.

These are either boolean comparison expressions (like [Field.Comparer] and [Field.Orderer]) that return true or false, value assignment expressions (like [Field.Converter] and [Field.Copier]) which assign values to a destination value, or void inspection expressions that don't return anything (but may panic). Fields are inspected in the order they appear in a source struct.

In any expression, if the pattern is the special value "skip", then it means explicitly ignore that field for expressions of that type instead of applying the default pattern for that expression type.

A field expression's pattern may contain "$-token" replacements that are replaced with a computed string when generating the source code for a new function:

  • "$a" and "$b" are replaced inside two-target boolean expressions with the name of two input struct value arguments.

  • "$src" and "$dest" are replaced inside two-target value assignment expressions with the name of the input and output struct value arguments.

  • "$self" is replaced inside a single-target expression with the name of the single target struct value argument.

  • "$this" is replaced inside a single-target boolean or void expression with the qualified field name on the single input for the field currently being mapped.

  • The tokens "$a", "$b", "$src", "$dest", and "$self" may be followed by a dot and another token specifying a named field (e.g. "$src.Foo"), which is then replaced with a qualified field name on the appropriate target (e.g. "myStruct.Foo").

  • Alternatively, instead of a named field, the tokens "$a", "$b", "$src", "$dest", and "$self" may instead be followed by a dot and "$" when not followed by any Go identifier (e.g. "$src.$.foo" is okay, but "$src.$foo" is not), which is then replaced with the qualified field name on the appropriate target for the source field currently being mapped.

Additionally:

  • Any token that would otherwise be replaced by any previous pattern may also be followed by a dot and "$type", in which case they are instead replaced with the type of the referenced struct value or field value.

  • Any token that would otherwise be replaced by any previous pattern, including with the ".$type" suffix, may also be followed by a dot and "$title" or "$untitle", in which case the replacement has the first character forced into an uppercase or lowercase variant, respectively.

In cases where a $-token is ambiguous, use parentheses e.g. "$(foo)Bar". For example, to call a method on "$src", use "$(src).Method()", and to call a function that is a field, use "$(src.Method)()".

type FieldExpressionType

type FieldExpressionType struct {
	// Targets specifies that this is an operation over this many input and/or
	// output struct values.
	//
	// Don't count any incidental input or output arguments e.g. database
	// handles or an error return value. These can be specified in a
	// function signature later.
	//
	// Allowed values are 1 and 2.
	Targets int

	// Name uniquely identifies the operation e.g. "Append". A [Field] can
	// only ever have one custom FieldExpression per given Name, and every
	// FieldExpression in a struct's list of fields that share this name must
	// have type fields that all point to the same FieldExpressionType.
	Name string

	// Default is a pattern applied if a pattern on a FieldExpression is the
	// empty string (indicating default behaviour).
	//
	// All "$"-tokens are replaced according to the rules specified by the
	// [FieldExpression] doc comment.
	//
	// An empty Default is treated as "skip".
	Default string

	// Returns specifies if the function is a boolean comparison expression,
	// value assignment expression, or a void inspection expression
	// (see [FieldExpression]).
	//
	// Allowed values are FieldExpressionTypeVoid, FieldExpressionTypeBool,
	// and FieldExpressionTypeValue.
	//
	// If the type is FieldExpressionTypeVoid, then Targets must be less than
	// 2.
	Type string

	// Comment is an optional comment set on a generated function e.g.
	// "$ converts [$src.$type] to [$dest.$type]". Leading "//" tokens are not
	// required, and the comment may contain linebreaks.
	//
	// The "$"-tokens "$a", "$b", "$src", "$dest", and "$self" appearing in
	// Comment are replaced according to the rules specified by the
	// [FieldExpression] doc comment.
	//
	// Additionally, the token "$", when not preceded by a dot, is (at a later
	// time) replaced by the name of the generated function when formatting a
	// function.
	Comment string

	// FieldComment is an optional comment generated above each expression for
	// each field e.g. "does $a.$ equal $b.$?". Leading "//" tokens are not
	// required, and the comment may contain linebreaks.
	//
	// All "$"-tokens are replaced according to the rules specified by the
	// [FieldExpression] doc comment.
	FieldComment string

	// Collect is a logical operator applied to all boolean results when
	// Returns is set to "bool". This must be set to a string representing
	// the Go boolean logical operator "&&" or "||". If set to "||", the
	// generated function returns true immediately on the first true value. If
	// set to "&&", the generated function returns false immediately on the
	// first false value.
	Collect string

	// Accessor returns the pattern of a FieldExpression of this type on a
	// given field, if one exists. If nil, calls [Field.GetCustomExpression]
	// with the FieldExpressionType.Name as an argument. It's likely that you
	// want to leave this as nil.
	Accessor func(f Field) string

	// Setter sets the field expression on a field of this FieldExpressionType.
	// If nil, calls [Field.SetCustomExpression] with the
	// FieldExpressionType.Name as an argument. It's likely that you want to
	// leave this as nil.
	Setter func(f *Field, pattern string)
}

FieldExpressionType describes some operation (e.g. copier, comparer, appender) on a struct value or on two struct values that can be implemented for a specific field by a FieldExpression. This controls how to generate the Go source code for a function that performs that operation.

Most users will just call [Struct.Converter], Struct.Comparer, etc. and do not need to use this unless using Morph to generate their own custom types of functions.

type FieldMapper

type FieldMapper func(input Field, emit func(output Field))

FieldMapper maps fields on a struct to fields on another struct.

A FieldMapper is called once for each field defined on an input struct. Each invocation of the emit callback function generates a field on the output struct.

As a shortcut, a "$" appearing in an emitted Field's Name or Type is replaced with the name or type of the input Field, respectively.

It is permitted to call the emit function zero, one, or more than one time to produce zero, one, or more fields from a single input field.

For example, for an input:

Field{Name: "Foo", Type: "int64"},

Then calling emit with the argument:

emit(Field{Name: "$Slice", Type: "[]$"})

Would generate a Field with the name "FooSlice" and type `[]int64`.

Example
package main

import (
	"fmt"

	"github.com/tawesoft/morph"
	"github.com/tawesoft/morph/fieldmappers"
	"github.com/tawesoft/morph/structmappers"
)

func must[X any](value X, err error) X {
	if err != nil {
		panic(err)
	}
	return value
}

func main() {
	source := `
package example

type Apple struct {
    Picked    time.Time
    LastEaten time.Time
    Weight    weight.Weight
}
`

	apple := must(morph.ParseStruct("test.go", source, ""))

	WeightToInt64 := func(input morph.Field, emit func(output morph.Field)) {
		if input.Type == "weight.Weight" {
			output := input       // copy
			output.Type = "int64" // rewrite the type
			emit(output)
		} else {
			emit(input)
		}
	}

	orange := apple.Map(
		structmappers.Rename("Orange"),
	).MapFields(
		fieldmappers.TimeToInt64,
		WeightToInt64,
	)
	fmt.Println(orange)

}
Output:

type Orange struct {
	Picked    int64 // time in seconds since Unix epoch
	LastEaten int64 // time in seconds since Unix epoch
	Weight    int64
}

func (FieldMapper) StructMapper

func (mapper FieldMapper) StructMapper() StructMapper

StructMapper returns a new StructMapper that applies the given FieldMapper to every field on the input struct.

If the FieldMapper is reversible, then so is the returned StructMapper.

type Function

type Function struct {
	Signature FunctionSignature
	Body      string
}

Function contains a parsed function signature and the raw source code of its body, excluding the enclosing "{" and "}" braces.

func StructConverter

func StructConverter(signature string, from Struct, to Struct) (Function, error)

StructConverter uses each field's defined Converter BuiltinFieldExpression to generate a function that maps values from one struct type to another struct type.

Converter is an assignment FieldExpression-like value for mapping a field on a source struct value to a field on a destination struct.

The default is to assign the source field unchanged using "=".

The signature argument is the function signature for the generated function (omit any leading "func" keyword). This supports the $-token replacements described in FieldExpression.

func (Function) String

func (fn Function) String() string

String formats a function or method as Go source code.

For example, gives a result like:

// Foo bars a baz.
func Foo(baz Baz) Bar {
    /* function body */
}

func (Function) Wrap

func (f Function) Wrap() WrappedFunction

Wrap turns a function into a wrapped function, ready for further wrapping.

type FunctionError

type FunctionError struct {
	Message   string
	Signature FunctionSignature
	Reason    error
}

func (FunctionError) Error

func (e FunctionError) Error() string

type FunctionSignature

type FunctionSignature struct {
	Comment   string
	Name      string
	Type      []Argument
	Arguments []Argument
	Returns   []Argument
	Receiver  Argument
}

FunctionSignature represents a parsed function signature, including any arguments, return types, method receiver, generic type constraints, etc.

func ParseFirstFunctionSignature

func ParseFirstFunctionSignature(filename string, src any) (result FunctionSignature, err error)

ParseFirstFunctionSignature is like ParseFunctionSignature, except it will return the first function found (including methods).

If src != nil, ParseFirstFunctionSignature parses the source from src and the filename is only used when recording position information. The type of the argument for the src parameter must be string, []byte, or io.Reader. If src == nil, instead parses the file specified by filename. This matches the behavior of go.Parser/ParseFile.

Parsing is performed without full object resolution. This means parsing will still succeed even on some files that may not actually compile.

func ParseFunctionSignature

func ParseFunctionSignature(filename string, src any, name string) (result FunctionSignature, err error)

ParseFunctionSignature parses a given source file, looking for a function with the given name, and recording its signature.

ParseFunctionSignature does not look for any methods on a type. For this, use ParseMethodSignature instead.

If src != nil, ParseFunctionSignature parses the source from src and the filename is only used when recording position information. The type of the argument for the src parameter must be string, []byte, or io.Reader. If src == nil, instead parses the file specified by filename. This matches the behavior of go.Parser/ParseFile.

Parsing is performed without full object resolution. This means parsing will still succeed even on some files that may not actually compile.

func ParseMethodSignature

func ParseMethodSignature(filename string, src any, Type string, name string) (result FunctionSignature, err error)

ParseMethodSignature is like ParseFunctionSignature, except it will match functions that are methods on the given type.

If src != nil, ParseFunction parses the source from src and the filename is only used when recording position information. The type of the argument for the src parameter must be string, []byte, or io.Reader. If src == nil, instead parses the file specified by filename. This matches the behavior of go.Parser/ParseFile.

For example, to look for a method signature such as `func (foo *Bar) Baz()`, i.e. method Baz on type Bar with a pointer receiver, then set the name argument to "Baz" and the type argument to "Bar" (it does not matter that foo is a pointer type). Generic type constraints are ignored.

Parsing is performed without full object resolution. This means parsing will still succeed even on some files that may not actually compile.

func (FunctionSignature) Copy

Copy returns a (deep) copy of a FunctionSignature, ensuring that slices aren't aliased.

func (FunctionSignature) Inputs

func (fs FunctionSignature) Inputs() []Argument

Inputs returns a slice containing a Argument for each input specified by the function signature, including the method receiver (if any) as the first argument.

func (FunctionSignature) String

func (fs FunctionSignature) String() string

String formats the function signature as Go source code, omitting the leading "func" keyword.

func (FunctionSignature) Value

func (fs FunctionSignature) Value() (string, error)

Value formats the function signature as Go source code as a value, without the leading func keyword, and its name omitted.

Methods are rewritten as functions with their receiver inserted at the start of the function's arguments.

Generic functions cannot be written this way.

type FunctionWrapper

type FunctionWrapper func(in WrappedFunction) (WrappedFunction, error)

type Struct

type Struct struct {
	Comment    string
	Name       string
	TypeParams []Field
	Fields     []Field
	Reverse    StructMapper
}

Struct represents a Go struct - it's type name, type constraints (if using generics), doc comment, and fields.

If the struct has been renamed, From is its previous name. Otherwise, it is the empty string.

func ParseStruct

func ParseStruct(filename string, src any, name string) (result Struct, err error)

ParseStruct parses a given source file, looking for a struct type definition that defines a struct type with the given name.

If name == "", ParseStruct returns the first struct found.

If src != nil, ParseStruct parses the source from src and the filename is only used when recording position information. The type of the argument for the src parameter must be string, []byte, or io.Reader. If src == nil, instead parses the file specified by filename. This matches the behavior of go/parser.ParseFile.

Parsing is performed without full object resolution. This means parsing will still succeed even on some files that may not actually compile.

ParseStruct only looks for struct type definitions in the top-level scope. This means that type definitions inside functions, etc. will be ignored.

Example (FromFile)
package main

import (
	"fmt"

	"github.com/tawesoft/morph"
)

func must[X any](value X, err error) X {
	if err != nil {
		panic(err)
	}
	return value
}

func main() {
	animal := must(morph.ParseStruct("examples_doc_test.go", nil, "Animal"))
	fmt.Println(animal)

}
Output:

type Animal struct {
	Name string
	Born time.Time
}
Example (FromString)
package main

import (
	"fmt"

	"github.com/tawesoft/morph"
)

func must[X any](value X, err error) X {
	if err != nil {
		panic(err)
	}
	return value
}

func main() {
	source := `
package example

type Apple struct {
    Picked    time.Time
    LastEaten time.Time
    Weight    weight.Weight
    Price     price.Price
}
`

	apple := must(morph.ParseStruct("test.go", source, "Apple"))
	fmt.Println(apple)

}
Output:

type Apple struct {
	Picked    time.Time
	LastEaten time.Time
	Weight    weight.Weight
	Price     price.Price
}

func (Struct) Comparer

func (s Struct) Comparer(signature string) (Function, error)

Comparer uses each field's defined Comparer BuiltinFieldExpression to generate a function that compares two struct values of the same type, returning true if all matching fields compare equal.

Comparer is a boolean FieldExpression-like value for comparing two matching fields on struct values of the same type, returning true if they compare equal.

A function generated using this expression returns immediately on evaluating any expression that evaluates to false.

The default is to compare with "==".

The signature argument is the function signature for the generated function (omit any leading "func" keyword). This supports the $-token replacements described in FieldExpression.

func (Struct) Copier

func (s Struct) Copier(signature string) (Function, error)

Copier uses each field's defined Copier BuiltinFieldExpression to copy a source struct value to a destination struct value of the same type.

CopierFieldExpression is an assignment FieldExpression-like value for mapping a field value on a source struct value to a matching field value on a destination struct value of the same type.

The default is to assign with "=".

The signature argument is the function signature for the generated function (omit any leading "func" keyword). This supports the $-token replacements described in FieldExpression.

func (Struct) Copy

func (s Struct) Copy() Struct

Copy returns a (deep) copy of a Struct, ensuring that slices aren't aliased.

func (Struct) CustomBinaryFunction

func (s Struct) CustomBinaryFunction(operation string, signature string, other Struct) (Function, error)

CustomBinaryFunction TODO docs

The struct specified as the method receiver is treated as argument "$a" or "$dest" for $-token replacement. The argument specified as "other" is treated as argument "$b" or "$src" for $-token replacement.

func (Struct) CustomUnaryFunction

func (s Struct) CustomUnaryFunction(operation string, signature string) (Function, error)

func (Struct) Map

func (s Struct) Map(mappers ...StructMapper) Struct

Map applies each given StructMapper (in order of the arguments provided) to a struct and returns the result.

func (Struct) MapFields

func (s Struct) MapFields(mappers ...FieldMapper) Struct

MapFields applies each given FieldMapper (in order of the arguments provided) to a struct and returns the result.

func (Struct) Orderer

func (s Struct) Orderer(signature string) (Function, error)

Orderer uses each field's defined Orderer BuiltinFieldExpression to generate a function that compares two struct values of the same type, returning true if all matching fields of the first compare less than all matching fields of the second.

Orderer is a boolean FieldExpression-like value for comparing two matching fields on struct values of the same type, returning true if the first is less than the second.

A function generated using this expression returns immediately on evaluating any expression that evaluates to false.

The default is to compare with "<".

The signature argument is the function signature for the generated function (omit any leading "func" keyword). This supports the $-token replacements described in FieldExpression.

func (Struct) Signature

func (s Struct) Signature() string

Signature returns the Go type signature of a struct as a string, including any generic type constraints, omitting the "type" and "struct" keywords.

For example, returns a result like "Orange" or "Orange[X, Y any]".

func (Struct) String

func (s Struct) String() string

String returns a Go source code representation of the given struct.

For example, returns a result like:

// Foo is a thing that bars.
type Foo struct {
    Field Type `tag:"value"` // Comment
}

func (Struct) Truther

func (s Struct) Truther(signature string) (Function, error)

Truther uses each field's defined Truther BuiltinFieldExpression to generate a function that returns true if a struct is considered true.

Truther is a boolean FieldExpression-like value for comparing a single field to a concept of zero or false.

A function generated using this expression returns immediately on evaluating any field that evaluates to non-zero.

The default is to compare with the Go-defined zero value for that type.

The signature argument is the function signature for the generated function (omit any leading "func" keyword). This supports the $-token replacements described in FieldExpression.

func (Struct) Zeroer

func (s Struct) Zeroer(signature string) (Function, error)

Zeroer uses each field's defined Zeroer BuiltinFieldExpression to generate a function that sets a struct value to its zero value.

Zeroer is an assignment FieldExpression-like value for setting a single field to its zero value.

The default is to skip the assignment, leaving the field at the zero type for that type as specified by the Go language.

The signature argument is the function signature for the generated function (omit any leading "func" keyword). This supports the $-token replacements described in FieldExpression.

Note: to conditionally zero a field, use Struct.Copier instead.

type StructMapper

type StructMapper func(in Struct) Struct

StructMapper maps a Struct to another Struct.

type Variable

type Variable struct {
	Name  string
	Type  string
	Value string
}

Variable represents a named and typed variable with a value.

type WrappedFunction

type WrappedFunction struct {
	Signature FunctionSignature
	Inputs    ArgRewriter // Rewritten inputs to wrapped function
	Outputs   ArgRewriter // Rewritten outputs from wrapped function
	Wraps     *WrappedFunction
}

func (WrappedFunction) Format

func (w WrappedFunction) Format() (string, error)

func (WrappedFunction) Function

func (w WrappedFunction) Function() (Function, error)

Function returns the result of converting a wrapped function into a concrete implementation representing Go source code.

func (WrappedFunction) String

func (w WrappedFunction) String() string

String returns the result of WrappedFunction.Format.

In the event of error, a suitable error message is formatted as a Go comment literal, instead.

func (WrappedFunction) Wrap

func (f WrappedFunction) Wrap(wrappers ...FunctionWrapper) (WrappedFunction, error)

Wrap applies function wrappers to a given wrapped function.

The earliest wrappers in the list are the first to be applied.

For example, analogous to function composition, `x.Wrap(f, g)` applies `g(f(x))`.

Returns the first error encountered.

Directories

Path Synopsis
Package fieldmappers provides helpful composable functions that implement [morph.FieldMapper] for mapping the fields between two structs using morph.
Package fieldmappers provides helpful composable functions that implement [morph.FieldMapper] for mapping the fields between two structs using morph.
fieldops
Package fieldops implements morph FieldMappers that set appropriate Comparer, Copier, and Orderer expressions on morph Fields.
Package fieldops implements morph FieldMappers that set appropriate Comparer, Copier, and Orderer expressions on morph Fields.
Package structmappers implements some useful transformations from one struct to another, in a "functional options" style for the [morph.Struct.Map] method.
Package structmappers implements some useful transformations from one struct to another, in a "functional options" style for the [morph.Struct.Map] method.
Package tag provides useful features for manipulating struct field tags.
Package tag provides useful features for manipulating struct field tags.

Jump to

Keyboard shortcuts

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