From 50f93d8239eda07da7bdecdecf05ee6b24e889cb Mon Sep 17 00:00:00 2001 From: AshwiniK-protean Date: Fri, 14 Feb 2025 11:05:54 +0530 Subject: [PATCH] created a plugin for schema validation and related unit test --- go.mod | 10 + go.sum | 10 + plugins/config.yaml | 6 + plugins/implementations/plugin_impl.go | 35 +++ plugins/manager.go | 97 ++++++++ plugins/manager_test.go | 35 +++ plugins/model.go | 22 ++ plugins/plugin.go | 11 + plugins/schemas/payload.json | 80 +++++++ plugins/schemas/schema.json | 294 +++++++++++++++++++++++++ 10 files changed, 600 insertions(+) create mode 100644 go.mod create mode 100644 go.sum create mode 100644 plugins/config.yaml create mode 100644 plugins/implementations/plugin_impl.go create mode 100644 plugins/manager.go create mode 100644 plugins/manager_test.go create mode 100644 plugins/model.go create mode 100644 plugins/plugin.go create mode 100644 plugins/schemas/payload.json create mode 100644 plugins/schemas/schema.json diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..7ea3206 --- /dev/null +++ b/go.mod @@ -0,0 +1,10 @@ +module beckn-onix + +go 1.23.4 + +require ( + github.com/santhosh-tekuri/jsonschema/v6 v6.0.1 + gopkg.in/yaml.v2 v2.4.0 +) + +require golang.org/x/text v0.14.0 // indirect diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..2c8aa28 --- /dev/null +++ b/go.sum @@ -0,0 +1,10 @@ +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/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= +golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= diff --git a/plugins/config.yaml b/plugins/config.yaml new file mode 100644 index 0000000..9783d62 --- /dev/null +++ b/plugins/config.yaml @@ -0,0 +1,6 @@ +plugins: + validation_plugin: + id: tekuriValidator + config: + schema: schemas/schema.json + plugin_path: implementations/ # Path to the directory containing the .so files \ No newline at end of file diff --git a/plugins/implementations/plugin_impl.go b/plugins/implementations/plugin_impl.go new file mode 100644 index 0000000..52fd534 --- /dev/null +++ b/plugins/implementations/plugin_impl.go @@ -0,0 +1,35 @@ +package main + +import ( + "context" + "encoding/json" + + "github.com/santhosh-tekuri/jsonschema/v6" +) + +// tekuriValidator implements the Validator interface using the santhosh-tekuri/jsonschema package. +type tekuriValidator struct { + schema *jsonschema.Schema +} + +// Validate validates the given data against the schema. +func (v *tekuriValidator) Validate(ctx context.Context, data []byte) error { + var jsonData interface{} + if err := json.Unmarshal(data, &jsonData); err != nil { + return err + } + return v.schema.Validate(jsonData) +} + +type tekuriValidatorProvider struct{} + +func (vp tekuriValidatorProvider) New(schemaPath string) (*tekuriValidator, error) { + compiler := jsonschema.NewCompiler() + schema, err := compiler.Compile(schemaPath) + if err != nil { + return nil, err + } + return &tekuriValidator{schema: schema}, nil +} + +var Provider = tekuriValidatorProvider{} diff --git a/plugins/manager.go b/plugins/manager.go new file mode 100644 index 0000000..31266c9 --- /dev/null +++ b/plugins/manager.go @@ -0,0 +1,97 @@ +package main + +import ( + //"context" + "fmt" + "io/ioutil" + + //"log" + "plugin" + + "gopkg.in/yaml.v2" +) + +// PluginManager manages the loading and execution of plugins. +type PluginManager struct { + validatorProvider ValidatorProvider +} + +// NewPluginManager initializes the PluginManager with the given configuration. //new +func New(pluginsConfig PluginConfig) (*PluginManager, error) { + validationPlugin := pluginsConfig.Plugins.ValidationPlugin + + if validationPlugin.ID == "" { + return nil, fmt.Errorf("validation_plugin ID is empty") + } + + pluginPath := validationPlugin.PluginPath + validationPlugin.ID + ".so" + + // Check if the plugin path is empty + if pluginPath == "" { + return nil, fmt.Errorf("plugin path is empty") + } + + // Load the plugin + p, err := plugin.Open(pluginPath) + if err != nil { + return nil, fmt.Errorf("failed to open plugin: %v", err) + } + + vpSymbol, err := p.Lookup("Provider") + if err != nil { + return nil, err + } + + validatorProvider, ok := vpSymbol.(ValidatorProvider) + if !ok { + return nil, fmt.Errorf("failed to cast to ValidatorProvider") + } + + return &PluginManager{validatorProvider: validatorProvider}, nil +} + +// loadPluginsConfig loads the plugins configuration from a YAML file. +func loadPluginsConfig(filePath string) (PluginConfig, error) { + data, err := ioutil.ReadFile(filePath) + if err != nil { + return PluginConfig{}, err + } + + var config PluginConfig + err = yaml.Unmarshal(data, &config) + if err != nil { + return PluginConfig{}, err + } + + return config, nil +} + +// func main() { +// pluginsConfig, err := loadPluginsConfig("schema.yaml") +// if err != nil { +// log.Fatalf("Failed to load plugins configuration: %v", err) +// } + +// pm, err := New(pluginsConfig) +// if err != nil { +// log.Fatalf("Failed to create PluginManager: %v", err) +// } +// schemaPath := pluginsConfig.ValidationPlugin.Config.Schema + +// payloadData, err := ioutil.ReadFile("schemas/payload.json") +// if err != nil { +// log.Fatalf("Failed to read payload data: %v", err) +// } + +// validator, err := pm.validatorProvider.New(schemaPath) +// if err != nil { +// log.Fatalf("Failed to get validator: %v", err) +// } + +// err = validator.Validate(context.Background(), payloadData) +// if err != nil { +// log.Printf("Validation failed: %v", err) +// } else { +// log.Println("Validation succeeded!") +// } +// } diff --git a/plugins/manager_test.go b/plugins/manager_test.go new file mode 100644 index 0000000..81bacf1 --- /dev/null +++ b/plugins/manager_test.go @@ -0,0 +1,35 @@ +package main + +import ( + "testing" +) + +func TestLoadPluginsConfig(t *testing.T) { + // Test loading a valid configuration + config, err := loadPluginsConfig("config.yaml") + if err != nil { + t.Fatalf("Expected no error, got %v", err) + } + + if config.Plugins.ValidationPlugin.ID == "" { + t.Fatal("Expected validation_plugin ID to be set") + } +} + +// func TestNewPluginManager(t *testing.T) { +// // Load the configuration +// config, err := loadPluginsConfig("config.yaml") +// if err != nil { +// t.Fatalf("Failed to load plugins configuration: %v", err) +// } + +// // Create a new PluginManager +// pm, err := New(config) +// if err != nil { +// t.Fatalf("Failed to create PluginManager: %v", err) +// } + +// if pm == nil { +// t.Fatal("Expected PluginManager to be created") +// } +// } diff --git a/plugins/model.go b/plugins/model.go new file mode 100644 index 0000000..bf1be81 --- /dev/null +++ b/plugins/model.go @@ -0,0 +1,22 @@ +package main + +type PluginConfig struct { + Plugins Plugins `yaml:"plugins"` +} + +// PluginConfig represents the configuration for plugins. +type Plugins struct { + ValidationPlugin ValidationPlugin `yaml:"validation_plugin"` +} + +// ValidationPlugin represents the configuration for a validation plugin. +type ValidationPlugin struct { + ID string `yaml:"id"` + Config PluginDetails `yaml:"config"` + PluginPath string `yaml:"plugin_path"` +} + +// PluginDetails represents the details of the plugin configuration. +type PluginDetails struct { + Schema string `yaml:"schema"` +} diff --git a/plugins/plugin.go b/plugins/plugin.go new file mode 100644 index 0000000..4e7303e --- /dev/null +++ b/plugins/plugin.go @@ -0,0 +1,11 @@ +package main + +import "context" + +type Validator interface { + Validate(ctx context.Context, b []byte) error //context parameter +} + +type ValidatorProvider interface { + New(p string) (Validator, error) +} diff --git a/plugins/schemas/payload.json b/plugins/schemas/payload.json new file mode 100644 index 0000000..644fb4e --- /dev/null +++ b/plugins/schemas/payload.json @@ -0,0 +1,80 @@ +{ + "context": { + "action": "search", + "bap_id": "example-bap.com", + "bap_uri": "https://example-bap.com/prod/trv10", + "domain": "ONDC:TRV10", + "location": { + "city": { + "code": "std:080" + }, + "country": { + "code": "IND" + } + }, + "message_id": "40963dc1-e402-4f4d-ae70-7c5864ca682c", + "timestamp": "2023-12-09T13:39:56.645Z", + "transaction_id": "870782be-6757-43f1-945c-8eeaf9536259", + "ttl": "PT30S", + "version": "2.0.0" + }, + "message": { + "intent": { + "fulfillment": { + "stops": [ + { + "location": { + "gps": "13.0089, 77.644408" + }, + "type": "START" + }, + { + "location": { + "gps": "12.971186, 77.586812" + }, + "type": "END" + } + ] + }, + "payment": { + "collected_by": "BPP", + "tags": [ + { + "descriptor": { + "code": "BUYER_FINDER_FEES" + }, + "display": false, + "list": [ + { + "descriptor": { + "code": "BUYER_FINDER_FEES_PERCENTAGE" + }, + "value": "1" + } + ] + }, + { + "descriptor": { + "code": "SETTLEMENT_TERMS" + }, + "display": false, + "list": [ + { + "descriptor": { + "code": "DELAY_INTEREST" + }, + "value": "5" + }, + { + "descriptor": { + "code": "STATIC_TERMS" + }, + "value": "example-test-bap.com/static-terms.txt" + } + ] + } + ] + } + } + } + } \ No newline at end of file diff --git a/plugins/schemas/schema.json b/plugins/schemas/schema.json new file mode 100644 index 0000000..b30c707 --- /dev/null +++ b/plugins/schemas/schema.json @@ -0,0 +1,294 @@ +{ + "$id": "http://example.com/schema/search", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "context": { + "type": "object", + "properties": { + "domain": { + "type": "string", + "description": "Domain code relevant to this transaction context" + }, + "location": { + "type": "object", + "properties": { + "city": { + "type": "object", + "properties": { + "code": { + "type": "string" + } + }, + "required": ["code"] + }, + "country": { + "type": "object", + "properties": { + "code": { + "type": "string", + "enum": ["IND"] + } + }, + "required": ["code"] + } + }, + "required": ["city", "country"] + }, + "action": { + "type": "string", + "enum": ["search"] + }, + "bap_id": { + "type": "string" + }, + "bap_uri": { + "type": "string", + "format": "uri" + }, + "bpp_id": { + "type": "string" + }, + "bpp_uri": { + "type": "string", + "format": "uri" + }, + "transaction_id": { + "type": "string", + "format": "uuid" + }, + "message_id": { + "type": "string", + "format": "uuid" + }, + "timestamp": { + "type": "string", + "format": "date-time" + }, + "ttl": { + "type": "string", + "format": "duration" + } + }, + "required": [ + "domain", + "location", + "action", + "bap_id", + "bap_uri", + "transaction_id", + "message_id", + "timestamp", + "ttl" + ] + }, + "message": { + "type": "object", + "properties": { + "intent": { + "type": "object", + "properties": { + "fulfillment": { + "type": "object", + "properties": { + "stops": { + "type": "array", + "items": { + "type": "object", + "properties": { + "location": { + "type": "object", + "properties": { + "gps": { + "type": "string" + } + }, + "required": ["gps"] + }, + "type": { + "type": "string", + "enum": ["START", "END"] + } + }, + "required": ["location", "type"] + }, + "minItems": 2, + "maxItems": 2 + } + }, + "required": ["stops"] + }, + "payment": { + "type": "object", + "properties": { + "collected_by": { + "type": "string", + "enum": ["BPP", "BAP"] + }, + "tags": { + "type": "array", + "minItems": 2, + "maxItems": 2, + "uniqueItems": true, + "items": { + "type": "object", + "properties": { + "descriptor": { + "type": "object", + "properties": { + "code": { + "type": "string", + "enum": ["SETTLEMENT_TERMS", "BUYER_FINDER_FEES"] + } + }, + "required": ["code"] + } + }, + "allOf": [ + { + "if": { + "properties": { + "descriptor": { + "properties": { + "code": { + "const": "SETTLEMENT_TERMS" + } + }, + "required": ["code"] + } + } + }, + "then": { + "properties": { + "list": { + "type": "array", + "items": { + "type": "object", + "properties": { + "descriptor": { + "type": "object", + "properties": { + "code": { + "type": "string", + "enum": [ + "SETTLEMENT_BASIS", + "SETTLEMENT_WINDOW", + "STATIC_TERMS", + "SETTLEMENT_TYPE", + "DELAY_INTEREST" + ] + } + }, + "required": ["code"] + }, + "value": { + "type": "string" + } + }, + "required": ["descriptor", "value"], + "allOf": [ + { + "if": { + "properties": { + "descriptor": { + "properties": { + "code": { + "const": "SETTLEMENT_TYPE" + } + }, + "required": ["code"] + } + } + }, + "then": { + "properties": { + "value": { + "enum": ["upi", "neft", "rtgs"] + } + } + } + }, + { + "if": { + "properties": { + "descriptor": { + "properties": { + "code": { + "const": "DELAY_INTEREST" + } + }, + "required": ["code"] + } + } + }, + "then": { + "properties": { + "value": { + "pattern": "^\\d+(\\.\\d{1,2})?$" + } + } + } + } + ] + } + } + } + } + }, + { + "if": { + "properties": { + "descriptor": { + "properties": { + "code": { + "const": "BUYER_FINDER_FEES" + } + }, + "required": ["code"] + } + } + }, + "then": { + "properties": { + "list": { + "type": "array", + "items": { + "type": "object", + "properties": { + "descriptor": { + "type": "object", + "properties": { + "code": { + "enum": ["BUYER_FINDER_FEES_PERCENTAGE"] + } + }, + "required": ["code"] + }, + "value": { + "type": "string", + "pattern": "^-?\\d+(\\.\\d+)?$" + } + }, + "required": ["descriptor", "value"] + } + } + } + } + } + ], + "required": ["descriptor"] + } + } + }, + "required": ["collected_by", "tags"] + } + }, + "required": ["fulfillment", "payment"] + } + }, + "required": ["intent"] + } + }, + "required": ["context", "message"] + } + \ No newline at end of file