740 lines
21 KiB
Go
740 lines
21 KiB
Go
// Package hcfs provides a Go SDK for the Context-Aware Hierarchical Context File System
|
|
package hcfs
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"net/url"
|
|
"strconv"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"golang.org/x/time/rate"
|
|
)
|
|
|
|
// Version of the HCFS Go SDK
|
|
const Version = "2.0.0"
|
|
|
|
// Default configuration values
|
|
const (
|
|
DefaultTimeout = 30 * time.Second
|
|
DefaultRetries = 3
|
|
DefaultRateLimit = 100 // requests per second
|
|
DefaultCacheSize = 1000
|
|
DefaultCacheTTL = time.Hour
|
|
)
|
|
|
|
// ContextStatus represents the status of a context
|
|
type ContextStatus string
|
|
|
|
const (
|
|
ContextStatusActive ContextStatus = "active"
|
|
ContextStatusArchived ContextStatus = "archived"
|
|
ContextStatusDeleted ContextStatus = "deleted"
|
|
ContextStatusDraft ContextStatus = "draft"
|
|
)
|
|
|
|
// SearchType represents the type of search to perform
|
|
type SearchType string
|
|
|
|
const (
|
|
SearchTypeSemantic SearchType = "semantic"
|
|
SearchTypeKeyword SearchType = "keyword"
|
|
SearchTypeHybrid SearchType = "hybrid"
|
|
SearchTypeFuzzy SearchType = "fuzzy"
|
|
)
|
|
|
|
// Context represents a context in the HCFS system
|
|
type Context struct {
|
|
ID *int `json:"id,omitempty"`
|
|
Path string `json:"path"`
|
|
Content string `json:"content"`
|
|
Summary *string `json:"summary,omitempty"`
|
|
Author *string `json:"author,omitempty"`
|
|
Tags []string `json:"tags,omitempty"`
|
|
Metadata map[string]interface{} `json:"metadata,omitempty"`
|
|
Status *ContextStatus `json:"status,omitempty"`
|
|
CreatedAt *time.Time `json:"created_at,omitempty"`
|
|
UpdatedAt *time.Time `json:"updated_at,omitempty"`
|
|
Version *int `json:"version,omitempty"`
|
|
SimilarityScore *float64 `json:"similarity_score,omitempty"`
|
|
}
|
|
|
|
// ContextCreate represents data for creating a new context
|
|
type ContextCreate struct {
|
|
Path string `json:"path"`
|
|
Content string `json:"content"`
|
|
Summary *string `json:"summary,omitempty"`
|
|
Author *string `json:"author,omitempty"`
|
|
Tags []string `json:"tags,omitempty"`
|
|
Metadata map[string]interface{} `json:"metadata,omitempty"`
|
|
}
|
|
|
|
// ContextUpdate represents data for updating a context
|
|
type ContextUpdate struct {
|
|
Content *string `json:"content,omitempty"`
|
|
Summary *string `json:"summary,omitempty"`
|
|
Tags []string `json:"tags,omitempty"`
|
|
Metadata map[string]interface{} `json:"metadata,omitempty"`
|
|
Status *ContextStatus `json:"status,omitempty"`
|
|
}
|
|
|
|
// SearchResult represents a search result
|
|
type SearchResult struct {
|
|
Context Context `json:"context"`
|
|
Score float64 `json:"score"`
|
|
Explanation *string `json:"explanation,omitempty"`
|
|
Highlights []string `json:"highlights,omitempty"`
|
|
}
|
|
|
|
// SearchOptions represents search configuration options
|
|
type SearchOptions struct {
|
|
SearchType *SearchType `json:"search_type,omitempty"`
|
|
TopK *int `json:"top_k,omitempty"`
|
|
SimilarityThreshold *float64 `json:"similarity_threshold,omitempty"`
|
|
PathPrefix *string `json:"path_prefix,omitempty"`
|
|
SemanticWeight *float64 `json:"semantic_weight,omitempty"`
|
|
IncludeContent *bool `json:"include_content,omitempty"`
|
|
IncludeHighlights *bool `json:"include_highlights,omitempty"`
|
|
MaxHighlights *int `json:"max_highlights,omitempty"`
|
|
}
|
|
|
|
// ContextFilter represents filtering options for listing contexts
|
|
type ContextFilter struct {
|
|
PathPrefix *string `json:"path_prefix,omitempty"`
|
|
Author *string `json:"author,omitempty"`
|
|
Status *ContextStatus `json:"status,omitempty"`
|
|
Tags []string `json:"tags,omitempty"`
|
|
CreatedAfter *time.Time `json:"created_after,omitempty"`
|
|
CreatedBefore *time.Time `json:"created_before,omitempty"`
|
|
ContentContains *string `json:"content_contains,omitempty"`
|
|
MinContentLength *int `json:"min_content_length,omitempty"`
|
|
MaxContentLength *int `json:"max_content_length,omitempty"`
|
|
}
|
|
|
|
// PaginationOptions represents pagination configuration
|
|
type PaginationOptions struct {
|
|
Page *int `json:"page,omitempty"`
|
|
PageSize *int `json:"page_size,omitempty"`
|
|
SortBy *string `json:"sort_by,omitempty"`
|
|
SortOrder *string `json:"sort_order,omitempty"`
|
|
}
|
|
|
|
// PaginationMeta represents pagination metadata
|
|
type PaginationMeta struct {
|
|
Page int `json:"page"`
|
|
PageSize int `json:"page_size"`
|
|
TotalItems int `json:"total_items"`
|
|
TotalPages int `json:"total_pages"`
|
|
HasNext bool `json:"has_next"`
|
|
HasPrevious bool `json:"has_previous"`
|
|
}
|
|
|
|
// BatchResult represents the result of a batch operation
|
|
type BatchResult struct {
|
|
SuccessCount int `json:"success_count"`
|
|
ErrorCount int `json:"error_count"`
|
|
TotalItems int `json:"total_items"`
|
|
SuccessfulItems []interface{} `json:"successful_items"`
|
|
FailedItems []map[string]interface{} `json:"failed_items"`
|
|
ExecutionTime time.Duration `json:"execution_time"`
|
|
SuccessRate float64 `json:"success_rate"`
|
|
}
|
|
|
|
// APIResponse represents a generic API response wrapper
|
|
type APIResponse[T any] struct {
|
|
Success bool `json:"success"`
|
|
Data T `json:"data"`
|
|
Timestamp time.Time `json:"timestamp"`
|
|
APIVersion string `json:"api_version"`
|
|
RequestID *string `json:"request_id,omitempty"`
|
|
}
|
|
|
|
// ListResponse represents a paginated list response
|
|
type ListResponse[T any] struct {
|
|
APIResponse[[]T]
|
|
Pagination PaginationMeta `json:"pagination"`
|
|
}
|
|
|
|
// SearchResponse represents a search response
|
|
type SearchResponse struct {
|
|
Success bool `json:"success"`
|
|
Data []SearchResult `json:"data"`
|
|
Query string `json:"query"`
|
|
SearchType SearchType `json:"search_type"`
|
|
TotalResults int `json:"total_results"`
|
|
SearchTimeMs float64 `json:"search_time_ms"`
|
|
FiltersApplied map[string]any `json:"filters_applied,omitempty"`
|
|
Timestamp time.Time `json:"timestamp"`
|
|
APIVersion string `json:"api_version"`
|
|
}
|
|
|
|
// HealthStatus represents health status
|
|
type HealthStatus string
|
|
|
|
const (
|
|
HealthStatusHealthy HealthStatus = "healthy"
|
|
HealthStatusDegraded HealthStatus = "degraded"
|
|
HealthStatusUnhealthy HealthStatus = "unhealthy"
|
|
)
|
|
|
|
// ComponentHealth represents health of a system component
|
|
type ComponentHealth struct {
|
|
Name string `json:"name"`
|
|
Status HealthStatus `json:"status"`
|
|
ResponseTimeMs *float64 `json:"response_time_ms,omitempty"`
|
|
ErrorMessage *string `json:"error_message,omitempty"`
|
|
}
|
|
|
|
// HealthResponse represents health check response
|
|
type HealthResponse struct {
|
|
Status HealthStatus `json:"status"`
|
|
Version string `json:"version"`
|
|
UptimeSeconds float64 `json:"uptime_seconds"`
|
|
Components []ComponentHealth `json:"components"`
|
|
}
|
|
|
|
// Config represents HCFS client configuration
|
|
type Config struct {
|
|
BaseURL string
|
|
APIKey string
|
|
JWTToken string
|
|
Timeout time.Duration
|
|
UserAgent string
|
|
MaxRetries int
|
|
RetryDelay time.Duration
|
|
RateLimit float64
|
|
MaxConcurrentRequests int
|
|
CacheEnabled bool
|
|
CacheSize int
|
|
CacheTTL time.Duration
|
|
}
|
|
|
|
// DefaultConfig returns a default configuration
|
|
func DefaultConfig(baseURL, apiKey string) *Config {
|
|
return &Config{
|
|
BaseURL: baseURL,
|
|
APIKey: apiKey,
|
|
Timeout: DefaultTimeout,
|
|
UserAgent: fmt.Sprintf("hcfs-go/%s", Version),
|
|
MaxRetries: DefaultRetries,
|
|
RetryDelay: time.Second,
|
|
RateLimit: DefaultRateLimit,
|
|
MaxConcurrentRequests: 100,
|
|
CacheEnabled: true,
|
|
CacheSize: DefaultCacheSize,
|
|
CacheTTL: DefaultCacheTTL,
|
|
}
|
|
}
|
|
|
|
// Client represents the HCFS API client
|
|
type Client struct {
|
|
config *Config
|
|
httpClient *http.Client
|
|
rateLimiter *rate.Limiter
|
|
cache *cache
|
|
analytics *analytics
|
|
mu sync.RWMutex
|
|
}
|
|
|
|
// NewClient creates a new HCFS client
|
|
func NewClient(config *Config) *Client {
|
|
if config == nil {
|
|
panic("config cannot be nil")
|
|
}
|
|
|
|
client := &Client{
|
|
config: config,
|
|
httpClient: &http.Client{
|
|
Timeout: config.Timeout,
|
|
},
|
|
rateLimiter: rate.NewLimiter(rate.Limit(config.RateLimit), int(config.RateLimit)),
|
|
analytics: newAnalytics(),
|
|
}
|
|
|
|
if config.CacheEnabled {
|
|
client.cache = newCache(config.CacheSize, config.CacheTTL)
|
|
}
|
|
|
|
return client
|
|
}
|
|
|
|
// HealthCheck checks the API health status
|
|
func (c *Client) HealthCheck(ctx context.Context) (*HealthResponse, error) {
|
|
var response HealthResponse
|
|
err := c.request(ctx, "GET", "/health", nil, nil, &response)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &response, nil
|
|
}
|
|
|
|
// CreateContext creates a new context
|
|
func (c *Client) CreateContext(ctx context.Context, contextData *ContextCreate) (*Context, error) {
|
|
if contextData == nil {
|
|
return nil, fmt.Errorf("contextData cannot be nil")
|
|
}
|
|
|
|
if !validatePath(contextData.Path) {
|
|
return nil, fmt.Errorf("invalid context path: %s", contextData.Path)
|
|
}
|
|
|
|
// Normalize path
|
|
contextData.Path = normalizePath(contextData.Path)
|
|
|
|
var response APIResponse[Context]
|
|
err := c.request(ctx, "POST", "/api/v1/contexts", nil, contextData, &response)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Invalidate relevant cache entries
|
|
c.invalidateCache("/api/v1/contexts")
|
|
|
|
return &response.Data, nil
|
|
}
|
|
|
|
// GetContext retrieves a context by ID
|
|
func (c *Client) GetContext(ctx context.Context, contextID int) (*Context, error) {
|
|
path := fmt.Sprintf("/api/v1/contexts/%d", contextID)
|
|
|
|
// Check cache first
|
|
if c.cache != nil {
|
|
if cached, ok := c.cache.get(path); ok {
|
|
if context, ok := cached.(*Context); ok {
|
|
c.analytics.recordCacheHit()
|
|
return context, nil
|
|
}
|
|
}
|
|
c.analytics.recordCacheMiss()
|
|
}
|
|
|
|
var response APIResponse[Context]
|
|
err := c.request(ctx, "GET", path, nil, nil, &response)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Cache the result
|
|
if c.cache != nil {
|
|
c.cache.set(path, &response.Data)
|
|
}
|
|
|
|
return &response.Data, nil
|
|
}
|
|
|
|
// ListContexts lists contexts with filtering and pagination
|
|
func (c *Client) ListContexts(ctx context.Context, filter *ContextFilter, pagination *PaginationOptions) ([]Context, *PaginationMeta, error) {
|
|
params := url.Values{}
|
|
|
|
// Add filter parameters
|
|
if filter != nil {
|
|
if filter.PathPrefix != nil {
|
|
params.Set("path_prefix", *filter.PathPrefix)
|
|
}
|
|
if filter.Author != nil {
|
|
params.Set("author", *filter.Author)
|
|
}
|
|
if filter.Status != nil {
|
|
params.Set("status", string(*filter.Status))
|
|
}
|
|
if filter.CreatedAfter != nil {
|
|
params.Set("created_after", filter.CreatedAfter.Format(time.RFC3339))
|
|
}
|
|
if filter.CreatedBefore != nil {
|
|
params.Set("created_before", filter.CreatedBefore.Format(time.RFC3339))
|
|
}
|
|
if filter.ContentContains != nil {
|
|
params.Set("content_contains", *filter.ContentContains)
|
|
}
|
|
if filter.MinContentLength != nil {
|
|
params.Set("min_content_length", strconv.Itoa(*filter.MinContentLength))
|
|
}
|
|
if filter.MaxContentLength != nil {
|
|
params.Set("max_content_length", strconv.Itoa(*filter.MaxContentLength))
|
|
}
|
|
}
|
|
|
|
// Add pagination parameters
|
|
if pagination != nil {
|
|
if pagination.Page != nil {
|
|
params.Set("page", strconv.Itoa(*pagination.Page))
|
|
}
|
|
if pagination.PageSize != nil {
|
|
params.Set("page_size", strconv.Itoa(*pagination.PageSize))
|
|
}
|
|
if pagination.SortBy != nil {
|
|
params.Set("sort_by", *pagination.SortBy)
|
|
}
|
|
if pagination.SortOrder != nil {
|
|
params.Set("sort_order", *pagination.SortOrder)
|
|
}
|
|
}
|
|
|
|
var response ListResponse[Context]
|
|
err := c.request(ctx, "GET", "/api/v1/contexts", params, nil, &response)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
return response.Data, &response.Pagination, nil
|
|
}
|
|
|
|
// UpdateContext updates an existing context
|
|
func (c *Client) UpdateContext(ctx context.Context, contextID int, updates *ContextUpdate) (*Context, error) {
|
|
if updates == nil {
|
|
return nil, fmt.Errorf("updates cannot be nil")
|
|
}
|
|
|
|
path := fmt.Sprintf("/api/v1/contexts/%d", contextID)
|
|
|
|
var response APIResponse[Context]
|
|
err := c.request(ctx, "PUT", path, nil, updates, &response)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Invalidate cache
|
|
c.invalidateCache(path)
|
|
c.invalidateCache("/api/v1/contexts")
|
|
|
|
return &response.Data, nil
|
|
}
|
|
|
|
// DeleteContext deletes a context
|
|
func (c *Client) DeleteContext(ctx context.Context, contextID int) error {
|
|
path := fmt.Sprintf("/api/v1/contexts/%d", contextID)
|
|
|
|
err := c.request(ctx, "DELETE", path, nil, nil, nil)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Invalidate cache
|
|
c.invalidateCache(path)
|
|
c.invalidateCache("/api/v1/contexts")
|
|
|
|
return nil
|
|
}
|
|
|
|
// SearchContexts searches contexts using various search methods
|
|
func (c *Client) SearchContexts(ctx context.Context, query string, options *SearchOptions) ([]SearchResult, error) {
|
|
if query == "" {
|
|
return nil, fmt.Errorf("query cannot be empty")
|
|
}
|
|
|
|
searchData := map[string]interface{}{
|
|
"query": query,
|
|
}
|
|
|
|
if options != nil {
|
|
if options.SearchType != nil {
|
|
searchData["search_type"] = string(*options.SearchType)
|
|
}
|
|
if options.TopK != nil {
|
|
searchData["top_k"] = *options.TopK
|
|
}
|
|
if options.SimilarityThreshold != nil {
|
|
searchData["similarity_threshold"] = *options.SimilarityThreshold
|
|
}
|
|
if options.PathPrefix != nil {
|
|
searchData["path_prefix"] = *options.PathPrefix
|
|
}
|
|
if options.SemanticWeight != nil {
|
|
searchData["semantic_weight"] = *options.SemanticWeight
|
|
}
|
|
if options.IncludeContent != nil {
|
|
searchData["include_content"] = *options.IncludeContent
|
|
}
|
|
if options.IncludeHighlights != nil {
|
|
searchData["include_highlights"] = *options.IncludeHighlights
|
|
}
|
|
if options.MaxHighlights != nil {
|
|
searchData["max_highlights"] = *options.MaxHighlights
|
|
}
|
|
}
|
|
|
|
var response SearchResponse
|
|
err := c.request(ctx, "POST", "/api/v1/search", nil, searchData, &response)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return response.Data, nil
|
|
}
|
|
|
|
// BatchCreateContexts creates multiple contexts in batch
|
|
func (c *Client) BatchCreateContexts(ctx context.Context, contexts []*ContextCreate) (*BatchResult, error) {
|
|
if len(contexts) == 0 {
|
|
return nil, fmt.Errorf("contexts cannot be empty")
|
|
}
|
|
|
|
startTime := time.Now()
|
|
|
|
// Validate and normalize all contexts
|
|
for _, context := range contexts {
|
|
if !validatePath(context.Path) {
|
|
return nil, fmt.Errorf("invalid context path: %s", context.Path)
|
|
}
|
|
context.Path = normalizePath(context.Path)
|
|
}
|
|
|
|
batchData := map[string]interface{}{
|
|
"contexts": contexts,
|
|
}
|
|
|
|
var response APIResponse[BatchResult]
|
|
err := c.request(ctx, "POST", "/api/v1/contexts/batch", nil, batchData, &response)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Calculate additional metrics
|
|
result := response.Data
|
|
result.ExecutionTime = time.Since(startTime)
|
|
result.SuccessRate = float64(result.SuccessCount) / float64(result.TotalItems)
|
|
|
|
// Invalidate cache
|
|
c.invalidateCache("/api/v1/contexts")
|
|
|
|
return &result, nil
|
|
}
|
|
|
|
// IterateContexts iterates through all contexts with automatic pagination
|
|
func (c *Client) IterateContexts(ctx context.Context, filter *ContextFilter, pageSize int, callback func(Context) error) error {
|
|
if pageSize <= 0 {
|
|
pageSize = 100
|
|
}
|
|
|
|
page := 1
|
|
for {
|
|
pagination := &PaginationOptions{
|
|
Page: &page,
|
|
PageSize: &pageSize,
|
|
}
|
|
|
|
contexts, paginationMeta, err := c.ListContexts(ctx, filter, pagination)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if len(contexts) == 0 {
|
|
break
|
|
}
|
|
|
|
for _, context := range contexts {
|
|
if err := callback(context); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
// If we got fewer contexts than requested, we've reached the end
|
|
if len(contexts) < pageSize || !paginationMeta.HasNext {
|
|
break
|
|
}
|
|
|
|
page++
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// GetAnalytics returns client analytics
|
|
func (c *Client) GetAnalytics() map[string]interface{} {
|
|
c.mu.RLock()
|
|
defer c.mu.RUnlock()
|
|
|
|
analytics := map[string]interface{}{
|
|
"session_start": c.analytics.sessionStart,
|
|
"operation_count": c.analytics.operationCount,
|
|
"error_count": c.analytics.errorCount,
|
|
"total_requests": c.analytics.totalRequests,
|
|
"failed_requests": c.analytics.failedRequests,
|
|
}
|
|
|
|
if c.cache != nil {
|
|
analytics["cache_stats"] = map[string]interface{}{
|
|
"enabled": true,
|
|
"size": c.cache.size(),
|
|
"max_size": c.cache.maxSize,
|
|
"hit_rate": c.analytics.getCacheHitRate(),
|
|
}
|
|
} else {
|
|
analytics["cache_stats"] = map[string]interface{}{
|
|
"enabled": false,
|
|
}
|
|
}
|
|
|
|
return analytics
|
|
}
|
|
|
|
// ClearCache clears the client cache
|
|
func (c *Client) ClearCache() {
|
|
if c.cache != nil {
|
|
c.cache.clear()
|
|
}
|
|
}
|
|
|
|
// Close closes the client and cleans up resources
|
|
func (c *Client) Close() error {
|
|
if c.cache != nil {
|
|
c.cache.clear()
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Internal method to make HTTP requests
|
|
func (c *Client) request(ctx context.Context, method, path string, params url.Values, body interface{}, result interface{}) error {
|
|
// Rate limiting
|
|
if err := c.rateLimiter.Wait(ctx); err != nil {
|
|
return fmt.Errorf("rate limit error: %w", err)
|
|
}
|
|
|
|
// Build URL
|
|
u, err := url.Parse(c.config.BaseURL + path)
|
|
if err != nil {
|
|
return fmt.Errorf("invalid URL: %w", err)
|
|
}
|
|
|
|
if params != nil {
|
|
u.RawQuery = params.Encode()
|
|
}
|
|
|
|
// Prepare body
|
|
var bodyReader io.Reader
|
|
if body != nil {
|
|
bodyBytes, err := json.Marshal(body)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to marshal body: %w", err)
|
|
}
|
|
bodyReader = bytes.NewReader(bodyBytes)
|
|
}
|
|
|
|
// Create request
|
|
req, err := http.NewRequestWithContext(ctx, method, u.String(), bodyReader)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to create request: %w", err)
|
|
}
|
|
|
|
// Set headers
|
|
req.Header.Set("User-Agent", c.config.UserAgent)
|
|
req.Header.Set("Content-Type", "application/json")
|
|
|
|
if c.config.APIKey != "" {
|
|
req.Header.Set("X-API-Key", c.config.APIKey)
|
|
}
|
|
if c.config.JWTToken != "" {
|
|
req.Header.Set("Authorization", "Bearer "+c.config.JWTToken)
|
|
}
|
|
|
|
// Execute request with retries
|
|
var resp *http.Response
|
|
for attempt := 0; attempt <= c.config.MaxRetries; attempt++ {
|
|
c.analytics.recordRequest()
|
|
|
|
resp, err = c.httpClient.Do(req)
|
|
if err != nil {
|
|
if attempt == c.config.MaxRetries {
|
|
c.analytics.recordError(err.Error())
|
|
return fmt.Errorf("request failed after %d attempts: %w", c.config.MaxRetries+1, err)
|
|
}
|
|
time.Sleep(c.config.RetryDelay * time.Duration(attempt+1))
|
|
continue
|
|
}
|
|
|
|
// Check if we should retry based on status code
|
|
if shouldRetry(resp.StatusCode) && attempt < c.config.MaxRetries {
|
|
resp.Body.Close()
|
|
time.Sleep(c.config.RetryDelay * time.Duration(attempt+1))
|
|
continue
|
|
}
|
|
|
|
break
|
|
}
|
|
|
|
defer resp.Body.Close()
|
|
|
|
// Handle error responses
|
|
if resp.StatusCode >= 400 {
|
|
c.analytics.recordError(fmt.Sprintf("HTTP %d", resp.StatusCode))
|
|
return c.handleHTTPError(resp)
|
|
}
|
|
|
|
// Parse response if result is provided
|
|
if result != nil {
|
|
if err := json.NewDecoder(resp.Body).Decode(result); err != nil {
|
|
return fmt.Errorf("failed to decode response: %w", err)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// Handle HTTP errors and convert to appropriate error types
|
|
func (c *Client) handleHTTPError(resp *http.Response) error {
|
|
body, _ := io.ReadAll(resp.Body)
|
|
|
|
var errorResp struct {
|
|
Error string `json:"error"`
|
|
ErrorDetails []struct {
|
|
Field string `json:"field"`
|
|
Message string `json:"message"`
|
|
Code string `json:"code"`
|
|
} `json:"error_details"`
|
|
}
|
|
|
|
json.Unmarshal(body, &errorResp)
|
|
|
|
message := errorResp.Error
|
|
if message == "" {
|
|
message = fmt.Sprintf("HTTP %d error", resp.StatusCode)
|
|
}
|
|
|
|
switch resp.StatusCode {
|
|
case 400:
|
|
return &ValidationError{Message: message, Details: errorResp.ErrorDetails}
|
|
case 401:
|
|
return &AuthenticationError{Message: message}
|
|
case 404:
|
|
return &NotFoundError{Message: message}
|
|
case 429:
|
|
retryAfter := resp.Header.Get("Retry-After")
|
|
return &RateLimitError{Message: message, RetryAfter: retryAfter}
|
|
case 500, 502, 503, 504:
|
|
return &ServerError{Message: message, StatusCode: resp.StatusCode}
|
|
default:
|
|
return &APIError{Message: message, StatusCode: resp.StatusCode}
|
|
}
|
|
}
|
|
|
|
// Utility functions
|
|
func validatePath(path string) bool {
|
|
return strings.HasPrefix(path, "/") && !strings.Contains(path, "//")
|
|
}
|
|
|
|
func normalizePath(path string) string {
|
|
if !strings.HasPrefix(path, "/") {
|
|
path = "/" + path
|
|
}
|
|
// Remove duplicate slashes
|
|
for strings.Contains(path, "//") {
|
|
path = strings.ReplaceAll(path, "//", "/")
|
|
}
|
|
return path
|
|
}
|
|
|
|
func (c *Client) invalidateCache(pattern string) {
|
|
if c.cache == nil {
|
|
return
|
|
}
|
|
c.cache.invalidatePattern(pattern)
|
|
}
|
|
|
|
func shouldRetry(statusCode int) bool {
|
|
return statusCode == 429 || statusCode >= 500
|
|
} |