# 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` (2–100 runes), `email` (RFC 5322), `service` (allow-list), `message` (10–5,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.