Help us shape the future of Go! Take the 2026 Go Developer Survey

go2waa

package module
v0.1.1 Latest Latest
Warning

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

Go to latest
Published: Aug 14, 2026 License: MIT Imports: 11 Imported by: 0

README

go2waa

Go core for the Alaska Xbase++ WAA gateway protocol: hold one complete conversation with a WAA server — the backend behind WAA1GATE.EXE — from any Go code, with or without a web server in between.

The wire framing, the conversation flow and the CGI output handling are compatible with Alaska's WAA gateway. The surface around them is new: a fully virtual context (WaaCtx) decides where every answer comes from and where the output goes — an HTTP request, a plain Go call, a test, anything.

The pieces

Piece File Role
WaaCtx waa_ctx.go The interface: ten On* methods answering the server's commands
KvEntry waa_ctx.go One form variable with its collected values
NullWaaCtx null_waa_ctx.go Do-nothing context — embed it, override what you need
DirectWaaCtx direct_waa_ctx.go Fill-by-hand context: data in, results collected in memory
WaaTargetParam waa_target_param.go The virtual destination + Call(param, ctx)
WaaTarget waa_target.go How to reach one WAA server: host, port, timeouts
WaaRouter waa_router.go Multibinding: WAA_PACKAGE picks the target
Framing protocol.go Message, GetMessage/PutMessage, the Cmd* constants

A direct call, no web server

ctx := go2waa.NewDirectWaaCtx(nil)
ctx.SetValue("WAA_PACKAGE", "echo")
ctx.SetValue("WAA_FORM", "echo")
ctx.AddValue("multi", "one")
ctx.AddValue("multi", "two")

target := &go2waa.WaaTarget{}
target.Init("127.0.0.1", 1027)
if err, _ := go2waa.Call(target, ctx); err != nil {
    log.Fatal(err)
}

// ctx.Header []string  — the CGI header lines
// ctx.Body   []byte    — the response body
// ctx.Files            — files the application sent, name -> buffer
// ctx.Emails           — raw email messages it requested

One WaaTarget is safe for any number of concurrent calls — each call opens its own connection, and each live call needs its own context.

Several backends, and a way out

The destination of a call is virtual (WaaTargetParam): a WaaTarget is one server; a WaaRouter holds several and picks by the package variable (WAA_PACKAGE unless you SetPackageVarName another) — unmapped packages go to the first target, mapped ones to their own backend.

And one mapping is special: "!" means not WAA's — elsewhere is anything else you serve your own way. The typical use is the migration lever for an Xbase++/WAA program made of several packages: as each package's Go replacement lands, map it to "!" — Call answers dispatched == false, your catch takes over, and WAA keeps serving the rest. Package by package, no seams.

r := &go2waa.WaaRouter{}
r.AddTarget("main", "10.0.0.1", 1027)  // the default for every package
r.MapPackageToTarget("ventas", "!")    // ventas already lives in Go

if err, dispatched := go2waa.Call(r, ctx); !dispatched {
    // not WAA's: serve it with the new stack
}

And since Call only sees the WaaTargetParam interface, special needs are one method away: implement your own — pick the target by form, by user, by anything the context can answer.

How a conversation works

The gateway starts, the server ends: Call connects, sends CONNECT, waits for READY, and then serves the application's commands until DISCONNECT — environment variables, form variables (single or all), document root, script name, output, file transfers, email requests. Every command becomes one method call on your WaaCtx.

The application's output is a CGI block — header lines, an empty line, then the body. The engine does the framing once, for every context: each header line arrives through OnHeaderLine (with its terminator: the int value carries the actual EOL bytes, 0x0D0A for CRLF), the separator through OnEndHeader, and everything after it streams through OnPutData. If the application never emits a separator, all its output is delivered as body when the conversation ends — exactly what the C gateway did.

The transport is anything that reads and writes bytes; the connection is never persistent: one call, one TCP connection, gone when Call returns.

Install

go get github.com/pablo-botella/go2waa

Tests

go test ./...

The suite under test/ uses only the public API: protocol framing over net.Pipe, and full conversations through WaaClient.Call against a scripted mock server on a loopback port.

TestWaaCall goes further: driven by test/test.json (the Go flavor of the Xbase ao_waa_call request file — a server block instead of the in-process application one), it performs one real call against a live WAA server and writes the response to output.txt/output.html. It is opt-in, behind a build tag — a plain go test ./... never runs it:

go test -tags live -run TestWaaCall ./test

It needs the echo package (test/waa/echo.dll) loaded in a running WAA server; WAA_HOST and WAA_PORT override the JSON, WAA_CALL_REQUEST points at another request file. Once invoked, a missing server is a failure, not a skip — on purpose: a test that cannot do its job should say so.

test/waa/ carries that echo fixture: an Xbase++ package (echo.prg, built by build.bat into echo.dll) that dumps every form variable it receives back as an HTML table — multi-values included.

License

MIT — see LICENSE.md.

Documentation

Index

Constants

View Source
const (
	CmdConnect       int32 = 0   // First connection from the gateway
	CmdDisconnect    int32 = 1   // Last command from the server
	CmdReady         int32 = 2   // Server ready
	CmdAck           int32 = 3   // Positive acknowledgement
	CmdNak           int32 = 4   // Negative acknowledgement
	CmdGetEnv        int32 = 100 // Get environment variable
	CmdPutEnv        int32 = 110 // Response to GetEnv
	CmdGetVar        int32 = 200 // Get form variable
	CmdPutVar        int32 = 210 // Response to GetVar
	CmdPut           int32 = 201 // Write HTML output
	CmdGetAllVars    int32 = 300 // Get all variables
	CmdPutAllVars    int32 = 310 // Response to GetAllVars
	CmdOpenFile      int32 = 400 // Open file for writing
	CmdPutFileData   int32 = 401 // Write data to file
	CmdCloseFile     int32 = 402 // Close file
	CmdGetDocRoot    int32 = 500 // Get document root
	CmdGetScriptName int32 = 600 // Get script name
	CmdEmail         int32 = 700 // Send email
)

CGI protocol commands

View Source
const (
	EolLf   = int(linereader.EolLf)
	EolCr   = int(linereader.EolCr)
	EolCrLf = int(linereader.EolCrLf)
)

Line terminators reported to OnHeaderLine and OnEndHeader; the values are linereader's EolType.

Variables

View Source
var ErrInvalidTargetName = errors.New("invalid target name")

ErrInvalidTargetName is AddTarget refusing the reserved name "!".

View Source
var ErrNoWaaTargetConfigured = errors.New("no WAA target configured")

ErrNoWaaTargetConfigured means the destination resolved to nothing at all: no target was ever configured.

View Source
var ErrShouldBeDispatchedElsewhere = errors.New("should be dispatched elsewhere")

ErrShouldBeDispatchedElsewhere means this request is not WAA's: the package is mapped to "!" or no default target exists. Elsewhere is anything that is not WAA — typically the package's replacement in another technology, but whatever the caller decides. Call answers it with dispatched == false.

View Source
var ErrTargetIndexOutOfRange = errors.New("target index out of range")

ErrTargetIndexOutOfRange means a package mapping points past the target list — a router wired by hand into an inconsistent state.

View Source
var ErrTargetNameAlreadyExists = errors.New("target name already exists")

ErrTargetNameAlreadyExists is AddTarget refusing a duplicate name.

View Source
var ErrTargetNameNotFound = errors.New("target name not found")

ErrTargetNameNotFound means no target carries the requested name.

Functions

func Call added in v0.1.1

func Call(w WaaTargetParam, ctx WaaCtx) (error, bool)

Call resolves the destination through w, dials it, runs one complete conversation against ctx and closes the connection — not persistent: one call, one connection, gone when the call returns.

The second result says whether the request was dispatched to WAA at all: false means nobody was dialed (e.g. the router answered ErrShouldBeDispatchedElsewhere) and the request belongs outside WAA — typically a package already migrated to another technology, but whatever the caller decides: it should serve the request itself.

func PutMessage

func PutMessage(conn io.Writer, cmd int32, data []byte) error

PutMessage sends a message to the WAA transport Format: <msgLen 4 bytes><cmd 4 bytes><data>

func PutMessageString

func PutMessageString(conn io.Writer, cmd int32, data string) error

PutMessageString sends a message with string data

Types

type DirectWaaCtx

type DirectWaaCtx struct {
	NullWaaCtx

	// Env answers OnGetEnv verbatim (CGI names, e.g. "SERVER_NAME").
	// REQUEST_METHOD defaults to "POST" when not set.
	Env map[string]string

	// Header collects the CGI header lines emitted by the application,
	// one entry per line, without terminators.
	Header []string

	// Body collects the application output after the header separator —
	// or all of it, when the application never emitted a separator.
	Body []byte

	// Files collects the files sent by the application, name -> content.
	Files map[string]*bytes.Buffer

	// Emails collects the email messages requested by the application,
	// raw and in order of arrival.
	Emails [][]byte
	// contains filtered or unexported fields
}

DirectWaaCtx is a WaaCtx you fill by hand: direct data in, output collected in memory to read after the call. It does not perform the call itself — pass it to WaaClient.Call. A DirectWaaCtx belongs to one conversation: do not share a live one between concurrent calls.

func NewDirectWaaCtx

func NewDirectWaaCtx(env map[string]string) *DirectWaaCtx

NewDirectWaaCtx returns a DirectWaaCtx ready to fill. env may be nil (empty environment).

func (*DirectWaaCtx) AddValue

func (c *DirectWaaCtx) AddValue(name, value string)

AddValue adds one more value to a form variable, creating it if it does not exist.

func (*DirectWaaCtx) OnEmail

func (c *DirectWaaCtx) OnEmail(rfc822data []byte) error

func (*DirectWaaCtx) OnGetAllVars

func (c *DirectWaaCtx) OnGetAllVars() []KvEntry

func (*DirectWaaCtx) OnGetEnv

func (c *DirectWaaCtx) OnGetEnv(name string) string

func (*DirectWaaCtx) OnGetVar

func (c *DirectWaaCtx) OnGetVar(name string) ([]string, bool)

func (*DirectWaaCtx) OnHeaderLine

func (c *DirectWaaCtx) OnHeaderLine(line string, lineTerminator int) error

func (*DirectWaaCtx) OnOpenFile

func (c *DirectWaaCtx) OnOpenFile(filename string) (io.WriteCloser, error)

func (*DirectWaaCtx) OnPutData

func (c *DirectWaaCtx) OnPutData(data []byte) error

func (*DirectWaaCtx) SetMethod

func (c *DirectWaaCtx) SetMethod(method string)

SetMethod sets the REQUEST_METHOD environment variable.

func (*DirectWaaCtx) SetValue

func (c *DirectWaaCtx) SetValue(name, value string)

SetValue sets a form variable to a single value, replacing whatever it had.

func (*DirectWaaCtx) SetValues

func (c *DirectWaaCtx) SetValues(name string, values []string)

SetValues sets a form variable to the given values, replacing whatever it had. The variable keeps the name case of its first appearance.

type KvEntry

type KvEntry struct {
	Key    string
	Values []string
}

KvEntry is one form variable with its collected values, in order of first appearance.

type Message

type Message struct {
	Cmd  int32
	Data []byte
}

Message represents a WAA protocol message

func GetMessage

func GetMessage(conn io.Reader) (*Message, error)

GetMessage reads a message from the WAA transport Format: <msgLen 4 bytes><cmd 4 bytes><data>

func (*Message) DataString

func (m *Message) DataString() string

DataString returns the message data as a string

type NullWaaCtx

type NullWaaCtx struct{}

NullWaaCtx is a do-nothing WaaCtx: empty answers, discarded output, files and email rejected (NAK). Embed it to implement only the methods you need.

func (NullWaaCtx) OnEmail

func (NullWaaCtx) OnEmail(rfc822data []byte) error

func (NullWaaCtx) OnEndHeader

func (NullWaaCtx) OnEndHeader(lineTerminator int) error

func (NullWaaCtx) OnGetAllVars

func (NullWaaCtx) OnGetAllVars() []KvEntry

func (NullWaaCtx) OnGetDocRoot

func (NullWaaCtx) OnGetDocRoot() string

func (NullWaaCtx) OnGetEnv

func (NullWaaCtx) OnGetEnv(name string) string

func (NullWaaCtx) OnGetScriptName

func (NullWaaCtx) OnGetScriptName() string

func (NullWaaCtx) OnGetVar

func (NullWaaCtx) OnGetVar(name string) ([]string, bool)

func (NullWaaCtx) OnHeaderLine

func (NullWaaCtx) OnHeaderLine(line string, lineTerminator int) error

func (NullWaaCtx) OnOpenFile

func (NullWaaCtx) OnOpenFile(filename string) (io.WriteCloser, error)

func (NullWaaCtx) OnPutData

func (NullWaaCtx) OnPutData(data []byte) error

type WaaCtx

type WaaCtx interface {
	// OnGetEnv answers CmdGetEnv: the value of an environment variable,
	// empty if unknown.
	OnGetEnv(name string) string

	// OnGetVar answers CmdGetVar: the decoded values of a form variable
	// (a form key may repeat), and whether it exists. The protocol
	// answer carries the first value.
	OnGetVar(name string) ([]string, bool)

	// OnGetAllVars answers CmdGetAllVars: all form variables in order of
	// first appearance, each with its collected values (a form key may
	// repeat). The protocol answer carries them URL-encoded.
	OnGetAllVars() []KvEntry

	// OnGetDocRoot answers CmdGetDocRoot.
	OnGetDocRoot() string

	// OnGetScriptName answers CmdGetScriptName.
	OnGetScriptName() string

	// OnHeaderLine receives one CGI header line emitted by the WAA
	// application, without its terminator; lineTerminator is one of the
	// Eol* constants. Returning an error aborts the conversation.
	OnHeaderLine(line string, lineTerminator int) error

	// OnEndHeader signals the empty line that closes the header block,
	// with its terminator. Returning an error aborts the conversation.
	OnEndHeader(lineTerminator int) error

	// OnPutData receives the application output that follows the header
	// block — the body. If the application never emits a header
	// separator, all its output arrives here at the end of the
	// conversation. Returning an error aborts the conversation.
	OnPutData(data []byte) error

	// OnOpenFile returns the destination stream for a file sent by the WAA
	// server. The loop writes the received data to it and closes it when
	// the server closes the file. Error or nil writer = NAK.
	OnOpenFile(filename string) (io.WriteCloser, error)

	// OnEmail sends an email message, passed through untouched as received
	// from the WAA server. Error = NAK.
	OnEmail(rfc822data []byte) error
}

WaaCtx is what one WAA conversation talks to. It is fully virtual: only methods, no data. The protocol loop (Run) translates each server command into a method call, so implementations decide where the data comes from and where the output goes — an HTTP request, a plain Go call, a test, anything.

type WaaRouter added in v0.1.1

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

WaaRouter is the multibinding destination: it holds several targets and picks one per call by the package variable of the conversation (WAA_PACKAGE by default). Unmapped packages go to the default target (the first one added, unless SetDefaultTarget says otherwise), and a package mapped to "!" is served elsewhere — anything that is not WAA: the call answers ErrShouldBeDispatchedElsewhere and the caller serves it its own way. The typical use is the migration lever: an Xbase++/WAA program moves to Go package by package, each replacement mapped to "!", WAA serving the rest, no seams visible.

func (*WaaRouter) AddTarget added in v0.1.1

func (r *WaaRouter) AddTarget(name string, host string, port int, timeouts ...int) (error, *WaaTarget)

AddTarget registers one WAA server under a name ("" auto-names it "host:port"; "!" is reserved and refused) and returns the created target. The first target added is the default one. Timeouts are optional: conn, read, write, in seconds.

func (*WaaRouter) GetTargetIndexByName added in v0.1.1

func (r *WaaRouter) GetTargetIndexByName(name string) int

GetTargetIndexByName finds a target by its name, case-insensitively; -1 when no target carries it.

func (*WaaRouter) GetWaaTargetParam added in v0.1.1

func (w *WaaRouter) GetWaaTargetParam(ctx WaaCtx) (error, *WaaTarget)

GetWaaTargetParam resolves the destination for this conversation: the package variable is asked to the ctx, mapped packages go to their target, unmapped ones to the default, and no default (or a "!" mapping) answers ErrShouldBeDispatchedElsewhere — not WAA's request.

func (*WaaRouter) MapPackageToTarget added in v0.1.1

func (r *WaaRouter) MapPackageToTarget(pkgName string, targetName string) error

MapPackageToTarget binds one package value to a target by name. Target "!" marks the package as already migrated: it will be dispatched elsewhere (the migration lever, one package at a time).

func (*WaaRouter) SetDefaultTarget added in v0.1.1

func (r *WaaRouter) SetDefaultTarget(name string) error

SetDefaultTarget picks the target that serves the unmapped packages. "!" means no default at all: anything unmapped should be dispatched elsewhere.

func (*WaaRouter) SetPackageVarName added in v0.1.1

func (r *WaaRouter) SetPackageVarName(name string)

SetPackageVarName changes the variable that picks the binding. Usually you migrate package by package (WAA_PACKAGE, the default), but inside one package another variable may play the pivot; "" resets to the default.

type WaaTarget added in v0.1.1

type WaaTarget struct {
	Host string // default 127.0.0.1
	Port int    // default 1024

	// Timeouts in seconds
	ConnTimeout  int    // default 30
	ReadTimeout  int    // default 60
	WriteTimeout int    // default 60
	Name         string // optional name for this target, used in a router
	// contains filtered or unexported fields
}

WaaTarget holds how to reach a WAA server: host, port, timeouts.

func (*WaaTarget) GetWaaTargetParam added in v0.1.1

func (w *WaaTarget) GetWaaTargetParam(ctx WaaCtx) (error, *WaaTarget)

GetWaaTargetParam satisfies WaaTargetParam by answering itself: a WaaTarget IS its own destination, whatever the conversation carries.

func (*WaaTarget) Init added in v0.1.1

func (t *WaaTarget) Init(host string, port int, timeouts ...int)

Init fills the target: empty host → 127.0.0.1, zero port → 1024, and the optional timeouts are conn, read, write in seconds (zero → 30, 60, 60). A target used without Init gets the same defaults on its first call.

type WaaTargetParam added in v0.1.1

type WaaTargetParam interface {
	GetWaaTargetParam(ctx WaaCtx) (error, *WaaTarget)
}

WaaTargetParam is the virtual destination of a call: given the conversation context it answers WHICH server to dial. A WaaTarget answers itself; a WaaRouter picks by the package variable; anything else may implement its own policy.

Jump to

Keyboard shortcuts

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