Skip to content

jhonsferg/traverse

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

188 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Traverse

A declarative OData v2/v4 client for Go.

Go Version CI Tests Coverage CodeQL Trivy Release pkg.go.dev Go Report Card License: MIT


Documentation - pkg.go.dev - Quick Start - Features - Extensions

Overview

Traverse is a Go library for consuming OData v2 and v4 services. It handles all protocol details - pagination, CSRF tokens, ETag concurrency control, delta sync, batch requests, async long-running operations, actions, functions, and schema validation - so you can focus on the data.

Built on relay for HTTP transport. Well-suited for SAP environments (ABAP Gateway / OData v2, S/4HANA / OData v4), Microsoft Graph, and any standards-compliant OData service.

go get github.com/jhonsferg/traverse

Requires Go 1.24 or later.


Why traverse?

The verb to traverse means to walk through something large, complex, or extended - one step at a time, without needing to hold the whole thing in memory. In computer science, tree traversal and graph traversal describe exactly that: visiting every node of a structure incrementally rather than materialising it all at once.

That is the problem this library solves:

other clients:  GET /MaterialSet → load 1 000 000 records into RAM → out of memory
traverse:       GET /MaterialSet → visit each record one by one   → constant memory

The difference is not what you fetch - it is how you move through it. Traverse treats a remote OData collection the way a graph traversal treats a tree: as a path to walk, not a payload to download.

Three principles follow naturally from the name:

The path matters more than the destination. You do not wait to have all the data before you start working. Each record is actionable the moment it arrives - that is exactly the for result := range client.From("MaterialSet").Stream(ctx) pattern at the core of the library.

Respect for the terrain. A careful traversal does not tear up the ground beneath it. Traverse is deliberately gentle on the services it talks to: rate limiting and circuit breaking are inherited from relay, page size follows the server's own nextLink rhythm, and CSRF tokens are managed transparently without extra round-trips.

The map is not the territory. A tree traversal does not require the full tree in memory - only the current node and a pointer to the next. Traverse does the same with OData: you can walk a million SAP materials without keeping a million structs alive simultaneously.

The name also has an intentional honesty to it: it does not promise to be an OData library or a SAP library. It promises to help you move through large, remote datasets. Today that means OData. Tomorrow it could mean any cursor-based protocol - the name stays valid.


Quick Start

package main

import (
    "context"
    "fmt"
    "log"

    "github.com/jhonsferg/traverse"
)

type Product struct {
    ID    int     `json:"ProductID"`
    Name  string  `json:"ProductName"`
    Price float64 `json:"UnitPrice"`
}

func main() {
    client, err := traverse.New(
        traverse.WithBaseURL("https://services.odata.org/V4/Northwind/Northwind.svc/"),
    )
    if err != nil {
        log.Fatal(err)
    }

    products := traverse.From[Product](client, "Products")

    results, err := products.
        Filter("UnitPrice lt 20").
        OrderBy("ProductName").
        Top(10).
        List(context.Background())
    if err != nil {
        log.Fatal(err)
    }

    for _, p := range results {
        fmt.Printf("%s - $%.2f\n", p.Name, p.Price)
    }
}

Features

Feature Description
Typed query builder From[T]() with .Filter(), .Select(), .Expand(), .OrderBy(), .Top(), .Skip()
Type-safe filter builder F("Field").Eq(value), And(), Or(), Not() - no raw strings
CRUD operations .Get(), .List(), .Create(), .Update(), .Delete(), .Upsert()
ETag & concurrency Automatic ETag tracking for optimistic concurrency on updates
Entity change tracking Track and PATCH only modified fields
Typed pagination Paginator[T] with .NextPage() and .Stream()
Async operations Automatic polling for 202 Accepted long-running operations
Streaming Channel-based streaming via json.Decoder - constant memory
Batch requests $batch with transactional changesets; OData 4.01 JSON batch format
Delta sync $deltatoken tracking for incremental data sync
Lambda filters any() / all() on collection navigation properties
Deep insert Create entity graphs in a single request
Deep update PATCH nested entity graphs in a single round-trip
BulkUpdate PATCH /EntitySet?$filter=... for mass updates (OData 4.01)
Singletons First-class singleton access: client.Singleton("me").Page(ctx)
Type casting AsType("Model.Manager") path segments; IsOf() / Cast() filter helpers
$expand $levels Expand("Children", traverse.WithExpandLevels(traverse.LevelsMax))
Geospatial GeographyPoint, GeometryPoint, GeoDistance, GeoIntersects filter functions
$ref link operations LinkTo() / UnlinkFrom() for managing navigation property references
Actions & Functions ActionBuilder / FunctionBuilder - bound and unbound
Schema validation Client-side field name validation on $filter / $orderby
Prefer headers PreferHandlingStrict, ReturnMinimal, ReturnRepresentation, PreferTrackChanges
$schemaversion WithSchemaVersion("2.0") at client or per-query level
Atom/XML responses OData v2 application/atom+xml streaming parser (auto-detected)
OData v2 $inlinecount $inlinecount=allpages emitted for v2 services; d.__count parsed
SAP TLS WithSAPTLSConfig(cfg) for custom CA bundles and self-signed certs
SAP property path FetchPropertyAs[T] for scalar/complex property fetch by key and path
Code generation traverse-gen generates typed clients from $metadata
SAP compatibility CSRF tokens, X-Requested-With, SAP sap:* metadata attributes

Full feature documentation: jhonsferg.github.io/traverse


CSDL JSON Support

Traverse can parse both EDMX/XML and CSDL JSON (the OData v4.01 JSON format used by Microsoft Graph). The client auto-detects the format by Content-Type when fetching $metadata:

// Auto-detected - no code change needed when the service returns JSON metadata
client, _ := traverse.New(traverse.WithBaseURL("https://api.example.com/odata/v4/"))
meta, err := client.Metadata(ctx)

For direct parsing:

import "github.com/jhonsferg/traverse"

// From bytes
meta, err := traverse.ParseCSDLJSON(data)

// From a reader (e.g. http.Response.Body)
meta, err := traverse.ParseCSDLJSONReader(resp.Body)

XML Support

Traverse supports both JSON and XML response formats. Some OData backends (particularly SAP) may return XML instead of JSON, even when JSON is requested. Use the explicit format methods to handle both:

type Product struct {
    ID    int    `json:"ProductID" xml:"ProductID"`
    Name  string `json:"ProductName" xml:"ProductName"`
}

client, _ := traverse.New(traverse.WithBaseURL("https://api.example.com/odata/"))

ctx := context.Background()

qb := client.From("Products").Filter("Price lt 100")

products, err := traverse.CollectJsonAs[Product](qb, ctx)

if err != nil {
    products, err = traverse.CollectXmlAs[Product](qb, ctx)
    if err != nil {
        log.Fatal(err)
    }
}

for _, p := range products {
    fmt.Printf("%s\n", p.Name)
}

Streaming with XML:

type Order struct {
    OrderID string `json:"OrderID" xml:"OrderID"`
    Amount  float64 `json:"Amount" xml:"Amount"`
}

qb := client.From("Orders").Top(1000)

for result := range traverse.StreamXmlAs[Order](qb, context.Background()) {
    if result.Err != nil {
        log.Printf("stream error: %v", result.Err)
        break
    }
    fmt.Printf("Order %s: $%.2f\n", result.Value.OrderID, result.Value.Amount)
}

All CRUD and query operations have explicit JSON/XML variants:

Operation JSON XML
Create CreateJsonAs[T]() CreateXmlAs[T]()
Collect (list) CollectJsonAs[T]() CollectXmlAs[T]()
Stream (paginated) StreamJsonAs[T]() StreamXmlAs[T]()
First (single) FirstJsonAs[T]() FirstXmlAs[T]()
FindByKey (get) FindByKeyJsonAs[T]() FindByKeyXmlAs[T]()
Functions ExecuteFunctionJsonAs[T]() ExecuteFunctionXmlAs[T]()
Actions ExecuteActionJsonAs[T]() ExecuteActionXmlAs[T]()
Delta sync DeltaSyncJsonAs[T] DeltaSyncXmlAs[T]

Struct tags determine marshaling behavior:

type Material struct {
    ID    string `json:"MatID" xml:"MatID"`
    Name  string `json:"Name" xml:"Name"`
    Stock int    `json:"StockQty" xml:"StockQty"`
}

json_mats, _ := traverse.CollectJsonAs[Material](qb, ctx)

xml_mats, _ := traverse.CollectXmlAs[Material](qb, ctx)

OpenAPI 3.1 Export

Convert OData metadata to an OpenAPI 3.1 document:

import (
    "encoding/json"
    "github.com/jhonsferg/traverse"
    "github.com/jhonsferg/traverse/ext/openapi"
)

meta, _ := client.Metadata(ctx)
doc, err := openapi.Export(meta,
    openapi.WithTitle("My OData API"),
    openapi.WithVersion("1.0.0"),
    openapi.WithServerURL("https://api.example.com/odata/v4/"),
)

out, _ := json.MarshalIndent(doc, "", "  ")
fmt.Println(string(out))
go get github.com/jhonsferg/traverse/ext/openapi

OData Vocabularies

Properties carry parsed Core and Validation vocabulary annotations:

meta, _ := client.Metadata(ctx)
for _, et := range meta.EntityTypes {
    for _, prop := range et.Properties {
        core := traverse.ParseCoreVocabulary(prop.Annotations)
        val  := traverse.ParseValidationVocabulary(prop.Annotations)

        fmt.Printf("%s: %s", prop.Name, core.Description)
        if val.Pattern != "" {
            fmt.Printf(" (pattern: %s)", val.Pattern)
        }
        if val.Required {
            fmt.Print(" [required]")
        }
        fmt.Println()
    }
}

Available types: CoreVocabulary (Description, LongDescription, Immutable, Computed, Permissions, …) and ValidationVocabulary (Minimum, Maximum, Pattern, AllowedValues, Required).


Tools

traverse-gen

traverse-gen generates type-safe Go clients from an OData $metadata endpoint:

go run github.com/jhonsferg/traverse/cmd/traverse-gen \
  -metadata https://services.odata.org/V4/Northwind/Northwind.svc/$metadata \
  -out ./northwind

traverse-tui

Interactive terminal UI for exploring OData endpoints, building queries, and inspecting results:

go run github.com/jhonsferg/traverse/cmd/traverse-tui

SAP OData Mock Server

A local SAP NetWeaver OData v2 simulator for integration testing without a real SAP system:

go run github.com/jhonsferg/traverse/cmd/sap-mock

Simulates CSRF token lifecycle, Basic Auth, $metadata responses, entity-set queries, key-predicate lookups, and property-path navigation. Logs all incoming requests with headers, query parameters, and body for inspection.

SAP OData Mock Server
  Listen: http://localhost:44300
  Auth:   enabled (user=sapuser pass=sappass)

Extensions

Module Import path Description
ext/sap github.com/jhonsferg/traverse/ext/sap SAP Gateway CSRF, session handling, and Fiori UI annotations
ext/openapi github.com/jhonsferg/traverse/ext/openapi OpenAPI 3.1 export from OData metadata
ext/oauth2 github.com/jhonsferg/traverse/ext/oauth2 OAuth2 token provider
ext/prometheus github.com/jhonsferg/traverse/ext/prometheus Prometheus metrics
ext/tracing github.com/jhonsferg/traverse/ext/tracing OpenTelemetry tracing
ext/graphql github.com/jhonsferg/traverse/ext/graphql GraphQL-to-OData bridge
ext/cache github.com/jhonsferg/traverse/ext/cache HTTP response and metadata caching
ext/offline github.com/jhonsferg/traverse/ext/offline Persistent offline store with JSON cache
ext/dataverse github.com/jhonsferg/traverse/ext/dataverse Microsoft Dataverse adapter
ext/azure github.com/jhonsferg/traverse/ext/azure Azure Event Grid change events
ext/webhooks github.com/jhonsferg/traverse/ext/webhooks OData webhook subscriptions
ext/audit github.com/jhonsferg/traverse/ext/audit Audit trail middleware

Extension documentation: jhonsferg.github.io/traverse/extensions

SAP Fiori UI Annotations

ext/sap includes support for SAP UI annotations parsed from EDMX attributes (sap:label, sap:sortable, sap:filterable, etc.):

import "github.com/jhonsferg/traverse/ext/sap"

ann := sap.ParseSAPUIAnnotation(property.RawAttributes)
fmt.Printf("Label: %s, Filterable: %v\n", ann.Label, ann.Filterable)

// Get all annotated properties from an entity type
props := sap.AnnotatedProperties(entityType, meta)
for _, p := range props {
    fmt.Printf("%s → label=%s sortable=%v\n", p.Property.Name, p.Annotation.Label, p.Annotation.Sortable)
}

Microsoft Graph

rc := relay.New(relay.WithBearerToken(token))
gc := traverse.NewGraphClient(rc, traverse.GraphConfig{
    AccessToken: token,
})

type User struct {
    ID          string `json:"id"`
    DisplayName string `json:"displayName"`
}

users, err := traverse.From[User](gc, "users").
    Filter("department eq 'Engineering'").
    Select("id", "displayName").
    List(ctx)

Microsoft Graph guide


Documentation

The full documentation is at jhonsferg.github.io/traverse:


License

MIT - see LICENSE.

About

OData v2/v4 client for Go, built on relay - streaming, batch, SAP-compatible

Topics

Resources

License

Contributing

Security policy

Stars

Watchers

Forks

Packages

 
 
 

Contributors

Languages