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

build

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

Languages