// Package utils provides utility functions for the Yandex Music API client, // including JSON handling, request signing, schema mismatch tracking, and error types. package utils import ( "fmt" "log" "strings" "sync" ) // SchemaMismatch represents a mismatch between expected and actual API response. // It captures details about missing fields, unknown fields, and unknown values. type SchemaMismatch struct { // ModelName is the name of the model that had the mismatch. ModelName string // MissingFields lists fields that were expected but not found in the response. MissingFields []string // UnknownFields lists fields that were not expected but found in the response. UnknownFields []string // UnknownValues maps unknown field names to their values. UnknownValues map[string]any // Endpoint is the API endpoint where the mismatch occurred. Endpoint string // LibraryVersion is the version of the library when the mismatch occurred. LibraryVersion string } // Describe returns a human-readable description of the schema mismatch. func (sm *SchemaMismatch) Describe() string { var parts []string parts = append(parts, fmt.Sprintf("Model: %s", sm.ModelName)) if len(sm.MissingFields) > 0 { parts = append(parts, fmt.Sprintf("Missing fields: %v", sm.MissingFields)) } if len(sm.UnknownFields) > 0 { parts = append(parts, fmt.Sprintf("Unknown fields: %v", sm.UnknownFields)) } if len(sm.UnknownValues) > 0 { parts = append(parts, fmt.Sprintf("Unknown field structures: %v", sm.UnknownValues)) } parts = append(parts, fmt.Sprintf("Endpoint: %s", sm.Endpoint)) parts = append(parts, fmt.Sprintf("Library version: %s", sm.LibraryVersion)) return strings.Join(parts, "\n") } // SchemaMismatchHandler is a callback for handling schema mismatches. // It is invoked when a schema mismatch is detected, if configured. type SchemaMismatchHandler func(*SchemaMismatch) // SchemaMismatchConfig holds configuration for schema mismatch handling. type SchemaMismatchConfig struct { // ReportUnknownFields indicates whether unknown fields should be reported. ReportUnknownFields bool // Strict indicates whether schema mismatches should be treated as errors. Strict bool // OnSchemaMismatch is an optional callback for custom mismatch handling. OnSchemaMismatch SchemaMismatchHandler } // DefaultSchemaMismatchConfig returns the default configuration for schema mismatch handling. // The default config does not report unknown fields, is not strict, and has no callback. func DefaultSchemaMismatchConfig() SchemaMismatchConfig { return SchemaMismatchConfig{ ReportUnknownFields: false, Strict: false, OnSchemaMismatch: nil, } } // ReportSchemaMismatch reports a schema mismatch according to the provided config. // If a callback is configured, it is invoked with the mismatch details. // If strict mode is enabled, an error is returned. Otherwise, a warning is logged. func ReportSchemaMismatch(config SchemaMismatchConfig, modelName, endpoint string, missingFields, unknownFields []string, unknownValues map[string]any) error { mismatch := &SchemaMismatch{ ModelName: modelName, MissingFields: missingFields, UnknownFields: unknownFields, UnknownValues: unknownValues, Endpoint: endpoint, LibraryVersion: "go-port-0.1.0", } if config.OnSchemaMismatch != nil { config.OnSchemaMismatch(mismatch) return nil } msg := mismatch.Describe() if config.Strict { return fmt.Errorf("schema mismatch: %s", msg) } log.Printf("WARNING: Schema mismatch for %s:\n%s", modelName, msg) return nil } // SchemaMismatchTracker tracks reported mismatches to avoid duplicate reports. // It is safe for concurrent use. type SchemaMismatchTracker struct { mu sync.Mutex reported map[string]bool } // NewSchemaMismatchTracker creates a new SchemaMismatchTracker. func NewSchemaMismatchTracker() *SchemaMismatchTracker { return &SchemaMismatchTracker{ reported: make(map[string]bool), } } // ShouldReport returns true if the given model and fields have not been reported before. // It marks them as reported so subsequent calls return false for the same key. func (t *SchemaMismatchTracker) ShouldReport(modelName string, fields []string) bool { t.mu.Lock() defer t.mu.Unlock() key := modelName + ":" + strings.Join(fields, ",") if t.reported[key] { return false } t.reported[key] = true return true }