Simple guides introducing Fabrica with hands-on examples.
This guide explains how to use Fabrica’s validation package to ensure your resources meet required standards.
Fabrica provides a powerful validation system that combines:
Proper validation:
package v1
import (
"github.com/openchami/fabrica/pkg/fabrica"
"github.com/openchami/fabrica/pkg/validation"
)
type Device struct {
APIVersion string `json:"apiVersion"`
Kind string `json:"kind"`
Metadata fabrica.Metadata `json:"metadata"`
Spec DeviceSpec `json:"spec" validate:"required"`
Status DeviceStatus `json:"status,omitempty"`
}
type DeviceSpec struct {
Name string `json:"name" validate:"required,k8sname,min=3,max=63"`
Type string `json:"type" validate:"required,oneof=server switch router"`
IPAddress string `json:"ipAddress" validate:"required,ip"`
MACAddress string `json:"macAddress,omitempty" validate:"omitempty,mac"`
Labels map[string]string `json:"labels" validate:"dive,keys,labelkey,endkeys,labelvalue"`
}
Note: Resources use explicit
APIVersion,Kind, andMetadata fabrica.Metadatafields rather than embedding.
func CreateDeviceHandler(w http.ResponseWriter, r *http.Request) {
var device Device
// Decode request
if err := json.NewDecoder(r.Body).Decode(&device); err != nil {
http.Error(w, "Invalid JSON", http.StatusBadRequest)
return
}
// Validate the device
if err := validation.ValidateResource(&device); err != nil {
// Return structured validation errors
if validationErrs, ok := err.(validation.ValidationErrors); ok {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusBadRequest)
json.NewEncoder(w).Encode(map[string]interface{}{
"error": "Validation failed",
"details": validationErrs.Errors,
})
return
}
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
// Proceed with resource creation
// ...
}
resp, err := http.Post(url, "application/json", body)
if err != nil {
log.Fatal(err)
}
if resp.StatusCode == http.StatusBadRequest {
var errorResp struct {
Error string `json:"error"`
Details []validation.FieldError `json:"details"`
}
json.NewDecoder(resp.Body).Decode(&errorResp)
for _, fieldErr := range errorResp.Details {
fmt.Printf("Error in %s: %s\n", fieldErr.Field, fieldErr.Message)
}
}
Important: In validation tags, you specify validator function names (like
ip,uuid), NOT field names. The JSON field name comes from thejsontag.Example:
IPAddress string \json:”ipAddress” validate:”required,ip”``
ipAddressis the JSON field name (used in API requests)ipis the validator function (checks if value is a valid IP address)
type Resource struct {
Name string `json:"name" validate:"required"` // Must be present
Optional string `json:"optional" validate:"omitempty"` // Validated only if present
}
type Resource struct {
Name string `json:"name" validate:"min=3,max=63"` // Length constraints
Email string `json:"email" validate:"email"` // Email format
URL string `json:"url" validate:"url"` // URL format
Type string `json:"type" validate:"oneof=a b c"` // Enumeration
NoSpaces string `json:"noSpaces" validate:"excludes= "` // Exclude characters
AlphaNum string `json:"alphaNum" validate:"alphanum"` // Alphanumeric only
}
type Resource struct {
Port int `json:"port" validate:"min=1,max=65535"` // Range validation
Age int `json:"age" validate:"gte=0,lte=150"` // Greater/less than or equal
Score float64 `json:"score" validate:"min=0.0,max=100.0"` // Float ranges
Count int `json:"count" validate:"eq=10"` // Exact value
}
type Resource struct {
IP string `json:"ip" validate:"ip"` // Any IP address
IPv4 string `json:"ipv4" validate:"ipv4"` // IPv4 only
IPv6 string `json:"ipv6" validate:"ipv6"` // IPv6 only
CIDR string `json:"cidr" validate:"cidr"` // CIDR notation
MAC string `json:"mac" validate:"mac"` // MAC address
Hostname string `json:"hostname" validate:"hostname"` // Hostname format
}
type Resource struct {
// Kubernetes resource name (lowercase, alphanumeric, -, .)
Name string `json:"name" validate:"k8sname"`
// DNS label (1-63 chars, alphanumeric or -)
Label string `json:"label" validate:"dnslabel"`
// DNS subdomain (max 253 chars, dot-separated labels)
Domain string `json:"domain" validate:"dnssubdomain"`
// Label keys and values
Labels map[string]string `json:"labels" validate:"dive,keys,labelkey,endkeys,labelvalue"`
}
type Resource struct {
// Validate each element in slice
Tags []string `json:"tags" validate:"dive,k8sname"`
// Validate map keys and values
Labels map[string]string `json:"labels" validate:"dive,keys,labelkey,endkeys,labelvalue"`
// Nested struct validation
Metadata Metadata `json:"metadata" validate:"required"`
}
type Resource struct {
Password string `json:"password" validate:"required,min=8"`
ConfirmPassword string `json:"confirmPassword" validate:"required,eqfield=Password"`
StartDate string `json:"startDate" validate:"required"`
EndDate string `json:"endDate" validate:"required,gtefield=StartDate"`
}
For complex validation that can’t be expressed with tags, implement the CustomValidator interface:
type Device struct {
APIVersion string `json:"apiVersion"`
Kind string `json:"kind"`
Metadata Metadata `json:"metadata"`
Spec DeviceSpec `json:"spec" validate:"required"`
}
func (d *Device) Validate(ctx context.Context) error {
// Custom business rules
if d.Spec.Type == "server" {
if d.Spec.MACAddress == "" {
return errors.New("server devices must have a MAC address")
}
if !strings.HasPrefix(d.Spec.Name, "srv-") {
return errors.New("server names must start with 'srv-'")
}
}
// Context-aware validation
if deadline, ok := ctx.Deadline(); ok {
if time.Until(deadline) < time.Second {
return errors.New("validation timeout approaching")
}
}
return nil
}
// Use ValidateWithContext to run both struct and custom validation
if err := validation.ValidateWithContext(ctx, &device); err != nil {
// Handle error
}
type ValidationErrors struct {
Errors []FieldError `json:"errors"`
}
type FieldError struct {
Field string `json:"field"` // JSON field name
Tag string `json:"tag"` // Validation tag that failed
Value string `json:"value"` // Actual value (optional)
Message string `json:"message"` // User-friendly message
}
Return validation errors in a consistent format:
{
"error": "Validation failed",
"details": [
{
"field": "name",
"tag": "k8sname",
"value": "Invalid_Name",
"message": "name must be a valid Kubernetes name (lowercase alphanumeric, -, or .)"
},
{
"field": "ipAddress",
"tag": "ip",
"value": "not-an-ip",
"message": "ipAddress must be a valid IP address"
}
]
}
Create middleware for consistent error handling:
func ValidationMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Capture panics
defer func() {
if err := recover(); err != nil {
if validationErrs, ok := err.(validation.ValidationErrors); ok {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusBadRequest)
json.NewEncoder(w).Encode(map[string]interface{}{
"error": "Validation failed",
"details": validationErrs.Errors,
})
return
}
panic(err) // Re-panic if not a validation error
}
}()
next.ServeHTTP(w, r)
})
}
Register your own validators:
import "github.com/go-playground/validator/v10"
func init() {
// Register a custom validator for semantic versioning
validation.RegisterCustomValidator("semver", func(fl validator.FieldLevel) bool {
version := fl.Field().String()
return validation.SemanticVersionRegex.MatchString(version)
})
}
type Release struct {
Version string `json:"version" validate:"required,semver"`
}
// Validator that checks a value against a database
validation.RegisterCustomValidator("uniqueuser", func(fl validator.FieldLevel) bool {
username := fl.Field().String()
// Check database (pseudo-code)
exists, err := db.UserExists(username)
if err != nil {
return false
}
return !exists
})
type User struct {
Username string `json:"username" validate:"required,uniqueuser"`
}
// Validate as soon as data enters your system
func CreateHandler(w http.ResponseWriter, r *http.Request) {
var resource MyResource
json.NewDecoder(r.Body).Decode(&resource)
// Immediate validation
if err := validation.ValidateResource(&resource); err != nil {
// Return error immediately
return
}
// Continue processing
}
// Good: Specific validation
type Device struct {
Name string `json:"name" validate:"required,k8sname,min=3,max=63"`
Type string `json:"type" validate:"required,oneof=server switch router"`
}
// Avoid: Too generic
type Device struct {
Name string `json:"name" validate:"required"`
Type string `json:"type" validate:"required"`
}
// Good: Return structured errors
if err := validation.ValidateResource(&device); err != nil {
if validationErrs, ok := err.(validation.ValidationErrors); ok {
w.WriteHeader(http.StatusBadRequest)
json.NewEncoder(w).Encode(map[string]interface{}{
"error": "Validation failed",
"details": validationErrs.Errors,
})
return
}
}
// Avoid: Generic error messages
if err := validation.ValidateResource(&device); err != nil {
http.Error(w, "Bad request", http.StatusBadRequest)
}
// Good: Clear documentation
type Device struct {
// Name must be a valid Kubernetes name (lowercase alphanumeric, -, or .)
// Length: 3-63 characters
Name string `json:"name" validate:"required,k8sname,min=3,max=63"`
// Type must be one of: server, switch, router
Type string `json:"type" validate:"required,oneof=server switch router"`
}
type Device struct {
APIVersion string `json:"apiVersion"`
Kind string `json:"kind"`
Metadata Metadata `json:"metadata"`
Spec DeviceSpec `json:"spec" validate:"required"`
}
func (d *Device) Validate(ctx context.Context) error {
// Use struct validation for basic rules
if err := validation.ValidateResource(&d.Spec); err != nil {
return err
}
// Add custom business logic
if d.Spec.Type == "server" && d.Spec.MACAddress == "" {
return errors.New("servers must have a MAC address")
}
return nil
}
func (s *Server) CreateDevice(w http.ResponseWriter, r *http.Request) {
var device Device
if err := json.NewDecoder(r.Body).Decode(&device); err != nil {
http.Error(w, "Invalid JSON", http.StatusBadRequest)
return
}
if err := validation.ValidateWithContext(r.Context(), &device); err != nil {
s.handleValidationError(w, err)
return
}
// Store device
if err := s.storage.Create(r.Context(), &device); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.WriteHeader(http.StatusCreated)
json.NewEncoder(w).Encode(device)
}
func (s *Server) handleValidationError(w http.ResponseWriter, err error) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusBadRequest)
if validationErrs, ok := err.(validation.ValidationErrors); ok {
json.NewEncoder(w).Encode(map[string]interface{}{
"error": "Validation failed",
"details": validationErrs.Errors,
})
} else {
json.NewEncoder(w).Encode(map[string]string{
"error": err.Error(),
})
}
}
Update your templates to include validation:
// In handlers template
func (s *Server) Create(w http.ResponseWriter, r *http.Request) {
var resource
if err := json.NewDecoder(r.Body).Decode(&resource); err != nil {
http.Error(w, "Invalid JSON", http.StatusBadRequest)
return
}
// Auto-generated validation call
if err := validation.ValidateWithContext(r.Context(), &resource); err != nil {
handleValidationError(w, err)
return
}
// Continue with resource creation...
}
func TestDeviceValidation(t *testing.T) {
tests := []struct {
name string
device Device
wantErr bool
errField string
}{
{
name: "valid device",
device: Device{
Spec: DeviceSpec{
Name: "my-server",
Type: "server",
IPAddress: "192.168.1.1",
},
},
wantErr: false,
},
{
name: "invalid name",
device: Device{
Spec: DeviceSpec{
Name: "Invalid_Name",
Type: "server",
IPAddress: "192.168.1.1",
},
},
wantErr: true,
errField: "name",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := validation.ValidateResource(&tt.device)
if (err != nil) != tt.wantErr {
t.Errorf("wanted error: %v, got: %v", tt.wantErr, err)
}
if tt.wantErr {
validationErrs := err.(validation.ValidationErrors)
if validationErrs.Errors[0].Field != tt.errField {
t.Errorf("wanted error in field %s, got %s",
tt.errField, validationErrs.Errors[0].Field)
}
}
})
}
}
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
if err := validation.ValidateWithContext(ctx, &resource); err != nil {
// Handle error
}