Phase 2 build initial
This commit is contained in:
740
sdks/go/hcfs.go
Normal file
740
sdks/go/hcfs.go
Normal file
@@ -0,0 +1,740 @@
|
||||
// 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
|
||||
}
|
||||
Reference in New Issue
Block a user