Files
nuntius/docs/library-usage.md
T

232 lines
7.1 KiB
Markdown
Raw Normal View 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
```bash
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.
```go
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`
```go
type Response struct {
OK bool `json:"ok"`
}
```
A successful submission returns `{"ok": true}`.
### `FieldError`
```go
type FieldError struct {
Field string `json:"field"`
Message string `json:"message"`
}
```
One entry in the `details[]` array of a `validation` error.
### `ErrorResponse`
```go
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`
```go
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:
```bash
go run ./examples/minimal
```
Source (`examples/minimal/main.go`):
```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`:
```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
```go
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.