package dediregistry import ( "context" "encoding/json" "fmt" "io" "net/http" "time" "github.com/beckn-one/beckn-onix/pkg/log" "github.com/beckn-one/beckn-onix/pkg/model" "github.com/hashicorp/go-retryablehttp" ) // Config holds configuration parameters for the DeDi registry client. type Config struct { BaseURL string `yaml:"baseURL" json:"baseURL"` ApiKey string `yaml:"apiKey" json:"apiKey"` NamespaceID string `yaml:"namespaceID" json:"namespaceID"` RegistryName string `yaml:"registryName" json:"registryName"` Timeout int `yaml:"timeout" json:"timeout"` } // DeDiRegistryClient encapsulates the logic for calling the DeDi registry endpoints. type DeDiRegistryClient struct { config *Config client *retryablehttp.Client } // validate checks if the provided DeDi registry configuration is valid. func validate(cfg *Config) error { if cfg == nil { return fmt.Errorf("DeDi registry config cannot be nil") } if cfg.BaseURL == "" { return fmt.Errorf("baseURL cannot be empty") } if cfg.ApiKey == "" { return fmt.Errorf("apiKey cannot be empty") } if cfg.NamespaceID == "" { return fmt.Errorf("namespaceID cannot be empty") } if cfg.RegistryName == "" { return fmt.Errorf("registryName cannot be empty") } return nil } // New creates a new instance of DeDiRegistryClient. func New(ctx context.Context, cfg *Config) (*DeDiRegistryClient, func() error, error) { log.Debugf(ctx, "Initializing DeDi Registry client with config: %+v", cfg) if err := validate(cfg); err != nil { return nil, nil, err } retryClient := retryablehttp.NewClient() // Configure timeout if provided if cfg.Timeout > 0 { retryClient.HTTPClient.Timeout = time.Duration(cfg.Timeout) * time.Second } client := &DeDiRegistryClient{ config: cfg, client: retryClient, } // Cleanup function closer := func() error { log.Debugf(ctx, "Cleaning up DeDi Registry client resources") if client.client != nil { client.client.HTTPClient.CloseIdleConnections() } return nil } log.Infof(ctx, "DeDi Registry client connection established successfully") return client, closer, nil } // Lookup implements RegistryLookup interface - calls the DeDi lookup endpoint and returns Subscription. func (c *DeDiRegistryClient) Lookup(ctx context.Context, req *model.Subscription) ([]model.Subscription, error) { // Extract subscriber ID from request subscriberID := req.SubscriberID log.Infof(ctx, "DeDI Registry: Looking up subscriber ID: %s", subscriberID) if subscriberID == "" { return nil, fmt.Errorf("subscriber_id is required for DeDi lookup") } lookupURL := fmt.Sprintf("%s/dedi/lookup/%s/%s/%s", c.config.BaseURL, c.config.NamespaceID, c.config.RegistryName, subscriberID) httpReq, err := retryablehttp.NewRequest("GET", lookupURL, nil) if err != nil { return nil, fmt.Errorf("failed to create request: %w", err) } httpReq.Header.Set("Authorization", fmt.Sprintf("Bearer %s", c.config.ApiKey)) httpReq = httpReq.WithContext(ctx) log.Debugf(ctx, "Making DeDi lookup request to: %s", lookupURL) resp, err := c.client.Do(httpReq) if err != nil { return nil, fmt.Errorf("failed to send DeDi lookup request: %w", err) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { body, _ := io.ReadAll(resp.Body) log.Errorf(ctx, nil, "DeDi lookup request failed with status: %s, response: %s", resp.Status, string(body)) return nil, fmt.Errorf("DeDi lookup request failed with status: %s", resp.Status) } body, err := io.ReadAll(resp.Body) if err != nil { return nil, fmt.Errorf("failed to read response body: %w", err) } // Parse response var responseData map[string]interface{} err = json.Unmarshal(body, &responseData) if err != nil { return nil, fmt.Errorf("failed to unmarshal response body: %w", err) } log.Debugf(ctx, "DeDi lookup request successful") // Extract data field data, ok := responseData["data"].(map[string]interface{}) if !ok { return nil, fmt.Errorf("invalid response format: missing data field") } // Extract details field details, ok := data["details"].(map[string]interface{}) if !ok { return nil, fmt.Errorf("invalid response format: missing details field") } // Extract required fields from details keyID, _ := details["key_id"].(string) signingPublicKey, ok := details["signing_public_key"].(string) if !ok || signingPublicKey == "" { return nil, fmt.Errorf("invalid or missing signing_public_key in response") } encrPublicKey, _ := details["encr_public_key"].(string) detailsStatus, ok := details["status"].(string) if !ok { return nil, fmt.Errorf("missing status in response") } detailsCreated, _ := details["created"].(string) detailsUpdated, _ := details["updated"].(string) validFromStr, _ := details["valid_from"].(string) validUntilStr, _ := details["valid_until"].(string) // Extract record_name as subscriber ID recordName, _ := data["record_name"].(string) if recordName == "" { recordName = subscriberID } // Convert to Subscription format subscription := model.Subscription{ Subscriber: model.Subscriber{ SubscriberID: recordName, URL: req.URL, Domain: req.Domain, Type: req.Type, }, KeyID: keyID, SigningPublicKey: signingPublicKey, EncrPublicKey: encrPublicKey, ValidFrom: parseTime(validFromStr), ValidUntil: parseTime(validUntilStr), Status: detailsStatus, Created: parseTime(detailsCreated), Updated: parseTime(detailsUpdated), } return []model.Subscription{subscription}, nil } // parseTime converts string timestamp to time.Time func parseTime(timeStr string) time.Time { if timeStr == "" { return time.Time{} } parsedTime, err := time.Parse(time.RFC3339, timeStr) if err != nil { return time.Time{} } return parsedTime }