remedy

package module
v1.1.0 Latest Latest
Warning

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

Go to latest
Published: Mar 24, 2026 License: MIT Imports: 15 Imported by: 0

README

go-remedy

A native Go client library for the BMC Remedy AR System REST API.

Go Reference Go Report Card CI Go Version

Features

  • JWT authentication with automatic token management
  • Entry CRUD operations (Create, Read, Update, Delete, Merge)
  • Attachment upload and download
  • Type-safe query builder for AR qualifications
  • Built-in request serialization (avoids BMC Error 9093)
  • Token bucket rate limiting
  • Context-aware with cancellation support
  • Zero external runtime dependencies (stdlib only)

Installation

go get github.com/tphakala/go-remedy

Requires Go 1.25 or later.

Quick Start

package main

import (
    "context"
    "log"
    "time"

    "github.com/tphakala/go-remedy"
)

func main() {
    // Create client with options
    client := remedy.New("https://remedy.example.com:8443",
        remedy.WithTimeout(30*time.Second),
        remedy.WithRateLimit(10), // 10 requests/second
    )
    defer client.Close()

    ctx := context.Background()

    // Authenticate
    if err := client.Login(ctx, "username", "password"); err != nil {
        log.Fatal(err)
    }
    defer client.Logout(ctx)

    // List entries with filtering
    entries, err := client.Entries().List(ctx, "HPD:Help Desk",
        remedy.WithQualification("'Status' = \"Open\""),
        remedy.WithFields("Request ID", "Summary", "Status"),
        remedy.WithLimit(100),
    )
    if err != nil {
        log.Fatal(err)
    }

    for _, entry := range entries.Entries {
        log.Printf("Request: %s - %s",
            entry.Values["Request ID"],
            entry.Values["Summary"])
    }
}

Usage

Client Configuration
// Basic client
client := remedy.New("https://remedy.example.com:8443")

// With options
client := remedy.New("https://remedy.example.com:8443",
    remedy.WithHTTPClient(customHTTPClient),    // Custom HTTP client
    remedy.WithTimeout(60*time.Second),         // Request timeout
    remedy.WithRateLimit(5),                    // 5 requests/second
    remedy.WithTokenLifetime(time.Hour),        // JWT token lifetime (default: 1h)
    remedy.WithRefreshThreshold(5*time.Minute), // Refresh before expiry (default: 5m)
    remedy.WithAutoRefresh(true),               // Enable auto-refresh (default: true)
)
Authentication
// Standard login
err := client.Login(ctx, "username", "password")

// Login with additional auth string (for servers requiring extra context)
err := client.LoginWithAuth(ctx, "username", "password", "authString")

// Check authentication status
if client.IsAuthenticated() {
    // ...
}

// Logout
err := client.Logout(ctx)
Automatic Token Refresh

BMC Remedy JWT tokens expire after 1 hour. The client automatically handles token refresh:

  • Credentials are stored after Login() for automatic re-authentication
  • Proactive refresh occurs when the token is within 5 minutes of expiry (configurable)
  • Concurrent safety ensures only one refresh occurs even under high load
// Tokens refresh automatically - no action needed for long-running applications
client := remedy.New("https://remedy.example.com:8443")
client.Login(ctx, "user", "pass")

// This works even hours later - token refreshes automatically
entries, _ := client.Entries().List(ctx, "HPD:Help Desk")

// For security-sensitive applications, clear stored credentials when done
client.ClearCredentials() // Disables auto-refresh, credentials removed from memory

// Disable auto-refresh entirely if preferred
client := remedy.New("https://remedy.example.com:8443",
    remedy.WithAutoRefresh(false),
)
Entry Operations
// Get single entry
entry, err := client.Entries().Get(ctx, "HPD:Help Desk", "REQ000001")

// List entries with options
entries, err := client.Entries().List(ctx, "HPD:Help Desk",
    remedy.WithQualification("'Status' = \"Open\""),
    remedy.WithFields("Request ID", "Summary", "Status"),
    remedy.WithSort("Create Date", remedy.SortDesc),
    remedy.WithLimit(50),
    remedy.WithOffset(100),
)

// Create entry
entry, err := client.Entries().Create(ctx, "HPD:Help Desk", map[string]any{
    "Summary":     "New ticket summary",
    "Description": "Detailed description",
    "Status":      "New",
})

// Update entry
err := client.Entries().Update(ctx, "HPD:Help Desk", "REQ000001", map[string]any{
    "Status": "In Progress",
})

// Delete entry
err := client.Entries().Delete(ctx, "HPD:Help Desk", "REQ000001")

// Delete with force option
err := client.Entries().Delete(ctx, "HPD:Help Desk", "REQ000001",
    remedy.DeleteOptionForce)

// Merge entry (create or update based on matching criteria)
entry, err := client.Entries().Merge(ctx, "HPD:Help Desk", map[string]any{
    "Summary": "Ticket summary",
    // Matching fields determine if create or update
})
Query Builder

Build type-safe AR qualification strings:

// Simple query
q := remedy.NewQuery().
    And("Status", "=", "Open").
    Build()
// Result: 'Status' = "Open"

// Complex query with multiple conditions
q := remedy.NewQuery().
    And("Status", "=", "Open").
    And("Priority", "<", 3).
    Or("Urgency", "=", "Critical").
    Build()
// Result: 'Status' = "Open" AND 'Priority' < 3 OR 'Urgency' = "Critical"

// Raw qualification for complex expressions
q := remedy.NewQuery().
    And("Status", "=", "Open").
    Raw("'Priority' < 3 OR 'Urgency' = \"High\"").
    Build()
// Result: 'Status' = "Open" AND ('Priority' < 3 OR 'Urgency' = "High")

// Use with List
entries, err := client.Entries().List(ctx, "Form",
    remedy.WithQualification(q),
)

Supported value types:

  • Strings: "value" -> "value"
  • Integers: 123 -> 123
  • Floats: 3.14 -> 3.14
  • Booleans: true -> 1, false -> 0
  • Nil: nil -> $NULL$
Attachments
// Download attachment
reader, err := client.Attachments().Get(ctx, "Form", "EntryID", "FieldName")
if err != nil {
    log.Fatal(err)
}
defer reader.Close()

data, err := io.ReadAll(reader)

// Upload attachment
file, err := os.Open("document.pdf")
if err != nil {
    log.Fatal(err)
}
defer file.Close()

if err := client.Attachments().Upload(ctx, "Form", "EntryID", "FieldName",
    "document.pdf", file); err != nil {
    log.Fatal(err)
}
Error Handling
entry, err := client.Entries().Get(ctx, "Form", "InvalidID")
if err != nil {
    // Check for specific error types
    if errors.Is(err, remedy.ErrNotFound) {
        log.Println("Entry not found")
    } else if errors.Is(err, remedy.ErrUnauthorized) {
        log.Println("Authentication required")
    } else if errors.Is(err, remedy.ErrForbidden) {
        log.Println("Permission denied")
    } else if errors.Is(err, remedy.ErrNoCredentials) {
        log.Println("Token expired and no credentials for refresh")
    }

    // Get detailed API error
    var apiErr *remedy.APIError
    if errors.As(err, &apiErr) {
        log.Printf("API Error %d: %s - %s",
            apiErr.MessageNumber,
            apiErr.MessageText,
            apiErr.MessageAppendedText)
    }
}

Request Serialization

BMC Remedy enforces per-user session limits. Concurrent requests with the same user account trigger Error 9093: "User is currently connected from another machine or incompatible session".

This library automatically serializes requests to prevent this error. All API calls pass through an internal queue ensuring only one request executes at a time per client.

Testing

The library provides interfaces for all services, making it easy to mock in tests:

// Your code uses interfaces
type EntryServicer interface {
    Get(ctx context.Context, form, entryID string, opts ...QueryOption) (*Entry, error)
    List(ctx context.Context, form string, opts ...QueryOption) (*EntryList, error)
    Create(ctx context.Context, form string, values map[string]any) (*Entry, error)
    Update(ctx context.Context, form, entryID string, values map[string]any) error
    Delete(ctx context.Context, form, entryID string, opts ...DeleteOption) error
    Merge(ctx context.Context, form string, values map[string]any) (*Entry, error)
}

Use mockery to generate mocks:

mockery --all --dir=. --output=mocks --outpkg=mocks

License

MIT License - see LICENSE for details.

Documentation

Overview

Package remedy provides a Go client for the BMC Remedy AR System REST API.

The client handles authentication, request serialization (to avoid session conflicts), rate limiting, and provides a type-safe interface for interacting with Remedy forms and entries.

Basic usage:

client := remedy.New("https://remedy.example.com:8443",
    remedy.WithTimeout(30*time.Second),
    remedy.WithRateLimit(10), // 10 requests/second
)

err := client.Login(ctx, "username", "password")
if err != nil {
    log.Fatal(err)
}
defer client.Logout(ctx)

entries, err := client.Entries().List(ctx, "HPD:Help Desk",
    remedy.WithQualification("'Status' = \"Open\""),
    remedy.WithLimit(100),
)

Index

Constants

View Source
const (
	OpEqual        = "="
	OpNotEqual     = "!="
	OpLessThan     = "<"
	OpLessEqual    = "<="
	OpGreaterThan  = ">"
	OpGreaterEqual = ">="
	OpLike         = "LIKE"
)

Common operators for convenience.

Variables

View Source
var (
	// ErrUnauthorized indicates authentication failure (HTTP 401).
	ErrUnauthorized = errors.New("remedy: unauthorized")

	// ErrForbidden indicates the user lacks permission (HTTP 403).
	ErrForbidden = errors.New("remedy: forbidden")

	// ErrNotFound indicates the requested resource does not exist (HTTP 404).
	ErrNotFound = errors.New("remedy: not found")

	// ErrNotAuthenticated indicates no valid token is available.
	ErrNotAuthenticated = errors.New("remedy: not authenticated")

	// ErrNoCredentials indicates credentials are not stored for automatic token refresh.
	ErrNoCredentials = errors.New("remedy: no credentials stored for token refresh")

	// ErrEmptyFormName indicates a form name parameter was empty.
	ErrEmptyFormName = errors.New("remedy: form name cannot be empty")

	// ErrEmptyEntryID indicates an entry ID parameter was empty.
	ErrEmptyEntryID = errors.New("remedy: entry ID cannot be empty")
)

Sentinel errors for common API error conditions.

View Source
var ErrTokenTooLarge = errors.New("remedy: token too large")

ErrTokenTooLarge is returned when a token response exceeds maxTokenSize.

Functions

This section is empty.

Types

type APIError

type APIError struct {
	// StatusCode is the HTTP status code of the response.
	StatusCode int

	// MessageType indicates the severity: ERROR, WARNING, FATAL, BAD STATUS.
	MessageType string

	// MessageText is the primary error description.
	MessageText string

	// MessageAppendedText provides additional context for the error.
	MessageAppendedText string

	// MessageNumber is the numeric error identifier.
	MessageNumber int
}

APIError represents an error returned by the BMC Remedy REST API. It contains the structured error information from the API response.

func (*APIError) Error

func (e *APIError) Error() string

Error implements the error interface.

func (*APIError) Is

func (e *APIError) Is(target error) bool

Is implements errors.Is support for APIError. It allows checking against sentinel errors based on status code.

type AttachmentServicer

type AttachmentServicer interface {
	// Get retrieves an attachment from an entry.
	Get(ctx context.Context, form, entryID, fieldName string) (io.ReadCloser, error)

	// Upload uploads an attachment to an entry.
	Upload(ctx context.Context, form, entryID, fieldName, filename string, data io.Reader) error
}

AttachmentServicer defines attachment operations for the Remedy API. This interface enables mocking the attachment service in tests.

type Client

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

Client is a BMC Remedy REST API client. It handles authentication, request serialization, and rate limiting.

TODO(go1.26): Consider using runtime/secret package for token storage when Go 1.26 is available. See: https://go.dev/doc/go1.26#new-experimental-runtimesecret-package

func New

func New(baseURL string, opts ...Option) *Client

New creates a new Remedy client with the specified base URL and options.

func (*Client) Attachments

func (c *Client) Attachments() AttachmentServicer

Attachments returns the attachment service for file operations.

func (*Client) ClearCredentials

func (c *Client) ClearCredentials()

ClearCredentials removes stored credentials from memory. After calling this, automatic token refresh will be disabled.

func (*Client) Close

func (c *Client) Close()

Close releases resources associated with the client.

func (*Client) Entries

func (c *Client) Entries() EntryServicer

Entries returns the entry service for CRUD operations on form entries.

func (*Client) IsAuthenticated

func (c *Client) IsAuthenticated() bool

IsAuthenticated returns true if the client has a valid token. Note: This only checks if a token exists, not if it's still valid.

func (*Client) Login

func (c *Client) Login(ctx context.Context, username, password string) error

Login authenticates with the Remedy server using username and password. The JWT token is stored internally and used for subsequent requests. Credentials are stored for automatic token refresh.

func (*Client) LoginWithAuth

func (c *Client) LoginWithAuth(ctx context.Context, username, password, authString string) error

LoginWithAuth authenticates with an additional authentication string. This is used for servers that require additional authentication context. Credentials are stored for automatic token refresh.

func (*Client) Logout

func (c *Client) Logout(ctx context.Context) error

Logout terminates the current session and clears the stored token.

type DeleteOption

type DeleteOption string

DeleteOption defines options for delete operations.

const (
	// DeleteOptionNone performs a standard delete.
	DeleteOptionNone DeleteOption = "NONE"
	// DeleteOptionForce forces deletion even if entry is locked.
	DeleteOptionForce DeleteOption = "FORCE"
	// DeleteOptionNoCascade prevents cascade delete of related entries.
	DeleteOptionNoCascade DeleteOption = "NOCASCADE"
)

type Entry

type Entry struct {
	Values map[string]any `json:"values"`
	Links  []Link         `json:"_links,omitzero"`
}

Entry represents a single entry (record) from a BMC Remedy form.

type EntryList

type EntryList struct {
	Entries []Entry `json:"entries"`
	Links   []Link  `json:"_links,omitzero"`
}

EntryList represents a collection of entries returned from a list query.

type EntryServicer

type EntryServicer interface {
	// Get retrieves a single entry by ID.
	Get(ctx context.Context, form, entryID string, opts ...QueryOption) (*Entry, error)

	// List retrieves multiple entries with optional filtering and pagination.
	List(ctx context.Context, form string, opts ...QueryOption) (*EntryList, error)

	// Create creates a new entry in the specified form.
	Create(ctx context.Context, form string, values map[string]any) (*Entry, error)

	// Update updates an existing entry.
	Update(ctx context.Context, form, entryID string, values map[string]any) error

	// Delete removes an entry.
	Delete(ctx context.Context, form, entryID string, opts ...DeleteOption) error

	// Merge creates or updates an entry based on matching criteria.
	Merge(ctx context.Context, form string, values map[string]any) (*Entry, error)
}

EntryServicer defines entry operations for the Remedy API. This interface enables mocking the entry service in tests.

type Field

type Field struct {
	ID       int    `json:"fieldId"`
	Name     string `json:"fieldName"`
	DataType string `json:"dataType"`
}

Field represents metadata about a form field.

type HTTPDoer

type HTTPDoer interface {
	Do(req *http.Request) (*http.Response, error)
}

HTTPDoer abstracts the HTTP client for testing. *http.Client implements this interface.

type Link struct {
	Rel  string `json:"rel"`
	Href string `json:"href"`
}

Link represents a HATEOAS link in API responses.

type Option

type Option func(*Client)

Option configures a Client.

func WithAutoRefresh

func WithAutoRefresh(enabled bool) Option

WithAutoRefresh enables or disables automatic token refresh. When enabled (default), the client will automatically re-authenticate using stored credentials when the token is near expiry.

func WithHTTPClient

func WithHTTPClient(httpClient HTTPDoer) Option

WithHTTPClient sets a custom HTTP client for the Remedy client. This allows customizing transport, timeouts, and other HTTP settings. The client must implement the HTTPDoer interface (e.g., *http.Client).

func WithRateLimit

func WithRateLimit(requestsPerSecond float64) Option

WithRateLimit enables rate limiting with the specified requests per second. This helps prevent overwhelming the Remedy server.

func WithRefreshThreshold

func WithRefreshThreshold(d time.Duration) Option

WithRefreshThreshold sets how long before expiry to refresh the token. The default is 5 minutes before expiry.

func WithTimeout

func WithTimeout(d time.Duration) Option

WithTimeout sets the timeout for individual API requests. The default is 30 seconds.

func WithTokenLifetime

func WithTokenLifetime(d time.Duration) Option

WithTokenLifetime sets how long tokens are considered valid. The default is 1 hour, matching BMC Remedy's standard token lifetime.

type Query

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

Query builds AR System qualification strings in a type-safe manner.

Example usage:

q := remedy.NewQuery().
    And("Status", "=", "Open").
    And("Priority", "<", 3).
    Build()
// Result: 'Status' = "Open" AND 'Priority' < 3

func NewQuery

func NewQuery() *Query

NewQuery creates a new empty query builder.

func (*Query) And

func (q *Query) And(field, op string, value any) *Query

And adds a condition with AND conjunction.

func (*Query) AndSafe

func (q *Query) AndSafe(field, op string, value any) *Query

AndSafe adds a condition with AND conjunction, validating the operator. If the operator is invalid, the error is stored and returned by BuildSafe.

func (*Query) Build

func (q *Query) Build() string

Build returns the complete qualification string.

func (*Query) BuildSafe

func (q *Query) BuildSafe() (string, error)

BuildSafe returns the qualification string and any validation errors. Use this with AndSafe/OrSafe for validated query building.

func (*Query) Or

func (q *Query) Or(field, op string, value any) *Query

Or adds a condition with OR conjunction.

func (*Query) OrSafe

func (q *Query) OrSafe(field, op string, value any) *Query

OrSafe adds a condition with OR conjunction, validating the operator. If the operator is invalid, the error is stored and returned by BuildSafe.

func (*Query) Raw

func (q *Query) Raw(qualification string) *Query

Raw adds a raw qualification string with AND conjunction. Use this for complex expressions that can't be built with And/Or.

type QueryOption

type QueryOption func(*queryOptions)

QueryOption configures entry query operations.

func WithExpand

func WithExpand(associations ...string) QueryOption

WithExpand specifies associations to expand in the response.

func WithFields

func WithFields(fields ...string) QueryOption

WithFields specifies which fields to return in the response.

func WithLimit

func WithLimit(limit int) QueryOption

WithLimit sets the maximum number of entries to return.

func WithOffset

func WithOffset(offset int) QueryOption

WithOffset sets the starting offset for pagination.

func WithQualification

func WithQualification(q string) QueryOption

WithQualification sets the AR qualification string for filtering entries.

func WithSort

func WithSort(field string, order SortOrder) QueryOption

WithSort sets the field and order for sorting results.

type RemedyClient

type RemedyClient interface {
	// Login authenticates with the Remedy server.
	Login(ctx context.Context, username, password string) error

	// LoginWithAuth authenticates with additional auth string.
	LoginWithAuth(ctx context.Context, username, password, authString string) error

	// Logout terminates the current session.
	Logout(ctx context.Context) error

	// Entries returns the entry service.
	Entries() EntryServicer

	// Attachments returns the attachment service.
	Attachments() AttachmentServicer
}

RemedyClient defines the full client interface for the Remedy API. This interface enables mocking the entire client in consumer tests.

type SortOrder

type SortOrder string

SortOrder defines the sort direction for queries.

const (
	// SortAsc sorts in ascending order.
	SortAsc SortOrder = "ASC"
	// SortDesc sorts in descending order.
	SortDesc SortOrder = "DESC"
)

Directories

Path Synopsis
internal
queue
Package queue provides request serialization for BMC Remedy API clients.
Package queue provides request serialization for BMC Remedy API clients.
ratelimit
Package ratelimit provides a token bucket rate limiter for API requests.
Package ratelimit provides a token bucket rate limiter for API requests.

Jump to

Keyboard shortcuts

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