mirror of
https://github.com/j3ssie/osmedeus.git
synced 2026-09-01 12:48:49 +02:00
207 lines
5.7 KiB
Go
207 lines
5.7 KiB
Go
// Package client provides HTTP client utilities for interacting with a remote osmedeus server.
|
|
package client
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"net/url"
|
|
"os"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
const (
|
|
// EnvRemoteURL is the environment variable for the remote server URL
|
|
EnvRemoteURL = "OSM_REMOTE_URL"
|
|
// EnvAuthKey is the environment variable for the API authentication key
|
|
EnvAuthKey = "OSM_REMOTE_AUTH_KEY"
|
|
// APIBasePath is the base path for API endpoints
|
|
APIBasePath = "/osm/api"
|
|
// DefaultTimeout is the default HTTP request timeout
|
|
DefaultTimeout = 30 * time.Second
|
|
)
|
|
|
|
// Client is an HTTP client for interacting with a remote osmedeus server
|
|
type Client struct {
|
|
baseURL string
|
|
authKey string
|
|
httpClient *http.Client
|
|
}
|
|
|
|
// NewClient creates a new client with the given base URL and auth key.
|
|
// If baseURL or authKey are empty, they will be read from environment variables.
|
|
func NewClient(baseURL, authKey string) (*Client, error) {
|
|
// Resolve base URL
|
|
if baseURL == "" {
|
|
baseURL = os.Getenv(EnvRemoteURL)
|
|
}
|
|
if baseURL == "" {
|
|
return nil, fmt.Errorf("remote URL is required (set %s or use --remote-url)", EnvRemoteURL)
|
|
}
|
|
|
|
// Validate URL format
|
|
parsedURL, err := url.Parse(baseURL)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("invalid remote URL: %w", err)
|
|
}
|
|
if parsedURL.Scheme != "http" && parsedURL.Scheme != "https" {
|
|
return nil, fmt.Errorf("invalid URL scheme: %s (must be http or https)", parsedURL.Scheme)
|
|
}
|
|
|
|
// Remove trailing slash from base URL
|
|
baseURL = strings.TrimRight(baseURL, "/")
|
|
|
|
// Resolve auth key (optional - server may allow unauthenticated access)
|
|
if authKey == "" {
|
|
authKey = os.Getenv(EnvAuthKey)
|
|
}
|
|
|
|
return &Client{
|
|
baseURL: baseURL,
|
|
authKey: authKey,
|
|
httpClient: &http.Client{
|
|
Timeout: DefaultTimeout,
|
|
},
|
|
}, nil
|
|
}
|
|
|
|
// buildURL constructs the full URL for an API endpoint
|
|
func (c *Client) buildURL(path string, query url.Values) string {
|
|
fullPath := c.baseURL + APIBasePath + path
|
|
if len(query) > 0 {
|
|
fullPath += "?" + query.Encode()
|
|
}
|
|
return fullPath
|
|
}
|
|
|
|
// doRequest performs an HTTP request with authentication
|
|
func (c *Client) doRequest(ctx context.Context, method, fullURL string, body io.Reader) (*http.Response, error) {
|
|
req, err := http.NewRequestWithContext(ctx, method, fullURL, body)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to create request: %w", err)
|
|
}
|
|
|
|
// Set headers
|
|
req.Header.Set("Content-Type", "application/json")
|
|
req.Header.Set("Accept", "application/json")
|
|
if c.authKey != "" {
|
|
req.Header.Set("x-osm-api-key", c.authKey)
|
|
}
|
|
|
|
resp, err := c.httpClient.Do(req)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("request failed: %w", err)
|
|
}
|
|
|
|
return resp, nil
|
|
}
|
|
|
|
// handleResponse reads and parses the response, returning an error for non-2xx status codes
|
|
func handleResponse(resp *http.Response, result interface{}) error {
|
|
defer func() { _ = resp.Body.Close() }()
|
|
|
|
bodyBytes, err := io.ReadAll(resp.Body)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to read response body: %w", err)
|
|
}
|
|
|
|
// Check for error status codes
|
|
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
|
var errResp ErrorResponse
|
|
if json.Unmarshal(bodyBytes, &errResp) == nil && errResp.Message != "" {
|
|
return &APIError{
|
|
StatusCode: resp.StatusCode,
|
|
Message: errResp.Message,
|
|
}
|
|
}
|
|
return &APIError{
|
|
StatusCode: resp.StatusCode,
|
|
Message: fmt.Sprintf("HTTP %d: %s", resp.StatusCode, string(bodyBytes)),
|
|
}
|
|
}
|
|
|
|
// Parse successful response
|
|
if result != nil && len(bodyBytes) > 0 {
|
|
if err := json.Unmarshal(bodyBytes, result); err != nil {
|
|
return fmt.Errorf("failed to parse response: %w", err)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// Get performs a GET request to the given path with optional query parameters
|
|
func (c *Client) Get(ctx context.Context, path string, query url.Values, result interface{}) error {
|
|
fullURL := c.buildURL(path, query)
|
|
resp, err := c.doRequest(ctx, http.MethodGet, fullURL, nil)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return handleResponse(resp, result)
|
|
}
|
|
|
|
// Post performs a POST request to the given path with a JSON body
|
|
func (c *Client) Post(ctx context.Context, path string, body interface{}, result interface{}) error {
|
|
var bodyReader io.Reader
|
|
if body != nil {
|
|
bodyBytes, err := json.Marshal(body)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to marshal request body: %w", err)
|
|
}
|
|
bodyReader = bytes.NewReader(bodyBytes)
|
|
}
|
|
|
|
fullURL := c.buildURL(path, nil)
|
|
resp, err := c.doRequest(ctx, http.MethodPost, fullURL, bodyReader)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return handleResponse(resp, result)
|
|
}
|
|
|
|
// Delete performs a DELETE request to the given path
|
|
func (c *Client) Delete(ctx context.Context, path string, result interface{}) error {
|
|
fullURL := c.buildURL(path, nil)
|
|
resp, err := c.doRequest(ctx, http.MethodDelete, fullURL, nil)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return handleResponse(resp, result)
|
|
}
|
|
|
|
// GetRaw performs a GET request and returns the raw response body
|
|
func (c *Client) GetRaw(ctx context.Context, path string, query url.Values) ([]byte, error) {
|
|
fullURL := c.buildURL(path, query)
|
|
resp, err := c.doRequest(ctx, http.MethodGet, fullURL, nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer func() { _ = resp.Body.Close() }()
|
|
|
|
bodyBytes, err := io.ReadAll(resp.Body)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to read response body: %w", err)
|
|
}
|
|
|
|
// Check for error status codes
|
|
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
|
var errResp ErrorResponse
|
|
if json.Unmarshal(bodyBytes, &errResp) == nil && errResp.Message != "" {
|
|
return nil, &APIError{
|
|
StatusCode: resp.StatusCode,
|
|
Message: errResp.Message,
|
|
}
|
|
}
|
|
return nil, &APIError{
|
|
StatusCode: resp.StatusCode,
|
|
Message: fmt.Sprintf("HTTP %d: %s", resp.StatusCode, string(bodyBytes)),
|
|
}
|
|
}
|
|
|
|
return bodyBytes, nil
|
|
}
|