Skip to content

jhonsferg/traverse

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

183 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)

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