Skip to content

SPAN Digital's implementation of the Functional Options Pattern using Go Generics

License

Notifications You must be signed in to change notification settings

SPANDigital/with

Repository files navigation

with

SPAN Digital's take on the Functional Options Pattern using Generics

Open in Dev Containers Develop Go Action Workflow Status Main Go Action Workflow Status Release status

Features

  • Supports default options (use SetDefaults() pointer receiver )
    • Supports validation of options as a whole (use Validate() pointer receiver)
  • Supports validation of individual With... functions, by returning an error
  • Allows for option to be passed as structs, you can ignore functional options if you so wish
  • Allows callers to create their own With... functions

Installation

go get github.com/spandigital/with

How to use

  1. Import with package
import github.com/spandigital/with
  1. Define a type to represent your options, typically a struct, but not necessarily. NB: If you want callers to modify options export options and parameters by capitalizing.
type Options struct {
   Host    string
   Port    int
   Timeout time.Duration
}
  1. Optionally, write a SetDefaults function (with.Defaultable interface)
func (o *Options) SetDefaults() {
   o.Timeout = 5 * time.Minutes
}
  1. Optionally, write a Validate function (with.Validated interface)
func (o *Options) Validate() (err error) {
   switch {
      case o.Host == "":
        err = errors.New("host is required")
      case o.Port == 0:
        err = errors.New("port is required")
      case !(o.Port > 0 && o.port < 65535):
        err = errors.New("port must be between 1 and 65535")
      case o.Timeout == 0:
        err = errors.New("timeout is required")
      }
   return
}
  1. Optionally, Write High Order Functions prefixed with With... which manipulate the options. You have an option of returning an error the option is not valid.
func WithHost(host string) with.Func[Options] {
   return func(options *Options) (err error) {
      options.Host = host
      return
   }
}

func WithPort(port int) with.Func[Options] {
   return func(options *Options) (err error) {
      switch {
         case Port == 0:
            return errors.New("port is required")
         case !(Port > 0 && Port < 65535):
            return errors.New("port must be between 1 and 65535")
      }
      options.Port = port
      return
   }
}

func WithTimeout(timeout time.Duration) with.Func[Options] {
   return func(options *options) (err error) {
      options.Timeout = timeout
      return
   }
}
  1. Write constructors using

    To start with defaults, and apply 0..n use With... functions

    To start with a options str

func NewServer(withOptions ...with.Func[Options]) (server *server, err error) {
   o := &Options{}
   if err = with.DefaultThenAddWith(o, withOptions); err == nil {
	   server = newServer(o)
   }
   return
}

func NewServerFromOptions(options *Options, withOptions ...with.Func[Options]) (server *server, err error) {
   if err = with.AddWith(options, withOptions); err == nil {
	   server = newServer(options)
   }
   return
}

func newServer(options *Options) *server {
   return &server{
     host:    options.Host,
     port:    options.Port,
     timeout: options.Timeout,
   }
}
  1. Use:
server, err := NewServer(
	WithHost("localhost"),
	WithPort(10000),
	WithTimeout(3 * time.Second),
)

or

server, err = NewServerFromOptions(
	&Options{
	    Host: "localhost",	
    },   
)

Usage samples

See /samples for usage samples.

package server

import (
   "errors"
   "fmt"
   "github.com/spandigital/with"
   "time"
)

type Server interface {
   Run()
}

type Options struct {
   Host    string
   Port    int
   Timeout time.Duration
}

func (o *Options) Validate() (err error) {
   switch {
   case o.Host == "":
      err = errors.New("host is required")
   case o.Port == 0:
      err = errors.New("port is required")
   case !(o.Port > 0 && o.Port < 65535):
      err = errors.New("port must be between 1 and 65535")
   case o.Timeout == 0:
      err = errors.New("timeout is required")
   }
   return
}

func WithHost(host string) with.Func[Options] {
   return func(options *Options) (err error) {
      options.Host = host
      return
   }
}

func WithPort(port int) with.Func[Options] {
   return func(options *Options) (err error) {
      switch {
      case port == 0:
         return errors.New("port is required")
      case !(port > 0 && port < 65535):
         return errors.New("port must be between 1 and 65535")
      }
      options.Port = port
      return
   }
}

func WithTimeout(timeout time.Duration) with.Func[Options] {
   return func(options *Options) (err error) {
      options.Timeout = timeout
      return
   }
}

type server struct {
   host    string
   port    int
   timeout time.Duration
}

func NewServer(withOptions ...with.Func[Options]) (server *server, err error) {
   o := &Options{}
   if err = with.DefaultThenAddWith(o, withOptions); err == nil {
      server = newServer(o)
   }
   return
}

func NewServerFromOptions(options *Options, withOptions ...with.Func[Options]) (server *server, err error) {
   if err = with.AddWith(options, withOptions); err == nil {
      server = newServer(options)
   }
   return
}

func newServer(options *Options) *server {
   return &server{
      host:    options.Host,
      port:    options.Port,
      timeout: options.Timeout,
   }
}

func (s *server) Run() {
   fmt.Printf("server listening on %s:%d\n", s.host, s.port)
}

About

SPAN Digital's implementation of the Functional Options Pattern using Go Generics

Topics

Resources

License

Code of conduct

Security policy

Stars

Watchers

Forks

Packages

No packages published