feat: initialize nuntius project with full server implementation
This commit is contained in:
@@ -0,0 +1,231 @@
|
||||
# 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 codeberg.org/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"
|
||||
|
||||
"codeberg.org/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"
|
||||
|
||||
"codeberg.org/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.
|
||||
Reference in New Issue
Block a user