Router plugin

This commit is contained in:
tanyamadaan
2025-03-19 22:48:50 +05:30
parent d422743dcc
commit 8cd3c80ced
7 changed files with 695 additions and 0 deletions

1
go.mod
View File

@@ -13,4 +13,5 @@ require (
require (
golang.org/x/sys v0.31.0 // indirect
golang.org/x/text v0.23.0 // indirect
gopkg.in/yaml.v3 v3.0.1
)

2
go.sum
View File

@@ -12,3 +12,5 @@ gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

View File

@@ -0,0 +1,24 @@
package definition
import (
"context"
"net/url"
)
// Route defines the structure for the Route returned
type Route struct {
RoutingType string // "url" or "msgq"
TopicID string // For message queues
TargetURL string // For API calls
}
// RouterProvider initializes the a new Router instance with the given config
type RouterProvider interface {
New(ctx context.Context, config map[string]string) (Router, func() error, error)
}
// Router defines the interface for routing requests
type Router interface {
// Route determines the routing destination based on the request context.
Route(ctx context.Context, url *url.URL, body []byte) (*Route, error)
}

View File

@@ -0,0 +1,31 @@
package main
import (
"context"
"errors"
definition "github.com/beckn/beckn-onix/pkg/plugin/definition"
router "github.com/beckn/beckn-onix/pkg/plugin/implementation/router"
)
// RouterProvider provides instances of Router.
type RouterProvider struct{}
// New initializes a new Router instance.
func (rp RouterProvider) New(ctx context.Context, config map[string]string) (definition.Router, func() error, error) {
if ctx == nil {
return nil, nil, errors.New("context cannot be nil")
}
// Parse the routing_config key from the config map
routingConfig, ok := config["routing_config"]
if !ok {
return nil, nil, errors.New("routing_config is required in the configuration")
}
return router.New(ctx, &router.Config{
RoutingConfig: routingConfig,
})
}
// Provider is the exported symbol that the plugin manager will look for.
var Provider definition.RouterProvider = RouterProvider{}

View File

@@ -0,0 +1,145 @@
package main
import (
"context"
"os"
"path/filepath"
"strings"
"testing"
)
// setupTestConfig creates a temporary directory and writes a sample routing rules file.
func setupTestConfig(t *testing.T) string {
t.Helper()
// Create a temporary directory for the routing rules
configDir, err := os.MkdirTemp("", "routing_rules")
if err != nil {
t.Fatalf("Failed to create temp directory: %v", err)
}
// Define sample routing rules
rulesContent := `
routing_rules:
- domain: "ONDC:TRV11"
version: "2.0.0"
routing_type: "url"
target:
url: "https://services-backend/trv/v1"
endpoints:
- select
- init
- confirm
- status
- domain: "ONDC:TRV11"
version: "2.0.0"
routing_type: "msgq"
target:
topic_id: "trv_topic_id1"
endpoints:
- search
`
// Write the routing rules to a file
rulesFilePath := filepath.Join(configDir, "routing_rules.yaml")
if err := os.WriteFile(rulesFilePath, []byte(rulesContent), 0644); err != nil {
t.Fatalf("Failed to write routing rules file: %v", err)
}
return rulesFilePath
}
// TestRouterProvider_Success tests the RouterProvider implementation for success cases.
func TestRouterProvider_Success(t *testing.T) {
rulesFilePath := setupTestConfig(t)
defer os.RemoveAll(filepath.Dir(rulesFilePath))
// Define test cases
tests := []struct {
name string
ctx context.Context
config map[string]string
}{
{
name: "Valid configuration",
ctx: context.Background(),
config: map[string]string{
"routing_config": rulesFilePath,
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
provider := RouterProvider{}
router, closeFunc, err := provider.New(tt.ctx, tt.config)
// Ensure no error occurred
if err != nil {
t.Errorf("unexpected error: %v", err)
return
}
// Ensure the router and close function are not nil
if router == nil {
t.Error("expected a non-nil Router instance, got nil")
}
if closeFunc == nil {
t.Error("expected a non-nil close function, got nil")
}
// Test the close function
if err := closeFunc(); err != nil {
t.Errorf("close function returned an error: %v", err)
}
})
}
}
// TestRouterProvider_Failure tests the RouterProvider implementation for failure cases.
func TestRouterProvider_Failure(t *testing.T) {
rulesFilePath := setupTestConfig(t)
defer os.RemoveAll(filepath.Dir(rulesFilePath))
// Define test cases
tests := []struct {
name string
ctx context.Context
config map[string]string
expectedError string
}{
{
name: "Empty routing config path",
ctx: context.Background(),
config: map[string]string{
"routing_config": "",
},
expectedError: "failed to load routing rules: routing_config path is empty",
},
{
name: "Missing routing config key",
ctx: context.Background(),
config: map[string]string{},
expectedError: "routing_config is required in the configuration",
},
{
name: "Nil context",
ctx: nil,
config: map[string]string{"routing_config": rulesFilePath},
expectedError: "context cannot be nil",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
provider := RouterProvider{}
_, _, err := provider.New(tt.ctx, tt.config)
// Check for expected error
if err == nil || !strings.Contains(err.Error(), tt.expectedError) {
t.Errorf("expected error %q, got %v", tt.expectedError, err)
}
})
}
}

View File

@@ -0,0 +1,157 @@
package router
import (
"context"
"encoding/json"
"fmt"
"net/url"
"os"
"path"
"strings"
definition "github.com/beckn/beckn-onix/pkg/plugin/definition"
"gopkg.in/yaml.v3"
)
// Config holds the configuration for the Router plugin.
type Config struct {
RoutingConfig string `json:"routing_config"`
}
// RoutingConfig represents the structure of the routing configuration file.
type routingConfig struct {
RoutingRules []routingRule `yaml:"routing_rules"`
}
// Router implements Router interface
type Router struct {
config *Config
rules []routingRule
}
// RoutingRule represents a single routing rule.
type routingRule struct {
Domain string `yaml:"domain"`
Version string `yaml:"version"`
RoutingType string `yaml:"routing_type"` // "url" or "msgq"
Target target `yaml:"target"`
Endpoints []string `yaml:"endpoints"`
}
// Target contains destination-specific details.
type target struct {
URL string `yaml:"url,omitempty"` // For "url" type
TopicID string `yaml:"topic_id,omitempty"` // For "msgq" type
}
// New initializes a new ProxyRouter instance.
func New(ctx context.Context, config *Config) (*Router, func() error, error) {
// Check if config is nil
if config == nil {
return nil, nil, fmt.Errorf("config cannot be nil")
}
router := &Router{
config: config,
}
// Load rules at bootup
if err := router.loadRules(); err != nil {
return nil, nil, fmt.Errorf("failed to load routing rules: %w", err)
}
return router, router.Close, nil
}
// LoadRules reads and parses routing rules from the YAML configuration file.
func (r *Router) loadRules() error {
if r.config.RoutingConfig == "" {
return fmt.Errorf("routing_config path is empty")
}
data, err := os.ReadFile(r.config.RoutingConfig)
if err != nil {
return fmt.Errorf("error reading config file: %w", err)
}
// fmt.Println("Config file content:", string(data))
var config routingConfig
if err := yaml.Unmarshal(data, &config); err != nil {
return fmt.Errorf("error parsing YAML: %w", err)
}
fmt.Println("Parsed config:", config)
fmt.Println("Loaded rules:", config.RoutingRules)
// Validate rules
if err := validateRules(config.RoutingRules); err != nil {
return fmt.Errorf("invalid routing rules: %w", err)
}
r.rules = config.RoutingRules
fmt.Println("Loaded rules:", r.rules)
return nil
}
// validateRules performs basic validation on the loaded routing rules.
func validateRules(rules []routingRule) error {
for _, rule := range rules {
if rule.Domain == "" || rule.Version == "" || rule.RoutingType == "" {
return fmt.Errorf("invalid rule: domain, version, and routing_type are required")
}
switch rule.RoutingType {
case "url":
if rule.Target.URL == "" {
return fmt.Errorf("invalid rule: url is required for routing_type 'url'")
}
case "msgq":
if rule.Target.TopicID == "" {
return fmt.Errorf("invalid rule: topic_id is required for routing_type 'msgq'")
}
default:
return fmt.Errorf("invalid rule: unknown routing_type '%s'", rule.RoutingType)
}
}
return nil
}
// Route determines the routing destination based on the request context.
func (r *Router) Route(ctx context.Context, url *url.URL, body []byte) (*definition.Route, error) {
// Parse the body to extract domain and version
var requestBody struct {
Context struct {
Domain string `json:"domain"`
Version string `json:"version"`
} `json:"context"`
}
if err := json.Unmarshal(body, &requestBody); err != nil {
return nil, fmt.Errorf("error parsing request body: %w", err)
}
// Match the rule
for _, rule := range r.rules {
if rule.Domain == requestBody.Context.Domain && rule.Version == requestBody.Context.Version {
// Check if the endpoint matches
endpoint := path.Base(url.Path)
for _, ep := range rule.Endpoints {
if strings.EqualFold(ep, endpoint) {
return &definition.Route{
RoutingType: rule.RoutingType,
TopicID: rule.Target.TopicID,
TargetURL: rule.Target.URL,
}, nil
}
}
// If domain and version match but endpoint is not found, return an error
return nil, fmt.Errorf("endpoint '%s' is not supported for domain %s and version %s", endpoint, requestBody.Context.Domain, requestBody.Context.Version)
}
}
// return nil, fmt.Errorf("no matching routing rule found for domain %s and version %s", requestBody.Context.Domain, requestBody.Context.Version)
return nil, fmt.Errorf("no matching routing rule found for domain %s and version %s", requestBody.Context.Domain, requestBody.Context.Version)
}
// Close releases resources (mock implementation returning nil).
func (r *Router) Close() error {
return nil
}

View File

@@ -0,0 +1,335 @@
package router
import (
"context"
"net/url"
"os"
"path/filepath"
"strings"
"testing"
)
// setupTestConfig creates a temporary directory and writes a sample routing rules file.
func setupTestConfig(t *testing.T) string {
t.Helper()
// Create a temporary directory for the routing rules
configDir, err := os.MkdirTemp("", "routing_rules")
if err != nil {
t.Fatalf("Failed to create temp directory: %v", err)
}
// Define sample routing rules
rulesContent := `
routing_rules:
- domain: "ONDC:TRV11"
version: "2.0.0"
routing_type: "url"
target:
url: "https://services-backend/trv/v1"
endpoints:
- select
- init
- confirm
- status
- domain: "ONDC:TRV11"
version: "2.0.0"
routing_type: "msgq"
target:
topic_id: "trv_topic_id1"
endpoints:
- search
`
// Write the routing rules to a file
rulesFilePath := filepath.Join(configDir, "routing_rules.yaml")
if err := os.WriteFile(rulesFilePath, []byte(rulesContent), 0644); err != nil {
t.Fatalf("Failed to write routing rules file: %v", err)
}
return rulesFilePath
}
// TestNew tests the New function.
func TestNew(t *testing.T) {
ctx := context.Background()
rulesFilePath := setupTestConfig(t)
defer os.RemoveAll(filepath.Dir(rulesFilePath))
// Define test cases
tests := []struct {
name string
config *Config
expectedError string
}{
{
name: "Valid configuration",
config: &Config{
RoutingConfig: rulesFilePath,
},
expectedError: "",
},
{
name: "Empty routing config path",
config: &Config{
RoutingConfig: "",
},
expectedError: "routing_config path is empty",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
router, closeFunc, err := New(ctx, tt.config)
// Check for expected error
if tt.expectedError != "" {
if err == nil || !strings.Contains(err.Error(), tt.expectedError) {
t.Errorf("expected error %q, got %v", tt.expectedError, err)
}
return
}
// Ensure no error occurred
if err != nil {
t.Errorf("unexpected error: %v", err)
return
}
// Ensure the router and close function are not nil
if router == nil {
t.Error("expected a non-nil Router instance, got nil")
}
if closeFunc == nil {
t.Error("expected a non-nil close function, got nil")
}
// Test the close function
if err := closeFunc(); err != nil {
t.Errorf("close function returned an error: %v", err)
}
})
}
}
// TestValidateRules_Success tests the validate function for success cases.
func TestValidateRules_Success(t *testing.T) {
tests := []struct {
name string
rules []routingRule
}{
{
name: "Valid rules with url routing",
rules: []routingRule{
{
Domain: "example.com",
Version: "1.0.0",
RoutingType: "url",
Target: target{
URL: "https://example.com/api",
},
Endpoints: []string{"search", "select"},
},
},
},
{
name: "Valid rules with msgq routing",
rules: []routingRule{
{
Domain: "example.com",
Version: "1.0.0",
RoutingType: "msgq",
Target: target{
TopicID: "example_topic",
},
Endpoints: []string{"search", "select"},
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := validateRules(tt.rules)
if err != nil {
t.Errorf("unexpected error: %v", err)
}
})
}
}
// TestValidateRules_Failure tests the validate function for failure cases.
func TestValidateRules_Failure(t *testing.T) {
tests := []struct {
name string
rules []routingRule
expectedErr string
}{
{
name: "Missing domain",
rules: []routingRule{
{
Version: "1.0.0",
RoutingType: "url",
Target: target{
URL: "https://example.com/api",
},
Endpoints: []string{"search", "select"},
},
},
expectedErr: "invalid rule: domain, version, and routing_type are required",
},
{
name: "Missing version",
rules: []routingRule{
{
Domain: "example.com",
RoutingType: "url",
Target: target{
URL: "https://example.com/api",
},
Endpoints: []string{"search", "select"},
},
},
expectedErr: "invalid rule: domain, version, and routing_type are required",
},
{
name: "Missing routing_type",
rules: []routingRule{
{
Domain: "example.com",
Version: "1.0.0",
Target: target{
URL: "https://example.com/api",
},
Endpoints: []string{"search", "select"},
},
},
expectedErr: "invalid rule: domain, version, and routing_type are required",
},
{
name: "Invalid routing_type",
rules: []routingRule{
{
Domain: "example.com",
Version: "1.0.0",
RoutingType: "invalid_type",
Target: target{
URL: "https://example.com/api",
},
Endpoints: []string{"search", "select"},
},
},
expectedErr: "invalid rule: unknown routing_type 'invalid_type'",
},
{
name: "Missing url for routing_type: url",
rules: []routingRule{
{
Domain: "example.com",
Version: "1.0.0",
RoutingType: "url",
Target: target{
// URL is missing
},
Endpoints: []string{"search", "select"},
},
},
expectedErr: "invalid rule: url is required for routing_type 'url'",
},
{
name: "Missing topic_id for routing_type: msgq",
rules: []routingRule{
{
Domain: "example.com",
Version: "1.0.0",
RoutingType: "msgq",
Target: target{
// TopicID is missing
},
Endpoints: []string{"search", "select"},
},
},
expectedErr: "invalid rule: topic_id is required for routing_type 'msgq'",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := validateRules(tt.rules)
if err == nil {
t.Errorf("expected error: %v, got nil", tt.expectedErr)
} else if err.Error() != tt.expectedErr {
t.Errorf("expected error: %v, got: %v", tt.expectedErr, err)
}
})
}
}
// TestRoute tests the Route function.
func TestRoute(t *testing.T) {
ctx := context.Background()
rulesFilePath := setupTestConfig(t)
defer os.RemoveAll(filepath.Dir(rulesFilePath))
config := &Config{
RoutingConfig: rulesFilePath,
}
router, closeFunc, err := New(ctx, config)
if err != nil {
t.Fatalf("New failed: %v", err)
}
defer func() {
if err := closeFunc(); err != nil {
t.Errorf("closeFunc failed: %v", err)
}
}()
// Define test cases
tests := []struct {
name string
url string
body string
expectedError string
}{
{
name: "Valid domain, version, and endpoint",
url: "https://example.com/v1/ondc/select",
body: `{"context": {"domain": "ONDC:TRV11", "version": "2.0.0"}}`,
},
{
name: "Unsupported endpoint",
url: "https://example.com/v1/ondc/unsupported",
body: `{"context": {"domain": "ONDC:TRV11", "version": "2.0.0"}}`,
expectedError: "endpoint 'unsupported' is not supported for domain ONDC:TRV11 and version 2.0.0",
},
{
name: "No matching rule",
url: "https://example.com/v1/ondc/select",
body: `{"context": {"domain": "ONDC:SRV11", "version": "2.0.0"}}`,
expectedError: "no matching routing rule found for domain ONDC:SRV11 and version 2.0.0",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
parsedURL, _ := url.Parse(tt.url)
_, err := router.Route(ctx, parsedURL, []byte(tt.body))
// Check for expected error
if tt.expectedError != "" {
if err == nil || !strings.Contains(err.Error(), tt.expectedError) {
t.Errorf("expected error %q, got %v", tt.expectedError, err)
}
return
}
// Ensure no error occurred
if err != nil {
t.Errorf("unexpected error: %v", err)
}
})
}
}