Capabilities Validation¶
The capabilities API lets you parse the OData v4 Capabilities vocabulary from a service's $metadata document and enforce restrictions at query-build time -- before any HTTP request is sent.
Overview¶
OData services annotate their entity sets with vocabulary terms such as Capabilities.FilterRestrictions and Capabilities.SortRestrictions to advertise what operations are permitted. traverse can read these annotations and surface clear errors when a query violates them.
Parsing capabilities¶
import "github.com/jhonsferg/traverse"
// Fetch raw metadata XML
metaBytes, err := client.MetadataBytes(ctx)
if err != nil {
log.Fatal(err)
}
registry, err := traverse.ParseCapabilities(metaBytes)
if err != nil {
log.Fatal(err)
}
ParseCapabilities returns a *CapabilitiesRegistry populated from every Capabilities.* annotation in the EDMX document.
Enabling validation on a client¶
Pass the registry as a client option:
client := traverse.New(traverse.Config{
BaseURL: "https://api.example.com/odata/",
}, traverse.WithCapabilitiesValidation(registry))
From this point on, query builders that violate the capabilities will return a *CapabilityError immediately, without making a network call.
CapabilityError¶
type CapabilityError struct {
EntitySet string
Operation string // "filter", "sort", or "expand"
Property string // populated for property-level restrictions
Message string
}
products := traverse.From[Product](client, "Products")
_, err := products.Filter("CreatedAt gt 2024-01-01").List(ctx)
var capErr *traverse.CapabilityError
if errors.As(err, &capErr) {
fmt.Printf("%s: %s on %s\n", capErr.EntitySet, capErr.Operation, capErr.Property)
// Products: filter on CreatedAt
}
EntityCapabilities¶
CapabilitiesRegistry.Get returns an EntityCapabilities struct:
type EntityCapabilities struct {
Filterable bool
NonFilterableProperties []string
Sortable bool
NonSortableProperties []string
ExpandableNavigationProperties []string
Insertable bool
Updatable bool
Deletable bool
}
When an entity set has no annotations, all capabilities default to true and both restriction lists are empty.
Inspecting capabilities directly¶
cap := registry.Get("Products")
if !cap.Filterable {
fmt.Println("Products cannot be filtered")
}
fmt.Println("Non-filterable:", cap.NonFilterableProperties)
fmt.Println("Non-sortable:", cap.NonSortableProperties)
Supported Capabilities annotations¶
| Annotation term | Fields checked |
|---|---|
Capabilities.FilterRestrictions | Filterable, NonFilterableProperties |
Capabilities.SortRestrictions | Sortable, NonSortableProperties |
Capabilities.ExpandRestrictions | ExpandableProperties |
Capabilities.InsertRestrictions | Insertable |
Capabilities.UpdateRestrictions | Updatable |
Capabilities.DeleteRestrictions | Deletable |
Complete example¶
package main
import (
"context"
"errors"
"fmt"
"log"
"github.com/jhonsferg/traverse"
)
type Product struct {
ID int `json:"ProductID"`
Name string `json:"ProductName"`
Price float64 `json:"UnitPrice"`
}
func main() {
ctx := context.Background()
// 1. Create a bare client to fetch metadata
bare := traverse.New(traverse.Config{
BaseURL: "https://api.example.com/odata/",
})
metaBytes, err := bare.MetadataBytes(ctx)
if err != nil {
log.Fatal(err)
}
// 2. Parse capabilities
registry, err := traverse.ParseCapabilities(metaBytes)
if err != nil {
log.Fatal(err)
}
// 3. Create a validated client
client := traverse.New(traverse.Config{
BaseURL: "https://api.example.com/odata/",
}, traverse.WithCapabilitiesValidation(registry))
// 4. Queries are checked before any HTTP call
products := traverse.From[Product](client, "Products")
_, err = products.Filter("CreatedAt gt 2024-01-01").List(ctx)
var capErr *traverse.CapabilityError
if errors.As(err, &capErr) {
fmt.Println("Caught capability error:", capErr)
}
}