rest

package module
v0.0.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Nov 10, 2025 License: Apache-2.0 Imports: 24 Imported by: 0

README

Go REST Client

A powerful Go RESTful client library with fluent API design and Server-Sent Events (SSE) support.

Features

  • 🔗 Fluent Chain API - Elegant API design with method chaining support
  • 📡 SSE Support - Complete Server-Sent Events client implementation
  • 🚀 High Performance - Zero-copy design based on K8s RawExtension pattern
  • 🛡️ Type Safety - Generic type-safe JSON parsing support
  • 🔄 Auto Retry - Built-in retry mechanism and error handling
  • Concurrency Safe - Thread-safe implementation

Quick Start

Installation
go get github.com/ComingCL/go-rest
Basic HTTP Requests
package main

import (
    "context"
    "fmt"
    "net/url"
    "time"
    
    "github.com/ComingCL/go-rest"
)

func main() {
    // Create client
    serverURL, _ := url.Parse("https://api.example.com")
    client := rest.NewRESTClient(serverURL, "v1", rest.ClientContentConfig{
        ContentType: "application/json",
    }, nil)

    // Chain method calls to send request
    var result map[string]interface{}
    err := client.Verb("GET").
        Prefix("users").
        Param("page", "1").
        SetHeader("Authorization", "Bearer token").
        Timeout(30*time.Second).
        Do(context.TODO()).
        Into(&result)
    
    if err != nil {
        fmt.Printf("Request failed: %v\n", err)
        return
    }
    
    fmt.Printf("Result: %+v\n", result)
}
Server-Sent Events (SSE)
Method 1: Using Event Handler
package main

import (
    "context"
    "fmt"
    "net/url"
    "time"
    
    "github.com/ComingCL/go-rest"
)

// Implement event handler
type MyEventHandler struct{}

func (h *MyEventHandler) OnEvent(event *rest.Event) {
    fmt.Printf("Received event: %s\n", event.GetDataAsString())
    
    // Type-safe JSON parsing
    if data, err := rest.ParseTo[map[string]interface{}](event); err == nil {
        fmt.Printf("Parsed data: %+v\n", *data)
    }
}

func (h *MyEventHandler) OnError(err error) {
    fmt.Printf("SSE error: %v\n", err)
}

func (h *MyEventHandler) OnConnect() {
    fmt.Println("SSE connection established")
}

func (h *MyEventHandler) OnDisconnect() {
    fmt.Println("SSE connection closed")
}

func main() {
    serverURL, _ := url.Parse("https://api.example.com")
    client := rest.NewRESTClient(serverURL, "v1", rest.ClientContentConfig{
        ContentType: "application/json",
    }, nil)

    handler := &MyEventHandler{}

    // Send SSE request with body data support
    requestData := map[string]interface{}{
        "user_id": "user123",
        "filters": map[string]string{"category": "news"},
    }

    ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
    defer cancel()

    watcher, err := client.SSE().
        Prefix("stream").
        Body(requestData).                   // Send request body data
        WithEventHandler(handler).           // Set event handler
        WithRetryInterval(3*time.Second).    // Set retry interval
        MaxRetries(5).                       // Set max retries
        WithBufferSize(100).                 // Set buffer size
        SSEWatch(ctx)                        // Start listening

    if err != nil {
        fmt.Printf("Failed to start SSE: %v\n", err)
        return
    }
    defer watcher.Stop()

    // Listen for events and errors
    for {
        select {
        case event := <-watcher.Events():
            if event != nil {
                fmt.Printf("Channel event: %s\n", event.GetDataAsString())
            }
        case err := <-watcher.Errors():
            if err != nil {
                fmt.Printf("Channel error: %v\n", err)
            }
        case <-ctx.Done():
            fmt.Println("Context timeout")
            return
        }
    }
}
Method 2: Using Stream Reading
func streamExample() {
    serverURL, _ := url.Parse("https://api.example.com")
    client := rest.NewRESTClient(serverURL, "v1", rest.ClientContentConfig{
        ContentType: "application/json",
    }, nil)

    requestData := map[string]interface{}{
        "query": "realtime data",
    }

    ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
    defer cancel()

    // Create SSE stream
    streamResult, err := client.SSE().
        Prefix("stream").
        Body(requestData).        // Send request body data
        Stream(ctx)

    if err != nil {
        fmt.Printf("Failed to create stream: %v\n", err)
        return
    }
    defer streamResult.Close()

    // Manually read events
    for {
        event, err := streamResult.ReadEvent()
        if err != nil {
            if err.Error() == "EOF" {
                fmt.Println("Stream ended")
                break
            }
            fmt.Printf("Read error: %v\n", err)
            break
        }

        if event != nil {
            fmt.Printf("Event: %s\n", event.GetDataAsString())
            
            // Type-safe parsing
            if data, err := rest.ParseTo[map[string]interface{}](event); err == nil {
                fmt.Printf("Data: %+v\n", *data)
            }
        }
    }
}

API Reference

RESTClient
// Create client
client := rest.NewRESTClient(baseURL, apiPath, contentConfig, httpClient)

// HTTP methods
client.Verb("GET|POST|PUT|DELETE")

// Path building
client.Prefix("api", "v1")        // Add path prefix
client.Suffix("users", "123")     // Add path suffix

// Parameter setting
client.Param("key", "value")      // Query parameters
client.SetHeader("key", "value")  // Request headers
client.Body(data)                 // Request body
client.Timeout(duration)          // Timeout setting
client.MaxRetries(count)          // Retry count
SSE Configuration
// SSE specific configuration
client.SSE().
    WithLastEventID("event-id").      // Set last event ID
    WithRetryInterval(duration).      // Retry interval
    WithEventHandler(handler).        // Event handler
    WithBufferSize(size).            // Buffer size
    SSEWatch(ctx)                    // Start listening
Event Handling
// Basic parsing
err := event.ParseEventData(&target)

// Type-safe generic parsing
data, err := rest.ParseTo[MyStruct](event)
panicData := rest.MustParseTo[MyStruct](event)

// Data access
rawBytes := event.GetRawData()
dataString := event.GetDataAsString()

// Event information
eventType := event.GetEventType()
eventID := event.GetEventID()
timestamp := event.GetTimestamp()

Design Features

Method Chaining

All APIs support method chaining for fluent programming experience:

result := client.Verb("POST").
    Prefix("api", "v1").
    Suffix("users").
    SetHeader("Content-Type", "application/json").
    Body(userData).
    Timeout(30*time.Second).
    MaxRetries(3).
    Do(ctx)
High-Performance SSE
  • Zero-Copy Design: Based on K8s RawExtension pattern, directly uses raw bytes
  • Type Safety: Generic compile-time type checking support
  • Memory Optimization: Avoids unnecessary string conversions and memory allocations
Error Handling

Built-in comprehensive error handling and retry mechanism:

client.MaxRetries(5).                    // Maximum retry count
    WithRetryInterval(2*time.Second).    // Retry interval
    Timeout(30*time.Second)              // Request timeout

Performance

The SSE implementation features significant performance improvements:

  • Raw Byte Storage: Direct byte array processing without string conversion overhead
  • Generic Type Safety: Compile-time type checking with zero runtime cost
  • Efficient Parsing: Direct JSON unmarshaling from raw bytes
  • Memory Efficient: Minimal memory allocations and garbage collection pressure

License

MIT License

Contributing

Issues and Pull Requests are welcome!


Author: ComingCL

Documentation

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func ParseTo

func ParseTo[T any](e *Event) (*T, error)

ParseTo parses event data directly into the provided type (generic method)

Types

type BackoffManager

type BackoffManager interface {
	UpdateBackoff(actualUrl *url.URL, err error, responseCode int)
	CalculateBackoff(actualUrl *url.URL) time.Duration
	Sleep(d time.Duration)
}

type ClientContentConfig

type ClientContentConfig struct {
	// AcceptContentTypes specifies the types the client will accept and is optional.
	// If not set, ContentType will be used to define the Accept header
	AcceptContentTypes string
	// ContentType specifies the wire format used to communicate with the server.
	// This value will be set as the Accept header on requests made to the server if
	// AcceptContentTypes is not set, and as the default content type on any object
	// sent to the server. If not set, "application/json" is used.
	ContentType string
	// Negotiator is used for obtaining encoders and decoders for multiple
	// supported media types.
	Negotiator runtime.ClientNegotiator
}

type Event

type Event struct {
	// Type is the event type (optional, defaults to "message")
	Type EventType `json:"type,omitempty"`
	// ID is the event ID (optional)
	ID string `json:"id,omitempty"`
	// Raw contains the original bytes for efficient parsing (K8s RawExtension pattern)
	Raw []byte `json:"raw"`
	// Retry is the retry time in milliseconds (optional)
	Retry int `json:"retry,omitempty"`
	// Timestamp is when the event was received
	Timestamp time.Time `json:"timestamp"`
}

Event represents a Server-Sent Event with K8s-inspired design patterns

func (*Event) GetDataAsString

func (e *Event) GetDataAsString() string

GetDataAsString returns the raw data as string for display purposes

func (*Event) GetEventID

func (e *Event) GetEventID() string

GetEventID implements EventObject interface

func (*Event) GetEventType

func (e *Event) GetEventType() EventType

GetEventType implements EventObject interface

func (*Event) GetRawData

func (e *Event) GetRawData() []byte

GetRawData returns the raw bytes for direct processing

func (*Event) GetTimestamp

func (e *Event) GetTimestamp() time.Time

GetTimestamp implements EventObject interface

func (*Event) ParseEventData

func (e *Event) ParseEventData(target interface{}) error

ParseEventData parses event data as JSON into the target object

func (*Event) String

func (e *Event) String() string

String returns a string representation of the event

type EventHandler

type EventHandler interface {
	// OnEvent is called when an event is received
	OnEvent(event *Event)
	// OnError is called when an error occurs
	OnError(err error)
	// OnConnect is called when the connection is established
	OnConnect()
	// OnDisconnect is called when the connection is closed
	OnDisconnect()
}

EventHandler defines the interface for handling SSE events

type EventObject

type EventObject interface {
	// GetEventType returns the event type
	GetEventType() EventType
	// GetEventID returns the event ID
	GetEventID() string
	// GetTimestamp returns the event timestamp
	GetTimestamp() time.Time
}

EventObject represents a parsed event with typed data (K8s Object pattern)

type EventType

type EventType string

EventType represents the type of SSE event

const (
	// EventTypeData represents a data event
	EventTypeData EventType = "data"
	// EventTypeError represents an error event
	EventTypeError EventType = "error"
	// EventTypeClose represents a connection close event
	EventTypeClose EventType = "close"
	// EventTypeOpen represents a connection open event
	EventTypeOpen EventType = "open"
	// EventTypeRetry represents a retry event
	EventTypeRetry EventType = "retry"
)

type Interface

type Interface interface {
	Verb(verb string) *Request
	Get() *Request
	Post() *Request
	Put() *Request
	Delete() *Request
	// SSE creates a Server-Sent Events watcher
	SSE() *Request
}

Interface captures the set of operations for generically interacting with other services

type IsRetryableErrorFunc

type IsRetryableErrorFunc func(request *http.Request, err error) bool

IsRetryableErrorFunc allows the client to provide its own function that determines whether the specified err from the server is retryable.

request: the original request sent to the server err: the server sent this error to us

The function returns true if the error is retryable and the request can be retried, otherwise it returns false. We have four mode of communications - 'Stream', 'Watch', 'Do' and 'DoRaw', this function allows us to customize the retryability aspect of each.

func (IsRetryableErrorFunc) IsErrorRetryable

func (r IsRetryableErrorFunc) IsErrorRetryable(request *http.Request, err error) bool

type NoBackoff

type NoBackoff struct {
}

NoBackoff is a stub implementation, can be used for mocking or else as a default.

func (*NoBackoff) CalculateBackoff

func (n *NoBackoff) CalculateBackoff(actualUrl *url.URL) time.Duration

func (*NoBackoff) Sleep

func (n *NoBackoff) Sleep(d time.Duration)

func (*NoBackoff) UpdateBackoff

func (n *NoBackoff) UpdateBackoff(actualUrl *url.URL, err error, responseCode int)

type RESTClient

type RESTClient struct {
	// contains filtered or unexported fields
}

func NewRESTClient

func NewRESTClient(baseURL *url.URL, versionedAPIPath string, config ClientContentConfig, client *http.Client) *RESTClient

func (*RESTClient) Delete

func (c *RESTClient) Delete() *Request

Delete begins a DELETE request. Short for c.Verb("DELETE").

func (*RESTClient) Get

func (c *RESTClient) Get() *Request

Get begins a GET request. Short for c.Verb("GET").

func (*RESTClient) Post

func (c *RESTClient) Post() *Request

Post begins a POST request. Short for c.Verb("POST").

func (*RESTClient) Put

func (c *RESTClient) Put() *Request

Put begins a PUT request. Short for c.Verb("PUT").

func (*RESTClient) SSE

func (c *RESTClient) SSE() *Request

SSE begins a Server-Sent Events request. Short for c.Verb("GET") with SSE headers.

func (*RESTClient) Verb

func (c *RESTClient) Verb(verb string) *Request

type Request

type Request struct {
	// contains filtered or unexported fields
}

func NewRequest

func NewRequest(c *RESTClient) *Request

func (*Request) Body

func (r *Request) Body(obj interface{}) *Request

Body makes the request use obj as the body. Optional. If obj is a string, try to read a file of that name. If obj is a []byte, send it directly. If obj is an io.Reader, use it directly. Otherwise, set an error.

func (*Request) Do

func (r *Request) Do(ctx context.Context) Result

Do formats and executes the request. Returns a Result object for easy response processing.

Error type:

  • If the server responds with a status: *errors.StatusError or *errors.UnexpectedObjectError
  • http.Client.Do errors are returned directly.

func (*Request) MaxRetries

func (r *Request) MaxRetries(maxRetries int) *Request

func (*Request) Param

func (r *Request) Param(paramName, s string) *Request

Param creates a query parameter with the given string value.

func (*Request) Prefix

func (r *Request) Prefix(segments ...string) *Request

func (*Request) RequestURI

func (r *Request) RequestURI(uri string) *Request

func (*Request) SSEWatch

func (r *Request) SSEWatch(ctx context.Context) (SSEWatcher, error)

SSEWatch creates an SSE watcher for streaming events

func (*Request) SetHeader

func (r *Request) SetHeader(key string, values ...string) *Request

func (*Request) SetHost

func (r *Request) SetHost(host string) *Request

func (*Request) Stream

func (r *Request) Stream(ctx context.Context) (*StreamResult, error)

Stream executes the request and returns a streaming result for SSE

func (*Request) Suffix

func (r *Request) Suffix(segments ...string) *Request

func (*Request) Timeout

func (r *Request) Timeout(d time.Duration) *Request

Timeout makes the request use the given duration as an overall timeout for the request. Additionally, if set passes the value as "timeout" parameter in URL.

func (*Request) URL

func (r *Request) URL() *url.URL

URL returns the current working URL. Check the result of Error() to ensure that the returned URL is valid.

func (*Request) Verb

func (r *Request) Verb(verb string) *Request

Verb sets the verb this request will use.

func (*Request) WithBufferSize

func (r *Request) WithBufferSize(bufferSize int) *Request

WithBufferSize sets the buffer size for SSE event channels

func (*Request) WithEventHandler

func (r *Request) WithEventHandler(handler EventHandler) *Request

WithEventHandler sets the event handler for SSE connections

func (*Request) WithLastEventID

func (r *Request) WithLastEventID(lastEventID string) *Request

WithLastEventID sets the last event ID for SSE reconnection

func (*Request) WithRetryInterval

func (r *Request) WithRetryInterval(interval time.Duration) *Request

WithRetryInterval sets the retry interval for SSE connections

type Result

type Result struct {
	// contains filtered or unexported fields
}

Result contains the result of calling Request.Do().

func (Result) Error

func (r Result) Error() error

Error return remote server errors

func (Result) Into

func (r Result) Into(target any) error

Into parse result body to target, WARNING: target should be a pointer

func (Result) Raw

func (r Result) Raw() ([]byte, error)

Raw returns the raw result.

type RetryAfter

type RetryAfter struct {
	// Wait is the duration the server has asked us to wait before
	// the next retry is initiated.
	// This is the value of the 'Retry-After' response header in seconds.
	Wait time.Duration

	// Attempt is the Nth attempt after which we have received a retryable
	// error or a 'Retry-After' response header from the server.
	Attempt int

	// Reason describes why we are retrying the request
	Reason string
}

RetryAfter holds information associated with the next retry.

type SSEWatcher

type SSEWatcher interface {
	// Start begins watching for SSE events
	Start(ctx context.Context) error
	// Stop stops watching for SSE events
	Stop()
	// Events returns a channel to receive events
	Events() <-chan *Event
	// Errors returns a channel to receive errors
	Errors() <-chan error
	// IsStopped returns true if the watcher is stopped
	IsStopped() bool
}

SSEWatcher defines the interface for SSE watching functionality

type StreamResult

type StreamResult struct {
	// contains filtered or unexported fields
}

StreamResult represents a streaming HTTP response

func (*StreamResult) Close

func (sr *StreamResult) Close() error

Close closes the streaming response

func (*StreamResult) ReadEvent

func (sr *StreamResult) ReadEvent() (*Event, error)

ReadEvent reads the next SSE event from the stream

func (*StreamResult) Response

func (sr *StreamResult) Response() *http.Response

Response returns the underlying HTTP response

type URLBackoff

type URLBackoff struct {
	// Uses backoff as underlying implementation.
	Backoff *flowcontrol.Backoff
}

URLBackoff struct implements the semantics on top of Backoff which we need for URL specific exponential backoff.

func (*URLBackoff) CalculateBackoff

func (b *URLBackoff) CalculateBackoff(actualUrl *url.URL) time.Duration

CalculateBackoff takes a url and back's off exponentially, based on its knowledge of existing failures.

func (*URLBackoff) Disable

func (b *URLBackoff) Disable()

Disable makes the backoff trivial, i.e., sets it to zero. This might be used by tests which want to run 1000s of mock requests without slowing down.

func (*URLBackoff) Sleep

func (b *URLBackoff) Sleep(d time.Duration)

func (*URLBackoff) UpdateBackoff

func (b *URLBackoff) UpdateBackoff(actualUrl *url.URL, err error, responseCode int)

UpdateBackoff updates backoff metadata

type WarningHandler

type WarningHandler interface {
	// HandleWarningHeader is called with the warn code, agent, and text when a warning header is countered.
	HandleWarningHeader(code int, agent string, text string)
}

WarningHandler is an interface for handling warning headers

type WarningLogger

type WarningLogger struct{}

WarningLogger is an implementation of WarningHandler that logs code 299 warnings

func (WarningLogger) HandleWarningHeader

func (WarningLogger) HandleWarningHeader(code int, agent string, message string)

type WithRetry

type WithRetry interface {
	// IsNextRetry advances the retry counter appropriately
	// and returns true if the request should be retried,
	// otherwise it returns false, if:
	//  - we have already reached the maximum retry threshold.
	//  - the error does not fall into the retryable category.
	//  - the server has not sent us a 429, or 5xx status code and the
	//    'Retry-After' response header is not set with a value.
	//  - we need to seek to the beginning of the request body before we
	//    initiate the next retry, the function should log an error and
	//    return false if it fails to do so.
	//
	// restReq: the associated rest.Request
	// httpReq: the HTTP Request sent to the server
	// resp: the response sent from the server, it is set if err is nil
	// err: the server sent this error to us, if err is set then resp is nil.
	// f: a IsRetryableErrorFunc function provided by the client that determines
	//    if the err sent by the server is retryable.
	IsNextRetry(ctx context.Context, restReq *Request, httpReq *http.Request, resp *http.Response, err error, f IsRetryableErrorFunc) bool

	// Before should be invoked prior to each attempt, including
	// the first one. If an error is returned, the request should
	// be aborted immediately.
	//
	// Before may also be additionally responsible for preparing
	// the request for the next retry, namely in terms of resetting
	// the request body in case it has been read.
	Before(ctx context.Context, r *Request) error

	// After should be invoked immediately after an attempt is made.
	After(ctx context.Context, r *Request, resp *http.Response, err error)

	// WrapPreviousError wraps the error from any previous attempt into
	// the final error specified in 'finalErr', so the user has more
	// context why the request failed.
	// For example, if a request times out after multiple retries then
	// we see a generic context.Canceled or context.DeadlineExceeded
	// error which is not very useful in debugging. This function can
	// wrap any error from previous attempt(s) to provide more context to
	// the user. The error returned in 'err' must satisfy the
	// following conditions:
	//  a: errors.Unwrap(err) = errors.Unwrap(finalErr) if finalErr
	//     implements Unwrap
	//  b: errors.Unwrap(err) = finalErr if finalErr does not
	//     implements Unwrap
	//  c: errors.Is(err, otherErr) = errors.Is(finalErr, otherErr)
	WrapPreviousError(finalErr error) (err error)
}

WithRetry allows the client to retry a request up to a certain number of times Note that WithRetry is not safe for concurrent use by multiple goroutines without additional locking or coordination.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL