From fe1ffa356cfe29d7373bea73537a20789ef761e8 Mon Sep 17 00:00:00 2001 From: tanyamadaan Date: Sat, 22 Mar 2025 00:05:08 +0530 Subject: [PATCH 1/5] small fixes --- go.mod | 15 +- go.sum | 17 - pkg/plugin/definition/router.go | 24 - .../implementation/router/cmd/plugin.go | 31 -- .../implementation/router/cmd/plugin_test.go | 137 ------ pkg/plugin/implementation/router/router.go | 204 -------- .../implementation/router/router_test.go | 452 ------------------ .../router/testData/bap_caller.yaml | 25 - .../router/testData/bap_receiver.yaml | 20 - .../router/testData/bpp_caller.yaml | 23 - .../router/testData/bpp_receiver.yaml | 30 -- .../schemaValidator/cmd/plugin.go | 6 +- .../schemaValidator/cmd/plugin_test.go | 14 +- .../schemaValidator/schemaValidator.go | 7 +- .../schemaValidator/schemaValidator_test.go | 20 +- pkg/plugin/manager.go | 26 +- 16 files changed, 35 insertions(+), 1016 deletions(-) delete mode 100644 pkg/plugin/definition/router.go delete mode 100644 pkg/plugin/implementation/router/cmd/plugin.go delete mode 100644 pkg/plugin/implementation/router/cmd/plugin_test.go delete mode 100644 pkg/plugin/implementation/router/router.go delete mode 100644 pkg/plugin/implementation/router/router_test.go delete mode 100644 pkg/plugin/implementation/router/testData/bap_caller.yaml delete mode 100644 pkg/plugin/implementation/router/testData/bap_receiver.yaml delete mode 100644 pkg/plugin/implementation/router/testData/bpp_caller.yaml delete mode 100644 pkg/plugin/implementation/router/testData/bpp_receiver.yaml diff --git a/go.mod b/go.mod index c2692fb..91f393a 100644 --- a/go.mod +++ b/go.mod @@ -1,26 +1,15 @@ module github.com/beckn/beckn-onix -go 1.23.4 - -toolchain go1.23.7 +go 1.24.1 require ( github.com/santhosh-tekuri/jsonschema/v6 v6.0.1 golang.org/x/crypto v0.36.0 ) -require ( - github.com/kr/pretty v0.3.1 // indirect - github.com/rogpeppe/go-internal v1.13.1 // indirect - 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 ( - golang.org/x/sys v0.31.0 // indirect - gopkg.in/yaml.v3 v3.0.1 -) +require golang.org/x/sys v0.31.0 // indirect diff --git a/go.sum b/go.sum index 896f459..50bb32e 100644 --- a/go.sum +++ b/go.sum @@ -1,17 +1,5 @@ -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= -github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= -github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= -github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= -github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= -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= @@ -22,8 +10,3 @@ 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 diff --git a/pkg/plugin/definition/router.go b/pkg/plugin/definition/router.go deleted file mode 100644 index b9f7e41..0000000 --- a/pkg/plugin/definition/router.go +++ /dev/null @@ -1,24 +0,0 @@ -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) -} diff --git a/pkg/plugin/implementation/router/cmd/plugin.go b/pkg/plugin/implementation/router/cmd/plugin.go deleted file mode 100644 index f38ffa9..0000000 --- a/pkg/plugin/implementation/router/cmd/plugin.go +++ /dev/null @@ -1,31 +0,0 @@ -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["routingConfig"] - if !ok { - return nil, nil, errors.New("routingConfig 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{} diff --git a/pkg/plugin/implementation/router/cmd/plugin_test.go b/pkg/plugin/implementation/router/cmd/plugin_test.go deleted file mode 100644 index afa0c97..0000000 --- a/pkg/plugin/implementation/router/cmd/plugin_test.go +++ /dev/null @@ -1,137 +0,0 @@ -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("", "routingRules") - if err != nil { - t.Fatalf("Failed to create temp directory: %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 -} - -// TestRouterProviderSuccess tests the RouterProvider implementation for success cases. -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 - }{ - { - name: "Valid configuration", - ctx: context.Background(), - config: map[string]string{ - "routingConfig": rulesFilePath, - }, - }, - } - - 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 { - 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") - } - }) - } -} - -// TestRouterProviderFailure tests the RouterProvider implementation for failure cases. -func TestRouterProviderFailure(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{ - "routingConfig": "", - }, - expectedError: "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: "Nil context", - ctx: nil, - config: map[string]string{"routingConfig": 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) - } - }) - } -} diff --git a/pkg/plugin/implementation/router/router.go b/pkg/plugin/implementation/router/router.go deleted file mode 100644 index 2649dbd..0000000 --- a/pkg/plugin/implementation/router/router.go +++ /dev/null @@ -1,204 +0,0 @@ -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:"routingConfig"` -} - -// RoutingConfig represents the structure of the routing configuration file. -type routingConfig struct { - RoutingRules []routingRule `yaml:"routingRules"` -} - -// 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:"routingType"` // "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 -} - -// 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. -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, nil, nil -} - -// LoadRules reads and parses routing rules from the YAML configuration file. -func (r *Router) loadRules() error { - if r.config.RoutingConfig == "" { - return fmt.Errorf("routingConfig path is empty") - } - data, err := os.ReadFile(r.config.RoutingConfig) - if err != nil { - return fmt.Errorf("error reading config file at %s: %w", r.config.RoutingConfig, err) - } - var config routingConfig - if err := yaml.Unmarshal(data, &config); err != nil { - return fmt.Errorf("error parsing YAML: %w", err) - } - - // Validate rules - if err := validateRules(config.RoutingRules); err != nil { - return fmt.Errorf("invalid routing rules: %w", err) - } - r.rules = config.RoutingRules - 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") - } - - // Validate based on routingType - switch rule.RoutingType { - case "url": - if rule.Target.URL == "" { - return fmt.Errorf("invalid rule: url is required for routingType '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 "bpp", "bap": - // 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 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"` - BppURI string `json:"bpp_uri,omitempty"` - BapURI string `json:"bap_uri,omitempty"` - } `json:"context"` - } - if err := json.Unmarshal(body, &requestBody); err != nil { - return nil, fmt.Errorf("error parsing request body: %w", err) - } - - // 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) - } - - // 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) - } - } - } - } - - // 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 -} diff --git a/pkg/plugin/implementation/router/router_test.go b/pkg/plugin/implementation/router/router_test.go deleted file mode 100644 index 3d3fe70..0000000 --- a/pkg/plugin/implementation/router/router_test.go +++ /dev/null @@ -1,452 +0,0 @@ -package router - -import ( - "context" - "embed" - "net/url" - "os" - "path/filepath" - "strings" - "testing" -) - -//go:embed testData/* -var testData embed.FS - -func setupTestConfig(t *testing.T, yamlFileName string) 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) - } - - // 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) - } - - 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) -} - -// setupRouter is a helper function to create router instance. -func setupRouter(t *testing.T, configFile string) (*Router, func() error, string) { - rulesFilePath := setupTestConfig(t, configFile) - config := &Config{ - RoutingConfig: rulesFilePath, - } - router, _, err := New(context.Background(), config) - if err != nil { - t.Fatalf("New failed: %v", err) - } - return router, nil, rulesFilePath -} - -// TestNew tests the New function. -func TestNew(t *testing.T) { - ctx := context.Background() - - // List of YAML files in the testData directory - yamlFiles := []string{ - "bap_caller.yaml", - "bap_receiver.yaml", - "bpp_caller.yaml", - "bpp_receiver.yaml", - } - - for _, yamlFile := range yamlFiles { - t.Run(yamlFile, func(t *testing.T) { - rulesFilePath := setupTestConfig(t, yamlFile) - 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 config", - config: nil, - expectedError: "config cannot be nil", - }, - { - name: "Empty routing config path", - config: &Config{ - RoutingConfig: "", - }, - expectedError: "routingConfig path is empty", - }, - { - name: "Routing config file does not exist", - config: &Config{ - RoutingConfig: "/nonexistent/path/to/rules.yaml", - }, - expectedError: "error reading config file", - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(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) - } - 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") - } - }) - } - }) - } -} - -// TestValidateRulesSuccess tests the validate function for success cases. -func TestValidateRulesSuccess(t *testing.T) { - tests := []struct { - name string - rules []routingRule - }{ - { - name: "Valid rules with url routing", - rules: []routingRule{ - { - Domain: "retail", - Version: "1.0.0", - RoutingType: "url", - Target: target{ - URL: "https://example.com/api", - }, - Endpoints: []string{"on_search", "on_select"}, - }, - }, - }, - { - name: "Valid rules with msgq routing", - rules: []routingRule{ - { - Domain: "retail", - Version: "1.0.0", - RoutingType: "msgq", - Target: target{ - TopicID: "example_topic", - }, - Endpoints: []string{"on_search", "on_select"}, - }, - }, - }, - { - name: "Valid rules with bpp routing to gateway", - rules: []routingRule{ - { - Domain: "retail", - Version: "1.0.0", - RoutingType: "bpp", - Target: target{ - URL: "https://mock_gateway.com/api", - }, - Endpoints: []string{"search"}, - }, - }, - }, - { - name: "Valid rules with bpp routing", - rules: []routingRule{ - { - Domain: "retail", - Version: "1.0.0", - RoutingType: "bpp", - Endpoints: []string{"select"}, - }, - }, - }, - { - name: "Valid rules with bap routing", - rules: []routingRule{ - { - Domain: "retail", - Version: "1.0.0", - RoutingType: "bap", - Endpoints: []string{"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) - } - }) - } -} - -// TestValidateRulesFailure tests the validate function for failure cases. -func TestValidateRulesFailure(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 routingType are required", - }, - { - name: "Missing version", - rules: []routingRule{ - { - Domain: "retail", - RoutingType: "url", - Target: target{ - URL: "https://example.com/api", - }, - Endpoints: []string{"search", "select"}, - }, - }, - expectedErr: "invalid rule: domain, version, and routingType are required", - }, - { - name: "Missing routingType", - rules: []routingRule{ - { - Domain: "retail", - Version: "1.0.0", - Target: target{ - URL: "https://example.com/api", - }, - Endpoints: []string{"search", "select"}, - }, - }, - expectedErr: "invalid rule: domain, version, and routingType are required", - }, - { - name: "Invalid routingType", - rules: []routingRule{ - { - Domain: "retail", - Version: "1.0.0", - RoutingType: "invalid", - Target: target{ - URL: "https://example.com/api", - }, - Endpoints: []string{"search", "select"}, - }, - }, - expectedErr: "invalid rule: unknown routingType 'invalid'", - }, - { - name: "Missing url for routingType: url", - rules: []routingRule{ - { - Domain: "retail", - Version: "1.0.0", - RoutingType: "url", - Target: target{ - // URL is missing - }, - Endpoints: []string{"search", "select"}, - }, - }, - expectedErr: "invalid rule: url is required for routingType 'url'", - }, - { - name: "Missing topic_id for routingType: msgq", - rules: []routingRule{ - { - Domain: "retail", - Version: "1.0.0", - RoutingType: "msgq", - Target: target{ - // TopicID is missing - }, - Endpoints: []string{"search", "select"}, - }, - }, - expectedErr: "invalid rule: topicId is required for routingType '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) - } - }) - } -} - -// TestRouteSuccess tests the Route function for success cases. -func TestRouteSuccess(t *testing.T) { - ctx := context.Background() - - // Define success test cases - tests := []struct { - name string - configFile string - url string - body string - }{ - { - name: "Valid domain, version, and endpoint (bpp routing with gateway URL)", - configFile: "bap_caller.yaml", - url: "https://example.com/v1/ondc/search", - body: `{"context": {"domain": "ONDC:TRV10", "version": "2.0.0"}}`, - }, - { - name: "Valid domain, version, and endpoint (bpp routing with bpp_uri)", - configFile: "bap_caller.yaml", - url: "https://example.com/v1/ondc/select", - body: `{"context": {"domain": "ONDC:TRV10", "version": "2.0.0", "bpp_uri": "https://bpp1.example.com"}}`, - }, - { - name: "Valid domain, version, and endpoint (url routing)", - configFile: "bpp_receiver.yaml", - url: "https://example.com/v1/ondc/select", - body: `{"context": {"domain": "ONDC:TRV10", "version": "2.0.0"}}`, - }, - { - name: "Valid domain, version, and endpoint (msgq routing)", - configFile: "bpp_receiver.yaml", - url: "https://example.com/v1/ondc/search", - body: `{"context": {"domain": "ONDC:TRV10", "version": "2.0.0"}}`, - }, - { - name: "Valid domain, version, and endpoint (bap routing with bap_uri)", - configFile: "bpp_caller.yaml", - url: "https://example.com/v1/ondc/on_select", - body: `{"context": {"domain": "ONDC:TRV10", "version": "2.0.0", "bap_uri": "https://bap1.example.com"}}`, - }, - { - name: "Valid domain, version, and endpoint (bpp routing with bpp_uri)", - configFile: "bap_receiver.yaml", - url: "https://example.com/v1/ondc/on_select", - body: `{"context": {"domain": "ONDC:TRV10", "version": "2.0.0", "bpp_uri": "https://bpp1.example.com"}}`, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - router, _, rulesFilePath := setupRouter(t, tt.configFile) - defer os.RemoveAll(filepath.Dir(rulesFilePath)) - - parsedURL, _ := url.Parse(tt.url) - _, err := router.Route(ctx, parsedURL, []byte(tt.body)) - - // Ensure no error occurred - if err != nil { - t.Errorf("unexpected error: %v", err) - } - }) - } -} - -// TestRouteFailure tests the Route function for failure cases. -func TestRouteFailure(t *testing.T) { - ctx := context.Background() - - // Define failure test cases - tests := []struct { - name string - configFile string - url string - body string - expectedError 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: "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: "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 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", - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - router, _, rulesFilePath := setupRouter(t, tt.configFile) - defer os.RemoveAll(filepath.Dir(rulesFilePath)) - - parsedURL, _ := url.Parse(tt.url) - _, 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) - } - }) - } -} diff --git a/pkg/plugin/implementation/router/testData/bap_caller.yaml b/pkg/plugin/implementation/router/testData/bap_caller.yaml deleted file mode 100644 index b1d5a44..0000000 --- a/pkg/plugin/implementation/router/testData/bap_caller.yaml +++ /dev/null @@ -1,25 +0,0 @@ -routingRules: - - domain: "ONDC:TRV10" - version: "2.0.0" - routingType: "bpp" - target: - url: "https://gateway.example.com" - endpoints: - - search - - domain: "ONDC:TRV10" - version: "2.0.0" - routingType: "bpp" - endpoints: - - select - - init - - confirm - - status - - cancel - - domain: "ONDC:TRV12" - version: "2.0.0" - routingType: "bpp" - endpoints: - - select - - init - - confirm - - status \ No newline at end of file diff --git a/pkg/plugin/implementation/router/testData/bap_receiver.yaml b/pkg/plugin/implementation/router/testData/bap_receiver.yaml deleted file mode 100644 index ca4a478..0000000 --- a/pkg/plugin/implementation/router/testData/bap_receiver.yaml +++ /dev/null @@ -1,20 +0,0 @@ -routingRules: - - domain: "ONDC:TRV10" - version: "2.0.0" - routingType: "url" - target: - url: "https://services-backend/trv/v1" - endpoints: - - on_select - - on_init - - on_confirm - - on_status - - on_update - - on_cancel - - domain: "ONDC:TRV10" - version: "2.0.0" - routingType: "msgq" - target: - topic_id: "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 deleted file mode 100644 index 0d9a670..0000000 --- a/pkg/plugin/implementation/router/testData/bpp_caller.yaml +++ /dev/null @@ -1,23 +0,0 @@ -routingRules: - - domain: "ONDC:TRV10" - version: "2.0.0" - routingType: "bap" - endpoints: - - on_search - - on_select - - on_init - - on_confirm - - on_status - - on_update - - on_cancel - - domain: "ONDC:TRV11" - version: "2.0.0" - routingType: "bap" - endpoints: - - on_search - - on_select - - on_init - - on_confirm - - on_status - - on_update - - on_cancel \ No newline at end of file diff --git a/pkg/plugin/implementation/router/testData/bpp_receiver.yaml b/pkg/plugin/implementation/router/testData/bpp_receiver.yaml deleted file mode 100644 index 6febce6..0000000 --- a/pkg/plugin/implementation/router/testData/bpp_receiver.yaml +++ /dev/null @@ -1,30 +0,0 @@ -routingRules: - - domain: "ONDC:TRV10" - version: "2.0.0" - routingType: "url" - target: - url: "https://services-backend/trv/v1" - endpoints: - - select - - init - - confirm - - status - - cancel - - - domain: "ONDC:TRV10" - version: "2.0.0" - routingType: "msgq" - target: - topic_id: "trv_topic_id1" - endpoints: - - search - - - domain: "ONDC:TRV11" - version: "2.0.0" - routingType: "url" - target: - url: "https://services-backend/trv/v1" - endpoints: - - select - - init - - confirm \ No newline at end of file diff --git a/pkg/plugin/implementation/schemaValidator/cmd/plugin.go b/pkg/plugin/implementation/schemaValidator/cmd/plugin.go index 6882ce4..2a8f44a 100644 --- a/pkg/plugin/implementation/schemaValidator/cmd/plugin.go +++ b/pkg/plugin/implementation/schemaValidator/cmd/plugin.go @@ -17,10 +17,10 @@ func (vp schemaValidatorProvider) New(ctx context.Context, config map[string]str return nil, nil, errors.New("context cannot be nil") } - // Extract schema_dir from the config map - schemaDir, ok := config["schema_dir"] + // Extract schemaDir from the config map + schemaDir, ok := config["schemaDir"] if !ok || schemaDir == "" { - return nil, nil, errors.New("config must contain 'schema_dir'") + return nil, nil, errors.New("config must contain 'schemaDir'") } // Create a new schemaValidator instance with the provided configuration diff --git a/pkg/plugin/implementation/schemaValidator/cmd/plugin_test.go b/pkg/plugin/implementation/schemaValidator/cmd/plugin_test.go index 7e06b55..59be949 100644 --- a/pkg/plugin/implementation/schemaValidator/cmd/plugin_test.go +++ b/pkg/plugin/implementation/schemaValidator/cmd/plugin_test.go @@ -63,7 +63,7 @@ func TestValidatorProviderSuccess(t *testing.T) { { name: "Valid schema directory", ctx: context.Background(), // Valid context - config: map[string]string{"schema_dir": schemaDir}, + config: map[string]string{"schemaDir": schemaDir}, expectedError: "", }, } @@ -114,24 +114,24 @@ func TestValidatorProviderFailure(t *testing.T) { name: "Config is empty", ctx: context.Background(), config: map[string]string{}, - expectedError: "config must contain 'schema_dir'", + expectedError: "config must contain 'schemaDir'", }, { - name: "schema_dir is empty", + name: "schemaDir is empty", ctx: context.Background(), - config: map[string]string{"schema_dir": ""}, - expectedError: "config must contain 'schema_dir'", + config: map[string]string{"schemaDir": ""}, + expectedError: "config must contain 'schemaDir'", }, { name: "Invalid schema directory", ctx: context.Background(), // Valid context - config: map[string]string{"schema_dir": "/invalid/dir"}, + config: map[string]string{"schemaDir": "/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}, + config: map[string]string{"schemaDir": schemaDir}, expectedError: "context cannot be nil", }, } diff --git a/pkg/plugin/implementation/schemaValidator/schemaValidator.go b/pkg/plugin/implementation/schemaValidator/schemaValidator.go index a46ceb2..2d6b189 100644 --- a/pkg/plugin/implementation/schemaValidator/schemaValidator.go +++ b/pkg/plugin/implementation/schemaValidator/schemaValidator.go @@ -49,7 +49,7 @@ func New(ctx context.Context, config *Config) (*SchemaValidator, func() error, e if err := v.initialise(); err != nil { return nil, nil, fmt.Errorf("failed to initialise schemaValidator: %v", err) } - return v, v.Close, nil + return v, nil, nil } // Validate validates the given data against the schema. @@ -195,8 +195,3 @@ func (v *SchemaValidator) initialise() error { 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 index b60c834..277b539 100644 --- a/pkg/plugin/implementation/schemaValidator/schemaValidator_test.go +++ b/pkg/plugin/implementation/schemaValidator/schemaValidator_test.go @@ -289,7 +289,7 @@ func TestValidator_Initialise(t *testing.T) { } } -func TestValidator_New_Success(t *testing.T) { +func TestValidatorNew_Success(t *testing.T) { schemaDir := setupTestSchema(t) defer os.RemoveAll(schemaDir) @@ -300,7 +300,7 @@ func TestValidator_New_Success(t *testing.T) { } } -func TestValidator_New_Failure(t *testing.T) { +func TestValidatorNewFailure(t *testing.T) { tests := []struct { name string config *Config @@ -315,22 +315,6 @@ func TestValidator_New_Failure(t *testing.T) { }, 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{ diff --git a/pkg/plugin/manager.go b/pkg/plugin/manager.go index 209e0e6..5e75357 100644 --- a/pkg/plugin/manager.go +++ b/pkg/plugin/manager.go @@ -27,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 @@ -79,7 +74,13 @@ 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 schema validation plugin. + svp, err := provider[definition.SchemaValidatorProvider](cfg.Root, cfg.Encrypter.ID) + if err != nil { + return nil, fmt.Errorf("failed to load schema validation plugin: %w", err) + } + + return &Manager{sp: sp, vp: vp, pb: pb, ep: ep, dp: dp, svp: svp, cfg: cfg}, nil } // provider loads a plugin dynamically and retrieves its provider instance. @@ -176,3 +177,16 @@ func (m *Manager) Publisher(ctx context.Context) (definition.Publisher, error) { } return publisher, nil } + +// Encrypter retrieves the encryption plugin instance. +func (m *Manager) SchemaValidation(ctx context.Context) (definition.SchemaValidator, func() error, error) { + if m.svp == nil { + return nil, nil, fmt.Errorf("schema validation 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: %w", err) + } + return schemaValidator, close, nil +} From 671d0b614bb6770fa6e7cde8a921578882c16398 Mon Sep 17 00:00:00 2001 From: tanyamadaan Date: Sat, 22 Mar 2025 00:13:43 +0530 Subject: [PATCH 2/5] test case fixed --- .../schemaValidator/cmd/plugin_test.go | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/pkg/plugin/implementation/schemaValidator/cmd/plugin_test.go b/pkg/plugin/implementation/schemaValidator/cmd/plugin_test.go index 59be949..75fdce0 100644 --- a/pkg/plugin/implementation/schemaValidator/cmd/plugin_test.go +++ b/pkg/plugin/implementation/schemaValidator/cmd/plugin_test.go @@ -72,7 +72,7 @@ func TestValidatorProviderSuccess(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { vp := schemaValidatorProvider{} - schemaValidator, close, err := vp.New(tt.ctx, tt.config) + schemaValidator, _, err := vp.New(tt.ctx, tt.config) // Ensure no error occurred if err != nil { @@ -84,16 +84,6 @@ func TestValidatorProviderSuccess(t *testing.T) { 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) - } }) } } From eec1125fee37005ebb6e6c141f3d9d4639d1118e Mon Sep 17 00:00:00 2001 From: AbhishekHS220 Date: Mon, 24 Mar 2025 11:39:17 +0530 Subject: [PATCH 3/5] Pipeline for testing coverage over a package --- .github/workflows/beckn_ci_test.yml | 74 +++++++++++++++++++++++++++++ 1 file changed, 74 insertions(+) create mode 100644 .github/workflows/beckn_ci_test.yml diff --git a/.github/workflows/beckn_ci_test.yml b/.github/workflows/beckn_ci_test.yml new file mode 100644 index 0000000..c9c1414 --- /dev/null +++ b/.github/workflows/beckn_ci_test.yml @@ -0,0 +1,74 @@ +name: CI/CD Test Pipeline + +on: + pull_request: + branches: + - beckn-onix-v1.0-develop + +env: + APP_DIRECTORY: "shared/plugin" # Root directory to start searching from + +jobs: + lint_and_test: + runs-on: ubuntu-latest + if: github.event_name == 'pull_request' + timeout-minutes: 10 # Increased timeout due to additional steps + steps: + # 1. Checkout the code from the test branch (triggered by PR) + - name: Checkout code + uses: actions/checkout@v4 + + # 2. Set up Go environment + - name: Set up Go 1.24.0 + uses: actions/setup-go@v4 + with: + go-version: '1.24.0' + + # 3. Install golangci-lint + - name: Install golangci-lint + run: | + go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest + + # 4. Run golangci-lint on the entire repo, starting from the root directory + - name: Run golangci-lint + run: | + golangci-lint run ./... # This will lint all Go files in the repo and subdirectories + + # 5. Run unit tests with coverage in the entire repository + - name: Run unit tests with coverage + run: | + # Create a directory to store coverage files + mkdir -p $GITHUB_WORKSPACE/coverage_files + # Find all *_test.go files + test_files=$(find ./ -type f -name '*_test.go') + # Check if there are any test files + if [ -z "$test_files" ]; then + echo "No test cases found in the repository. Skipping test execution." + exit 0 + fi + # Run tests and store coverage for each test directory + for test_file in $test_files; do + test_dir=$(dirname "$test_file") + go_file="${test_file/_test.go/.go}" + echo "Running tests in $test_dir for $go_file" + go test -v -coverprofile=$GITHUB_WORKSPACE/coverage_files/coverage_$(basename "$go_file" .go).out $test_dir || echo "Tests failed, but continuing." + done + + # 6. Check coverage for each generated coverage file + - name: Check coverage for each test file + run: | + coverage_files=$(find $GITHUB_WORKSPACE/coverage_files -name "coverage_*.out") + # If no coverage files were generated, exit without failure + if [ -z "$coverage_files" ]; then + echo "No coverage files found. Skipping coverage check." + exit 0 + fi + for coverage_file in $coverage_files; do + echo "Checking coverage for $coverage_file" + coverage=$(go tool cover -func=$coverage_file | grep total | awk '{print $3}' | sed 's/%//') + echo "Coverage for $coverage_file: $coverage%" + if (( $(echo "$coverage < 80" | bc -l) )); then + echo "Coverage for $coverage_file is below 80%. Failing the job." + exit 1 + fi + done From 519cca19af566102351ac270fd04eeaa39d35d00 Mon Sep 17 00:00:00 2001 From: AbhishekHS220 Date: Tue, 25 Mar 2025 10:40:16 +0530 Subject: [PATCH 4/5] Update beckn_ci_test.yml --- .github/workflows/beckn_ci_test.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/beckn_ci_test.yml b/.github/workflows/beckn_ci_test.yml index c9c1414..fb6a4cf 100644 --- a/.github/workflows/beckn_ci_test.yml +++ b/.github/workflows/beckn_ci_test.yml @@ -67,8 +67,8 @@ jobs: echo "Checking coverage for $coverage_file" coverage=$(go tool cover -func=$coverage_file | grep total | awk '{print $3}' | sed 's/%//') echo "Coverage for $coverage_file: $coverage%" - if (( $(echo "$coverage < 80" | bc -l) )); then - echo "Coverage for $coverage_file is below 80%. Failing the job." + if (( $(echo "$coverage < 90" | bc -l) )); then + echo "Coverage for $coverage_file is below 90%. Failing the job." exit 1 fi done From 9f921949d139731132b1f2cb0438e2fdfb4d39dd Mon Sep 17 00:00:00 2001 From: tanyamadaan Date: Tue, 25 Mar 2025 16:21:04 +0530 Subject: [PATCH 5/5] Reverting to resolve merge conflicts --- pkg/plugin/manager.go | 35 +++++++---------------------------- pkg/plugin/plugin.yaml | 6 ------ schemas.zip | Bin 0 -> 59493 bytes 3 files changed, 7 insertions(+), 34 deletions(-) delete mode 100644 pkg/plugin/plugin.yaml create mode 100644 schemas.zip diff --git a/pkg/plugin/manager.go b/pkg/plugin/manager.go index 5e75357..86a0b02 100644 --- a/pkg/plugin/manager.go +++ b/pkg/plugin/manager.go @@ -12,13 +12,12 @@ 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"` + Root string `yaml:"root"` + Signer PluginConfig `yaml:"signer"` + Verifier PluginConfig `yaml:"verifier"` + Decrypter PluginConfig `yaml:"decrypter"` + Encrypter PluginConfig `yaml:"encrypter"` + Publisher PluginConfig `yaml:"publisher"` } // PluginConfig represents configuration details for a plugin. @@ -34,7 +33,6 @@ type Manager struct { dp definition.DecrypterProvider ep definition.EncrypterProvider pb definition.PublisherProvider - svp definition.SchemaValidatorProvider 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 schema validation plugin. - svp, err := provider[definition.SchemaValidatorProvider](cfg.Root, cfg.Encrypter.ID) - if err != nil { - return nil, fmt.Errorf("failed to load schema validation plugin: %w", err) - } - - return &Manager{sp: sp, vp: vp, pb: pb, ep: ep, dp: dp, svp: svp, 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,16 +169,3 @@ func (m *Manager) Publisher(ctx context.Context) (definition.Publisher, error) { } return publisher, nil } - -// Encrypter retrieves the encryption plugin instance. -func (m *Manager) SchemaValidation(ctx context.Context) (definition.SchemaValidator, func() error, error) { - if m.svp == nil { - return nil, nil, fmt.Errorf("schema validation 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: %w", err) - } - return schemaValidator, close, nil -} diff --git a/pkg/plugin/plugin.yaml b/pkg/plugin/plugin.yaml deleted file mode 100644 index 174211b..0000000 --- a/pkg/plugin/plugin.yaml +++ /dev/null @@ -1,6 +0,0 @@ -root: pkg/plugin/implementation/ -schema_validator: - id: schemaValidator - config: - schema_dir: schemas #Directory where the schema files are stored - plugin_path: pkg/plugin/implementations/ diff --git a/schemas.zip b/schemas.zip new file mode 100644 index 0000000000000000000000000000000000000000..d72c46d5994fd3b866bfcc9091a9ae9b35f111e1 GIT binary patch literal 59493 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