Issue 527 - feat: update DeDi registry plugin to handle new response structure
This commit is contained in:
@@ -1,41 +1,22 @@
|
||||
# DeDi Registry Plugin
|
||||
|
||||
A **registry type plugin** for Beckn-ONIX that integrates with DeDi (Decentralized Digital Infrastructure) registry services. This plugin implements the `RegistryLookup` interface, making it a specialized type of registry plugin.
|
||||
A **registry type plugin** for Beckn-ONIX that integrates with DeDi (Decentralized Digital Infrastructure) registry services.
|
||||
|
||||
## Overview
|
||||
|
||||
The DeDi Registry plugin is a **registry implementation** that enables Beckn-ONIX to lookup participant records from DeDi registries. It converts DeDi API responses to standard Beckn Subscription format, allowing it to work interchangeably with the standard registry plugin through the same `RegistryLookup` interface.
|
||||
|
||||
## Plugin Type Classification
|
||||
|
||||
**Registry Type Plugin**: This plugin is a **type of registry plugin**, not a standalone plugin category.
|
||||
|
||||
- **Interface**: Implements `RegistryLookup` interface (same as standard registry plugin)
|
||||
- **Interchangeable**: Can replace or work alongside standard registry plugin
|
||||
- **Manager Access**: Available via `manager.Registry()` method
|
||||
- **Plugin Category**: Registry
|
||||
|
||||
## Features
|
||||
|
||||
- **Standard Registry Interface**: Implements `RegistryLookup` interface for seamless integration
|
||||
- **DeDi API Integration**: GET requests to DeDi registry endpoints with Bearer authentication
|
||||
- **Dynamic Participant Lookup**: Uses subscriber IDs from request context (not static configuration)
|
||||
- **Data Conversion**: Converts DeDi responses to standard Beckn Subscription format
|
||||
- **HTTP Retry Logic**: Built-in retry mechanism using retryablehttp client
|
||||
- **Timeout Control**: Configurable request timeouts
|
||||
|
||||
The DeDi Registry plugin is a **registry implementation** that enables Beckn-ONIX to lookup participant records from remote DeDi registries via REST API calls.
|
||||
|
||||
## Configuration
|
||||
|
||||
```yaml
|
||||
plugins:
|
||||
dediRegistry:
|
||||
registry:
|
||||
id: dediregistry
|
||||
config:
|
||||
baseURL: "https://dedi-registry.example.com"
|
||||
apiKey: "your-api-key"
|
||||
namespaceID: "beckn-network"
|
||||
registryName: "participants"
|
||||
baseURL: "https://dedi-api.example.com"
|
||||
apiKey: "your-bearer-token"
|
||||
namespaceID: "76EU8BF1gzRGGatgw7wZZb7nEVx77XSwkKDv4UDLdxh8ztty4zmbYU"
|
||||
registryName: "dedi_registry"
|
||||
timeout: "30" # seconds
|
||||
```
|
||||
|
||||
@@ -44,7 +25,7 @@ plugins:
|
||||
| Parameter | Required | Description | Default |
|
||||
|-----------|----------|-------------|---------|
|
||||
| `baseURL` | Yes | DeDi registry API base URL | - |
|
||||
| `apiKey` | Yes | API key for authentication | - |
|
||||
| `apiKey` | Yes | Bearer token for API authentication | - |
|
||||
| `namespaceID` | Yes | DeDi namespace identifier | - |
|
||||
| `registryName` | Yes | Registry name to query | - |
|
||||
| `timeout` | No | Request timeout in seconds | 30 |
|
||||
@@ -106,7 +87,12 @@ if len(results) > 0 {
|
||||
{baseURL}/dedi/lookup/{namespaceID}/{registryName}/{subscriberID}
|
||||
```
|
||||
|
||||
**Example**: `https://dedi-registry.com/dedi/lookup/beckn-network/participants/bap-network`
|
||||
**Example**: `https://dedi-api.com/dedi/lookup/76EU8BF1gzRGGatgw7wZZb7nEVx77XSwkKDv4UDLdxh8ztty4zmbYU/dedi_registry/bap-network`
|
||||
|
||||
### Authentication
|
||||
```
|
||||
Authorization: Bearer {apiKey}
|
||||
```
|
||||
|
||||
### Expected DeDi Response Format
|
||||
|
||||
@@ -114,35 +100,47 @@ if len(results) > 0 {
|
||||
{
|
||||
"message": "Resource retrieved successfully",
|
||||
"data": {
|
||||
"record_name": "bap.example.com",
|
||||
"namespace": "dediregistry",
|
||||
"namespace_id": "76EU8BF1gzRGGatgw7wZZb7nEVx77XSwkKDv4UDLdxh8ztty4zmbYU",
|
||||
"registry_name": "dedi_registry",
|
||||
"record_name": "bap-network",
|
||||
"details": {
|
||||
"entity_name": "BAP Example Provider",
|
||||
"entity_url": "https://bap.example.com",
|
||||
"publicKey": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA...",
|
||||
"keyType": "RSA",
|
||||
"keyFormat": "PEM"
|
||||
"key_id": "b692d295-5425-40f5-af77-d62646841dca",
|
||||
"signing_public_key": "YK3Xqc83Bpobc1UT0ObAe6mBJMiAOkceIsNtmph9WTc=",
|
||||
"encr_public_key": "YK3Xqc83Bpobc1UT0ObAe6mBJMiAOkceIsNtmph9WTc=",
|
||||
"status": "SUBSCRIBED",
|
||||
"created": "2024-01-15T10:00:00Z",
|
||||
"updated": "2024-01-15T10:00:00Z",
|
||||
"valid_from": "2024-01-01T00:00:00Z",
|
||||
"valid_until": "2025-12-31T23:59:59Z"
|
||||
},
|
||||
"state": "live",
|
||||
"created_at": "2025-09-23T07:45:10.357Z",
|
||||
"updated_at": "2025-09-23T07:51:39.923Z"
|
||||
"created_at": "2025-10-09T06:09:48.295Z"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Converted to Standard Beckn Format
|
||||
### Field Mapping to Beckn Subscription
|
||||
|
||||
The plugin converts DeDi responses to standard Beckn Subscription format:
|
||||
| DeDi Field | Beckn Field | Description |
|
||||
|------------|-------------|-------------|
|
||||
| `data.record_name` | `subscriber_id` | Participant identifier |
|
||||
| `data.details.key_id` | `key_id` | Unique key identifier |
|
||||
| `data.details.signing_public_key` | `signing_public_key` | Public key for signature verification |
|
||||
| `data.details.encr_public_key` | `encr_public_key` | Public key for encryption |
|
||||
| `data.details.status` | `status` | Subscription status |
|
||||
| `data.details.created` | `created` | Creation timestamp |
|
||||
| `data.details.updated` | `updated` | Last update timestamp |
|
||||
| `data.details.valid_from` | `valid_from` | Key validity start |
|
||||
| `data.details.valid_until` | `valid_until` | Key validity end |
|
||||
|
||||
```json
|
||||
{
|
||||
"subscriber_id": "bap.example.com",
|
||||
"url": "https://bap.example.com",
|
||||
"signing_public_key": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA...",
|
||||
"status": "live",
|
||||
"created": "2025-09-23T07:45:10.357Z",
|
||||
"updated": "2025-09-23T07:51:39.923Z"
|
||||
}
|
||||
```
|
||||
### Lookup Flow
|
||||
|
||||
1. **Request Processing**: Plugin extracts `subscriber_id` from incoming request
|
||||
2. **API Call**: Makes GET request to DeDi API with static config + dynamic subscriber ID
|
||||
3. **Response Parsing**: Extracts data from `data.details` object
|
||||
4. **Format Conversion**: Maps DeDi fields to Beckn Subscription format
|
||||
5. **Return**: Returns array of Subscription objects
|
||||
|
||||
## Testing
|
||||
|
||||
@@ -152,28 +150,32 @@ Run plugin tests:
|
||||
go test ./pkg/plugin/implementation/dediregistry -v
|
||||
```
|
||||
|
||||
Test coverage includes:
|
||||
- Configuration validation
|
||||
- Successful API responses
|
||||
- HTTP error handling
|
||||
- Network failures
|
||||
- Invalid JSON responses
|
||||
- Missing required fields
|
||||
|
||||
## Dependencies
|
||||
|
||||
- `github.com/hashicorp/go-retryablehttp`: HTTP client with retry logic
|
||||
- Standard Go libraries for HTTP and JSON handling
|
||||
|
||||
## Plugin Architecture
|
||||
## Error Handling
|
||||
|
||||
### Registry Type Plugin Classification
|
||||
- **Configuration Errors**: Missing required config parameters
|
||||
- **Network Errors**: Connection failures, timeouts
|
||||
- **HTTP Errors**: Non-200 status codes from DeDi API
|
||||
- **Data Errors**: Missing required fields in response
|
||||
- **Validation Errors**: Empty subscriber ID in request
|
||||
|
||||
```
|
||||
Plugin Manager
|
||||
├── Registry Plugins (RegistryLookup interface)
|
||||
│ ├── registry (standard YAML-based registry)
|
||||
│ └── dediregistry (DeDi API-based registry) ← This plugin
|
||||
└── Other Plugin Types...
|
||||
```
|
||||
|
||||
### Integration Notes
|
||||
|
||||
- **Plugin Type**: Registry implementation
|
||||
- **Interface**: Implements `RegistryLookup` interface with `Lookup(ctx, *model.Subscription) ([]model.Subscription, error)`
|
||||
- **Interchangeable**: Drop-in replacement for standard registry plugin
|
||||
- **Manager Access**: Available via `manager.Registry()` method (same as standard registry)
|
||||
- **Dynamic Lookup**: Uses `req.SubscriberID` from request context, not static configuration
|
||||
- **Data Conversion**: Automatically converts DeDi API format to Beckn Subscription format
|
||||
|
||||
@@ -31,6 +31,8 @@ func (d dediRegistryProvider) New(ctx context.Context, config map[string]string)
|
||||
if timeoutStr, exists := config["timeout"]; exists && timeoutStr != "" {
|
||||
if timeout, err := strconv.Atoi(timeoutStr); err == nil {
|
||||
dediConfig.Timeout = timeout
|
||||
} else {
|
||||
log.Warnf(ctx, "Invalid timeout value '%s', using default", timeoutStr)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -120,7 +120,7 @@ func (c *DeDiRegistryClient) Lookup(ctx context.Context, req *model.Subscription
|
||||
return nil, fmt.Errorf("failed to read response body: %w", err)
|
||||
}
|
||||
|
||||
// Parse response using local variables
|
||||
// Parse response
|
||||
var responseData map[string]interface{}
|
||||
err = json.Unmarshal(body, &responseData)
|
||||
if err != nil {
|
||||
@@ -129,56 +129,56 @@ func (c *DeDiRegistryClient) Lookup(ctx context.Context, req *model.Subscription
|
||||
|
||||
log.Debugf(ctx, "DeDi lookup request successful")
|
||||
|
||||
// Extract data using local variables
|
||||
// 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 which contains the actual participant data
|
||||
// Extract details field
|
||||
details, ok := data["details"].(map[string]interface{})
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("invalid response format: missing details field")
|
||||
}
|
||||
|
||||
// Extract values from details field
|
||||
entityName, ok := details["entity_name"].(string)
|
||||
if !ok || entityName == "" {
|
||||
return nil, fmt.Errorf("invalid or missing entity_name in response")
|
||||
// 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")
|
||||
}
|
||||
|
||||
entityURL, ok := details["entity_url"].(string)
|
||||
if !ok || entityURL == "" {
|
||||
return nil, fmt.Errorf("invalid or missing entity_url 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)
|
||||
|
||||
publicKey, ok := details["publicKey"].(string)
|
||||
if !ok || publicKey == "" {
|
||||
return nil, fmt.Errorf("invalid or missing publicKey in response")
|
||||
}
|
||||
|
||||
// Extract record_name as the subscriber ID (fallback to entity_name)
|
||||
// Extract record_name as subscriber ID
|
||||
recordName, _ := data["record_name"].(string)
|
||||
if recordName == "" {
|
||||
recordName = entityName
|
||||
recordName = subscriberID
|
||||
}
|
||||
|
||||
state, _ := data["state"].(string)
|
||||
createdAt, _ := data["created_at"].(string)
|
||||
updatedAt, _ := data["updated_at"].(string)
|
||||
|
||||
// Convert to Subscription format
|
||||
subscription := model.Subscription{
|
||||
Subscriber: model.Subscriber{
|
||||
SubscriberID: recordName, // Use record_name as subscriber ID
|
||||
URL: entityURL,
|
||||
SubscriberID: recordName,
|
||||
URL: req.URL,
|
||||
Domain: req.Domain,
|
||||
Type: req.Type,
|
||||
},
|
||||
SigningPublicKey: publicKey,
|
||||
Status: state,
|
||||
Created: parseTime(createdAt),
|
||||
Updated: parseTime(updatedAt),
|
||||
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
|
||||
|
||||
@@ -139,11 +139,14 @@ func TestLookup(t *testing.T) {
|
||||
"data": map[string]interface{}{
|
||||
"record_name": "bap-network",
|
||||
"details": map[string]interface{}{
|
||||
"entity_name": "BAP Network Provider",
|
||||
"entity_url": "https://bap-network.example.com",
|
||||
"publicKey": "test-public-key",
|
||||
"keyType": "ed25519",
|
||||
"keyFormat": "base64",
|
||||
"key_id": "b692d295-5425-40f5-af77-d62646841dca",
|
||||
"signing_public_key": "test-public-key",
|
||||
"encr_public_key": "test-encr-key",
|
||||
"status": "SUBSCRIBED",
|
||||
"created": "2023-01-01T00:00:00Z",
|
||||
"updated": "2023-01-01T00:00:00Z",
|
||||
"valid_from": "2023-01-01T00:00:00Z",
|
||||
"valid_until": "2024-01-01T00:00:00Z",
|
||||
},
|
||||
"state": "live",
|
||||
"created_at": "2023-01-01T00:00:00Z",
|
||||
@@ -192,8 +195,8 @@ func TestLookup(t *testing.T) {
|
||||
if subscription.SigningPublicKey != "test-public-key" {
|
||||
t.Errorf("Expected signing_public_key test-public-key, got %s", subscription.SigningPublicKey)
|
||||
}
|
||||
if subscription.Status != "live" {
|
||||
t.Errorf("Expected status live, got %s", subscription.Status)
|
||||
if subscription.Status != "SUBSCRIBED" {
|
||||
t.Errorf("Expected status SUBSCRIBED, got %s", subscription.Status)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -259,13 +262,13 @@ func TestLookup(t *testing.T) {
|
||||
})
|
||||
|
||||
// Test missing required fields
|
||||
t.Run("missing entity_name", func(t *testing.T) {
|
||||
t.Run("missing signing_public_key", func(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
response := map[string]interface{}{
|
||||
"data": map[string]interface{}{
|
||||
"details": map[string]interface{}{
|
||||
"entity_url": "https://test.example.com",
|
||||
"publicKey": "test-public-key",
|
||||
"key_id": "test-key-id",
|
||||
"status": "SUBSCRIBED",
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -294,7 +297,7 @@ func TestLookup(t *testing.T) {
|
||||
}
|
||||
_, err = client.Lookup(ctx, req)
|
||||
if err == nil {
|
||||
t.Error("Expected error for missing details field, got nil")
|
||||
t.Error("Expected error for missing signing_public_key, got nil")
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
Reference in New Issue
Block a user