From 96c8eec809ed33d5a1eeae0b0f32ecb6d91ff2be Mon Sep 17 00:00:00 2001 From: tanyamadaan Date: Fri, 21 Mar 2025 23:45:08 +0530 Subject: [PATCH 1/6] Changes to plugin manager --- pkg/plugin/manager.go | 44 ++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 43 insertions(+), 1 deletion(-) diff --git a/pkg/plugin/manager.go b/pkg/plugin/manager.go index 209e0e6..6b912f8 100644 --- a/pkg/plugin/manager.go +++ b/pkg/plugin/manager.go @@ -19,6 +19,7 @@ type Config struct { Encrypter PluginConfig `yaml:"encrypter"` Publisher PluginConfig `yaml:"publisher"` SchemaValidator PluginConfig `yaml:"schemaValidator"` + Router PluginConfig `yaml:"router"` } // PluginConfig represents configuration details for a plugin. @@ -40,6 +41,7 @@ type Manager struct { ep definition.EncrypterProvider pb definition.PublisherProvider svp definition.SchemaValidatorProvider + rp definition.RouterProvider cfg *Config } @@ -79,7 +81,19 @@ func NewManager(ctx context.Context, cfg *Config) (*Manager, error) { return nil, fmt.Errorf("failed to load encryption plugin: %w", err) } - return &Manager{sp: sp, vp: vp, pb: pb, ep: ep, dp: dp, cfg: cfg}, nil + // Load router plugin. + rp, err := provider[definition.RouterProvider](cfg.Root, cfg.Router.ID) + if err != nil { + return nil, fmt.Errorf("failed to load encryption plugin: %w", err) + } + + // Load schema validator plugin + svp, err := provider[definition.SchemaValidatorProvider](cfg.Root, cfg.SchemaValidator.ID) + if err != nil { + return nil, fmt.Errorf("failed to load validator plugin: %w", err) + } + + return &Manager{sp: sp, vp: vp, pb: pb, ep: ep, dp: dp, rp: rp, svp: svp, cfg: cfg}, nil } // provider loads a plugin dynamically and retrieves its provider instance. @@ -176,3 +190,31 @@ func (m *Manager) Publisher(ctx context.Context) (definition.Publisher, error) { } return publisher, nil } + +// SchemaValidator retrieves the validation plugin instances. +func (m *Manager) SchemaValidator(ctx context.Context) (definition.SchemaValidator, func() error, error) { + if m.svp == nil { + return nil, nil, fmt.Errorf("schema validator plugin provider not loaded") + + } + schemaValidator, close, err := m.svp.New(ctx, m.cfg.SchemaValidator.Config) + if err != nil { + + return nil, nil, fmt.Errorf("failed to initialize schema validator: %v", err) + } + return schemaValidator, close, nil +} + +// Router retrieves the router plugin instances. +func (m *Manager) Router(ctx context.Context) (definition.Router, func() error, error) { + if m.rp == nil { + return nil, nil, fmt.Errorf("router plugin provider not loaded") + + } + schemaValidator, close, err := m.rp.New(ctx, m.cfg.Router.Config) + if err != nil { + + return nil, nil, fmt.Errorf("failed to initialize schema validator: %v", err) + } + return schemaValidator, close, nil +} From a5e0c0ca198429e2286505edeaa812f7765d4521 Mon Sep 17 00:00:00 2001 From: tanyamadaan Date: Fri, 21 Mar 2025 23:48:04 +0530 Subject: [PATCH 2/6] Removed unused files --- pkg/plugin/definition/schemaValidator.go | 16 - .../schemaValidator/cmd/plugin.go | 33 -- .../schemaValidator/cmd/plugin_test.go | 160 -------- .../schemaValidator/schemaValidator.go | 202 ---------- .../schemaValidator/schemaValidator_test.go | 369 ------------------ pkg/plugin/manager.go | 43 +- 6 files changed, 8 insertions(+), 815 deletions(-) delete mode 100644 pkg/plugin/definition/schemaValidator.go delete mode 100644 pkg/plugin/implementation/schemaValidator/cmd/plugin.go delete mode 100644 pkg/plugin/implementation/schemaValidator/cmd/plugin_test.go delete mode 100644 pkg/plugin/implementation/schemaValidator/schemaValidator.go delete mode 100644 pkg/plugin/implementation/schemaValidator/schemaValidator_test.go diff --git a/pkg/plugin/definition/schemaValidator.go b/pkg/plugin/definition/schemaValidator.go deleted file mode 100644 index dc69ca5..0000000 --- a/pkg/plugin/definition/schemaValidator.go +++ /dev/null @@ -1,16 +0,0 @@ -package definition - -import ( - "context" - "net/url" -) - -// SchemaValidator interface for schema validation. -type SchemaValidator interface { - Validate(ctx context.Context, url *url.URL, payload []byte) error -} - -// SchemaValidatorProvider interface for creating validators. -type SchemaValidatorProvider interface { - New(ctx context.Context, config map[string]string) (SchemaValidator, func() error, error) -} diff --git a/pkg/plugin/implementation/schemaValidator/cmd/plugin.go b/pkg/plugin/implementation/schemaValidator/cmd/plugin.go deleted file mode 100644 index 6882ce4..0000000 --- a/pkg/plugin/implementation/schemaValidator/cmd/plugin.go +++ /dev/null @@ -1,33 +0,0 @@ -package main - -import ( - "context" - "errors" - - definition "github.com/beckn/beckn-onix/pkg/plugin/definition" - schemaValidator "github.com/beckn/beckn-onix/pkg/plugin/implementation/schemaValidator" -) - -// schemaValidatorProvider provides instances of schemaValidator. -type schemaValidatorProvider struct{} - -// New initializes a new Verifier instance. -func (vp schemaValidatorProvider) New(ctx context.Context, config map[string]string) (definition.SchemaValidator, func() error, error) { - if ctx == nil { - return nil, nil, errors.New("context cannot be nil") - } - - // Extract schema_dir from the config map - schemaDir, ok := config["schema_dir"] - if !ok || schemaDir == "" { - return nil, nil, errors.New("config must contain 'schema_dir'") - } - - // Create a new schemaValidator instance with the provided configuration - return schemaValidator.New(ctx, &schemaValidator.Config{ - SchemaDir: schemaDir, - }) -} - -// Provider is the exported symbol that the plugin manager will look for. -var Provider definition.SchemaValidatorProvider = schemaValidatorProvider{} diff --git a/pkg/plugin/implementation/schemaValidator/cmd/plugin_test.go b/pkg/plugin/implementation/schemaValidator/cmd/plugin_test.go deleted file mode 100644 index 7e06b55..0000000 --- a/pkg/plugin/implementation/schemaValidator/cmd/plugin_test.go +++ /dev/null @@ -1,160 +0,0 @@ -package main - -import ( - "context" - "os" - "path/filepath" - "strings" - "testing" -) - -// setupTestSchema creates a temporary directory and writes a sample schema file. -func setupTestSchema(t *testing.T) string { - t.Helper() - - // Create a temporary directory for the schema - schemaDir, err := os.MkdirTemp("", "schemas") - if err != nil { - t.Fatalf("Failed to create temp directory: %v", err) - } - - // Create the directory structure for the schema file - schemaFilePath := filepath.Join(schemaDir, "example", "1.0", "test_schema.json") - if err := os.MkdirAll(filepath.Dir(schemaFilePath), 0755); err != nil { - t.Fatalf("Failed to create schema directory structure: %v", err) - } - - // Define a sample schema - schemaContent := `{ - "type": "object", - "properties": { - "context": { - "type": "object", - "properties": { - "domain": {"type": "string"}, - "version": {"type": "string"} - }, - "required": ["domain", "version"] - } - }, - "required": ["context"] - }` - - // Write the schema to the file - if err := os.WriteFile(schemaFilePath, []byte(schemaContent), 0644); err != nil { - t.Fatalf("Failed to write schema file: %v", err) - } - - return schemaDir -} - -// TestValidatorProviderSuccess tests successful ValidatorProvider implementation. -func TestValidatorProviderSuccess(t *testing.T) { - schemaDir := setupTestSchema(t) - defer os.RemoveAll(schemaDir) - - // Define test cases. - tests := []struct { - name string - ctx context.Context - config map[string]string - expectedError string - }{ - { - name: "Valid schema directory", - ctx: context.Background(), // Valid context - config: map[string]string{"schema_dir": schemaDir}, - expectedError: "", - }, - } - - // Test using table-driven tests - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - vp := schemaValidatorProvider{} - schemaValidator, close, err := vp.New(tt.ctx, tt.config) - - // Ensure no error occurred - if err != nil { - t.Errorf("unexpected error: %v", err) - return - } - - // Ensure the schemaValidator is not nil - if schemaValidator == nil { - t.Error("expected a non-nil schemaValidator, got nil") - } - - // Ensure the close function is not nil - if close == nil { - t.Error("expected a non-nil close function, got nil") - } - - // Test the close function - if err := close(); err != nil { - t.Errorf("close function returned an error: %v", err) - } - }) - } -} - -// TestValidatorProviderSuccess tests cases where ValidatorProvider creation should fail. -func TestValidatorProviderFailure(t *testing.T) { - schemaDir := setupTestSchema(t) - defer os.RemoveAll(schemaDir) - - // Define test cases. - tests := []struct { - name string - ctx context.Context - config map[string]string - expectedError string - }{ - { - name: "Config is empty", - ctx: context.Background(), - config: map[string]string{}, - expectedError: "config must contain 'schema_dir'", - }, - { - name: "schema_dir is empty", - ctx: context.Background(), - config: map[string]string{"schema_dir": ""}, - expectedError: "config must contain 'schema_dir'", - }, - { - name: "Invalid schema directory", - ctx: context.Background(), // Valid context - config: map[string]string{"schema_dir": "/invalid/dir"}, - expectedError: "failed to initialise schemaValidator: schema directory does not exist: /invalid/dir", - }, - { - name: "Nil context", - ctx: nil, // Nil context - config: map[string]string{"schema_dir": schemaDir}, - expectedError: "context cannot be nil", - }, - } - - // Test using table-driven tests - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - vp := schemaValidatorProvider{} - _, _, err := vp.New(tt.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 - } - }) - } -} diff --git a/pkg/plugin/implementation/schemaValidator/schemaValidator.go b/pkg/plugin/implementation/schemaValidator/schemaValidator.go deleted file mode 100644 index a46ceb2..0000000 --- a/pkg/plugin/implementation/schemaValidator/schemaValidator.go +++ /dev/null @@ -1,202 +0,0 @@ -package schemaValidator - -import ( - "context" - "encoding/json" - "fmt" - "net/url" - "os" - "path" - "path/filepath" - "strings" - - response "github.com/beckn/beckn-onix/pkg/response" - - "github.com/santhosh-tekuri/jsonschema/v6" -) - -// Payload represents the structure of the data payload with context information. -type payload struct { - Context struct { - Domain string `json:"domain"` - Version string `json:"version"` - } `json:"context"` -} - -// SchemaValidator implements the Validator interface. -type SchemaValidator struct { - config *Config - schemaCache map[string]*jsonschema.Schema -} - -// Config struct for SchemaValidator. -type Config struct { - SchemaDir string -} - -// New creates a new ValidatorProvider instance. -func New(ctx context.Context, config *Config) (*SchemaValidator, func() error, error) { - // Check if config is nil - if config == nil { - return nil, nil, fmt.Errorf("config cannot be nil") - } - v := &SchemaValidator{ - config: config, - schemaCache: make(map[string]*jsonschema.Schema), - } - - // Call Initialise function to load schemas and get validators - if err := v.initialise(); err != nil { - return nil, nil, fmt.Errorf("failed to initialise schemaValidator: %v", err) - } - return v, v.Close, nil -} - -// Validate validates the given data against the schema. -func (v *SchemaValidator) Validate(ctx context.Context, url *url.URL, data []byte) error { - var payloadData payload - err := json.Unmarshal(data, &payloadData) - if err != nil { - return fmt.Errorf("failed to parse JSON payload: %v", err) - } - - // Extract domain, version, and endpoint from the payload and uri. - cxt_domain := payloadData.Context.Domain - version := payloadData.Context.Version - version = fmt.Sprintf("v%s", version) - - endpoint := path.Base(url.String()) - // ToDo Add debug log here - fmt.Println("Handling request for endpoint:", endpoint) - domain := strings.ToLower(cxt_domain) - domain = strings.ReplaceAll(domain, ":", "_") - - // Construct the schema file name. - schemaFileName := fmt.Sprintf("%s_%s_%s", domain, version, endpoint) - - // Retrieve the schema from the cache. - schema, exists := v.schemaCache[schemaFileName] - if !exists { - return fmt.Errorf("schema not found for domain: %s", schemaFileName) - } - - var jsonData any - if err := json.Unmarshal(data, &jsonData); err != nil { - return fmt.Errorf("failed to parse JSON data: %v", err) - } - err = schema.Validate(jsonData) - if err != nil { - // Handle schema validation errors - if validationErr, ok := err.(*jsonschema.ValidationError); ok { - // Convert validation errors into an array of SchemaValError - var schemaErrors []response.Error - for _, cause := range validationErr.Causes { - // Extract the path and message from the validation error - path := strings.Join(cause.InstanceLocation, ".") // JSON path to the invalid field - message := cause.Error() // Validation error message - - // Append the error to the schemaErrors array - schemaErrors = append(schemaErrors, response.Error{ - Paths: path, - Message: message, - }) - } - // Return the array of schema validation errors - return &response.SchemaValidationErr{Errors: schemaErrors} - } - // Return a generic error for non-validation errors - return fmt.Errorf("validation failed: %v", err) - } - - // Return nil if validation succeeds - return nil -} - -// ValidatorProvider provides instances of Validator. -type ValidatorProvider struct{} - -// Initialise initialises the validator provider by compiling all the JSON schema files -// from the specified directory and storing them in a cache indexed by their schema filenames. -func (v *SchemaValidator) initialise() error { - schemaDir := v.config.SchemaDir - // Check if the directory exists and is accessible. - info, err := os.Stat(schemaDir) - if err != nil { - if os.IsNotExist(err) { - return fmt.Errorf("schema directory does not exist: %s", schemaDir) - } - return fmt.Errorf("failed to access schema directory: %v", err) - } - if !info.IsDir() { - return fmt.Errorf("provided schema path is not a directory: %s", schemaDir) - } - - compiler := jsonschema.NewCompiler() - - // Helper function to process directories recursively. - var processDir func(dir string) error - processDir = func(dir string) error { - entries, err := os.ReadDir(dir) - if err != nil { - return fmt.Errorf("failed to read directory: %v", err) - } - - for _, entry := range entries { - path := filepath.Join(dir, entry.Name()) - if entry.IsDir() { - // Recursively process subdirectories. - if err := processDir(path); err != nil { - return err - } - } else if filepath.Ext(entry.Name()) == ".json" { - // Process JSON files. - compiledSchema, err := compiler.Compile(path) - if err != nil { - return fmt.Errorf("failed to compile JSON schema from file %s: %v", entry.Name(), err) - } - - // Use relative path from schemaDir to avoid absolute paths and make schema keys domain/version specific. - relativePath, err := filepath.Rel(schemaDir, path) - if err != nil { - return fmt.Errorf("failed to get relative path for file %s: %v", entry.Name(), err) - } - // Split the relative path to get domain, version, and schema. - parts := strings.Split(relativePath, string(os.PathSeparator)) - - // Ensure that the file path has at least 3 parts: domain, version, and schema file. - if len(parts) < 3 { - return fmt.Errorf("invalid schema file structure, expected domain/version/schema.json but got: %s", relativePath) - } - - // Extract domain, version, and schema filename from the parts. - // Validate that the extracted parts are non-empty. - domain := strings.TrimSpace(parts[0]) - version := strings.TrimSpace(parts[1]) - schemaFileName := strings.TrimSpace(parts[2]) - schemaFileName = strings.TrimSuffix(schemaFileName, ".json") - - if domain == "" || version == "" || schemaFileName == "" { - return fmt.Errorf("invalid schema file structure, one or more components are empty. Relative path: %s", relativePath) - } - - // Construct a unique key combining domain, version, and schema name (e.g., ondc_trv10_v2.0.0_schema). - uniqueKey := fmt.Sprintf("%s_%s_%s", domain, version, schemaFileName) - // Store the compiled schema in the SchemaCache using the unique key. - v.schemaCache[uniqueKey] = compiledSchema - } - } - return nil - } - - // Start processing from the root schema directory. - if err := processDir(schemaDir); err != nil { - return fmt.Errorf("failed to read schema directory: %v", err) - } - - return nil -} - -// Close releases resources (mock implementation returning nil). -func (v *SchemaValidator) Close() error { - return nil -} diff --git a/pkg/plugin/implementation/schemaValidator/schemaValidator_test.go b/pkg/plugin/implementation/schemaValidator/schemaValidator_test.go deleted file mode 100644 index b60c834..0000000 --- a/pkg/plugin/implementation/schemaValidator/schemaValidator_test.go +++ /dev/null @@ -1,369 +0,0 @@ -package schemaValidator - -import ( - "context" - "net/url" - "os" - "path/filepath" - "strings" - "testing" - - "github.com/santhosh-tekuri/jsonschema/v6" -) - -// setupTestSchema creates a temporary directory and writes a sample schema file. -func setupTestSchema(t *testing.T) string { - t.Helper() - - // Create a temporary directory for the schema - schemaDir, err := os.MkdirTemp("", "schemas") - if err != nil { - t.Fatalf("Failed to create temp directory: %v", err) - } - - // Create the directory structure for the schema file - schemaFilePath := filepath.Join(schemaDir, "example", "v1.0", "endpoint.json") - if err := os.MkdirAll(filepath.Dir(schemaFilePath), 0755); err != nil { - t.Fatalf("Failed to create schema directory structure: %v", err) - } - - // Define a sample schema - schemaContent := `{ - "type": "object", - "properties": { - "context": { - "type": "object", - "properties": { - "domain": {"type": "string"}, - "version": {"type": "string"}, - "action": {"type": "string"} - }, - "required": ["domain", "version", "action"] - } - }, - "required": ["context"] - }` - - // Write the schema to the file - if err := os.WriteFile(schemaFilePath, []byte(schemaContent), 0644); err != nil { - t.Fatalf("Failed to write schema file: %v", err) - } - - return schemaDir -} - -func TestValidator_Validate_Success(t *testing.T) { - tests := []struct { - name string - url string - payload string - wantErr bool - }{ - { - name: "Valid payload", - url: "http://example.com/endpoint", - payload: `{"context": {"domain": "example", "version": "1.0", "action": "endpoint"}}`, - wantErr: false, - }, - } - - // Setup a temporary schema directory for testing - schemaDir := setupTestSchema(t) - defer os.RemoveAll(schemaDir) - - config := &Config{SchemaDir: schemaDir} - v, _, err := New(context.Background(), config) - if err != nil { - t.Fatalf("Failed to create validator: %v", err) - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - u, _ := url.Parse(tt.url) - err := v.Validate(context.Background(), u, []byte(tt.payload)) - if err != nil { - t.Errorf("Unexpected error: %v", err) - } else { - t.Logf("Test %s passed with no errors", tt.name) - } - }) - } -} - -func TestValidator_Validate_Failure(t *testing.T) { - tests := []struct { - name string - url string - payload string - wantErr string - }{ - { - name: "Invalid JSON payload", - url: "http://example.com/endpoint", - payload: `{"context": {"domain": "example", "version": "1.0"`, - wantErr: "failed to parse JSON payload", - }, - { - name: "Schema validation failure", - url: "http://example.com/endpoint", - payload: `{"context": {"domain": "example", "version": "1.0"}}`, - wantErr: "context: at '/context': missing property 'action'", - }, - { - name: "Schema not found", - url: "http://example.com/unknown_endpoint", - payload: `{"context": {"domain": "example", "version": "1.0"}}`, - wantErr: "schema not found for domain", - }, - } - - // Setup a temporary schema directory for testing - schemaDir := setupTestSchema(t) - defer os.RemoveAll(schemaDir) - - config := &Config{SchemaDir: schemaDir} - v, _, err := New(context.Background(), config) - if err != nil { - t.Fatalf("Failed to create validator: %v", err) - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - u, _ := url.Parse(tt.url) - err := v.Validate(context.Background(), u, []byte(tt.payload)) - if tt.wantErr != "" { - if err == nil { - t.Errorf("Expected error containing '%s', but got nil", tt.wantErr) - } else if !strings.Contains(err.Error(), tt.wantErr) { - t.Errorf("Expected error containing '%s', but got '%v'", tt.wantErr, err) - } else { - t.Logf("Test %s passed with expected error: %v", tt.name, err) - } - } else { - if err != nil { - t.Errorf("Unexpected error: %v", err) - } else { - t.Logf("Test %s passed with no errors", tt.name) - } - } - }) - } -} - -func TestValidator_Initialise(t *testing.T) { - tests := []struct { - name string - setupFunc func(schemaDir string) error - wantErr string - }{ - { - name: "Schema directory does not exist", - setupFunc: func(schemaDir string) error { - // Do not create the schema directory - return nil - - }, - wantErr: "schema directory does not exist", - }, - { - name: "Schema path is not a directory", - setupFunc: func(schemaDir string) error { - // Create a file instead of a directory - return os.WriteFile(schemaDir, []byte{}, 0644) - }, - wantErr: "provided schema path is not a directory", - }, - { - name: "Invalid schema file structure", - setupFunc: func(schemaDir string) error { - // Create an invalid schema file structure - invalidSchemaFile := filepath.Join(schemaDir, "invalid_schema.json") - if err := os.MkdirAll(filepath.Dir(invalidSchemaFile), 0755); err != nil { - t.Fatalf("Failed to create directory: %v", err) - } - return os.WriteFile(invalidSchemaFile, []byte(`{}`), 0644) - }, - wantErr: "invalid schema file structure", - }, - { - name: "Failed to compile JSON schema", - setupFunc: func(schemaDir string) error { - // Create a schema file with invalid JSON - invalidSchemaFile := filepath.Join(schemaDir, "example", "1.0", "endpoint.json") - if err := os.MkdirAll(filepath.Dir(invalidSchemaFile), 0755); err != nil { - t.Fatalf("Failed to create directory: %v", err) - } - return os.WriteFile(invalidSchemaFile, []byte(`{invalid json}`), 0644) - }, - wantErr: "failed to compile JSON schema", - }, - { - name: "Invalid schema file structure with empty components", - setupFunc: func(schemaDir string) error { - // Create a schema file with empty domain, version, or schema name - invalidSchemaFile := filepath.Join(schemaDir, "", "1.0", "endpoint.json") - if err := os.MkdirAll(filepath.Dir(invalidSchemaFile), 0755); err != nil { - t.Fatalf("Failed to create directory: %v", err) - } - return os.WriteFile(invalidSchemaFile, []byte(`{ - "type": "object", - "properties": { - "context": { - "type": "object", - "properties": { - "domain": {"type": "string"}, - "version": {"type": "string"} - }, - "required": ["domain", "version"] - } - }, - "required": ["context"] - }`), 0644) - }, - wantErr: "failed to read schema directory: invalid schema file structure, expected domain/version/schema.json but got: 1.0/endpoint.json", - }, - { - name: "Failed to read directory", - setupFunc: func(schemaDir string) error { - // Create a directory and remove read permissions - if err := os.MkdirAll(schemaDir, 0000); err != nil { - t.Fatalf("Failed to create directory: %v", err) - } - return nil - }, - wantErr: "failed to read directory", - }, - { - name: "Valid schema directory", - setupFunc: func(schemaDir string) error { - // Create a valid schema file - validSchemaFile := filepath.Join(schemaDir, "example", "1.0", "endpoint.json") - if err := os.MkdirAll(filepath.Dir(validSchemaFile), 0755); err != nil { - t.Fatalf("Failed to create directory: %v", err) - } - return os.WriteFile(validSchemaFile, []byte(`{ - "type": "object", - "properties": { - "context": { - "type": "object", - "properties": { - "domain": {"type": "string"}, - "version": {"type": "string"} - }, - "required": ["domain", "version"] - } - }, - "required": ["context"] - }`), 0644) - }, - wantErr: "", - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - // Setup a temporary schema directory for testing - schemaDir := filepath.Join(os.TempDir(), "schemas") - defer os.RemoveAll(schemaDir) - - // Run the setup function to prepare the test case - if err := tt.setupFunc(schemaDir); err != nil { - t.Fatalf("setupFunc() error = %v", err) - } - - config := &Config{SchemaDir: schemaDir} - v := &SchemaValidator{ - config: config, - schemaCache: make(map[string]*jsonschema.Schema), - } - - err := v.initialise() - if (err != nil && !strings.Contains(err.Error(), tt.wantErr)) || (err == nil && tt.wantErr != "") { - t.Errorf("Error: initialise() returned error = %v, expected error = %v", err, tt.wantErr) - } else if err == nil { - t.Logf("Test %s passed: validator initialized successfully", tt.name) - } else { - t.Logf("Test %s passed with expected error: %v", tt.name, err) - } - }) - } -} - -func TestValidator_New_Success(t *testing.T) { - schemaDir := setupTestSchema(t) - defer os.RemoveAll(schemaDir) - - config := &Config{SchemaDir: schemaDir} - _, _, err := New(context.Background(), config) - if err != nil { - t.Errorf("Unexpected error: %v", err) - } -} - -func TestValidator_New_Failure(t *testing.T) { - tests := []struct { - name string - config *Config - setupFunc func(schemaDir string) error - wantErr string - }{ - { - name: "Config is nil", - config: nil, - setupFunc: func(schemaDir string) error { - return nil - }, - wantErr: "config cannot be nil", - }, - // { - // name: "Config is empty", - // config: &Config{}, - // setupFunc: func(schemaDir string) error { - // return nil - // }, - // wantErr: "config must contain 'schema_dir'", - // }, - // { - // name: "schema_dir is empty", - // config: &Config{SchemaDir: ""}, - // setupFunc: func(schemaDir string) error { - // return nil - // }, - // wantErr: "config must contain 'schema_dir'", - // }, - { - name: "Failed to initialise validators", - config: &Config{ - SchemaDir: "/invalid/path", - }, - setupFunc: func(schemaDir string) error { - // Do not create the schema directory - return nil - }, - wantErr: "ailed to initialise schemaValidator: schema directory does not exist: /invalid/path", - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - // Run the setup function if provided - if tt.setupFunc != nil { - schemaDir := "" - if tt.config != nil { - schemaDir = tt.config.SchemaDir - } - if err := tt.setupFunc(schemaDir); err != nil { - t.Fatalf("Setup function failed: %v", err) - } - } - - // Call the New function with the test config - _, _, err := New(context.Background(), tt.config) - if (err != nil && !strings.Contains(err.Error(), tt.wantErr)) || (err == nil && tt.wantErr != "") { - t.Errorf("Error: New() returned error = %v, expected error = %v", err, tt.wantErr) - } else { - t.Logf("Test %s passed with expected error: %v", tt.name, err) - } - }) - } -} diff --git a/pkg/plugin/manager.go b/pkg/plugin/manager.go index 6b912f8..6a4cecd 100644 --- a/pkg/plugin/manager.go +++ b/pkg/plugin/manager.go @@ -12,14 +12,13 @@ import ( // Config represents the plugin manager configuration. type Config struct { - Root string `yaml:"root"` - Signer PluginConfig `yaml:"signer"` - Verifier PluginConfig `yaml:"verifier"` - Decrypter PluginConfig `yaml:"decrypter"` - Encrypter PluginConfig `yaml:"encrypter"` - Publisher PluginConfig `yaml:"publisher"` - SchemaValidator PluginConfig `yaml:"schemaValidator"` - Router PluginConfig `yaml:"router"` + Root string `yaml:"root"` + Signer PluginConfig `yaml:"signer"` + Verifier PluginConfig `yaml:"verifier"` + Decrypter PluginConfig `yaml:"decrypter"` + Encrypter PluginConfig `yaml:"encrypter"` + Publisher PluginConfig `yaml:"publisher"` + Router PluginConfig `yaml:"router"` } // PluginConfig represents configuration details for a plugin. @@ -28,11 +27,6 @@ type PluginConfig struct { Config map[string]string `yaml:"config"` } -// SchemaDetails contains information about the plugin schema directory. -type SchemaDetails struct { - SchemaDir string `yaml:"schemaDir"` -} - // Manager handles dynamic plugin loading and management. type Manager struct { sp definition.SignerProvider @@ -40,7 +34,6 @@ type Manager struct { dp definition.DecrypterProvider ep definition.EncrypterProvider pb definition.PublisherProvider - svp definition.SchemaValidatorProvider rp definition.RouterProvider cfg *Config } @@ -87,13 +80,7 @@ func NewManager(ctx context.Context, cfg *Config) (*Manager, error) { return nil, fmt.Errorf("failed to load encryption plugin: %w", err) } - // Load schema validator plugin - svp, err := provider[definition.SchemaValidatorProvider](cfg.Root, cfg.SchemaValidator.ID) - if err != nil { - return nil, fmt.Errorf("failed to load validator plugin: %w", err) - } - - return &Manager{sp: sp, vp: vp, pb: pb, ep: ep, dp: dp, rp: rp, svp: svp, cfg: cfg}, nil + return &Manager{sp: sp, vp: vp, pb: pb, ep: ep, dp: dp, rp: rp, cfg: cfg}, nil } // provider loads a plugin dynamically and retrieves its provider instance. @@ -191,20 +178,6 @@ func (m *Manager) Publisher(ctx context.Context) (definition.Publisher, error) { return publisher, nil } -// SchemaValidator retrieves the validation plugin instances. -func (m *Manager) SchemaValidator(ctx context.Context) (definition.SchemaValidator, func() error, error) { - if m.svp == nil { - return nil, nil, fmt.Errorf("schema validator plugin provider not loaded") - - } - schemaValidator, close, err := m.svp.New(ctx, m.cfg.SchemaValidator.Config) - if err != nil { - - return nil, nil, fmt.Errorf("failed to initialize schema validator: %v", err) - } - return schemaValidator, close, nil -} - // Router retrieves the router plugin instances. func (m *Manager) Router(ctx context.Context) (definition.Router, func() error, error) { if m.rp == nil { From a9ffe29a6e0d330bd5db733aee84e1ab0d007cb6 Mon Sep 17 00:00:00 2001 From: tanyamadaan Date: Thu, 27 Mar 2025 11:28:45 +0530 Subject: [PATCH 3/6] updated router --- go.mod | 14 +- go.sum | 8 +- pkg/plugin/definition/router.go | 12 +- .../implementation/router/cmd/plugin.go | 2 +- .../implementation/router/cmd/plugin_test.go | 94 ++++---- pkg/plugin/implementation/router/router.go | 210 +++++++++-------- .../implementation/router/router_test.go | 216 ++++++++---------- .../router/testData/bap_caller.yaml | 6 +- .../router/testData/bap_receiver.yaml | 6 +- .../router/testData/bpp_caller.yaml | 4 +- .../router/testData/bpp_receiver.yaml | 8 +- pkg/plugin/manager.go | 24 +- 12 files changed, 282 insertions(+), 322 deletions(-) diff --git a/go.mod b/go.mod index c2692fb..8c19ef1 100644 --- a/go.mod +++ b/go.mod @@ -1,13 +1,8 @@ module github.com/beckn/beckn-onix -go 1.23.4 +go 1.24 -toolchain go1.23.7 - -require ( - github.com/santhosh-tekuri/jsonschema/v6 v6.0.1 - golang.org/x/crypto v0.36.0 -) +require golang.org/x/crypto v0.36.0 require ( github.com/kr/pretty v0.3.1 // indirect @@ -15,10 +10,7 @@ require ( gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect ) -require ( - github.com/zenazn/pkcs7pad v0.0.0-20170308005700-253a5b1f0e03 - golang.org/x/text v0.23.0 // indirect -) +require github.com/zenazn/pkcs7pad v0.0.0-20170308005700-253a5b1f0e03 require ( golang.org/x/sys v0.31.0 // indirect diff --git a/go.sum b/go.sum index 896f459..b4c0bb0 100644 --- a/go.sum +++ b/go.sum @@ -1,6 +1,4 @@ github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= -github.com/dlclark/regexp2 v1.11.0 h1:G/nrcoOa7ZXlpoa/91N3X7mM3r8eIlMBBJZvsz/mxKI= -github.com/dlclark/regexp2 v1.11.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= @@ -12,18 +10,14 @@ github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsK github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= -github.com/santhosh-tekuri/jsonschema/v6 v6.0.1 h1:PKK9DyHxif4LZo+uQSgXNqs0jj5+xZwwfKHgph2lxBw= -github.com/santhosh-tekuri/jsonschema/v6 v6.0.1/go.mod h1:JXeL+ps8p7/KNMjDQk3TCwPpBy0wYklyWTfbkIzdIFU= github.com/zenazn/pkcs7pad v0.0.0-20170308005700-253a5b1f0e03 h1:m1h+vudopHsI67FPT9MOncyndWhTcdUoBtI1R1uajGY= github.com/zenazn/pkcs7pad v0.0.0-20170308005700-253a5b1f0e03/go.mod h1:8sheVFH84v3PCyFY/O02mIgSQY9I6wMYPWsq7mDnEZY= golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34= golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc= golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= -golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= -golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= -gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= \ No newline at end of file +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/pkg/plugin/definition/router.go b/pkg/plugin/definition/router.go index b9f7e41..399143d 100644 --- a/pkg/plugin/definition/router.go +++ b/pkg/plugin/definition/router.go @@ -5,19 +5,19 @@ import ( "net/url" ) -// Route defines the structure for the Route returned +// 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 + TargetType string // "url" or "msgq" + PublisherID string // For message queues + URL string // For API calls } -// RouterProvider initializes the a new Router instance with the given config +// 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 +// 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) diff --git a/pkg/plugin/implementation/router/cmd/plugin.go b/pkg/plugin/implementation/router/cmd/plugin.go index f38ffa9..23816ee 100644 --- a/pkg/plugin/implementation/router/cmd/plugin.go +++ b/pkg/plugin/implementation/router/cmd/plugin.go @@ -17,7 +17,7 @@ func (rp RouterProvider) New(ctx context.Context, config map[string]string) (def return nil, nil, errors.New("context cannot be nil") } - // Parse the routing_config key from the config map + // Parse the routingConfig key from the config map routingConfig, ok := config["routingConfig"] if !ok { return nil, nil, errors.New("routingConfig is required in the configuration") diff --git a/pkg/plugin/implementation/router/cmd/plugin_test.go b/pkg/plugin/implementation/router/cmd/plugin_test.go index afa0c97..08c9bd7 100644 --- a/pkg/plugin/implementation/router/cmd/plugin_test.go +++ b/pkg/plugin/implementation/router/cmd/plugin_test.go @@ -4,6 +4,7 @@ import ( "context" "os" "path/filepath" + "runtime" "strings" "testing" ) @@ -12,42 +13,23 @@ import ( func setupTestConfig(t *testing.T) string { t.Helper() - // Create a temporary directory for the routing rules - configDir, err := os.MkdirTemp("", "routingRules") + // Get project root (assuming testData is in project root) + _, filename, _, _ := runtime.Caller(0) // Path to plugin_test.go + projectRoot := filepath.Dir(filepath.Dir(filename)) // Move up from cmd/ + yamlPath := filepath.Join(projectRoot, "testData", "bap_receiver.yaml") + + // Copy to temp file (to test file loading logic) + tempDir := t.TempDir() + tempPath := filepath.Join(tempDir, "routingRules.yaml") + content, err := os.ReadFile(yamlPath) if err != nil { - t.Fatalf("Failed to create temp directory: %v", err) + t.Fatalf("Failed to read test file: %v", err) + } + if err := os.WriteFile(tempPath, content, 0644); err != nil { + t.Fatalf("Failed to create temp config: %v", err) } - // Define sample routing rules - rulesContent := ` -routingRules: - - domain: "ONDC:TRV11" - version: "2.0.0" - routingType: "url" - target: - url: "https://services-backend/trv/v1" - endpoints: - - select - - init - - confirm - - status - - - domain: "ONDC:TRV11" - version: "2.0.0" - routingType: "msgq" - target: - topic_id: "trv_topic_id1" - endpoints: - - search -` - - // Write the routing rules to a file - rulesFilePath := filepath.Join(configDir, "routingRules.yaml") - if err := os.WriteFile(rulesFilePath, []byte(rulesContent), 0644); err != nil { - t.Fatalf("Failed to write routing rules file: %v", err) - } - - return rulesFilePath + return tempPath } // TestRouterProviderSuccess tests the RouterProvider implementation for success cases. @@ -57,9 +39,10 @@ func TestRouterProviderSuccess(t *testing.T) { // Define test cases tests := []struct { - name string - ctx context.Context - config map[string]string + name string + ctx context.Context + config map[string]string + wantErr bool }{ { name: "Valid configuration", @@ -67,6 +50,7 @@ func TestRouterProviderSuccess(t *testing.T) { config: map[string]string{ "routingConfig": rulesFilePath, }, + wantErr: false, }, } @@ -76,14 +60,14 @@ func TestRouterProviderSuccess(t *testing.T) { router, _, err := provider.New(tt.ctx, tt.config) // Ensure no error occurred - if err != nil { - t.Errorf("unexpected error: %v", err) + if (err != nil) != tt.wantErr { + t.Errorf("New(%v, %v) error = %v, wantErr %v", tt.ctx, tt.config, err, tt.wantErr) return } // Ensure the router and close function are not nil if router == nil { - t.Error("expected a non-nil Router instance, got nil") + t.Errorf("New(%v, %v) = nil router, want non-nil", tt.ctx, tt.config) } }) } @@ -96,10 +80,10 @@ func TestRouterProviderFailure(t *testing.T) { // Define test cases tests := []struct { - name string - ctx context.Context - config map[string]string - expectedError string + name string + ctx context.Context + config map[string]string + wantErr string }{ { name: "Empty routing config path", @@ -107,19 +91,19 @@ func TestRouterProviderFailure(t *testing.T) { config: map[string]string{ "routingConfig": "", }, - expectedError: "failed to load routing rules: routingConfig path is empty", + wantErr: "failed to load routing rules: routingConfig path is empty", }, { - name: "Missing routing config key", - ctx: context.Background(), - config: map[string]string{}, - expectedError: "routingConfig is required in the configuration", + name: "Missing routing config key", + ctx: context.Background(), + config: map[string]string{}, + wantErr: "routingConfig is required in the configuration", }, { - name: "Nil context", - ctx: nil, - config: map[string]string{"routingConfig": rulesFilePath}, - expectedError: "context cannot be nil", + name: "Nil context", + ctx: nil, + config: map[string]string{"routingConfig": rulesFilePath}, + wantErr: "context cannot be nil", }, } @@ -129,8 +113,10 @@ func TestRouterProviderFailure(t *testing.T) { _, _, 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) + if err == nil { + t.Errorf("New(%v, %v) = nil error, want error containing %q", tt.ctx, tt.config, tt.wantErr) + } else if !strings.Contains(err.Error(), tt.wantErr) { + t.Errorf("New(%v, %v) = %v, want error containing %q", tt.ctx, tt.config, err, tt.wantErr) } }) } diff --git a/pkg/plugin/implementation/router/router.go b/pkg/plugin/implementation/router/router.go index 2649dbd..ae98baf 100644 --- a/pkg/plugin/implementation/router/router.go +++ b/pkg/plugin/implementation/router/router.go @@ -24,27 +24,34 @@ type routingConfig struct { RoutingRules []routingRule `yaml:"routingRules"` } -// Router implements Router interface +// Router implements Router interface. type Router struct { - config *Config - rules []routingRule + rules map[string]map[string]map[string]*definition.Route // domain -> version -> endpoint -> route } // RoutingRule represents a single routing rule. type routingRule struct { - Domain string `yaml:"domain"` - Version string `yaml:"version"` - RoutingType string `yaml:"routingType"` // "url", "msgq", "bpp", or "bap" - Target target `yaml:"target,omitempty"` - Endpoints []string `yaml:"endpoints"` + Domain string `yaml:"domain"` + Version string `yaml:"version"` + TargetType string `yaml:"targetType"` // "url", "msgq", "bpp", or "bap" + Target target `yaml:"target,omitempty"` + Endpoints []string `yaml:"endpoints"` } // Target contains destination-specific details. type target struct { - URL string `yaml:"url,omitempty"` // URL for "url" or gateway endpoint for "bpp"/"bap" - TopicID string `yaml:"topic_id,omitempty"` // For "msgq" type + URL string `yaml:"url,omitempty"` // URL for "url" or gateway endpoint for "bpp"/"bap" + PublisherID string `yaml:"publisherId,omitempty"` // For "msgq" type } +// TargetType defines possible target destinations. +const ( + targetTypeURL = "url" // Route to a specific URL + targetTypeMSGQ = "msgq" // Route to a message queue + targetTypeBPP = "bpp" // Route to a BPP endpoint + targetTypeBAP = "bap" // Route to a BAP endpoint +) + // New initializes a new Router instance with the provided configuration. // It loads and validates the routing rules from the specified YAML file. // Returns an error if the configuration is invalid or the rules cannot be loaded. @@ -54,24 +61,24 @@ func New(ctx context.Context, config *Config) (*Router, func() error, error) { return nil, nil, fmt.Errorf("config cannot be nil") } router := &Router{ - config: config, + rules: make(map[string]map[string]map[string]*definition.Route), } // Load rules at bootup - if err := router.loadRules(); err != nil { + if err := router.loadRules(config.RoutingConfig); err != nil { return nil, nil, fmt.Errorf("failed to load routing rules: %w", err) } return router, nil, nil } // LoadRules reads and parses routing rules from the YAML configuration file. -func (r *Router) loadRules() error { - if r.config.RoutingConfig == "" { +func (r *Router) loadRules(configPath string) error { + if configPath == "" { return fmt.Errorf("routingConfig path is empty") } - data, err := os.ReadFile(r.config.RoutingConfig) + data, err := os.ReadFile(configPath) if err != nil { - return fmt.Errorf("error reading config file at %s: %w", r.config.RoutingConfig, err) + return fmt.Errorf("error reading config file at %s: %w", configPath, err) } var config routingConfig if err := yaml.Unmarshal(data, &config); err != nil { @@ -82,36 +89,74 @@ func (r *Router) loadRules() error { if err := validateRules(config.RoutingRules); err != nil { return fmt.Errorf("invalid routing rules: %w", err) } - r.rules = config.RoutingRules + // Build the optimized rule map + for _, rule := range config.RoutingRules { + // Initialize domain map if not exists + if _, ok := r.rules[rule.Domain]; !ok { + r.rules[rule.Domain] = make(map[string]map[string]*definition.Route) + } + + // Initialize version map if not exists + if _, ok := r.rules[rule.Domain][rule.Version]; !ok { + r.rules[rule.Domain][rule.Version] = make(map[string]*definition.Route) + } + + // Add all endpoints for this rule + for _, endpoint := range rule.Endpoints { + var route *definition.Route + switch rule.TargetType { + case targetTypeMSGQ: + route = &definition.Route{ + TargetType: rule.TargetType, + PublisherID: rule.Target.PublisherID, + } + case targetTypeURL: + route = &definition.Route{ + TargetType: rule.TargetType, + URL: rule.Target.URL, + } + case targetTypeBPP, targetTypeBAP: + route = &definition.Route{ + TargetType: rule.TargetType, + URL: rule.Target.URL, // Fallback URL if URI not provided in request + } + } + + fmt.Print(r.rules) + + r.rules[rule.Domain][rule.Version][endpoint] = route + } + } + return nil } // validateRules performs basic validation on the loaded routing rules. func validateRules(rules []routingRule) error { for _, rule := range rules { - // Ensure domain, version, and routingType are present - if rule.Domain == "" || rule.Version == "" || rule.RoutingType == "" { - return fmt.Errorf("invalid rule: domain, version, and routingType are required") + // Ensure domain, version, and TargetType are present + if rule.Domain == "" || rule.Version == "" || rule.TargetType == "" { + return fmt.Errorf("invalid rule: domain, version, and targetType are required") } - // Validate based on routingType - switch rule.RoutingType { - case "url": + // Validate based on TargetType + switch rule.TargetType { + case targetTypeURL: if rule.Target.URL == "" { - return fmt.Errorf("invalid rule: url is required for routingType 'url'") + return fmt.Errorf("invalid rule: url is required for targetType 'url'") } if _, err := url.ParseRequestURI(rule.Target.URL); err != nil { return fmt.Errorf("invalid URL in rule: %w", err) } - case "msgq": - if rule.Target.TopicID == "" { - return fmt.Errorf("invalid rule: topicId is required for routingType 'msgq'") + case targetTypeMSGQ: + if rule.Target.PublisherID == "" { + return fmt.Errorf("invalid rule: publisherID is required for targetType 'msgq'") } - case "bpp", "bap": + case targetTypeBPP, targetTypeBAP: // No target validation needed for bpp/bap, as they use URIs from the request body continue default: - return fmt.Errorf("invalid rule: unknown routingType '%s'", rule.RoutingType) + return fmt.Errorf("invalid rule: unknown targetType '%s'", rule.TargetType) } } return nil @@ -125,8 +170,8 @@ func (r *Router) Route(ctx context.Context, url *url.URL, body []byte) (*definit Context struct { Domain string `json:"domain"` Version string `json:"version"` - BppURI string `json:"bpp_uri,omitempty"` - BapURI string `json:"bap_uri,omitempty"` + BPPURI string `json:"bpp_uri,omitempty"` + BAPURI string `json:"bap_uri,omitempty"` } `json:"context"` } if err := json.Unmarshal(body, &requestBody); err != nil { @@ -136,69 +181,52 @@ func (r *Router) Route(ctx context.Context, url *url.URL, body []byte) (*definit // Extract the endpoint from the URL endpoint := path.Base(url.Path) - // Collect all matching rules for the domain and version - matchingRules := r.getMatchingRules(requestBody.Context.Domain, requestBody.Context.Version) - - // If no matching rules are found, return an error - if len(matchingRules) == 0 { - return nil, fmt.Errorf("no matching routing rule found for domain %s and version %s", requestBody.Context.Domain, requestBody.Context.Version) + // Lookup route in the optimized map + domainRules, ok := r.rules[requestBody.Context.Domain] + if !ok { + return nil, fmt.Errorf("no routing rules found for domain %s", requestBody.Context.Domain) } - // Match the rule - for _, rule := range matchingRules { - for _, ep := range rule.Endpoints { - if strings.EqualFold(ep, endpoint) { - switch rule.RoutingType { - case "msgq": - return &definition.Route{ - RoutingType: rule.RoutingType, - TopicID: rule.Target.TopicID, - }, nil - case "url": - return &definition.Route{ - RoutingType: rule.RoutingType, - TargetURL: rule.Target.URL, - }, nil - case "bpp": - return handleRouting(rule, requestBody.Context.BppURI, endpoint, "bpp") - case "bap": - return handleRouting(rule, requestBody.Context.BapURI, endpoint, "bap") - default: - return nil, fmt.Errorf("unsupported routingType: %s", rule.RoutingType) - } - } + versionRules, ok := domainRules[requestBody.Context.Version] + if !ok { + return nil, fmt.Errorf("no routing rules found for domain %s version %s", requestBody.Context.Domain, requestBody.Context.Version) + } + + route, ok := versionRules[endpoint] + if !ok { + return nil, fmt.Errorf("endpoint '%s' is not supported for domain %s and version %s in routing config", + endpoint, requestBody.Context.Domain, requestBody.Context.Version) + } + + // Handle BPP/BAP routing with request URIs + switch route.TargetType { + case targetTypeBPP: + uri := strings.TrimSpace(requestBody.Context.BPPURI) + target := strings.TrimSpace(route.URL) + if len(uri) != 0 { + target = uri + } + if len(target) == 0 { + return nil, fmt.Errorf("could not determine destination for endpoint '%s': neither request contained a BPP URI nor was a default URL configured in routing rules", endpoint) + } + route = &definition.Route{ + TargetType: route.TargetType, + URL: target, + } + case targetTypeBAP: + uri := strings.TrimSpace(requestBody.Context.BAPURI) + target := strings.TrimSpace(route.URL) + if len(uri) != 0 { + target = uri + } + if len(target) == 0 { + return nil, fmt.Errorf("could not determine destination for endpoint '%s': neither request contained a BAP URI nor was a default URL configured in routing rules", endpoint) + } + route = &definition.Route{ + TargetType: route.TargetType, + URL: target, } } - // 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) -} - -// getMatchingRules returns all rules that match the given domain and version -func (r *Router) getMatchingRules(domain, version string) []routingRule { - var matchingRules []routingRule - for _, rule := range r.rules { - if rule.Domain == domain && rule.Version == version { - matchingRules = append(matchingRules, rule) - } - } - return matchingRules -} - -// handleRouting handles routing for bap and bpp routing type -func handleRouting(rule routingRule, uri, endpoint string, routingType string) (*definition.Route, error) { - if uri == "" { - if rule.Target.URL != "" { - return &definition.Route{ - RoutingType: routingType, - TargetURL: rule.Target.URL, - }, nil - } else { - return nil, fmt.Errorf("no target URI or URL found for %s routing type and %s endpoint", routingType, endpoint) - } - } - return &definition.Route{ - RoutingType: routingType, - TargetURL: uri, - }, nil + return route, nil } diff --git a/pkg/plugin/implementation/router/router_test.go b/pkg/plugin/implementation/router/router_test.go index 3d3fe70..f67b577 100644 --- a/pkg/plugin/implementation/router/router_test.go +++ b/pkg/plugin/implementation/router/router_test.go @@ -15,35 +15,19 @@ var testData embed.FS func setupTestConfig(t *testing.T, yamlFileName string) string { t.Helper() + configDir := t.TempDir() - // Create a temporary directory for the routing rules - configDir, err := os.MkdirTemp("", "routing_rules") + content, err := testData.ReadFile("testData/" + yamlFileName) if err != nil { - t.Fatalf("Failed to create temp directory: %v", err) + t.Fatalf("ReadFile() err = %v, want nil", err) } - // Read the YAML file content - yamlContent := readYAMLFile(t, yamlFileName) - - // Write the routing rules to a file - rulesFilePath := filepath.Join(configDir, "routing_rules.yaml") - if err := os.WriteFile(rulesFilePath, []byte(yamlContent), 0644); err != nil { - t.Fatalf("Failed to write routing rules file: %v", err) + rulesPath := filepath.Join(configDir, "routing_rules.yaml") + if err := os.WriteFile(rulesPath, content, 0644); err != nil { + t.Fatalf("WriteFile() err = %v, want nil", err) } - return rulesFilePath -} - -func readYAMLFile(t *testing.T, fileName string) string { - t.Helper() - - // Read the YAML file - content, err := testData.ReadFile("testData/" + fileName) - if err != nil { - t.Fatalf("Failed to read YAML file: %v", err) - } - - return string(content) + return rulesPath } // setupRouter is a helper function to create router instance. @@ -78,35 +62,35 @@ func TestNew(t *testing.T) { // Define test cases tests := []struct { - name string - config *Config - expectedError string + name string + config *Config + wantErr string }{ { name: "Valid configuration", config: &Config{ RoutingConfig: rulesFilePath, }, - expectedError: "", + wantErr: "", }, { - name: "Empty config", - config: nil, - expectedError: "config cannot be nil", + name: "Empty config", + config: nil, + wantErr: "config cannot be nil", }, { name: "Empty routing config path", config: &Config{ RoutingConfig: "", }, - expectedError: "routingConfig path is empty", + wantErr: "routingConfig path is empty", }, { name: "Routing config file does not exist", config: &Config{ RoutingConfig: "/nonexistent/path/to/rules.yaml", }, - expectedError: "error reading config file", + wantErr: "error reading config file", }, } @@ -115,22 +99,22 @@ func TestNew(t *testing.T) { router, _, 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) + if tt.wantErr != "" { + if err == nil || !strings.Contains(err.Error(), tt.wantErr) { + t.Errorf("New(%v) = %v, want error containing %q", tt.config, err, tt.wantErr) } return } // Ensure no error occurred if err != nil { - t.Errorf("unexpected error: %v", err) + t.Errorf("New(%v) = %v, want nil error", tt.config, err) return } // Ensure the router and close function are not nil if router == nil { - t.Error("expected a non-nil Router instance, got nil") + t.Errorf("New(%v, %v) = nil router, want non-nil", ctx, tt.config) } }) } @@ -148,9 +132,9 @@ func TestValidateRulesSuccess(t *testing.T) { name: "Valid rules with url routing", rules: []routingRule{ { - Domain: "retail", - Version: "1.0.0", - RoutingType: "url", + Domain: "retail", + Version: "1.0.0", + TargetType: "url", Target: target{ URL: "https://example.com/api", }, @@ -162,11 +146,11 @@ func TestValidateRulesSuccess(t *testing.T) { name: "Valid rules with msgq routing", rules: []routingRule{ { - Domain: "retail", - Version: "1.0.0", - RoutingType: "msgq", + Domain: "retail", + Version: "1.0.0", + TargetType: "msgq", Target: target{ - TopicID: "example_topic", + PublisherID: "example_topic", }, Endpoints: []string{"on_search", "on_select"}, }, @@ -176,9 +160,9 @@ func TestValidateRulesSuccess(t *testing.T) { name: "Valid rules with bpp routing to gateway", rules: []routingRule{ { - Domain: "retail", - Version: "1.0.0", - RoutingType: "bpp", + Domain: "retail", + Version: "1.0.0", + TargetType: "bpp", Target: target{ URL: "https://mock_gateway.com/api", }, @@ -190,10 +174,10 @@ func TestValidateRulesSuccess(t *testing.T) { name: "Valid rules with bpp routing", rules: []routingRule{ { - Domain: "retail", - Version: "1.0.0", - RoutingType: "bpp", - Endpoints: []string{"select"}, + Domain: "retail", + Version: "1.0.0", + TargetType: "bpp", + Endpoints: []string{"select"}, }, }, }, @@ -201,10 +185,10 @@ func TestValidateRulesSuccess(t *testing.T) { name: "Valid rules with bap routing", rules: []routingRule{ { - Domain: "retail", - Version: "1.0.0", - RoutingType: "bap", - Endpoints: []string{"select"}, + Domain: "retail", + Version: "1.0.0", + TargetType: "bap", + Endpoints: []string{"select"}, }, }, }, @@ -214,7 +198,7 @@ func TestValidateRulesSuccess(t *testing.T) { t.Run(tt.name, func(t *testing.T) { err := validateRules(tt.rules) if err != nil { - t.Errorf("unexpected error: %v", err) + t.Errorf("validateRules(%v) = %v, want nil error", tt.rules, err) } }) } @@ -223,40 +207,40 @@ func TestValidateRulesSuccess(t *testing.T) { // TestValidateRulesFailure tests the validate function for failure cases. func TestValidateRulesFailure(t *testing.T) { tests := []struct { - name string - rules []routingRule - expectedErr string + name string + rules []routingRule + wantErr string }{ { name: "Missing domain", rules: []routingRule{ { - Version: "1.0.0", - RoutingType: "url", + Version: "1.0.0", + TargetType: "url", Target: target{ URL: "https://example.com/api", }, Endpoints: []string{"search", "select"}, }, }, - expectedErr: "invalid rule: domain, version, and routingType are required", + wantErr: "invalid rule: domain, version, and targetType are required", }, { name: "Missing version", rules: []routingRule{ { - Domain: "retail", - RoutingType: "url", + Domain: "retail", + TargetType: "url", Target: target{ URL: "https://example.com/api", }, Endpoints: []string{"search", "select"}, }, }, - expectedErr: "invalid rule: domain, version, and routingType are required", + wantErr: "invalid rule: domain, version, and targetType are required", }, { - name: "Missing routingType", + name: "Missing targetType", rules: []routingRule{ { Domain: "retail", @@ -267,62 +251,60 @@ func TestValidateRulesFailure(t *testing.T) { Endpoints: []string{"search", "select"}, }, }, - expectedErr: "invalid rule: domain, version, and routingType are required", + wantErr: "invalid rule: domain, version, and targetType are required", }, { - name: "Invalid routingType", + name: "Invalid targetType", rules: []routingRule{ { - Domain: "retail", - Version: "1.0.0", - RoutingType: "invalid", + Domain: "retail", + Version: "1.0.0", + TargetType: "invalid", Target: target{ URL: "https://example.com/api", }, Endpoints: []string{"search", "select"}, }, }, - expectedErr: "invalid rule: unknown routingType 'invalid'", + wantErr: "invalid rule: unknown targetType 'invalid'", }, { - name: "Missing url for routingType: url", + name: "Missing url for targetType: url", rules: []routingRule{ { - Domain: "retail", - Version: "1.0.0", - RoutingType: "url", - Target: target{ + Domain: "retail", + Version: "1.0.0", + TargetType: "url", + Target: target{ // URL is missing }, Endpoints: []string{"search", "select"}, }, }, - expectedErr: "invalid rule: url is required for routingType 'url'", + wantErr: "invalid rule: url is required for targetType 'url'", }, { - name: "Missing topic_id for routingType: msgq", + name: "Missing topic_id for targetType: msgq", rules: []routingRule{ { - Domain: "retail", - Version: "1.0.0", - RoutingType: "msgq", - Target: target{ - // TopicID is missing + Domain: "retail", + Version: "1.0.0", + TargetType: "msgq", + Target: target{ + // PublisherID is missing }, Endpoints: []string{"search", "select"}, }, }, - expectedErr: "invalid rule: topicId is required for routingType 'msgq'", + wantErr: "invalid rule: publisherID is required for targetType '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) + if err == nil || !strings.Contains(err.Error(), tt.wantErr) { + t.Errorf("validateRules(%v) = %v, want error containing %q", tt.rules, err, tt.wantErr) } }) } @@ -387,7 +369,7 @@ func TestRouteSuccess(t *testing.T) { // Ensure no error occurred if err != nil { - t.Errorf("unexpected error: %v", err) + t.Errorf("router.Route(%v, %v, %v) = %v, want nil error", ctx, parsedURL, []byte(tt.body), err) } }) } @@ -399,39 +381,39 @@ func TestRouteFailure(t *testing.T) { // Define failure test cases tests := []struct { - name string - configFile string - url string - body string - expectedError string + name string + configFile string + url string + body string + wantErr string }{ { - name: "Unsupported endpoint", - configFile: "bpp_receiver.yaml", - 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: "Unsupported endpoint", + configFile: "bpp_receiver.yaml", + url: "https://example.com/v1/ondc/unsupported", + body: `{"context": {"domain": "ONDC:TRV11", "version": "2.0.0"}}`, + wantErr: "endpoint 'unsupported' is not supported for domain ONDC:TRV11 and version 2.0.0", }, { - name: "No matching rule", - configFile: "bpp_receiver.yaml", - 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", + name: "No matching rule", + configFile: "bpp_receiver.yaml", + url: "https://example.com/v1/ondc/select", + body: `{"context": {"domain": "ONDC:SRV11", "version": "2.0.0"}}`, + wantErr: "no routing rules found for domain ONDC:SRV11", }, { - name: "Missing bap_uri for bap routing", - configFile: "bpp_caller.yaml", - url: "https://example.com/v1/ondc/on_search", - body: `{"context": {"domain": "ONDC:TRV10", "version": "2.0.0"}}`, - expectedError: "no target URI or URL found for bap routing type and on_search endpoint", + name: "Missing bap_uri for bap routing", + configFile: "bpp_caller.yaml", + url: "https://example.com/v1/ondc/on_search", + body: `{"context": {"domain": "ONDC:TRV10", "version": "2.0.0"}}`, + wantErr: "could not determine destination for endpoint 'on_search': neither request contained a BAP URI nor was a default URL configured in routing rules", }, { - name: "Missing bpp_uri for bpp routing", - configFile: "bap_caller.yaml", - url: "https://example.com/v1/ondc/select", - body: `{"context": {"domain": "ONDC:TRV10", "version": "2.0.0"}}`, - expectedError: "no target URI or URL found for bpp routing type and select endpoint", + name: "Missing bpp_uri for bpp routing", + configFile: "bap_caller.yaml", + url: "https://example.com/v1/ondc/select", + body: `{"context": {"domain": "ONDC:TRV10", "version": "2.0.0"}}`, + wantErr: "could not determine destination for endpoint 'select': neither request contained a BPP URI nor was a default URL configured in routing rules", }, } @@ -444,8 +426,8 @@ func TestRouteFailure(t *testing.T) { _, err := router.Route(ctx, parsedURL, []byte(tt.body)) // Check for expected error - if err == nil || !strings.Contains(err.Error(), tt.expectedError) { - t.Errorf("expected error %q, got %v", tt.expectedError, err) + if err == nil || !strings.Contains(err.Error(), tt.wantErr) { + t.Errorf("Route(%q, %q) = %v, want error containing %q", tt.url, tt.body, err, tt.wantErr) } }) } diff --git a/pkg/plugin/implementation/router/testData/bap_caller.yaml b/pkg/plugin/implementation/router/testData/bap_caller.yaml index b1d5a44..0c595a6 100644 --- a/pkg/plugin/implementation/router/testData/bap_caller.yaml +++ b/pkg/plugin/implementation/router/testData/bap_caller.yaml @@ -1,14 +1,14 @@ routingRules: - domain: "ONDC:TRV10" version: "2.0.0" - routingType: "bpp" + targetType: "bpp" target: url: "https://gateway.example.com" endpoints: - search - domain: "ONDC:TRV10" version: "2.0.0" - routingType: "bpp" + targetType: "bpp" endpoints: - select - init @@ -17,7 +17,7 @@ routingRules: - cancel - domain: "ONDC:TRV12" version: "2.0.0" - routingType: "bpp" + targetType: "bpp" endpoints: - select - init diff --git a/pkg/plugin/implementation/router/testData/bap_receiver.yaml b/pkg/plugin/implementation/router/testData/bap_receiver.yaml index ca4a478..353ecc7 100644 --- a/pkg/plugin/implementation/router/testData/bap_receiver.yaml +++ b/pkg/plugin/implementation/router/testData/bap_receiver.yaml @@ -1,7 +1,7 @@ routingRules: - domain: "ONDC:TRV10" version: "2.0.0" - routingType: "url" + targetType: "url" target: url: "https://services-backend/trv/v1" endpoints: @@ -13,8 +13,8 @@ routingRules: - on_cancel - domain: "ONDC:TRV10" version: "2.0.0" - routingType: "msgq" + targetType: "msgq" target: - topic_id: "trv_topic_id1" + publisherId: "trv_topic_id1" endpoints: - on_search \ No newline at end of file diff --git a/pkg/plugin/implementation/router/testData/bpp_caller.yaml b/pkg/plugin/implementation/router/testData/bpp_caller.yaml index 0d9a670..8d0c212 100644 --- a/pkg/plugin/implementation/router/testData/bpp_caller.yaml +++ b/pkg/plugin/implementation/router/testData/bpp_caller.yaml @@ -1,7 +1,7 @@ routingRules: - domain: "ONDC:TRV10" version: "2.0.0" - routingType: "bap" + targetType: "bap" endpoints: - on_search - on_select @@ -12,7 +12,7 @@ routingRules: - on_cancel - domain: "ONDC:TRV11" version: "2.0.0" - routingType: "bap" + targetType: "bap" endpoints: - on_search - on_select diff --git a/pkg/plugin/implementation/router/testData/bpp_receiver.yaml b/pkg/plugin/implementation/router/testData/bpp_receiver.yaml index 6febce6..9633923 100644 --- a/pkg/plugin/implementation/router/testData/bpp_receiver.yaml +++ b/pkg/plugin/implementation/router/testData/bpp_receiver.yaml @@ -1,7 +1,7 @@ routingRules: - domain: "ONDC:TRV10" version: "2.0.0" - routingType: "url" + targetType: "url" target: url: "https://services-backend/trv/v1" endpoints: @@ -13,15 +13,15 @@ routingRules: - domain: "ONDC:TRV10" version: "2.0.0" - routingType: "msgq" + targetType: "msgq" target: - topic_id: "trv_topic_id1" + publisherId: "trv_topic_id1" endpoints: - search - domain: "ONDC:TRV11" version: "2.0.0" - routingType: "url" + targetType: "url" target: url: "https://services-backend/trv/v1" endpoints: diff --git a/pkg/plugin/manager.go b/pkg/plugin/manager.go index 6a4cecd..86a0b02 100644 --- a/pkg/plugin/manager.go +++ b/pkg/plugin/manager.go @@ -18,7 +18,6 @@ type Config struct { Decrypter PluginConfig `yaml:"decrypter"` Encrypter PluginConfig `yaml:"encrypter"` Publisher PluginConfig `yaml:"publisher"` - Router PluginConfig `yaml:"router"` } // PluginConfig represents configuration details for a plugin. @@ -34,7 +33,6 @@ type Manager struct { dp definition.DecrypterProvider ep definition.EncrypterProvider pb definition.PublisherProvider - rp definition.RouterProvider cfg *Config } @@ -74,13 +72,7 @@ func NewManager(ctx context.Context, cfg *Config) (*Manager, error) { return nil, fmt.Errorf("failed to load encryption plugin: %w", err) } - // Load router plugin. - rp, err := provider[definition.RouterProvider](cfg.Root, cfg.Router.ID) - if err != nil { - return nil, fmt.Errorf("failed to load encryption plugin: %w", err) - } - - return &Manager{sp: sp, vp: vp, pb: pb, ep: ep, dp: dp, rp: rp, cfg: cfg}, nil + return &Manager{sp: sp, vp: vp, pb: pb, ep: ep, dp: dp, cfg: cfg}, nil } // provider loads a plugin dynamically and retrieves its provider instance. @@ -177,17 +169,3 @@ func (m *Manager) Publisher(ctx context.Context) (definition.Publisher, error) { } return publisher, nil } - -// Router retrieves the router plugin instances. -func (m *Manager) Router(ctx context.Context) (definition.Router, func() error, error) { - if m.rp == nil { - return nil, nil, fmt.Errorf("router plugin provider not loaded") - - } - schemaValidator, close, err := m.rp.New(ctx, m.cfg.Router.Config) - if err != nil { - - return nil, nil, fmt.Errorf("failed to initialize schema validator: %v", err) - } - return schemaValidator, close, nil -} From 086375e063af30a9301929b77a784750ec91d73d Mon Sep 17 00:00:00 2001 From: tanyamadaan Date: Thu, 27 Mar 2025 17:28:39 +0530 Subject: [PATCH 4/6] Resolving merge conflicts --- pkg/plugin/definition/schemaValidator.go | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 pkg/plugin/definition/schemaValidator.go diff --git a/pkg/plugin/definition/schemaValidator.go b/pkg/plugin/definition/schemaValidator.go new file mode 100644 index 0000000..dc69ca5 --- /dev/null +++ b/pkg/plugin/definition/schemaValidator.go @@ -0,0 +1,16 @@ +package definition + +import ( + "context" + "net/url" +) + +// SchemaValidator interface for schema validation. +type SchemaValidator interface { + Validate(ctx context.Context, url *url.URL, payload []byte) error +} + +// SchemaValidatorProvider interface for creating validators. +type SchemaValidatorProvider interface { + New(ctx context.Context, config map[string]string) (SchemaValidator, func() error, error) +} From dff7a5abb329e0d5e49f083c9f9c1498127ddca8 Mon Sep 17 00:00:00 2001 From: tanyamadaan Date: Fri, 28 Mar 2025 12:20:08 +0530 Subject: [PATCH 5/6] Changes as per integration testing --- pkg/plugin/definition/router.go | 6 +- .../implementation/router/cmd/plugin.go | 2 +- pkg/plugin/implementation/router/router.go | 121 ++++++++++++------ .../implementation/router/router_test.go | 64 ++++++++- .../router/testData/bap_receiver.yaml | 2 +- .../router/testData/bpp_receiver.yaml | 4 +- 6 files changed, 146 insertions(+), 53 deletions(-) diff --git a/pkg/plugin/definition/router.go b/pkg/plugin/definition/router.go index 21b1be6..05e2e30 100644 --- a/pkg/plugin/definition/router.go +++ b/pkg/plugin/definition/router.go @@ -7,9 +7,9 @@ import ( // Route defines the structure for the Route returned. type Route struct { - TargetType string // "url" or "msgq" or "bap" or "bpp" - PublisherID string // For message queues - URL string // For API calls + TargetType string // "url" or "msgq" or "bap" or "bpp" + PublisherID string // For message queues + URL *url.URL // For API calls } // RouterProvider initializes the a new Router instance with the given config. diff --git a/pkg/plugin/implementation/router/cmd/plugin.go b/pkg/plugin/implementation/router/cmd/plugin.go index 23816ee..556f129 100644 --- a/pkg/plugin/implementation/router/cmd/plugin.go +++ b/pkg/plugin/implementation/router/cmd/plugin.go @@ -28,4 +28,4 @@ func (rp RouterProvider) New(ctx context.Context, config map[string]string) (def } // Provider is the exported symbol that the plugin manager will look for. -var Provider definition.RouterProvider = RouterProvider{} +var Provider = RouterProvider{} diff --git a/pkg/plugin/implementation/router/router.go b/pkg/plugin/implementation/router/router.go index ae98baf..9d16767 100644 --- a/pkg/plugin/implementation/router/router.go +++ b/pkg/plugin/implementation/router/router.go @@ -33,7 +33,7 @@ type Router struct { type routingRule struct { Domain string `yaml:"domain"` Version string `yaml:"version"` - TargetType string `yaml:"targetType"` // "url", "msgq", "bpp", or "bap" + TargetType string `yaml:"targetType"` // "url", "publisher", "bpp", or "bap" Target target `yaml:"target,omitempty"` Endpoints []string `yaml:"endpoints"` } @@ -46,10 +46,10 @@ type target struct { // TargetType defines possible target destinations. const ( - targetTypeURL = "url" // Route to a specific URL - targetTypeMSGQ = "msgq" // Route to a message queue - targetTypeBPP = "bpp" // Route to a BPP endpoint - targetTypeBAP = "bap" // Route to a BAP endpoint + targetTypeURL = "url" // Route to a specific URL + targetTypePublisher = "publisher" // Route to a publisher + targetTypeBPP = "bpp" // Route to a BPP endpoint + targetTypeBAP = "bap" // Route to a BAP endpoint ) // New initializes a new Router instance with the provided configuration. @@ -71,6 +71,30 @@ func New(ctx context.Context, config *Config) (*Router, func() error, error) { return router, nil, nil } +// parseTargetURL parses a URL string into a url.URL object with strict validation +func parseTargetURL(urlStr string) (*url.URL, error) { + if urlStr == "" { + return nil, nil + } + + parsed, err := url.Parse(urlStr) + if err != nil { + return nil, fmt.Errorf("invalid URL '%s': %w", urlStr, err) + } + + // Enforce scheme requirement + if parsed.Scheme == "" { + return nil, fmt.Errorf("URL '%s' must include a scheme (http/https)", urlStr) + } + + // Optionally validate scheme is http or https + if parsed.Scheme != "https" { + return nil, fmt.Errorf("URL '%s' must use https scheme", urlStr) + } + + return parsed, nil +} + // LoadRules reads and parses routing rules from the YAML configuration file. func (r *Router) loadRules(configPath string) error { if configPath == "" { @@ -105,25 +129,33 @@ func (r *Router) loadRules(configPath string) error { for _, endpoint := range rule.Endpoints { var route *definition.Route switch rule.TargetType { - case targetTypeMSGQ: + case targetTypePublisher: route = &definition.Route{ TargetType: rule.TargetType, PublisherID: rule.Target.PublisherID, } case targetTypeURL: + parsedURL, err := parseTargetURL(rule.Target.URL) + if err != nil { + return fmt.Errorf("invalid URL in rule: %w", err) + } route = &definition.Route{ TargetType: rule.TargetType, - URL: rule.Target.URL, + URL: parsedURL, } case targetTypeBPP, targetTypeBAP: + var parsedURL *url.URL + if rule.Target.URL != "" { + parsedURL, err = parseTargetURL(rule.Target.URL) + if err != nil { + return fmt.Errorf("invalid URL in rule: %w", err) + } + } route = &definition.Route{ TargetType: rule.TargetType, - URL: rule.Target.URL, // Fallback URL if URI not provided in request + URL: parsedURL, } } - - fmt.Print(r.rules) - r.rules[rule.Domain][rule.Version][endpoint] = route } } @@ -145,15 +177,19 @@ func validateRules(rules []routingRule) error { if rule.Target.URL == "" { return fmt.Errorf("invalid rule: url is required for targetType 'url'") } - if _, err := url.ParseRequestURI(rule.Target.URL); err != nil { - return fmt.Errorf("invalid URL in rule: %w", err) + if _, err := parseTargetURL(rule.Target.URL); err != nil { + return fmt.Errorf("invalid URL - %s: %w", rule.Target.URL, err) } - case targetTypeMSGQ: + case targetTypePublisher: if rule.Target.PublisherID == "" { - return fmt.Errorf("invalid rule: publisherID is required for targetType 'msgq'") + return fmt.Errorf("invalid rule: publisherID is required for targetType 'publisher'") } case targetTypeBPP, targetTypeBAP: - // No target validation needed for bpp/bap, as they use URIs from the request body + if rule.Target.URL != "" { + if _, err := parseTargetURL(rule.Target.URL); err != nil { + return fmt.Errorf("invalid URL - %s defined in routing config for target type %s: %w", rule.Target.URL, rule.TargetType, err) + } + } continue default: return fmt.Errorf("invalid rule: unknown targetType '%s'", rule.TargetType) @@ -197,36 +233,43 @@ func (r *Router) Route(ctx context.Context, url *url.URL, body []byte) (*definit return nil, fmt.Errorf("endpoint '%s' is not supported for domain %s and version %s in routing config", endpoint, requestBody.Context.Domain, requestBody.Context.Version) } - // Handle BPP/BAP routing with request URIs switch route.TargetType { case targetTypeBPP: - uri := strings.TrimSpace(requestBody.Context.BPPURI) - target := strings.TrimSpace(route.URL) - if len(uri) != 0 { - target = uri - } - if len(target) == 0 { - return nil, fmt.Errorf("could not determine destination for endpoint '%s': neither request contained a BPP URI nor was a default URL configured in routing rules", endpoint) - } - route = &definition.Route{ - TargetType: route.TargetType, - URL: target, - } + return handleProtocolMapping(route, requestBody.Context.BPPURI, endpoint) case targetTypeBAP: - uri := strings.TrimSpace(requestBody.Context.BAPURI) - target := strings.TrimSpace(route.URL) - if len(uri) != 0 { - target = uri + return handleProtocolMapping(route, requestBody.Context.BAPURI, endpoint) + } + return route, nil +} + +// handleProtocolMapping handles both BPP and BAP routing with proper URL construction +func handleProtocolMapping(route *definition.Route, requestURI, endpoint string) (*definition.Route, error) { + uri := strings.TrimSpace(requestURI) + var targetURL *url.URL + if len(uri) != 0 { + parsedURL, err := parseTargetURL(uri) + if err != nil { + return nil, fmt.Errorf("invalid %s URI - %s in request body for %s: %w", strings.ToUpper(route.TargetType), uri, endpoint, err) } - if len(target) == 0 { - return nil, fmt.Errorf("could not determine destination for endpoint '%s': neither request contained a BAP URI nor was a default URL configured in routing rules", endpoint) + targetURL = parsedURL + } + + // If no request URI, fall back to configured URL with endpoint appended + if targetURL == nil { + if route.URL == nil { + return nil, fmt.Errorf("could not determine destination for endpoint '%s': neither request contained a %s URI nor was a default URL configured in routing rules", endpoint, strings.ToUpper(route.TargetType)) } - route = &definition.Route{ - TargetType: route.TargetType, - URL: target, + + targetURL = &url.URL{ + Scheme: route.URL.Scheme, + Host: route.URL.Host, + Path: path.Join(route.URL.Path, endpoint), } } - return route, nil + return &definition.Route{ + TargetType: targetTypeURL, + URL: targetURL, + }, nil } diff --git a/pkg/plugin/implementation/router/router_test.go b/pkg/plugin/implementation/router/router_test.go index f67b577..7937ca5 100644 --- a/pkg/plugin/implementation/router/router_test.go +++ b/pkg/plugin/implementation/router/router_test.go @@ -143,12 +143,12 @@ func TestValidateRulesSuccess(t *testing.T) { }, }, { - name: "Valid rules with msgq routing", + name: "Valid rules with publisher routing", rules: []routingRule{ { Domain: "retail", Version: "1.0.0", - TargetType: "msgq", + TargetType: "publisher", Target: target{ PublisherID: "example_topic", }, @@ -284,19 +284,64 @@ func TestValidateRulesFailure(t *testing.T) { wantErr: "invalid rule: url is required for targetType 'url'", }, { - name: "Missing topic_id for targetType: msgq", + name: "Invalid URL format for targetType: url", rules: []routingRule{ { Domain: "retail", Version: "1.0.0", - TargetType: "msgq", + TargetType: "url", + Target: target{ + URL: "htp://invalid-url.com", // Invalid scheme + }, + Endpoints: []string{"search"}, + }, + }, + wantErr: "invalid URL - htp://invalid-url.com: URL 'htp://invalid-url.com' must use https scheme", + }, + { + name: "Missing topic_id for targetType: publisher", + rules: []routingRule{ + { + Domain: "retail", + Version: "1.0.0", + TargetType: "publisher", Target: target{ // PublisherID is missing }, Endpoints: []string{"search", "select"}, }, }, - wantErr: "invalid rule: publisherID is required for targetType 'msgq'", + wantErr: "invalid rule: publisherID is required for targetType 'publisher'", + }, + { + name: "Invalid URL for BPP targetType", + rules: []routingRule{ + { + Domain: "retail", + Version: "1.0.0", + TargetType: "bpp", + Target: target{ + URL: "htp://invalid-url.com", // Invalid URL + }, + Endpoints: []string{"search"}, + }, + }, + wantErr: "invalid URL - htp://invalid-url.com defined in routing config for target type bpp", + }, + { + name: "Invalid URL for BAP targetType", + rules: []routingRule{ + { + Domain: "retail", + Version: "1.0.0", + TargetType: "bap", + Target: target{ + URL: "http://[invalid].com", // Invalid host + }, + Endpoints: []string{"search"}, + }, + }, + wantErr: "invalid URL - http://[invalid].com defined in routing config for target type bap", }, } @@ -340,7 +385,7 @@ func TestRouteSuccess(t *testing.T) { body: `{"context": {"domain": "ONDC:TRV10", "version": "2.0.0"}}`, }, { - name: "Valid domain, version, and endpoint (msgq routing)", + name: "Valid domain, version, and endpoint (publisher routing)", configFile: "bpp_receiver.yaml", url: "https://example.com/v1/ondc/search", body: `{"context": {"domain": "ONDC:TRV10", "version": "2.0.0"}}`, @@ -415,6 +460,13 @@ func TestRouteFailure(t *testing.T) { body: `{"context": {"domain": "ONDC:TRV10", "version": "2.0.0"}}`, wantErr: "could not determine destination for endpoint 'select': neither request contained a BPP URI nor was a default URL configured in routing rules", }, + { + name: "Invalid bpp_uri format in request", + configFile: "bap_caller.yaml", + url: "https://example.com/v1/ondc/select", + body: `{"context": {"domain": "ONDC:TRV10", "version": "2.0.0", "bpp_uri": "htp://invalid-url"}}`, // Invalid scheme (htp instead of http) + wantErr: "invalid BPP URI - htp://invalid-url in request body for select: URL 'htp://invalid-url' must use https scheme", + }, } for _, tt := range tests { diff --git a/pkg/plugin/implementation/router/testData/bap_receiver.yaml b/pkg/plugin/implementation/router/testData/bap_receiver.yaml index 353ecc7..af98401 100644 --- a/pkg/plugin/implementation/router/testData/bap_receiver.yaml +++ b/pkg/plugin/implementation/router/testData/bap_receiver.yaml @@ -13,7 +13,7 @@ routingRules: - on_cancel - domain: "ONDC:TRV10" version: "2.0.0" - targetType: "msgq" + targetType: "publisher" target: publisherId: "trv_topic_id1" endpoints: diff --git a/pkg/plugin/implementation/router/testData/bpp_receiver.yaml b/pkg/plugin/implementation/router/testData/bpp_receiver.yaml index 9633923..a8f668e 100644 --- a/pkg/plugin/implementation/router/testData/bpp_receiver.yaml +++ b/pkg/plugin/implementation/router/testData/bpp_receiver.yaml @@ -10,15 +10,13 @@ routingRules: - confirm - status - cancel - - domain: "ONDC:TRV10" version: "2.0.0" - targetType: "msgq" + targetType: "publisher" target: publisherId: "trv_topic_id1" endpoints: - search - - domain: "ONDC:TRV11" version: "2.0.0" targetType: "url" From a549323dd50dcf6e7cb6d47d277b64a9168fd808 Mon Sep 17 00:00:00 2001 From: tanyamadaan Date: Fri, 28 Mar 2025 17:01:47 +0530 Subject: [PATCH 6/6] yaml format changes --- .../implementation/router/cmd/plugin_test.go | 48 +++++-------------- .../router/testData/bap_caller.yaml | 20 ++++---- .../router/testData/bap_receiver.yaml | 16 +++---- .../router/testData/bpp_caller.yaml | 12 ++--- .../router/testData/bpp_receiver.yaml | 24 +++++----- 5 files changed, 49 insertions(+), 71 deletions(-) diff --git a/pkg/plugin/implementation/router/cmd/plugin_test.go b/pkg/plugin/implementation/router/cmd/plugin_test.go index 08c9bd7..c5ffe8e 100644 --- a/pkg/plugin/implementation/router/cmd/plugin_test.go +++ b/pkg/plugin/implementation/router/cmd/plugin_test.go @@ -32,44 +32,21 @@ func setupTestConfig(t *testing.T) string { return tempPath } -// TestRouterProviderSuccess tests the RouterProvider implementation for success cases. +// TestRouterProviderSuccess tests successful router creation. func TestRouterProviderSuccess(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 - wantErr bool - }{ - { - name: "Valid configuration", - ctx: context.Background(), - config: map[string]string{ - "routingConfig": rulesFilePath, - }, - wantErr: false, - }, + provider := RouterProvider{} + router, _, err := provider.New(context.Background(), map[string]string{ + "routingConfig": rulesFilePath, + }) + + if err != nil { + t.Fatalf("New() unexpected error: %v", err) } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - provider := RouterProvider{} - router, _, err := provider.New(tt.ctx, tt.config) - - // Ensure no error occurred - if (err != nil) != tt.wantErr { - t.Errorf("New(%v, %v) error = %v, wantErr %v", tt.ctx, tt.config, err, tt.wantErr) - return - } - - // Ensure the router and close function are not nil - if router == nil { - t.Errorf("New(%v, %v) = nil router, want non-nil", tt.ctx, tt.config) - } - }) + if router == nil { + t.Error("New() returned nil router, want non-nil") } } @@ -114,8 +91,9 @@ func TestRouterProviderFailure(t *testing.T) { // Check for expected error if err == nil { - t.Errorf("New(%v, %v) = nil error, want error containing %q", tt.ctx, tt.config, tt.wantErr) - } else if !strings.Contains(err.Error(), tt.wantErr) { + t.Fatalf("New(%v, %v) = nil error, want error containing %q", tt.ctx, tt.config, tt.wantErr) + } + if !strings.Contains(err.Error(), tt.wantErr) { t.Errorf("New(%v, %v) = %v, want error containing %q", tt.ctx, tt.config, err, tt.wantErr) } }) diff --git a/pkg/plugin/implementation/router/testData/bap_caller.yaml b/pkg/plugin/implementation/router/testData/bap_caller.yaml index 0c595a6..6a40a0f 100644 --- a/pkg/plugin/implementation/router/testData/bap_caller.yaml +++ b/pkg/plugin/implementation/router/testData/bap_caller.yaml @@ -1,23 +1,23 @@ routingRules: - - domain: "ONDC:TRV10" - version: "2.0.0" - targetType: "bpp" + - domain: ONDC:TRV10 + version: 2.0.0 + targetType: bpp target: - url: "https://gateway.example.com" + url: https://gateway.example.com endpoints: - search - - domain: "ONDC:TRV10" - version: "2.0.0" - targetType: "bpp" + - domain: ONDC:TRV10 + version: 2.0.0 + targetType: bpp endpoints: - select - init - confirm - status - cancel - - domain: "ONDC:TRV12" - version: "2.0.0" - targetType: "bpp" + - domain: ONDC:TRV12 + version: 2.0.0 + targetType: bpp endpoints: - select - init diff --git a/pkg/plugin/implementation/router/testData/bap_receiver.yaml b/pkg/plugin/implementation/router/testData/bap_receiver.yaml index af98401..17432db 100644 --- a/pkg/plugin/implementation/router/testData/bap_receiver.yaml +++ b/pkg/plugin/implementation/router/testData/bap_receiver.yaml @@ -1,9 +1,9 @@ routingRules: - - domain: "ONDC:TRV10" - version: "2.0.0" - targetType: "url" + - domain: ONDC:TRV10 + version: 2.0.0 + targetType: url target: - url: "https://services-backend/trv/v1" + url: https://services-backend/trv/v1 endpoints: - on_select - on_init @@ -11,10 +11,10 @@ routingRules: - on_status - on_update - on_cancel - - domain: "ONDC:TRV10" - version: "2.0.0" - targetType: "publisher" + - domain: ONDC:TRV10 + version: 2.0.0 + targetType: publisher target: - publisherId: "trv_topic_id1" + publisherId: trv_topic_id1 endpoints: - on_search \ No newline at end of file diff --git a/pkg/plugin/implementation/router/testData/bpp_caller.yaml b/pkg/plugin/implementation/router/testData/bpp_caller.yaml index 8d0c212..339b92d 100644 --- a/pkg/plugin/implementation/router/testData/bpp_caller.yaml +++ b/pkg/plugin/implementation/router/testData/bpp_caller.yaml @@ -1,7 +1,7 @@ routingRules: - - domain: "ONDC:TRV10" - version: "2.0.0" - targetType: "bap" + - domain: ONDC:TRV10 + version: 2.0.0 + targetType: bap endpoints: - on_search - on_select @@ -10,9 +10,9 @@ routingRules: - on_status - on_update - on_cancel - - domain: "ONDC:TRV11" - version: "2.0.0" - targetType: "bap" + - domain: ONDC:TRV11 + version: 2.0.0 + targetType: bap endpoints: - on_search - on_select diff --git a/pkg/plugin/implementation/router/testData/bpp_receiver.yaml b/pkg/plugin/implementation/router/testData/bpp_receiver.yaml index a8f668e..7355592 100644 --- a/pkg/plugin/implementation/router/testData/bpp_receiver.yaml +++ b/pkg/plugin/implementation/router/testData/bpp_receiver.yaml @@ -1,27 +1,27 @@ routingRules: - - domain: "ONDC:TRV10" - version: "2.0.0" - targetType: "url" + - domain: ONDC:TRV10 + version: 2.0.0 + targetType: url target: - url: "https://services-backend/trv/v1" + url: https://services-backend/trv/v1 endpoints: - select - init - confirm - status - cancel - - domain: "ONDC:TRV10" - version: "2.0.0" - targetType: "publisher" + - domain: ONDC:TRV10 + version: 2.0.0 + targetType: publisher target: - publisherId: "trv_topic_id1" + publisherId: trv_topic_id1 endpoints: - search - - domain: "ONDC:TRV11" - version: "2.0.0" - targetType: "url" + - domain: ONDC:TRV11 + version: 2.0.0 + targetType: url target: - url: "https://services-backend/trv/v1" + url: https://services-backend/trv/v1 endpoints: - select - init