diff --git a/plugins/config.yaml b/plugins/config.yaml index 9783d62..1ddc057 100644 --- a/plugins/config.yaml +++ b/plugins/config.yaml @@ -2,5 +2,5 @@ plugins: validation_plugin: id: tekuriValidator config: - schema: schemas/schema.json + schema_dir: schemas/ 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 index 52fd534..fd22a61 100644 --- a/plugins/implementations/plugin_impl.go +++ b/plugins/implementations/plugin_impl.go @@ -3,6 +3,12 @@ package main import ( "context" "encoding/json" + "fmt" + "io/ioutil" + "path/filepath" + "strings" + + "beckn-onix/plugins/plugin_definition" "github.com/santhosh-tekuri/jsonschema/v6" ) @@ -12,6 +18,10 @@ type tekuriValidator struct { schema *jsonschema.Schema } +type tekuriValidatorProvider struct { + compiledSchemas map[string]map[string]*jsonschema.Schema // Cache for compiled schemas +} + // Validate validates the given data against the schema. func (v *tekuriValidator) Validate(ctx context.Context, data []byte) error { var jsonData interface{} @@ -21,15 +31,127 @@ func (v *tekuriValidator) Validate(ctx context.Context, data []byte) error { 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 +func (vp *tekuriValidatorProvider) Initialize(schemaDir string) error { + // Check if vp is nil + if vp == nil { + return fmt.Errorf("tekuriValidatorProvider is not initialized") } - return &tekuriValidator{schema: schema}, nil + + // Initialize compiledSchemas + vp.compiledSchemas = make(map[string]map[string]*jsonschema.Schema) + + compiler := jsonschema.NewCompiler() + if compiler == nil { + return fmt.Errorf("jsonschema compiler is not initialized") + } + + files, err := ioutil.ReadDir(schemaDir) + if err != nil { + return fmt.Errorf("failed to read schema directory: %w", err) + } + + for _, file := range files { + if !file.IsDir() && strings.HasSuffix(file.Name(), ".json") { + schemaPath := filepath.Join(schemaDir, file.Name()) + absSchemaPath, _ := filepath.Abs(schemaPath) + fmt.Println("path:", absSchemaPath) + fmt.Printf("Reading schema file: %s\n", schemaPath) + + fileContent, err := ioutil.ReadFile(schemaPath) + if err != nil { + return fmt.Errorf("failed to read schema file %s: %w", schemaPath, err) + } + + var schemaData map[string]interface{} + if err := json.Unmarshal(fileContent, &schemaData); err != nil { + return fmt.Errorf("failed to unmarshal schema file %s: %w", schemaPath, err) + } + + if defs, ok := schemaData["$defs"].(map[string]interface{}); ok { + nameParts := strings.Split(strings.TrimSuffix(file.Name(), ".json"), "_") + if len(nameParts) != 3 { + return fmt.Errorf("invalid schema file name format: %s", file.Name()) + } + domain := strings.ReplaceAll(strings.ToLower(nameParts[0]+"_"+nameParts[1]), ":", "_") + version := strings.ToLower(nameParts[2]) + fmt.Printf("Domain: %s, Version: %s\n", domain, version) + + for defKey, defValue := range defs { + fmt.Println("value :", defValue) + tempSchema := map[string]interface{}{ + "$id": fmt.Sprintf("file://%s", absSchemaPath), + "$schema": schemaData["$schema"], + "$defs": map[string]interface{}{ + defKey: defValue, + }, + } + tempSchemaData, err := json.Marshal(tempSchema) + if err != nil { + return fmt.Errorf("failed to marshal temporary schema for $defs.%s: %w", defKey, err) + } + fmt.Println(" ") + + // Use a unique ID for the resource to avoid conflict + resourceId := fmt.Sprintf("file://%s/%s", absSchemaPath, defKey) + fmt.Println("resource Id :", resourceId) + compiler.AddResource(resourceId, strings.NewReader(string(tempSchemaData))) + + // Compile the specific $defs section + defSchemaPath := fmt.Sprintf("file://%s", absSchemaPath) + + fmt.Println("def schema path printing ::: ", defSchemaPath) + + compiledSchema, err := compiler.Compile(defSchemaPath) + if err != nil { + fmt.Printf("Failed to compile $defs.%s in schema file: %s\nError: %v\n", defKey, schemaPath, err) + continue + } + + fmt.Println("schema :", compiledSchema) + + // Initialize nested map if not already initialized + if _, exists := vp.compiledSchemas[domain]; !exists { + vp.compiledSchemas[domain] = make(map[string]*jsonschema.Schema) + } + + cacheKey := fmt.Sprintf("%s_%s_%s", domain, version, defKey) + fmt.Println("key :", cacheKey) + vp.compiledSchemas[domain][cacheKey] = compiledSchema + fmt.Printf("Compiled and cached $defs.%s schema: %s\n", defKey, cacheKey) + } + } + } + } + + return nil } -var Provider = tekuriValidatorProvider{} +func (vp *tekuriValidatorProvider) Get(schemaKey string) (plugin_definition.Validator, error) { + // Extract domain, version, and defKey from the schemaKey + schemaKey = strings.Replace(schemaKey, ":", "_", -1) + + parts := strings.Split(schemaKey, "_") + if len(parts) != 3 { + return nil, fmt.Errorf("invalid schema key format: %s", schemaKey) + } + domain := parts[0] + "_" + parts[1] + defKey := parts[2] + + // Look up the compiled schema in the nested map + if domainMap, ok := vp.compiledSchemas[domain]; ok { + if schema, ok := domainMap[defKey]; ok { + return &tekuriValidator{schema: schema}, nil + } + } + return nil, fmt.Errorf("schema not found: %s", schemaKey) +} + +// Ensure tekuriValidatorProvider implements ValidatorProvider +var _ plugin_definition.ValidatorProvider = (*tekuriValidatorProvider)(nil) + +var providerInstance = &tekuriValidatorProvider{} + +// Exported function to return the provider instance +func GetProvider() plugin_definition.ValidatorProvider { + return providerInstance +} diff --git a/plugins/implementations/tekuriValidator.so b/plugins/implementations/tekuriValidator.so new file mode 100644 index 0000000..5bdd5f5 Binary files /dev/null and b/plugins/implementations/tekuriValidator.so differ diff --git a/plugins/manager.go b/plugins/manager.go index 31266c9..d29e97a 100644 --- a/plugins/manager.go +++ b/plugins/manager.go @@ -1,23 +1,50 @@ package main import ( - //"context" + "beckn-onix/plugins/plugin_definition" + "context" + "encoding/json" "fmt" "io/ioutil" - - //"log" + "log" "plugin" + "strings" "gopkg.in/yaml.v2" ) -// PluginManager manages the loading and execution of plugins. -type PluginManager struct { - validatorProvider ValidatorProvider +type PluginConfig struct { + Plugins Plugins `yaml:"plugins"` } -// NewPluginManager initializes the PluginManager with the given configuration. //new -func New(pluginsConfig PluginConfig) (*PluginManager, error) { +type Plugins struct { + ValidationPlugin ValidationPlugin `yaml:"validation_plugin"` +} + +type ValidationPlugin struct { + ID string `yaml:"id"` + Config PluginDetails `yaml:"config"` + PluginPath string `yaml:"plugin_path"` +} + +type PluginDetails struct { + Schema string `yaml:"schema_dir"` +} + +type Payload struct { + Context struct { + Domain string `json:"domain"` + Version string `json:"version"` + } `json:"context"` +} + +// PluginManager manages the loading and execution of plugins. +type PluginManager struct { + validatorProvider plugin_definition.ValidatorProvider +} + +// NewValidatorProvider initializes the PluginManager with the given configuration. +func NewValidatorProvider(pluginsConfig PluginConfig) (*PluginManager, error) { validationPlugin := pluginsConfig.Plugins.ValidationPlugin if validationPlugin.ID == "" { @@ -37,16 +64,18 @@ func New(pluginsConfig PluginConfig) (*PluginManager, error) { return nil, fmt.Errorf("failed to open plugin: %v", err) } - vpSymbol, err := p.Lookup("Provider") + vpSymbol, err := p.Lookup("GetProvider") if err != nil { return nil, err } - validatorProvider, ok := vpSymbol.(ValidatorProvider) + getProviderFunc, ok := vpSymbol.(func() plugin_definition.ValidatorProvider) if !ok { - return nil, fmt.Errorf("failed to cast to ValidatorProvider") + return nil, fmt.Errorf("failed to cast to *plugin_definition.ValidatorProvider") } + validatorProvider := getProviderFunc() + return &PluginManager{validatorProvider: validatorProvider}, nil } @@ -66,32 +95,48 @@ func loadPluginsConfig(filePath string) (PluginConfig, error) { return config, nil } -// func main() { -// pluginsConfig, err := loadPluginsConfig("schema.yaml") -// if err != nil { -// log.Fatalf("Failed to load plugins configuration: %v", err) -// } +func main() { + pluginsConfig, err := loadPluginsConfig("config.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 + pm, err := NewValidatorProvider(pluginsConfig) + if err != nil { + log.Fatalf("Failed to create PluginManager: %v", err) + } -// payloadData, err := ioutil.ReadFile("schemas/payload.json") -// if err != nil { -// log.Fatalf("Failed to read payload data: %v", err) -// } + schemaDir := pluginsConfig.Plugins.ValidationPlugin.Config.Schema -// validator, err := pm.validatorProvider.New(schemaPath) -// if err != nil { -// log.Fatalf("Failed to get validator: %v", err) -// } + err = pm.validatorProvider.Initialize(schemaDir) + if err != nil { + log.Fatalf("Failed to initialize validator provider: %v", err) + } -// err = validator.Validate(context.Background(), payloadData) -// if err != nil { -// log.Printf("Validation failed: %v", err) -// } else { -// log.Println("Validation succeeded!") -// } -// } + payloadData, err := ioutil.ReadFile("test/payload.json") + if err != nil { + log.Fatalf("Failed to read payload data: %v", err) + } + + var payload Payload + if err := json.Unmarshal(payloadData, &payload); err != nil { + log.Fatalf("Failed to unmarshal payload: %v", err) + } + + // Construct the schema file name based on domain and version + schemaFileName := fmt.Sprintf("%s_%s.json", strings.ToLower(payload.Context.Domain), strings.ToLower(payload.Context.Version)) + + // Get the validator for the specific schema + validator, err := pm.validatorProvider.Get(schemaFileName) + if err != nil { + log.Fatalf("Failed to get validator: %v", err) + } + fmt.Println("printing validator :", validator) + + // Validate the payload against the schema + if err := validator.Validate(context.Background(), payloadData); err != nil { + log.Printf("Validation failed: %v", err) + } else { + log.Println("Validation succeeded!") + } +} diff --git a/plugins/model.go b/plugins/model.go deleted file mode 100644 index bf1be81..0000000 --- a/plugins/model.go +++ /dev/null @@ -1,22 +0,0 @@ -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 deleted file mode 100644 index 4e7303e..0000000 --- a/plugins/plugin.go +++ /dev/null @@ -1,11 +0,0 @@ -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/plugin_definition/plugin.go b/plugins/plugin_definition/plugin.go new file mode 100644 index 0000000..698ad46 --- /dev/null +++ b/plugins/plugin_definition/plugin.go @@ -0,0 +1,14 @@ +package plugin_definition + +import "context" + +// Validator interface for schema validation +type Validator interface { + Validate(ctx context.Context, b []byte) error +} + +// ValidatorProvider interface for creating validators +type ValidatorProvider interface { + Get(p string) (Validator, error) + Initialize(schemaDir string) error +} diff --git a/plugins/schemas/ondc_trv10_2.0.0.json b/plugins/schemas/ondc_trv10_2.0.0.json new file mode 100644 index 0000000..01beefb --- /dev/null +++ b/plugins/schemas/ondc_trv10_2.0.0.json @@ -0,0 +1,253 @@ +{ + "$id": "https://example.com/ondc/trv10/2.0.0", + "$schema": "https://json-schema.org/draft/2020-12/schema", + + "$defs": { + "search": { + "$id": "https://example.com/ondc/trv10/2.0.0/search", + "type": "object", + "properties": { + "context": { + "type": "object", + "properties": { + "domain": { + "type": "string" + }, + "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"] + } + } + } + } + }, + { + "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"] + } + } + } + } +} diff --git a/plugins/schemas/schema.json b/plugins/schemas/schema.json deleted file mode 100644 index b30c707..0000000 --- a/plugins/schemas/schema.json +++ /dev/null @@ -1,294 +0,0 @@ -{ - "$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 diff --git a/plugins/schemas/payload.json b/plugins/test/payload.json similarity index 100% rename from plugins/schemas/payload.json rename to plugins/test/payload.json