grouter

package module
v1.2.0 Latest Latest
Warning

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

Go to latest
Published: Sep 25, 2024 License: MIT Imports: 11 Imported by: 0

README

grouter

An elegant USSD router in go

Routers

A router sits between the routing engine and the end function. The job of a router is to create requests from the incoming web request and provide session management.

The routing engine already takes care of managing the different states that the end application might have.

Additional routers can be found in routers.

The router must implement the grouter.Router interface and provide implementations for grouter.UssdRequest, and grouter.UssdSession interfaces.

Templating support

The library also supports template usage with custom function bindings.

Usage
go get github.com/SharkFourSix/grouter
Example

Example grouter_test.go

package grouter_test

import (
	"net/http"
	"os"
	"testing"
	"text/template"

	"github.com/SharkFourSix/grouter"
	"github.com/SharkFourSix/grouter/routers/at" // include africastalking implementation
)

func TestMain(t *testing.T) {
	e, err := grouter.NewRouterEngine(
		grouter.DebugMode,
		grouter.WithRouter(at.RouterName),
		grouter.WithTemplateFS(os.DirFS("./testdata/templates"), ".", template.FuncMap{}),
	)
	if err != nil {
		t.Fatal(err)
		return
	}
	e.MenuOptions(
		grouter.NewMenuOption("", welcomeScreen, "welcomeScreen",
			grouter.NewMenuOption("1", showAccount, "accountMenu",
				grouter.NewMenuOption("1", accountBalance, "accountBalance",
					grouter.NewMenuOption("#", showAccount, "accountMenu"),
				),
				grouter.NewMenuOption("2", miniStatement, "miniStatement",
					grouter.NewMenuOption("#", showAccount, "accountMenu"),
				),
				grouter.NewMenuOption("3", makeTransfer, "makeTransfer",
					grouter.NewMenuOption("#", showAccount, "accountMenu"),
				),
				grouter.NewMenuOption("#", welcomeScreen, "welcomeScreen"),
			),
			grouter.NewMenuOption("#", endSession, "endSession"),
		),
	)
	http.Handle("/ussd", e)
	http.ListenAndServe(":1234", nil)
}

type transferStep int

const (
	ReadAccount transferStep = iota
	ReadAmount
	ConfirmTransfer
)

func makeTransfer(req grouter.UssdRequest) bool {
	var step transferStep
	if sif, ok := req.Session().Get("transferStep"); ok {
		step = sif.(transferStep)
	} else {
		step = ReadAccount
	}
	switch step {
	case ReadAccount:
		if req.Input() == "" {
			req.Prompt("Enter recipient account number")
		} else {
			req.Session().Set("transferAccount", req.Input())
			req.Session().Set("transferStep", ReadAmount)
			req.Prompt("Enter amount to transfer")
		}
	case ReadAmount:
		if req.Input() == "" {
			req.Prompt("Enter amount to transfer")
		} else {
			req.Session().Set("transferAmount", req.Input())
			req.Session().Set("transferStep", ConfirmTransfer)
			req.PromptWithTemplate(
				"transfer/confirm.tmpl", grouter.TemplateValues{
					"Account": req.Session().MustGet("transferAccount"),
					"Amount":  req.Session().MustGet("transferAmount"),
				},
			)
		}
	case ConfirmTransfer:
		if req.Input() == "" {
			req.PromptWithTemplate(
				"transfer/confirm.tmpl", grouter.TemplateValues{
					"Account": req.Session().MustGet("transferAccount"),
					"Amount":  req.Session().MustGet("transferAmount"),
				},
			)
		} else {
			switch req.Input() {
			case "1":
				req.End(
					"🤙 You transferred %s to %s.",
					req.Session().MustGet("transferAmount"),
					req.Session().MustGet("transferAccount"),
				)
			case "#":
				req.End("Transfer cancelled. Thank you. Come again")
			case "3":
				req.End("You entered a wrong option! Pay attention")
			}
		}
	}
	return true // retain the scrren context for the duration of the interaction
}

func showAccount(req grouter.UssdRequest) bool {
	req.ContinueWithTemplate("account.tmpl", grouter.TemplateValues{"Phone": req.MSISDN()})
	return false
}

func accountBalance(req grouter.UssdRequest) bool {
	req.ContinueWithTemplate("balance.tmpl", grouter.TemplateValues{"Phone": req.MSISDN()})
	return false
}

func miniStatement(req grouter.UssdRequest) bool {
	req.ContinueWithTemplate("statement.tmpl", grouter.TemplateValues{"Phone": req.MSISDN()})
	return false
}

func welcomeScreen(req grouter.UssdRequest) bool {
	req.ContinueWithTemplate("main.tmpl", grouter.TemplateValues{"Phone": req.MSISDN()})
	return false
}

func endSession(req grouter.UssdRequest) bool {
	req.End("Thank you %s. Please come again!", req.MSISDN())
	return false
}

You can test the above code with the following USSD simulators

  1. https://developers.africastalking.com/simulator
  2. https://play.google.com/store/apps/details?id=com.africastalking.sandbox&hl=en
  3. https://github.com/nndi-oss/dialoguss

Documentation

Overview

Session storage

Index

Constants

This section is empty.

Variables

View Source
var (
	WithSessionTimes = func(probeFrequency, timeToEviction time.Duration) RouterOption {
		return func(r *Engine) error {
			r.storageFrequency = probeFrequency
			r.storageEviction = timeToEviction
			return nil
		}
	}
	DebugMode = func(r *Engine) error {
		r.Debug = true
		switch v := r.Log.(type) {
		case *defaultLogger:
			v.shutup = false
		}
		return nil
	}
	WithRouter = func(routerName string) RouterOption {
		return func(r *Engine) error {
			if instance, ok := registry.Load(routerName); ok {
				r.router = instance.(UssdRouter)
			} else {
				return ErrRouterNotFound
			}
			return nil
		}
	}

	WithTemplateFS = func(fsys fs.FS, root string, funcs template.FuncMap) RouterOption {
		return func(r *Engine) error {
			err := fs.WalkDir(fsys, root, func(filepath string, d fs.DirEntry, err error) error {
				if err != nil {
					return err
				}
				ext := path.Ext(strings.ToLower(d.Name()))
				if !d.IsDir() && ext == ".tmpl" && len(d.Name()) >= 6 {
					fd, err := fsys.Open(path.Join(root, filepath))
					if err != nil {
						return err
					}
					defer fd.Close()
					b, err := io.ReadAll(fd)
					if err != nil {
						return err
					}
					templateName := strings.TrimPrefix(path.Join(root, filepath), root)
					tmpl, err := template.New(d.Name()).Funcs(funcs).Parse(string(b))
					if err != nil {
						return err
					}
					r.templateMap[templateName] = tmpl
				}
				return nil
			})
			return err
		}
	}
)
View Source
var (
	ErrRouterNotFound = fmt.Errorf("router not found")
)

Functions

func IsEmptyText

func IsEmptyText(text string) bool

func NewLineStrings

func NewLineStrings(text ...string) string

func RegisterRouter

func RegisterRouter(name string, router UssdRouter)

Registers a router. Must be called in package `init`

Types

type BufferedResponse

type BufferedResponse struct {
	// contains filtered or unexported fields
}

func (*BufferedResponse) Printf

func (r *BufferedResponse) Printf(format string, args ...any)

func (*BufferedResponse) RenderContinueTemplate

func (r *BufferedResponse) RenderContinueTemplate(name string, values TemplateValues)

func (*BufferedResponse) RenderEndTemplate

func (r *BufferedResponse) RenderEndTemplate(name string, values TemplateValues)

func (*BufferedResponse) RenderTemplate

func (r *BufferedResponse) RenderTemplate(name string, values TemplateValues, end bool)

func (*BufferedResponse) Write

func (r *BufferedResponse) Write(p []byte) (int, error)

type Engine

type Engine struct {
	Log      Logger
	Debug    bool
	NotFound RouteHandler

	Storage Storage
	// contains filtered or unexported fields
}

The main routing engine (Layer 0 router)

func NewRouterEngine

func NewRouterEngine(options ...RouterOption) (*Engine, error)

func (*Engine) MenuOptions

func (e *Engine) MenuOptions(opts ...*MenuOption)

func (*Engine) RouteFromHttpRequest

func (e *Engine) RouteFromHttpRequest(w http.ResponseWriter, req *http.Request)

func (*Engine) ServeHTTP

func (e *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request)

type Logger

type Logger interface {
	Printf(string, ...any)
}
type MenuOption struct {
	// contains filtered or unexported fields
}

func NewMenuOption

func NewMenuOption(code string, h RouteHandler, name string, sub ...*MenuOption) *MenuOption

type RouteHandler

type RouteHandler func(request UssdRequest) bool

USSD Request handler. Return true to remain in the same screen context or false to indicate to the routing engine to advance the context.

A screen context confines menu options to a set, allowing the same option values to be used without conflicts.

type RouterOption

type RouterOption func(r *Engine) error

type RouterState

type RouterState int
const (
	// Read value as input data.
	//
	// This can be used to handle custom routing inside a route(d) handler.
	//
	// To enter this mode, use any of the UssdRequest.PromptXXX() functions.
	READ_INPUT RouterState = iota
	// Read value as option to for routing.
	//
	// This is the default mode where the routing engine uses the value read
	// to match route handlers.
	//
	// Use any of the UssdRequest.ContinueXXX() functions to enter this mode.
	READ_OPTION
)

type Storage

type Storage interface {
	Set(key string, session UssdSession)
	Get(key string) UssdSession
	Del(key string)
	// Vacuum removes sessions that are as old as the given duration
	Vacuum(duration time.Duration)
}

func NewInMemorySessionStorage

func NewInMemorySessionStorage(frequency, sessionTTL time.Duration) Storage

type TemplateValues

type TemplateValues map[string]any

type UssdRequest

type UssdRequest interface {
	// MSISDN MSISDN returns the mobile subscriber identification number
	// assigned to the user by their network.
	MSISDN() string
	// Option Option returns the value entererd by the user after calling any
	// of the .Continue functions.
	//
	// The value is intepreted as an option by the routing engine, which is
	// used to match handlers. There is almost no need for handlers to use
	// this value.
	Option() string
	// Input Input returns the value entered by the user after calling any of
	// the .Prompt functions. The value is passed as is to the handler.
	Input() string
	// returns the session associated with this request
	Session() UssdSession
	// Continue This function causes the next input to be treated as an option
	// that will be handled by the routing engine to match a handler
	Continue(text string, args ...any)
	// Prompt This function causes the next input to be treated as input data,
	// which can be obtained using the .Input() function.
	//
	// The original option is also maintained and can be obtained by calling
	// the .Option() function.
	Prompt(text string, args ...any)
	// PromptWithTemplate Prompt using content from a template.
	//
	// Refer to UssdRequest.Prompt() function for more
	PromptWithTemplate(tmplName string, values TemplateValues)
	// ContinueWithTemplate Continue using content from a template.
	//
	// Refer to UssdRequest.Continue() function for more
	ContinueWithTemplate(tmplName string, values TemplateValues)
	// End Ends the session
	End(text string, args ...any)
	EndWithTemplate(tmplName string, values TemplateValues)
	// SetAttribute Set a request attribute.
	//
	// Request attributes are only valid for the duration of the request.
	// To store data for the duration of the session, use UssdRequest.Session() function
	// to use the session storage.
	SetAttribute(key string, value any)

	// GetAttribute Get request attribute
	GetAttribute(key string) any
}

UssdRequest UssdRequest interface to access various states and data from the USSD request session.

type UssdRouter

type UssdRouter interface {
	// Creates parses the incoming http request and creates a USSD request from it.
	//
	// # Session Management
	//
	// The router is responsible for providing and attaching a stage. The storage parameter
	// can be used to store or retrieve the session.
	//
	// # Returning responses
	//
	//	resp *BufferedResponse
	// should be used to buffer output from the handlers, through the UssdRequest interface.
	//
	// The routing engine utilizes this to create a final response back to the client.
	CreateRequest(resp *BufferedResponse, req *http.Request, storage Storage) (UssdRequest, error)
}

Responsible for creating USSD requests

type UssdSession

type UssdSession interface {
	// Unique ID of this session
	ID() string
	Set(key string, value any)
	Get(key string) (any, bool)
	MustGet(key string) any
	Del(key string)
	CreatedAt() time.Time
}

Directories

Path Synopsis
routers
at

Jump to

Keyboard shortcuts

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