Files
nuntius/docs/library-usage.md

232 lines
7.1 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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.