Files
nuntius/docs/library-usage.md

7.1 KiB
Raw Permalink Blame History

Library Usage — pkg/contactform

pkg/contactform is the public, reusable Go package that the nuntius HTTP server uses for validation. It has no third-party dependencies and no awareness of HTTP, SMTP, or the filesystem — drop it into any Go project that needs to validate a contact-style form payload.

Installation

go get sourcedock.dev/petrbalvin/nuntius/pkg/contactform

Because nuntius ships with no go.sum entries from third-party modules, this import pulls in only stdlib transitively.

Types

Request

The JSON body sent to the contact endpoint.

type Request struct {
    Name     string `json:"name"`
    Email    string `json:"email"`
    Service  string `json:"service,omitempty"`
    Message  string `json:"message"`
    Honeypot string `json:"-"`
}
Field JSON tag Notes
Name name Required for contact, feedback, generic
Email email Required for all four types
Service service,omitempty Optional. Validated against an allow-list for contact only.
Message message Required for contact, feedback, generic
Honeypot - Never marshalled to / from JSON. Read it from a separate field in your own request struct, or wire it through your favourite JSON tag.

The handler-side mapping (used inside the nuntius server) reads the honeypot value into Request.Honeypot based on the form's honeypot_field config. If you embed contactform.Request directly in your own API, you are responsible for reading the honeypot field separately.

Response

type Response struct {
    OK bool `json:"ok"`
}

A successful submission returns {"ok": true}.

FieldError

type FieldError struct {
    Field   string `json:"field"`
    Message string `json:"message"`
}

One entry in the details[] array of a validation error.

ErrorResponse

type ErrorResponse struct {
    Error   string       `json:"error"`
    Message string       `json:"message,omitempty"`
    Details []FieldError `json:"details,omitempty"`
}

Returned for any non-2xx response. The Error field is a short, machine-readable code (validation, invalid_json, origin_not_allowed, rate_limited, send_failed, storage_failed). Message is a human-readable summary; Details carries the per-field validation failures.

Functions

Validate

func Validate(r *Request, formType string) []FieldError

Dispatches to the right per-type validator. Returns a slice of field errors; an empty (or nil) slice means the request is valid.

formType What is checked
"contact" name (2100 runes), email (RFC 5322), service (allow-list), message (105,000 runes)
"feedback" name, email, message (no service)
"newsletter" email only
"generic" name, email, message (no service)
"" (empty) Falls back to contact
anything else Falls back to contact — the strict allow-list is enforced at the config layer, not here

Whitespace is trimmed from name, email, service, and message before validation. The trimmed values are what the validator sees and what the email body sees.

Constants

Name Value Used by
MinNameRunes 2 name minimum length
MaxNameRunes 100 name maximum length
MinMessageRunes 10 message minimum length
MaxMessageRunes 5000 message maximum length

Standalone Example

The examples/minimal/ directory contains a runnable demo. From the repo root:

go run ./examples/minimal

Source (examples/minimal/main.go):

// Minimal example showing how to use the contactform package
// standalone (without the HTTP server).
package main

import (
    "fmt"
    "os"

    "sourcedock.dev/petrbalvin/nuntius/pkg/contactform"
)

func main() {
    req := &contactform.Request{
        Name:    "Jane Doe",
        Email:   "jane@example.com",
        Service: "architecture",
        Message: "Hi, I'd like to discuss a possible engagement.",
    }

    if errs := contactform.Validate(req, "contact"); len(errs) > 0 {
        fmt.Fprintln(os.Stderr, "validation failed:")
        for _, e := range errs {
            fmt.Fprintf(os.Stderr, "  - %s: %s\n", e.Field, e.Message)
        }
        os.Exit(1)
    }

    fmt.Println("Request is valid:")
    fmt.Printf("  name:    %s\n", req.Name)
    fmt.Printf("  email:   %s\n", req.Email)
    fmt.Printf("  service: %s\n", req.Service)
    fmt.Printf("  message: %s\n", req.Message)
}

Custom Validators

If you need to extend the allow-list of service values, edit validServices in pkg/contactform/validate.go:

var validServices = map[string]bool{
    "":               true,
    "architecture":   true,
    "ai":             true,
    "infrastructure": true,
    "software":       true,
    "unix":           true,
    "other":          true,
    // add yours here, e.g.:
    // "security":     true,
}

If you need different length limits, edit the four Min* / Max* constants in the same file. Or fork the package — it is intentionally small (~150 LOC) so a copy-into-your-project is also reasonable.

Using Validate From Your Own HTTP Handler

package main

import (
    "encoding/json"
    "log"
    "net/http"

    "sourcedock.dev/petrbalvin/nuntius/pkg/contactform"
)

func contactHandler(w http.ResponseWriter, r *http.Request) {
    if r.Method != http.MethodPost {
        http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
        return
    }
    defer r.Body.Close()

    var req contactform.Request
    if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
        writeJSON(w, http.StatusBadRequest, contactform.ErrorResponse{
            Error:   "invalid_json",
            Message: "Could not parse JSON body.",
        })
        return
    }

    if errs := contactform.Validate(&req, "contact"); len(errs) > 0 {
        writeJSON(w, http.StatusBadRequest, contactform.ErrorResponse{
            Error:   "validation",
            Details: errs,
        })
        return
    }

    // ... your SMTP / persistence / webhook code here ...

    writeJSON(w, http.StatusOK, contactform.Response{OK: true})
}

func writeJSON(w http.ResponseWriter, code int, body any) {
    w.Header().Set("Content-Type", "application/json; charset=utf-8")
    w.WriteHeader(code)
    _ = json.NewEncoder(w).Encode(body)
}

func main() {
    http.HandleFunc("/api/contact", contactHandler)
    log.Fatal(http.ListenAndServe(":8080", nil))
}

The example deliberately omits CORS, rate limiting, and the honeypot — copy those from internal/handler/contact.go if you need them.

Why a Separate Package?

pkg/contactform is the one piece of nuntius that is not tied to HTTP, SMTP, or the filesystem. Keeping it in a separate, dependency-free package:

  • Makes it trivially embeddable in any Go service.
  • Makes it cheap to unit-test (see pkg/contactform/validate_test.go).
  • Makes the public API obvious — there is one entry point (Validate) and four types, all in one file pair.
  • Keeps internal/handler focused on HTTP, internal/email on SMTP, and internal/storage on disk.