builder

command module
v0.0.0-...-1716880 Latest Latest
Warning

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

Go to latest
Published: Jan 22, 2022 License: MIT Imports: 5 Imported by: 0

README

builder

Generate golang builders.

Similar project:

  • mapper: convert struct to struct and more
  • getter: generate getters for private struct fields, with inlining

Why not constructor?

  • use constructor when you need to ensure all fields needs to be valid for the creation of your object
  • use constructor to validate required fields at compile time
  • you can still do the same for builder by building the object, and validating them after it is build
  • useful when you have many fields
  • chaining each method can be done on a new line, keeping it readable, sortable
  • constructor values are mandatory, builders are not. But the generated code checks if you have invoked all the setters and will panic if one of them is not called
  • you can also call builder with build partial, useful in tests when you need to test only certain fields
  • builder method are dumb setters, it may sound like a disadvantage, but actually it is an advantage. You dont always want to build your entity as a complete object, sometimes you just need to load all entity fields from the database. Allowing such flexibility on your domain entity by allowing all setters is not a good pattern. Domain entity should have private fields with public getters, but setters only that guards against invariants.
  • builder separate setters from your entity
  • dont mix constructor vs builder, constructor is useful when you need to create valid entity, builder allows you to set reconstruct the enity from existing data. Constructor might evolve over time to handle new or old fields vs business logic, which could break old data im db (which could be created externally outside the application)
  • builder is also useful when you need to mass set private fields, which are not exported. Otherwise, you could just build the structs
  • using constructor guarantees compile time checking for missing fields. Using builder does not, and it can only be checked during runtime. One way to make runtime check more guaranteed to capture errors from missing field is to run the code in the init function.

Installation

$ go install github.com/alextanhongpin/builder

Usage

//go:generate go run ../main.go -type Simple
type Simple struct {
	name string
	age  int `build:"-"`
}
Output:
// Code generated by builder, DO NOT EDIT.
package main

import "fmt"

type SimpleBuilder struct {
	simple    Simple
	fields    []string
	fieldsSet uint64
}

func NewSimpleBuilder(additionalFields ...string) *SimpleBuilder {
	for _, field := range additionalFields {
		if field == "" {
			panic("builder: empty string in constructor")
		}
	}
	exists := make(map[string]bool)
	fields := append([]string{"name"}, additionalFields...)
	for _, field := range fields {
		if exists[field] {
			panic(fmt.Sprintf("builder: duplicate field %q", field))
		}
		exists[field] = true
	}
	return &SimpleBuilder{fields: fields}
}

// WithName sets name.
func (b SimpleBuilder) WithName(name string) SimpleBuilder {
	b.mustSet("name")
	b.simple.name = name
	return b
}

// Build returns Simple.
func (b SimpleBuilder) Build() Simple {
	for i, field := range b.fields {
		if !b.isSet(i) {
			panic(fmt.Sprintf("builder: %q not set", field))
		}
	}
	return b.simple
}

// Build returns Simple.
func (b SimpleBuilder) BuildPartial() Simple {
	return b.simple
}

func (b *SimpleBuilder) mustSet(field string) {
	i := b.indexOf(field)
	if b.isSet(i) {
		panic(fmt.Sprintf("builder: set %q twice", field))
	}
	b.fieldsSet |= 1 << i
}

func (b SimpleBuilder) isSet(pos int) bool {
	return (b.fieldsSet & (1 << pos)) == (1 << pos)
}

func (b SimpleBuilder) indexOf(field string) int {
	for i, f := range b.fields {
		if f == field {
			return i
		}
	}
	panic(fmt.Sprintf("builder: field: %q not found", field))
}

Ignoring and setting custom setter.

Sometimes you want a custom type, but also need to take advantage of the Build() which panics if not all the fields are set

// Extend simple builder and check if the field is set.
func (s SimpleBuilder) WithCustomAge(age int) SimpleBuilder {
	s.mustSet("age")
	s.simple.age = age
	return s
}

Build

func main() {
	builder := NewSimpleBuilder("age")                              // Pass custom fields that needs to be set.
	log.Println(builder.BuildPartial())                             // Allows the entity to be build partially.
	log.Println(builder)                                            // None of the values are set yet.
	log.Println(builder.WithName("john"))                           // name is set to true
	log.Println(builder.WithName("john").WithCustomAge(10).Build()) // name and age set and build success
	log.Println(builder)                                            // Every instance is immutable and they don't share state.
	log.Println(builder.Build())                                    // This will panic, since "name" and "age" is not set yet.
}

V2

Better way for setting fields. Also, the complete builder with all fields should also be generated as comments so that users can copy and paste the implementation.

// You can edit this code!
// Click here and start typing.
package main

import (
	"fmt"
	"sort"
	"strings"
)

func main() {
	b := &Builder{
		fields: make(map[string]int),
	}
	b.Register("Name")
	b.Register("Age")
	b.Register("MaritalStatus")
	b.Set("MaritalStatus")
	b.
		WithName("john").
		WithAge(10).
		Build()
}

type User struct {
	Name string
	Age  int
}

type Builder struct {
	fields map[string]int
	user   User
	set    int
}

func (b *Builder) WithName(name string) *Builder {
	b.user.Name = name
	b.Set("Name")
	return b
}

func (b *Builder) WithAge(age int) *Builder {
	b.user.Age = age
	b.Set("Age")
	return b
}

func (b *Builder) Set(name string) bool {
	n, ok := b.fields[name]
	if !ok {
		return false
	}
	b.set |= 1 << n
	return true
}

func (b *Builder) Register(field string) error {
	if _, ok := b.fields[field]; ok {
		return fmt.Errorf("field %q exists", field)
	}
	b.fields[field] = len(b.fields)
	return nil
}

func (b *Builder) Build() User {
	if b.IsPartial() {
		var fields []string
		for field, n := range b.fields {
			v := 1 << n
			if b.set&v == v {
				continue
			}
			fields = append(fields, field)
		}
		sort.Strings(fields)
		panic("field " + strings.Join(fields, ", ") + " is not set")
	}
	return b.user
}

func (b *Builder) BuildPartial() User {
	return b.user
}

func (b *Builder) IsPartial() bool {
	all := 1<<len(b.fields) - 1
	return all != b.set
}

Documentation

The Go Gopher

There is no documentation for this package.

Directories

Path Synopsis

Jump to

Keyboard shortcuts

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