From 2f8c88810140356bfec67d249460210b8a2c993f Mon Sep 17 00:00:00 2001 From: ameersohel45 Date: Fri, 14 Nov 2025 00:45:29 +0530 Subject: [PATCH 1/9] Issue 543 - feat: add new validator plugin --- .../schemav2validator/cmd/plugin.go | 42 +++ .../schemav2validator/schemav2validator.go | 346 ++++++++++++++++++ 2 files changed, 388 insertions(+) create mode 100644 pkg/plugin/implementation/schemav2validator/cmd/plugin.go create mode 100644 pkg/plugin/implementation/schemav2validator/schemav2validator.go diff --git a/pkg/plugin/implementation/schemav2validator/cmd/plugin.go b/pkg/plugin/implementation/schemav2validator/cmd/plugin.go new file mode 100644 index 0000000..78b6226 --- /dev/null +++ b/pkg/plugin/implementation/schemav2validator/cmd/plugin.go @@ -0,0 +1,42 @@ +package main + +import ( + "context" + "errors" + "strconv" + + "github.com/beckn-one/beckn-onix/pkg/plugin/definition" + "github.com/beckn-one/beckn-onix/pkg/plugin/implementation/schemav2validator" +) + +// schemav2ValidatorProvider provides instances of schemav2Validator. +type schemav2ValidatorProvider struct{} + +// New initialises a new Schemav2Validator instance. +func (vp schemav2ValidatorProvider) 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") + } + + url, ok := config["url"] + if !ok || url == "" { + return nil, nil, errors.New("url not configured") + } + + cacheTTL := 3600 + if ttlStr, ok := config["cacheTTL"]; ok { + if ttl, err := strconv.Atoi(ttlStr); err == nil && ttl > 0 { + cacheTTL = ttl + } + } + + cfg := &schemav2validator.Config{ + URL: url, + CacheTTL: cacheTTL, + } + + return schemav2validator.New(ctx, cfg) +} + +// Provider is the exported plugin provider. +var Provider schemav2ValidatorProvider diff --git a/pkg/plugin/implementation/schemav2validator/schemav2validator.go b/pkg/plugin/implementation/schemav2validator/schemav2validator.go new file mode 100644 index 0000000..e9b43db --- /dev/null +++ b/pkg/plugin/implementation/schemav2validator/schemav2validator.go @@ -0,0 +1,346 @@ +package schemav2validator + +import ( + "context" + "encoding/json" + "fmt" + "net/url" + "strings" + "sync" + "time" + + "github.com/beckn-one/beckn-onix/pkg/log" + "github.com/beckn-one/beckn-onix/pkg/model" + + "github.com/getkin/kin-openapi/openapi3" +) + +// payload represents the structure of the data payload with context information. +type payload struct { + Context struct { + Action string `json:"action"` + } `json:"context"` +} + +// schemav2Validator implements the SchemaValidator interface. +type schemav2Validator struct { + config *Config + spec *cachedSpec + specMutex sync.RWMutex +} + +// cachedSpec holds a cached OpenAPI spec. +type cachedSpec struct { + doc *openapi3.T + loadedAt time.Time +} + +// Config struct for Schemav2Validator. +type Config struct { + URL string + CacheTTL int +} + +// New creates a new Schemav2Validator instance. +func New(ctx context.Context, config *Config) (*schemav2Validator, func() error, error) { + if config == nil { + return nil, nil, fmt.Errorf("config cannot be nil") + } + if config.URL == "" { + return nil, nil, fmt.Errorf("config URL cannot be empty") + } + + if config.CacheTTL == 0 { + config.CacheTTL = 3600 + } + + v := &schemav2Validator{ + config: config, + } + + if err := v.initialise(ctx); err != nil { + return nil, nil, fmt.Errorf("failed to initialise schemav2Validator: %v", err) + } + + go v.refreshLoop(ctx) + + return v, nil, nil +} + +// Validate validates the given data against the OpenAPI schema. +func (v *schemav2Validator) Validate(ctx context.Context, reqURL *url.URL, data []byte) error { + var payloadData payload + err := json.Unmarshal(data, &payloadData) + if err != nil { + return model.NewBadReqErr(fmt.Errorf("failed to parse JSON payload: %v", err)) + } + + if payloadData.Context.Action == "" { + return model.NewBadReqErr(fmt.Errorf("missing field Action in context")) + } + + v.specMutex.RLock() + spec := v.spec + v.specMutex.RUnlock() + + if spec == nil || spec.doc == nil { + return model.NewBadReqErr(fmt.Errorf("no OpenAPI spec loaded")) + } + + action := payloadData.Context.Action + var schema *openapi3.SchemaRef + var matchedPath string + + // Search all spec paths for matching action in schema + for path, item := range spec.doc.Paths.Map() { + if item == nil { + continue + } + // Check all HTTP methods for this path + for _, op := range []*openapi3.Operation{item.Post, item.Get, item.Put, item.Patch, item.Delete} { + if op == nil || op.RequestBody == nil || op.RequestBody.Value == nil { + continue + } + content := op.RequestBody.Value.Content.Get("application/json") + if content == nil || content.Schema == nil || content.Schema.Value == nil { + continue + } + // Check if schema has action constraint matching our action + if v.schemaMatchesAction(content.Schema.Value, action) { + schema = content.Schema + matchedPath = path + break + } + } + if schema != nil { + break + } + } + + if schema == nil || schema.Value == nil { + return model.NewBadReqErr(fmt.Errorf("unsupported action: %s", action)) + } + + log.Debugf(ctx, "Validating action: %s, matched path: %s", action, matchedPath) + + var jsonData any + if err := json.Unmarshal(data, &jsonData); err != nil { + return model.NewBadReqErr(fmt.Errorf("invalid JSON: %v", err)) + } + + opts := []openapi3.SchemaValidationOption{ + openapi3.VisitAsRequest(), + openapi3.MultiErrors(), + openapi3.EnableFormatValidation(), + } + if err := schema.Value.VisitJSON(jsonData, opts...); err != nil { + log.Debugf(ctx, "Schema validation failed: %v", err) + return v.formatValidationError(err) + } + + return nil +} + +// initialise loads the OpenAPI spec from the configuration. +func (v *schemav2Validator) initialise(ctx context.Context) error { + return v.loadSpec(ctx) +} + +// loadSpec loads the OpenAPI spec from URL or local path. +func (v *schemav2Validator) loadSpec(ctx context.Context) error { + loader := openapi3.NewLoader() + + // Allow external references + loader.IsExternalRefsAllowed = true + + var doc *openapi3.T + var err error + + urlOrPath := v.config.URL + + if strings.HasPrefix(urlOrPath, "http://") || strings.HasPrefix(urlOrPath, "https://") { + u, parseErr := url.Parse(urlOrPath) + if parseErr != nil { + return fmt.Errorf("failed to parse URL: %v", parseErr) + } + doc, err = loader.LoadFromURI(u) + } else { + doc, err = loader.LoadFromFile(urlOrPath) + } + + if err != nil { + log.Errorf(ctx, err, "Invalid URL or unreachable: %s", urlOrPath) + return fmt.Errorf("failed to load OpenAPI document: %v", err) + } + + // Validate spec (skip strict validation to allow JSON Schema keywords) + if err := doc.Validate(ctx); err != nil { + log.Debugf(ctx, "Spec validation warnings (non-fatal): %v", err) + } else { + log.Debugf(ctx, "Spec validation passed") + } + + v.specMutex.Lock() + v.spec = &cachedSpec{ + doc: doc, + loadedAt: time.Now(), + } + v.specMutex.Unlock() + + log.Debugf(ctx, "Loaded OpenAPI spec from %s", urlOrPath) + return nil +} + +// refreshLoop periodically reloads expired specs based on TTL. +func (v *schemav2Validator) refreshLoop(ctx context.Context) { + ticker := time.NewTicker(time.Duration(v.config.CacheTTL) * time.Second) + defer ticker.Stop() + + for { + select { + case <-ctx.Done(): + return + case <-ticker.C: + v.reloadExpiredSpec(ctx) + } + } +} + +// reloadExpiredSpec reloads spec if it has exceeded its TTL. +func (v *schemav2Validator) reloadExpiredSpec(ctx context.Context) { + v.specMutex.RLock() + if v.spec == nil { + v.specMutex.RUnlock() + return + } + needsReload := time.Since(v.spec.loadedAt) >= time.Duration(v.config.CacheTTL)*time.Second + v.specMutex.RUnlock() + + if needsReload { + if err := v.loadSpec(ctx); err != nil { + log.Errorf(ctx, err, "Failed to reload spec") + } else { + log.Debugf(ctx, "Reloaded spec from %s", v.config.URL) + } + } +} + +// formatValidationError converts kin-openapi validation errors to ONIX error format. +func (v *schemav2Validator) formatValidationError(err error) error { + var schemaErrors []model.Error + + // Check if it's a MultiError (collection of errors) + if multiErr, ok := err.(openapi3.MultiError); ok { + for _, e := range multiErr { + v.extractSchemaErrors(e, &schemaErrors) + } + } else { + v.extractSchemaErrors(err, &schemaErrors) + } + + return &model.SchemaValidationErr{Errors: schemaErrors} +} + +// extractSchemaErrors recursively extracts detailed error information from SchemaError. +func (v *schemav2Validator) extractSchemaErrors(err error, schemaErrors *[]model.Error) { + if schemaErr, ok := err.(*openapi3.SchemaError); ok { + // If there's an origin error, recursively extract from it + if schemaErr.Origin != nil { + v.extractSchemaErrors(schemaErr.Origin, schemaErrors) + } else { + // Leaf error - extract the actual validation failure + pathParts := schemaErr.JSONPointer() + path := strings.Join(pathParts, "/") + if path == "" { + path = schemaErr.SchemaField + } + *schemaErrors = append(*schemaErrors, model.Error{ + Paths: path, + Message: schemaErr.Reason, + }) + } + } else if multiErr, ok := err.(openapi3.MultiError); ok { + // Nested MultiError + for _, e := range multiErr { + v.extractSchemaErrors(e, schemaErrors) + } + } else { + // Generic error + *schemaErrors = append(*schemaErrors, model.Error{ + Paths: "", + Message: err.Error(), + }) + } +} + +// schemaMatchesAction checks if a schema has an action constraint matching the given action. +func (v *schemav2Validator) schemaMatchesAction(schema *openapi3.Schema, action string) bool { + // Check direct properties + if ctxProp := schema.Properties["context"]; ctxProp != nil && ctxProp.Value != nil { + if v.checkActionEnum(ctxProp.Value, action) { + return true + } + } + + // Check allOf at schema level + for _, allOfSchema := range schema.AllOf { + if allOfSchema.Value != nil { + if ctxProp := allOfSchema.Value.Properties["context"]; ctxProp != nil && ctxProp.Value != nil { + if v.checkActionEnum(ctxProp.Value, action) { + return true + } + } + } + } + + return false +} + +// checkActionEnum checks if a context schema has action enum or const matching the given action. +func (v *schemav2Validator) checkActionEnum(contextSchema *openapi3.Schema, action string) bool { + // Check direct action property + if actionProp := contextSchema.Properties["action"]; actionProp != nil && actionProp.Value != nil { + // Check const field (stored in Extensions by kin-openapi) + if constVal, ok := actionProp.Value.Extensions["const"]; ok { + if constVal == action { + return true + } + } + // Check enum field + if len(actionProp.Value.Enum) > 0 { + for _, e := range actionProp.Value.Enum { + if e == action { + return true + } + } + } + } + + // Check allOf in context + for _, allOfSchema := range contextSchema.AllOf { + if allOfSchema.Value != nil { + if actionProp := allOfSchema.Value.Properties["action"]; actionProp != nil && actionProp.Value != nil { + // Check const field (stored in Extensions by kin-openapi) + if constVal, ok := actionProp.Value.Extensions["const"]; ok { + if constVal == action { + return true + } + } + // Check enum field + if len(actionProp.Value.Enum) > 0 { + for _, e := range actionProp.Value.Enum { + if e == action { + return true + } + } + } + } + // Recursively check nested allOf + if v.checkActionEnum(allOfSchema.Value, action) { + return true + } + } + } + + return false +} From f534e41b6c7907e2d4e3d7c9ca2ab9a064580a06 Mon Sep 17 00:00:00 2001 From: ameersohel45 Date: Fri, 14 Nov 2025 01:04:32 +0530 Subject: [PATCH 2/9] 543 - add: test cases --- .../schemav2validator/cmd/plugin_test.go | 157 +++++++++ .../schemav2validator_test.go | 298 ++++++++++++++++++ 2 files changed, 455 insertions(+) create mode 100644 pkg/plugin/implementation/schemav2validator/cmd/plugin_test.go create mode 100644 pkg/plugin/implementation/schemav2validator/schemav2validator_test.go diff --git a/pkg/plugin/implementation/schemav2validator/cmd/plugin_test.go b/pkg/plugin/implementation/schemav2validator/cmd/plugin_test.go new file mode 100644 index 0000000..af9c10d --- /dev/null +++ b/pkg/plugin/implementation/schemav2validator/cmd/plugin_test.go @@ -0,0 +1,157 @@ +package main + +import ( + "context" + "net/http" + "net/http/httptest" + "testing" +) + +const testSpec = `openapi: 3.1.0 +info: + title: Test API + version: 1.0.0 +paths: + /test: + post: + requestBody: + content: + application/json: + schema: + type: object + properties: + context: + type: object + properties: + action: + const: test +` + +func TestProvider_New(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Write([]byte(testSpec)) + })) + defer server.Close() + + tests := []struct { + name string + ctx context.Context + config map[string]string + wantErr bool + errMsg string + }{ + { + name: "nil context", + ctx: nil, + config: map[string]string{"url": server.URL}, + wantErr: true, + errMsg: "context cannot be nil", + }, + { + name: "missing url", + ctx: context.Background(), + config: map[string]string{}, + wantErr: true, + errMsg: "url not configured", + }, + { + name: "empty url", + ctx: context.Background(), + config: map[string]string{"url": ""}, + wantErr: true, + errMsg: "url not configured", + }, + { + name: "valid config with default TTL", + ctx: context.Background(), + config: map[string]string{"url": server.URL}, + wantErr: false, + }, + { + name: "valid config with custom TTL", + ctx: context.Background(), + config: map[string]string{ + "url": server.URL, + "cacheTTL": "7200", + }, + wantErr: false, + }, + { + name: "invalid TTL falls back to default", + ctx: context.Background(), + config: map[string]string{ + "url": server.URL, + "cacheTTL": "invalid", + }, + wantErr: false, + }, + { + name: "negative TTL falls back to default", + ctx: context.Background(), + config: map[string]string{ + "url": server.URL, + "cacheTTL": "-100", + }, + wantErr: false, + }, + { + name: "zero TTL falls back to default", + ctx: context.Background(), + config: map[string]string{ + "url": server.URL, + "cacheTTL": "0", + }, + wantErr: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + provider := schemav2ValidatorProvider{} + validator, cleanup, err := provider.New(tt.ctx, tt.config) + + if (err != nil) != tt.wantErr { + t.Errorf("New() error = %v, wantErr %v", err, tt.wantErr) + return + } + + if tt.wantErr && tt.errMsg != "" && err != nil { + if !contains(err.Error(), tt.errMsg) { + t.Errorf("New() error = %v, want error containing %v", err, tt.errMsg) + } + } + + if !tt.wantErr { + if validator == nil { + t.Error("Expected validator instance, got nil") + } + if cleanup != nil { + t.Error("Expected nil cleanup function, got non-nil") + } + } + }) + } +} + +func TestProvider_ExportedVariable(t *testing.T) { + if Provider == (schemav2ValidatorProvider{}) { + t.Log("Provider variable is properly exported") + } else { + t.Error("Provider variable has unexpected value") + } +} + +func contains(s, substr string) bool { + if len(substr) == 0 { + return true + } + if len(s) < len(substr) { + return false + } + for i := 0; i <= len(s)-len(substr); i++ { + if s[i:i+len(substr)] == substr { + return true + } + } + return false +} diff --git a/pkg/plugin/implementation/schemav2validator/schemav2validator_test.go b/pkg/plugin/implementation/schemav2validator/schemav2validator_test.go new file mode 100644 index 0000000..6f5a71f --- /dev/null +++ b/pkg/plugin/implementation/schemav2validator/schemav2validator_test.go @@ -0,0 +1,298 @@ +package schemav2validator + +import ( + "context" + "net/http" + "net/http/httptest" + "os" + "testing" +) + +const testSpec = `openapi: 3.1.0 +info: + title: Test API + version: 1.0.0 +paths: + /search: + post: + requestBody: + required: true + content: + application/json: + schema: + type: object + required: [context, message] + properties: + context: + type: object + required: [action] + properties: + action: + const: search + domain: + type: string + message: + type: object + /select: + post: + requestBody: + content: + application/json: + schema: + type: object + required: [context, message] + properties: + context: + allOf: + - type: object + properties: + action: + enum: [select] + message: + type: object + required: [order] + properties: + order: + type: object +` + +func TestNew(t *testing.T) { + tests := []struct { + name string + config *Config + wantErr bool + }{ + {"nil config", nil, true}, + {"empty URL", &Config{URL: ""}, true}, + {"invalid URL", &Config{URL: "http://invalid-domain-12345.com/spec.yaml"}, true}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + _, _, err := New(context.Background(), tt.config) + if (err != nil) != tt.wantErr { + t.Errorf("New() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} + +func TestValidate_ActionExtraction(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Write([]byte(testSpec)) + })) + defer server.Close() + + validator, _, err := New(context.Background(), &Config{URL: server.URL, CacheTTL: 3600}) + if err != nil { + t.Fatalf("Failed to create validator: %v", err) + } + + tests := []struct { + name string + payload string + wantErr bool + errMsg string + }{ + { + name: "valid search action", + payload: `{"context":{"action":"search","domain":"retail"},"message":{}}`, + wantErr: false, + }, + { + name: "valid select action with allOf", + payload: `{"context":{"action":"select"},"message":{"order":{}}}`, + wantErr: false, + }, + { + name: "missing action", + payload: `{"context":{},"message":{}}`, + wantErr: true, + errMsg: "missing field Action", + }, + { + name: "unsupported action", + payload: `{"context":{"action":"unknown"},"message":{}}`, + wantErr: true, + errMsg: "unsupported action: unknown", + }, + { + name: "action as number", + payload: `{"context":{"action":123},"message":{}}`, + wantErr: true, + errMsg: "failed to parse JSON payload", + }, + { + name: "invalid JSON", + payload: `{invalid json}`, + wantErr: true, + errMsg: "failed to parse JSON payload", + }, + { + name: "missing required field", + payload: `{"context":{"action":"search"}}`, + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := validator.Validate(context.Background(), nil, []byte(tt.payload)) + if (err != nil) != tt.wantErr { + t.Errorf("Validate() error = %v, wantErr %v", err, tt.wantErr) + } + if tt.wantErr && tt.errMsg != "" && err != nil { + if !contains(err.Error(), tt.errMsg) { + t.Errorf("Validate() error = %v, want error containing %v", err, tt.errMsg) + } + } + }) + } +} + +func TestValidate_NestedValidation(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Write([]byte(testSpec)) + })) + defer server.Close() + + validator, _, err := New(context.Background(), &Config{URL: server.URL, CacheTTL: 3600}) + if err != nil { + t.Fatalf("Failed to create validator: %v", err) + } + + tests := []struct { + name string + payload string + wantErr bool + }{ + { + name: "select missing required order", + payload: `{"context":{"action":"select"},"message":{}}`, + wantErr: true, + }, + { + name: "select with order", + payload: `{"context":{"action":"select"},"message":{"order":{}}}`, + wantErr: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := validator.Validate(context.Background(), nil, []byte(tt.payload)) + if (err != nil) != tt.wantErr { + t.Errorf("Validate() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} + +func TestLoadSpec_LocalFile(t *testing.T) { + tmpFile, err := os.CreateTemp("", "test-spec-*.yaml") + if err != nil { + t.Fatalf("Failed to create temp file: %v", err) + } + defer os.Remove(tmpFile.Name()) + + if _, err := tmpFile.Write([]byte(testSpec)); err != nil { + t.Fatalf("Failed to write temp file: %v", err) + } + tmpFile.Close() + + validator, _, err := New(context.Background(), &Config{URL: tmpFile.Name(), CacheTTL: 3600}) + if err != nil { + t.Fatalf("Failed to load local spec: %v", err) + } + + validator.specMutex.RLock() + defer validator.specMutex.RUnlock() + + if validator.spec == nil || validator.spec.doc == nil { + t.Error("Spec not loaded from local file") + } +} + +func TestCacheTTL_DefaultValue(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Write([]byte(testSpec)) + })) + defer server.Close() + + validator, _, err := New(context.Background(), &Config{URL: server.URL}) + if err != nil { + t.Fatalf("Failed to create validator: %v", err) + } + + if validator.config.CacheTTL != 3600 { + t.Errorf("Expected default CacheTTL 3600, got %d", validator.config.CacheTTL) + } +} + +func TestValidate_EdgeCases(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Write([]byte(testSpec)) + })) + defer server.Close() + + validator, _, err := New(context.Background(), &Config{URL: server.URL, CacheTTL: 3600}) + if err != nil { + t.Fatalf("Failed to create validator: %v", err) + } + + tests := []struct { + name string + payload string + wantErr bool + }{ + { + name: "empty payload", + payload: `{}`, + wantErr: true, + }, + { + name: "null context", + payload: `{"context":null,"message":{}}`, + wantErr: true, + }, + { + name: "empty string action", + payload: `{"context":{"action":""},"message":{}}`, + wantErr: true, + }, + { + name: "action with whitespace", + payload: `{"context":{"action":" search "},"message":{}}`, + wantErr: true, + }, + { + name: "case sensitive action", + payload: `{"context":{"action":"Search"},"message":{}}`, + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := validator.Validate(context.Background(), nil, []byte(tt.payload)) + if (err != nil) != tt.wantErr { + t.Errorf("Validate() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} + +func contains(s, substr string) bool { + if len(substr) == 0 { + return true + } + if len(s) < len(substr) { + return false + } + for i := 0; i <= len(s)-len(substr); i++ { + if s[i:i+len(substr)] == substr { + return true + } + } + return false +} From 85e0f3c1cd5882ecbdc10d71fce2e874a8983a08 Mon Sep 17 00:00:00 2001 From: ameersohel45 Date: Fri, 14 Nov 2025 10:43:11 +0530 Subject: [PATCH 3/9] 543 - add: adding schema2validator in script --- install/build-plugins.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/install/build-plugins.sh b/install/build-plugins.sh index bcf1275..1799f00 100755 --- a/install/build-plugins.sh +++ b/install/build-plugins.sh @@ -18,6 +18,7 @@ plugins=( "reqpreprocessor" "router" "schemavalidator" + "schemav2validator" "signer" "signvalidator" ) From 9f913e6aa9a97d96eb71f7ca2b9df25cf91255ed Mon Sep 17 00:00:00 2001 From: ameersohel45 Date: Fri, 14 Nov 2025 11:34:47 +0530 Subject: [PATCH 4/9] 543 - add : README file --- .../schemav2validator/README.md | 130 ++++++++++++++++++ 1 file changed, 130 insertions(+) create mode 100644 pkg/plugin/implementation/schemav2validator/README.md diff --git a/pkg/plugin/implementation/schemav2validator/README.md b/pkg/plugin/implementation/schemav2validator/README.md new file mode 100644 index 0000000..f72ac9b --- /dev/null +++ b/pkg/plugin/implementation/schemav2validator/README.md @@ -0,0 +1,130 @@ +# Schemav2Validator Plugin + +Validates Beckn protocol requests against OpenAPI 3.1 specifications using kin-openapi library. + +## Features + +- Validates requests against OpenAPI 3.1 specs +- Supports remote URL and local file loading +- Automatic external $ref resolution +- TTL-based caching with automatic refresh +- Generic path matching (no hardcoded paths) +- Direct schema validation without router overhead + +## Configuration + +```yaml +schemaValidator: + id: schemav2validator + config: + url: https://example.com/openapi-spec.yaml + cacheTTL: "3600" +``` + +### Configuration Parameters + +| Parameter | Type | Required | Default | Description | +|-----------|------|----------|---------|-------------| +| `url` | string | Yes | - | URL or file path to OpenAPI 3.1 specification | +| `cacheTTL` | string | No | "3600" | Cache TTL in seconds before reloading spec | + +## How It Works + +1. **Load Spec**: Loads OpenAPI spec from configured URL at startup +2. **Extract Action**: Extracts `action` from request `context.action` field +3. **Find Schema**: Searches all paths and HTTP methods in spec for schema with matching action: + - Checks `properties.context.action.enum` for the action value + - Also checks `properties.context.allOf[].properties.action.enum` + - Stops at first match +4. **Validate**: Validates request body against matched schema using `Schema.VisitJSON()` with: + - Required fields validation + - Data type validation (string, number, boolean, object, array) + - Format validation (email, uri, date-time, uuid, etc.) + - Constraint validation (min/max, pattern, enum, const) + - Nested object and array validation +5. **Return Errors**: Returns validation errors in ONIX format + +## Action-Based Matching + +The validator uses action-based schema matching, not URL path matching. It searches for schemas where the `context.action` field has an enum constraint containing the request's action value. + +### Example OpenAPI Schema + +```yaml +paths: + /beckn/search: + post: + requestBody: + content: + application/json: + schema: + properties: + context: + properties: + action: + enum: ["search"] # ← Matches action="search" +``` + +### Matching Examples + +| Request Action | Schema Enum | Match | +|----------------|-------------|-------| +| `search` | `enum: ["search"]` | ✅ Matches | +| `select` | `enum: ["select", "init"]` | ✅ Matches | +| `discover` | `enum: ["search"]` | ❌ No match | +| `on_search` | `enum: ["on_search"]` | ✅ Matches | + +## External References + +The validator automatically resolves external `$ref` references in OpenAPI specs: + +```yaml +# Main spec at https://example.com/api.yaml +paths: + /search: + post: + requestBody: + content: + application/json: + schema: + $ref: 'https://example.com/schemas/search.yaml#/SearchRequest' +``` + +The loader will automatically fetch and resolve the external reference. + +## Example Usage + +### Local File + +```yaml +schemaValidator: + id: schemav2validator + config: + url: ./validation-scripts/l2-config/mobility_1.1.0_openapi_3.1.yaml + cacheTTL: "3600" +``` + +### Remote URL + +```yaml +schemaValidator: + id: schemav2validator + config: + url: https://raw.githubusercontent.com/beckn/protocol-specifications/master/api/beckn-2.0.0.yaml + cacheTTL: "7200" +``` + +## Dependencies + +- `github.com/getkin/kin-openapi` - OpenAPI 3 parser and validator + +## Error Messages + +| Scenario | Error Message | +|----------|---------------| +| Action is number | `"failed to parse JSON payload: json: cannot unmarshal number into Go struct field .context.action of type string"` | +| Action is empty | `"missing field Action in context"` | +| Action not in spec | `"unsupported action: "` | +| Invalid URL | `"Invalid URL or unreachable: "` | +| Schema validation fails | Returns detailed field-level errors | + From 75880a4458d81833eacf00a0c5a5c94815705a27 Mon Sep 17 00:00:00 2001 From: ameersohel45 Date: Fri, 14 Nov 2025 13:06:31 +0530 Subject: [PATCH 5/9] 543 - update: to use updated config structure --- .../schemav2validator/README.md | 34 ++++++++----- .../schemav2validator/cmd/plugin.go | 27 +++++----- .../schemav2validator/cmd/plugin_test.go | 49 ++++++++++++++----- .../schemav2validator/schemav2validator.go | 34 ++++++++----- .../schemav2validator_test.go | 16 +++--- 5 files changed, 106 insertions(+), 54 deletions(-) diff --git a/pkg/plugin/implementation/schemav2validator/README.md b/pkg/plugin/implementation/schemav2validator/README.md index f72ac9b..b76ad99 100644 --- a/pkg/plugin/implementation/schemav2validator/README.md +++ b/pkg/plugin/implementation/schemav2validator/README.md @@ -17,7 +17,8 @@ Validates Beckn protocol requests against OpenAPI 3.1 specifications using kin-o schemaValidator: id: schemav2validator config: - url: https://example.com/openapi-spec.yaml + type: url + location: https://example.com/openapi-spec.yaml cacheTTL: "3600" ``` @@ -25,9 +26,12 @@ schemaValidator: | Parameter | Type | Required | Default | Description | |-----------|------|----------|---------|-------------| -| `url` | string | Yes | - | URL or file path to OpenAPI 3.1 specification | +| `type` | string | Yes | - | Type of spec source: "url" or "file" ("dir" reserved for future) | +| `location` | string | Yes | - | URL or file path to OpenAPI 3.1 spec | | `cacheTTL` | string | No | "3600" | Cache TTL in seconds before reloading spec | + + ## How It Works 1. **Load Spec**: Loads OpenAPI spec from configured URL at startup @@ -94,26 +98,30 @@ The loader will automatically fetch and resolve the external reference. ## Example Usage -### Local File - -```yaml -schemaValidator: - id: schemav2validator - config: - url: ./validation-scripts/l2-config/mobility_1.1.0_openapi_3.1.yaml - cacheTTL: "3600" -``` - ### Remote URL ```yaml schemaValidator: id: schemav2validator config: - url: https://raw.githubusercontent.com/beckn/protocol-specifications/master/api/beckn-2.0.0.yaml + type: url + location: https://raw.githubusercontent.com/beckn/protocol-specifications/master/api/beckn-2.0.0.yaml cacheTTL: "7200" ``` +### Local File + +```yaml +schemaValidator: + id: schemav2validator + config: + type: file + location: ./validation-scripts/l2-config/mobility_1.1.0_openapi_3.1.yaml + cacheTTL: "3600" +``` + + + ## Dependencies - `github.com/getkin/kin-openapi` - OpenAPI 3 parser and validator diff --git a/pkg/plugin/implementation/schemav2validator/cmd/plugin.go b/pkg/plugin/implementation/schemav2validator/cmd/plugin.go index 78b6226..9d147fe 100644 --- a/pkg/plugin/implementation/schemav2validator/cmd/plugin.go +++ b/pkg/plugin/implementation/schemav2validator/cmd/plugin.go @@ -18,21 +18,26 @@ func (vp schemav2ValidatorProvider) New(ctx context.Context, config map[string]s return nil, nil, errors.New("context cannot be nil") } - url, ok := config["url"] - if !ok || url == "" { - return nil, nil, errors.New("url not configured") - } + typeVal, hasType := config["type"] + locVal, hasLoc := config["location"] - cacheTTL := 3600 - if ttlStr, ok := config["cacheTTL"]; ok { - if ttl, err := strconv.Atoi(ttlStr); err == nil && ttl > 0 { - cacheTTL = ttl - } + if !hasType || typeVal == "" { + return nil, nil, errors.New("type not configured") + } + if !hasLoc || locVal == "" { + return nil, nil, errors.New("location not configured") } cfg := &schemav2validator.Config{ - URL: url, - CacheTTL: cacheTTL, + Type: typeVal, + Location: locVal, + CacheTTL: 3600, + } + + if ttlStr, ok := config["cacheTTL"]; ok { + if ttl, err := strconv.Atoi(ttlStr); err == nil && ttl > 0 { + cfg.CacheTTL = ttl + } } return schemav2validator.New(ctx, cfg) diff --git a/pkg/plugin/implementation/schemav2validator/cmd/plugin_test.go b/pkg/plugin/implementation/schemav2validator/cmd/plugin_test.go index af9c10d..42798dc 100644 --- a/pkg/plugin/implementation/schemav2validator/cmd/plugin_test.go +++ b/pkg/plugin/implementation/schemav2validator/cmd/plugin_test.go @@ -48,39 +48,64 @@ func TestProvider_New(t *testing.T) { errMsg: "context cannot be nil", }, { - name: "missing url", + name: "missing type", ctx: context.Background(), - config: map[string]string{}, + config: map[string]string{"location": server.URL}, wantErr: true, - errMsg: "url not configured", + errMsg: "type not configured", }, { - name: "empty url", + name: "missing location", ctx: context.Background(), - config: map[string]string{"url": ""}, + config: map[string]string{"type": "url"}, wantErr: true, - errMsg: "url not configured", + errMsg: "location not configured", + }, + { + name: "empty type", + ctx: context.Background(), + config: map[string]string{"type": "", "location": server.URL}, + wantErr: true, + errMsg: "type not configured", + }, + { + name: "empty location", + ctx: context.Background(), + config: map[string]string{"type": "url", "location": ""}, + wantErr: true, + errMsg: "location not configured", }, { name: "valid config with default TTL", ctx: context.Background(), - config: map[string]string{"url": server.URL}, + config: map[string]string{"type": "url", "location": server.URL}, wantErr: false, }, { name: "valid config with custom TTL", ctx: context.Background(), config: map[string]string{ - "url": server.URL, + "type": "url", + "location": server.URL, "cacheTTL": "7200", }, wantErr: false, }, + { + name: "valid file type", + ctx: context.Background(), + config: map[string]string{ + "type": "file", + "location": "/tmp/spec.yaml", + }, + wantErr: true, // file doesn't exist + }, { name: "invalid TTL falls back to default", ctx: context.Background(), config: map[string]string{ - "url": server.URL, + "type": "url", + "location": server.URL, "cacheTTL": "invalid", }, wantErr: false, @@ -89,7 +114,8 @@ func TestProvider_New(t *testing.T) { name: "negative TTL falls back to default", ctx: context.Background(), config: map[string]string{ - "url": server.URL, + "type": "url", + "location": server.URL, "cacheTTL": "-100", }, wantErr: false, @@ -98,7 +124,8 @@ func TestProvider_New(t *testing.T) { name: "zero TTL falls back to default", ctx: context.Background(), config: map[string]string{ - "url": server.URL, + "type": "url", + "location": server.URL, "cacheTTL": "0", }, wantErr: false, diff --git a/pkg/plugin/implementation/schemav2validator/schemav2validator.go b/pkg/plugin/implementation/schemav2validator/schemav2validator.go index e9b43db..bebbcca 100644 --- a/pkg/plugin/implementation/schemav2validator/schemav2validator.go +++ b/pkg/plugin/implementation/schemav2validator/schemav2validator.go @@ -37,7 +37,8 @@ type cachedSpec struct { // Config struct for Schemav2Validator. type Config struct { - URL string + Type string // "url", "file", or "dir" + Location string // URL, file path, or directory path CacheTTL int } @@ -46,8 +47,14 @@ func New(ctx context.Context, config *Config) (*schemav2Validator, func() error, if config == nil { return nil, nil, fmt.Errorf("config cannot be nil") } - if config.URL == "" { - return nil, nil, fmt.Errorf("config URL cannot be empty") + if config.Type == "" { + return nil, nil, fmt.Errorf("config type cannot be empty") + } + if config.Location == "" { + return nil, nil, fmt.Errorf("config location cannot be empty") + } + if config.Type != "url" && config.Type != "file" && config.Type != "dir" { + return nil, nil, fmt.Errorf("config type must be 'url', 'file', or 'dir'") } if config.CacheTTL == 0 { @@ -156,20 +163,23 @@ func (v *schemav2Validator) loadSpec(ctx context.Context) error { var doc *openapi3.T var err error - urlOrPath := v.config.URL - - if strings.HasPrefix(urlOrPath, "http://") || strings.HasPrefix(urlOrPath, "https://") { - u, parseErr := url.Parse(urlOrPath) + switch v.config.Type { + case "url": + u, parseErr := url.Parse(v.config.Location) if parseErr != nil { return fmt.Errorf("failed to parse URL: %v", parseErr) } doc, err = loader.LoadFromURI(u) - } else { - doc, err = loader.LoadFromFile(urlOrPath) + case "file": + doc, err = loader.LoadFromFile(v.config.Location) + case "dir": + return fmt.Errorf("directory loading not yet implemented") + default: + return fmt.Errorf("unsupported type: %s", v.config.Type) } if err != nil { - log.Errorf(ctx, err, "Invalid URL or unreachable: %s", urlOrPath) + log.Errorf(ctx, err, "Failed to load from %s: %s", v.config.Type, v.config.Location) return fmt.Errorf("failed to load OpenAPI document: %v", err) } @@ -187,7 +197,7 @@ func (v *schemav2Validator) loadSpec(ctx context.Context) error { } v.specMutex.Unlock() - log.Debugf(ctx, "Loaded OpenAPI spec from %s", urlOrPath) + log.Debugf(ctx, "Loaded OpenAPI spec from %s: %s", v.config.Type, v.config.Location) return nil } @@ -220,7 +230,7 @@ func (v *schemav2Validator) reloadExpiredSpec(ctx context.Context) { if err := v.loadSpec(ctx); err != nil { log.Errorf(ctx, err, "Failed to reload spec") } else { - log.Debugf(ctx, "Reloaded spec from %s", v.config.URL) + log.Debugf(ctx, "Reloaded spec from %s: %s", v.config.Type, v.config.Location) } } } diff --git a/pkg/plugin/implementation/schemav2validator/schemav2validator_test.go b/pkg/plugin/implementation/schemav2validator/schemav2validator_test.go index 6f5a71f..8852fff 100644 --- a/pkg/plugin/implementation/schemav2validator/schemav2validator_test.go +++ b/pkg/plugin/implementation/schemav2validator/schemav2validator_test.go @@ -63,8 +63,10 @@ func TestNew(t *testing.T) { wantErr bool }{ {"nil config", nil, true}, - {"empty URL", &Config{URL: ""}, true}, - {"invalid URL", &Config{URL: "http://invalid-domain-12345.com/spec.yaml"}, true}, + {"empty type", &Config{Type: "", Location: "http://example.com"}, true}, + {"empty location", &Config{Type: "url", Location: ""}, true}, + {"invalid type", &Config{Type: "invalid", Location: "http://example.com"}, true}, + {"invalid URL", &Config{Type: "url", Location: "http://invalid-domain-12345.com/spec.yaml"}, true}, } for _, tt := range tests { @@ -83,7 +85,7 @@ func TestValidate_ActionExtraction(t *testing.T) { })) defer server.Close() - validator, _, err := New(context.Background(), &Config{URL: server.URL, CacheTTL: 3600}) + validator, _, err := New(context.Background(), &Config{Type: "url", Location: server.URL, CacheTTL: 3600}) if err != nil { t.Fatalf("Failed to create validator: %v", err) } @@ -156,7 +158,7 @@ func TestValidate_NestedValidation(t *testing.T) { })) defer server.Close() - validator, _, err := New(context.Background(), &Config{URL: server.URL, CacheTTL: 3600}) + validator, _, err := New(context.Background(), &Config{Type: "url", Location: server.URL, CacheTTL: 3600}) if err != nil { t.Fatalf("Failed to create validator: %v", err) } @@ -200,7 +202,7 @@ func TestLoadSpec_LocalFile(t *testing.T) { } tmpFile.Close() - validator, _, err := New(context.Background(), &Config{URL: tmpFile.Name(), CacheTTL: 3600}) + validator, _, err := New(context.Background(), &Config{Type: "file", Location: tmpFile.Name(), CacheTTL: 3600}) if err != nil { t.Fatalf("Failed to load local spec: %v", err) } @@ -219,7 +221,7 @@ func TestCacheTTL_DefaultValue(t *testing.T) { })) defer server.Close() - validator, _, err := New(context.Background(), &Config{URL: server.URL}) + validator, _, err := New(context.Background(), &Config{Type: "url", Location: server.URL}) if err != nil { t.Fatalf("Failed to create validator: %v", err) } @@ -235,7 +237,7 @@ func TestValidate_EdgeCases(t *testing.T) { })) defer server.Close() - validator, _, err := New(context.Background(), &Config{URL: server.URL, CacheTTL: 3600}) + validator, _, err := New(context.Background(), &Config{Type: "url", Location: server.URL, CacheTTL: 3600}) if err != nil { t.Fatalf("Failed to create validator: %v", err) } From b7e8b8b02ddbbf8b4a1025e2cdc6322c1db1dec9 Mon Sep 17 00:00:00 2001 From: ameersohel45 Date: Fri, 14 Nov 2025 16:36:36 +0530 Subject: [PATCH 6/9] Add kin-openapi dependency for schemav2validator plugin --- go.mod | 12 ++++++++++-- go.sum | 50 +++++++++++++++++++++++++++++++++----------------- 2 files changed, 43 insertions(+), 19 deletions(-) diff --git a/go.mod b/go.mod index eb695cb..5de656d 100644 --- a/go.mod +++ b/go.mod @@ -3,11 +3,9 @@ module github.com/beckn-one/beckn-onix go 1.24.0 require ( - github.com/kr/pretty v0.3.1 // indirect github.com/rogpeppe/go-internal v1.13.1 // indirect github.com/santhosh-tekuri/jsonschema/v6 v6.0.1 golang.org/x/crypto v0.36.0 - gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect ) require github.com/stretchr/testify v1.10.0 @@ -29,6 +27,8 @@ require ( github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect github.com/go-jose/go-jose/v4 v4.0.1 // indirect + github.com/go-openapi/jsonpointer v0.21.0 // indirect + github.com/go-openapi/swag v0.23.0 // indirect github.com/google/go-cmp v0.6.0 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect github.com/hashicorp/go-multierror v1.1.1 // indirect @@ -37,17 +37,25 @@ require ( github.com/hashicorp/go-secure-stdlib/strutil v0.1.2 // indirect github.com/hashicorp/go-sockaddr v1.0.2 // indirect github.com/hashicorp/hcl v1.0.0 // indirect + github.com/josharian/intern v1.0.0 // indirect + github.com/mailru/easyjson v0.7.7 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/mitchellh/go-homedir v1.1.0 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect + github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect + github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037 // indirect + github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90 // indirect + github.com/perimeterx/marshmallow v1.1.5 // indirect github.com/ryanuber/go-glob v1.0.0 // indirect + github.com/woodsbury/decimal128 v1.3.0 // indirect golang.org/x/net v0.38.0 // indirect golang.org/x/sys v0.31.0 // indirect golang.org/x/time v0.0.0-20200416051211-89c76fbcd5d1 // indirect ) require ( + github.com/getkin/kin-openapi v0.133.0 github.com/google/uuid v1.6.0 github.com/hashicorp/go-retryablehttp v0.7.7 github.com/hashicorp/vault/api v1.16.0 diff --git a/go.sum b/go.sum index f7bbfb0..0250688 100644 --- a/go.sum +++ b/go.sum @@ -1,30 +1,35 @@ +github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= +github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs= github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c= github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA= github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0= -github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= -github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= -github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= -github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= -github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= -github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= +github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= 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/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM= github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE= +github.com/getkin/kin-openapi v0.133.0 h1:pJdmNohVIJ97r4AUFtEXRXwESr8b0bD721u/Tz6k8PQ= +github.com/getkin/kin-openapi v0.133.0/go.mod h1:boAciF6cXk5FhPqe/NQeBTeenbjqU4LhWBf09ILVvWE= github.com/go-jose/go-jose/v4 v4.0.1 h1:QVEPDE3OluqXBQZDcnNvQrInro2h0e4eqNbnZSWqS6U= github.com/go-jose/go-jose/v4 v4.0.1/go.mod h1:WVf9LFMHh/QVrmqrOfqun0C45tMe3RoiKJMPvgWwLfY= -github.com/go-test/deep v1.0.2 h1:onZX1rnHT3Wv6cqNgYyFOOlgVKJrksuCMCRvJStbMYw= -github.com/go-test/deep v1.0.2/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA= +github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ= +github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY= +github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE= +github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ= +github.com/go-test/deep v1.0.8 h1:TDsG77qcSprGbC6vTN8OuXp5g+J+b5Pcguhf7Zt61VM= +github.com/go-test/deep v1.0.8/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= @@ -55,13 +60,14 @@ github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= github.com/hashicorp/vault/api v1.16.0 h1:nbEYGJiAPGzT9U4oWgaaB0g+Rj8E59QuHKyA5LhwQN4= github.com/hashicorp/vault/api v1.16.0/go.mod h1:KhuUhzOD8lDSk29AtzNjgAu2kxRA9jL9NAbkFlqvkBA= -github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= +github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= 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/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= +github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= @@ -77,17 +83,23 @@ github.com/mitchellh/go-wordwrap v1.0.0/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUb github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= -github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= +github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 h1:RWengNIwukTxcDr9M+97sNutRR1RKhG96O6jWumTTnw= +github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8= +github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037 h1:G7ERwszslrBzRxj//JalHPu/3yz+De2J+4aLtSRlHiY= +github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037/go.mod h1:2bpvgLBZEtENV5scfDFEtB/5+1M4hkQhDQrccEJ/qGw= +github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90 h1:bQx3WeLcUWy+RletIKwUIt4x3t8n2SxavmoclizMb8c= +github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90/go.mod h1:y5+oSEHCPT/DGrS++Wc/479ERge0zTFxaF8PbGKcg2o= +github.com/perimeterx/marshmallow v1.1.5 h1:a2LALqQ1BlHM8PZblsDdidgv1mWi1DgC2UmX50IvK2s= +github.com/perimeterx/marshmallow v1.1.5/go.mod h1:dsXbUu8CRzfYP5a87xpp0xq9S3u0Vchtcl8we9tYaXw= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/redis/go-redis/v9 v9.8.0 h1:q3nRvjrlge/6UD7eTu/DSg2uYiU2mCL0G/uzBWqhicI= -github.com/redis/go-redis/v9 v9.8.0/go.mod h1:huWgSWd8mW6+m0VPhJjSSQ+d6Nh1VICQ6Q5lHuCH/Iw= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= github.com/rabbitmq/amqp091-go v1.10.0 h1:STpn5XsHlHGcecLmMFCtg7mqq0RnD+zFr4uzukfVhBw= github.com/rabbitmq/amqp091-go v1.10.0/go.mod h1:Hy4jKW5kQART1u+JkDTF9YYOQUHXqMuhrgxOEeS7G4o= -github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= +github.com/redis/go-redis/v9 v9.8.0 h1:q3nRvjrlge/6UD7eTu/DSg2uYiU2mCL0G/uzBWqhicI= +github.com/redis/go-redis/v9 v9.8.0/go.mod h1:huWgSWd8mW6+m0VPhJjSSQ+d6Nh1VICQ6Q5lHuCH/Iw= 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/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0= @@ -104,6 +116,10 @@ github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/ugorji/go/codec v1.2.7 h1:YPXUKf7fYbp/y8xloBqZOw2qaVggbfwMlI8WM3wZUJ0= +github.com/ugorji/go/codec v1.2.7/go.mod h1:WGN1fab3R1fzQlVQTkfxVtIBhWDRqOviHU95kRgeqEY= +github.com/woodsbury/decimal128 v1.3.0 h1:8pffMNWIlC0O5vbyHWFZAt5yWvWcrHA+3ovIIjVWss0= +github.com/woodsbury/decimal128 v1.3.0/go.mod h1:C5UTmyTjW3JftjUFzOVhC20BEQa2a4ZKOB5I6Zjb+ds= 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= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= @@ -131,4 +147,4 @@ gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 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= From f9fac4510693ae774f5f48e3fa4aa5f10b4be3ec Mon Sep 17 00:00:00 2001 From: ameersohel45 Date: Mon, 17 Nov 2025 16:19:54 +0530 Subject: [PATCH 7/9] Issue 552 - fix: optimized code creating in memory map for action to schema. --- .../schemav2validator/schemav2validator.go | 133 ++++++++---------- 1 file changed, 61 insertions(+), 72 deletions(-) diff --git a/pkg/plugin/implementation/schemav2validator/schemav2validator.go b/pkg/plugin/implementation/schemav2validator/schemav2validator.go index bebbcca..d089717 100644 --- a/pkg/plugin/implementation/schemav2validator/schemav2validator.go +++ b/pkg/plugin/implementation/schemav2validator/schemav2validator.go @@ -31,8 +31,9 @@ type schemav2Validator struct { // cachedSpec holds a cached OpenAPI spec. type cachedSpec struct { - doc *openapi3.T - loadedAt time.Time + doc *openapi3.T + actionSchemas map[string]*openapi3.SchemaRef // O(1) action lookup + loadedAt time.Time } // Config struct for Schemav2Validator. @@ -95,40 +96,14 @@ func (v *schemav2Validator) Validate(ctx context.Context, reqURL *url.URL, data } action := payloadData.Context.Action - var schema *openapi3.SchemaRef - var matchedPath string - - // Search all spec paths for matching action in schema - for path, item := range spec.doc.Paths.Map() { - if item == nil { - continue - } - // Check all HTTP methods for this path - for _, op := range []*openapi3.Operation{item.Post, item.Get, item.Put, item.Patch, item.Delete} { - if op == nil || op.RequestBody == nil || op.RequestBody.Value == nil { - continue - } - content := op.RequestBody.Value.Content.Get("application/json") - if content == nil || content.Schema == nil || content.Schema.Value == nil { - continue - } - // Check if schema has action constraint matching our action - if v.schemaMatchesAction(content.Schema.Value, action) { - schema = content.Schema - matchedPath = path - break - } - } - if schema != nil { - break - } - } + // O(1) lookup from action index + schema := spec.actionSchemas[action] if schema == nil || schema.Value == nil { return model.NewBadReqErr(fmt.Errorf("unsupported action: %s", action)) } - log.Debugf(ctx, "Validating action: %s, matched path: %s", action, matchedPath) + log.Debugf(ctx, "Validating action: %s", action) var jsonData any if err := json.Unmarshal(data, &jsonData); err != nil { @@ -190,14 +165,18 @@ func (v *schemav2Validator) loadSpec(ctx context.Context) error { log.Debugf(ctx, "Spec validation passed") } + // Build action→schema index for O(1) lookup + actionSchemas := v.buildActionIndex(ctx, doc) + v.specMutex.Lock() v.spec = &cachedSpec{ - doc: doc, - loadedAt: time.Now(), + doc: doc, + actionSchemas: actionSchemas, + loadedAt: time.Now(), } v.specMutex.Unlock() - log.Debugf(ctx, "Loaded OpenAPI spec from %s: %s", v.config.Type, v.config.Location) + log.Debugf(ctx, "Loaded OpenAPI spec from %s: %s with %d actions indexed", v.config.Type, v.config.Location, len(actionSchemas)) return nil } @@ -283,12 +262,42 @@ func (v *schemav2Validator) extractSchemaErrors(err error, schemaErrors *[]model } } -// schemaMatchesAction checks if a schema has an action constraint matching the given action. -func (v *schemav2Validator) schemaMatchesAction(schema *openapi3.Schema, action string) bool { +// buildActionIndex builds a map of action→schema for O(1) lookup. +func (v *schemav2Validator) buildActionIndex(ctx context.Context, doc *openapi3.T) map[string]*openapi3.SchemaRef { + actionSchemas := make(map[string]*openapi3.SchemaRef) + + for path, item := range doc.Paths.Map() { + if item == nil { + continue + } + // Check all HTTP methods + for _, op := range []*openapi3.Operation{item.Post, item.Get, item.Put, item.Patch, item.Delete} { + if op == nil || op.RequestBody == nil || op.RequestBody.Value == nil { + continue + } + content := op.RequestBody.Value.Content.Get("application/json") + if content == nil || content.Schema == nil || content.Schema.Value == nil { + continue + } + + // Extract action from schema + action := v.extractActionFromSchema(content.Schema.Value) + if action != "" { + actionSchemas[action] = content.Schema + log.Debugf(ctx, "Indexed action '%s' from path %s", action, path) + } + } + } + + return actionSchemas +} + +// extractActionFromSchema extracts the action value from a schema. +func (v *schemav2Validator) extractActionFromSchema(schema *openapi3.Schema) string { // Check direct properties if ctxProp := schema.Properties["context"]; ctxProp != nil && ctxProp.Value != nil { - if v.checkActionEnum(ctxProp.Value, action) { - return true + if action := v.getActionValue(ctxProp.Value); action != "" { + return action } } @@ -296,32 +305,29 @@ func (v *schemav2Validator) schemaMatchesAction(schema *openapi3.Schema, action for _, allOfSchema := range schema.AllOf { if allOfSchema.Value != nil { if ctxProp := allOfSchema.Value.Properties["context"]; ctxProp != nil && ctxProp.Value != nil { - if v.checkActionEnum(ctxProp.Value, action) { - return true + if action := v.getActionValue(ctxProp.Value); action != "" { + return action } } } } - return false + return "" } -// checkActionEnum checks if a context schema has action enum or const matching the given action. -func (v *schemav2Validator) checkActionEnum(contextSchema *openapi3.Schema, action string) bool { - // Check direct action property +// getActionValue extracts action value from context schema. +func (v *schemav2Validator) getActionValue(contextSchema *openapi3.Schema) string { if actionProp := contextSchema.Properties["action"]; actionProp != nil && actionProp.Value != nil { - // Check const field (stored in Extensions by kin-openapi) + // Check const field if constVal, ok := actionProp.Value.Extensions["const"]; ok { - if constVal == action { - return true + if action, ok := constVal.(string); ok { + return action } } - // Check enum field + // Check enum field (return first value) if len(actionProp.Value.Enum) > 0 { - for _, e := range actionProp.Value.Enum { - if e == action { - return true - } + if action, ok := actionProp.Value.Enum[0].(string); ok { + return action } } } @@ -329,28 +335,11 @@ func (v *schemav2Validator) checkActionEnum(contextSchema *openapi3.Schema, acti // Check allOf in context for _, allOfSchema := range contextSchema.AllOf { if allOfSchema.Value != nil { - if actionProp := allOfSchema.Value.Properties["action"]; actionProp != nil && actionProp.Value != nil { - // Check const field (stored in Extensions by kin-openapi) - if constVal, ok := actionProp.Value.Extensions["const"]; ok { - if constVal == action { - return true - } - } - // Check enum field - if len(actionProp.Value.Enum) > 0 { - for _, e := range actionProp.Value.Enum { - if e == action { - return true - } - } - } - } - // Recursively check nested allOf - if v.checkActionEnum(allOfSchema.Value, action) { - return true + if action := v.getActionValue(allOfSchema.Value); action != "" { + return action } } } - return false + return "" } From ab31e13e81f84fb4f4fe33c94a795a0bc84f6f1c Mon Sep 17 00:00:00 2001 From: ameersohel45 Date: Tue, 18 Nov 2025 02:43:04 +0530 Subject: [PATCH 8/9] Issue 554 - refactor: improve error response format --- .../schemav2validator/schemav2validator.go | 47 +++++++++++++------ 1 file changed, 32 insertions(+), 15 deletions(-) diff --git a/pkg/plugin/implementation/schemav2validator/schemav2validator.go b/pkg/plugin/implementation/schemav2validator/schemav2validator.go index d089717..cc6c717 100644 --- a/pkg/plugin/implementation/schemav2validator/schemav2validator.go +++ b/pkg/plugin/implementation/schemav2validator/schemav2validator.go @@ -112,7 +112,6 @@ func (v *schemav2Validator) Validate(ctx context.Context, reqURL *url.URL, data opts := []openapi3.SchemaValidationOption{ openapi3.VisitAsRequest(), - openapi3.MultiErrors(), openapi3.EnableFormatValidation(), } if err := schema.Value.VisitJSON(jsonData, opts...); err != nil { @@ -233,21 +232,39 @@ func (v *schemav2Validator) formatValidationError(err error) error { // extractSchemaErrors recursively extracts detailed error information from SchemaError. func (v *schemav2Validator) extractSchemaErrors(err error, schemaErrors *[]model.Error) { if schemaErr, ok := err.(*openapi3.SchemaError); ok { - // If there's an origin error, recursively extract from it - if schemaErr.Origin != nil { - v.extractSchemaErrors(schemaErr.Origin, schemaErrors) - } else { - // Leaf error - extract the actual validation failure - pathParts := schemaErr.JSONPointer() - path := strings.Join(pathParts, "/") - if path == "" { - path = schemaErr.SchemaField - } - *schemaErrors = append(*schemaErrors, model.Error{ - Paths: path, - Message: schemaErr.Reason, - }) + // Extract path from current error and message from Origin if available + pathParts := schemaErr.JSONPointer() + path := strings.Join(pathParts, "/") + if path == "" { + path = schemaErr.SchemaField } + + message := schemaErr.Reason + if schemaErr.Origin != nil { + originMsg := schemaErr.Origin.Error() + // Extract specific field error from nested message + if strings.Contains(originMsg, "Error at \"/") { + // Find last "Error at" which has the specific field error + parts := strings.Split(originMsg, "Error at \"") + if len(parts) > 1 { + lastPart := parts[len(parts)-1] + // Extract field path and update both path and message + if idx := strings.Index(lastPart, "\":"); idx > 0 { + fieldPath := lastPart[:idx] + fieldMsg := strings.TrimSpace(lastPart[idx+2:]) + path = strings.TrimPrefix(fieldPath, "/") + message = fieldMsg + } + } + } else { + message = originMsg + } + } + + *schemaErrors = append(*schemaErrors, model.Error{ + Paths: path, + Message: message, + }) } else if multiErr, ok := err.(openapi3.MultiError); ok { // Nested MultiError for _, e := range multiErr { From 4e69f5a106a065a3ad2f566070c25f3a90cceced Mon Sep 17 00:00:00 2001 From: ameersohel45 Date: Tue, 18 Nov 2025 14:56:56 +0530 Subject: [PATCH 9/9] Issue 553 - add : Add schemav2validator plugin documentation and configuration examples --- CONFIG.md | 41 ++++++++++++++++++++++++++++++++++++----- README.md | 9 ++++++--- SETUP.md | 22 ++++++++++++++++++++++ 3 files changed, 64 insertions(+), 8 deletions(-) diff --git a/CONFIG.md b/CONFIG.md index 6a221e2..991eb43 100644 --- a/CONFIG.md +++ b/CONFIG.md @@ -479,7 +479,38 @@ schemaValidator: --- -#### 5. Sign Validator Plugin +#### 5. Schema2Validator Plugin + +**Purpose**: Validate requests against OpenAPI 3.x specifications with action-based matching. + +```yaml +schemaValidator: + id: schemav2validator + config: + type: url + location: https://raw.githubusercontent.com/beckn/protocol-specifications-new/refs/heads/draft/api-specs/beckn-protocol-api.yaml + cacheTTL: "3600" +``` + +**Or for local files:** + +```yaml +schemaValidator: + id: schemav2validator + config: + type: file + location: ./validation-scripts/l2-config/mobility_1.1.0_openapi_3.1.yaml + cacheTTL: "3600" +``` + +**Parameters**: +- `type`: Source type - `"url"` for remote specs, `"file"` for local files +- `location`: URL or file path to OpenAPI 3.1 specification +- `cacheTTL`: Cache TTL in seconds before reloading spec (default: `"3600"`) + +--- + +#### 6. Sign Validator Plugin **Purpose**: Validate Ed25519 digital signatures on incoming requests. @@ -492,7 +523,7 @@ signValidator: --- -#### 6. Router Plugin +#### 7. Router Plugin **Purpose**: Determine routing destination based on rules. @@ -517,7 +548,7 @@ router: --- -#### 7. Signer Plugin +#### 8. Signer Plugin **Purpose**: Sign outgoing requests with Ed25519 signature. @@ -530,7 +561,7 @@ signer: --- -#### 8. Publisher Plugin +#### 9. Publisher Plugin **Purpose**: Publish messages to RabbitMQ or Pub/Sub for asynchronous processing. @@ -548,7 +579,7 @@ publisher: --- -#### 9. Middleware Plugin +#### 10. Middleware Plugin **Purpose**: Request preprocessing like UUID generation and header manipulation. diff --git a/README.md b/README.md index a24e40f..b524d58 100644 --- a/README.md +++ b/README.md @@ -121,7 +121,8 @@ The **Beckn Protocol** is an open protocol that enables location-aware, local co - **Router**: YAML-based routing rules engine for request forwarding - **Signer**: Ed25519 digital signature creation for outgoing requests - **SignValidator**: Ed25519 signature validation for incoming requests -- **SchemaValidator**: JSON schema validation +- **SchemaValidator**: JSON schema validation +- **Schemav2Validator**: OpenAPI 3.x schema validation with action-based matching - **KeyManager**: HashiCorp Vault integration for production key management - **SimpleKeyManager**: Embedded key management for local development (no external dependencies) - **Publisher**: RabbitMQ message publishing for asynchronous processing @@ -305,9 +306,11 @@ modules: config: routingConfig: ./config/routing.yaml schemaValidator: - id: schemavalidator + id: schemavalidator # or schemav2validator config: - schemaDir: ./schemas + schemaDir: ./schemas # for schemavalidator + # type: url # for schemav2validator + # location: https://example.com/spec.yaml steps: - validateSign - addRoute diff --git a/SETUP.md b/SETUP.md index 9ca0447..fb563a4 100644 --- a/SETUP.md +++ b/SETUP.md @@ -830,6 +830,28 @@ schemaValidator: schemaURL: https://schemas.beckn.org ``` +#### Schema2Validator Plugin + +**Remote URL Configuration:** +```yaml +schemaValidator: + id: schemav2validator + config: + type: url + location: https://raw.githubusercontent.com/beckn/protocol-specifications-new/refs/heads/draft/api-specs/beckn-protocol-api.yaml + cacheTTL: "3600" +``` + +**Local File Configuration:** +```yaml +schemaValidator: + id: schemav2validator + config: + type: file + location: ./schemas/beckn-protocol-api.yaml + cacheTTL: "3600" +``` + #### Router Plugin ```yaml router: