fix(reqpreprocessor): support camelCase context attributes for beckn spec migration

- Add transactionId and messageId as camelCase aliases in model.go contextKeys map,
  pointing to the same ContextKeyTxnID and ContextKeyMsgID constants. This allows
  adapter configs to reference either form without failing startup validation.

- In reqpreprocessor, add firstNonNil() helper for subscriber/caller ID lookups
  so bap_id/bapId and bpp_id/bppId are both resolved correctly regardless of
  which beckn spec version the payload uses. snake_case takes precedence when both
  are present.

- Add snakeToCamel() helper used in the context key loop so a single config entry
  (e.g. transaction_id) automatically also checks the camelCase form (transactionId)
  without requiring any config file changes.

- Add TestSnakeToCamel, TestCamelCaseSubscriberID, TestCamelCaseContextKeys to
  cover all new code paths.

Fixes #637

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Mayuresh
2026-03-25 15:40:36 +05:30
parent 6d4f45a632
commit 1be757a165
5 changed files with 275 additions and 7 deletions

View File

@@ -4,6 +4,8 @@ import (
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"strings"
"sync"
@@ -155,6 +157,27 @@ func (v *schemav2Validator) initialise(ctx context.Context) error {
return v.loadSpec(ctx)
}
// readFromURI fetches a URL and returns its raw bytes.
func readFromURI(ctx context.Context, u *url.URL) ([]byte, error) {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, u.String(), nil)
if err != nil {
return nil, fmt.Errorf("failed to build request for %s: %w", u, err)
}
resp, err := http.DefaultClient.Do(req)
if err != nil {
return nil, fmt.Errorf("failed to fetch %s: %w", u, err)
}
defer resp.Body.Close()
data, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("failed to read body from %s: %w", u, err)
}
log.Debugf(ctx, "External ref resolved: %s (%d bytes, HTTP %d)", u, len(data), resp.StatusCode)
return data, nil
}
// loadSpec loads the OpenAPI spec from URL or local path.
func (v *schemav2Validator) loadSpec(ctx context.Context) error {
loader := openapi3.NewLoader()
@@ -162,6 +185,30 @@ func (v *schemav2Validator) loadSpec(ctx context.Context) error {
// Allow external references
loader.IsExternalRefsAllowed = true
// Log every URI kin-openapi resolves so we can trace the full $ref chain.
//
// Exception: json-schema.org meta-schema URLs are intercepted and short-
// circuited with an empty schema object. kin-openapi follows the $schema
// dialect URI declared in each Beckn schema file, which leads it deep into
// the JSON Schema 2020-12 meta-schema hierarchy. Those meta-schemas use
// boolean schemas (e.g. "additionalProperties": false, "items": false) that
// are valid JSON Schema but cannot be parsed by kin-openapi's OpenAPI Schema
// Object model. Since the meta-schemas carry no Beckn-specific content and
// are not used for validation, returning {} is safe and correct.
loader.ReadFromURIFunc = func(loader *openapi3.Loader, u *url.URL) ([]byte, error) {
if u.Host == "json-schema.org" {
log.Debugf(ctx, "Skipping json-schema.org meta-schema (not an OpenAPI schema): %s", u)
return []byte(`{}`), nil
}
log.Debugf(ctx, "Resolving external $ref: %s", u)
data, err := readFromURI(ctx, u)
if err != nil {
log.Errorf(ctx, err, "Failed to resolve external $ref: %s", u)
return nil, err
}
return data, nil
}
var doc *openapi3.T
var err error
@@ -185,9 +232,10 @@ func (v *schemav2Validator) loadSpec(ctx context.Context) error {
return fmt.Errorf("failed to load OpenAPI document: %v", err)
}
// Validate spec (skip strict validation to allow JSON Schema keywords)
// Validate spec — this also triggers resolution of all $refs including external ones.
// Log the error but treat as non-fatal to allow JSON Schema keywords not in OpenAPI 3.0.
if err := doc.Validate(ctx); err != nil {
log.Debugf(ctx, "Spec validation warnings (non-fatal): %v", err)
log.Errorf(ctx, err, "Spec validation error (external refs may not have resolved): %v", err)
} else {
log.Debugf(ctx, "Spec validation passed")
}
@@ -195,6 +243,10 @@ func (v *schemav2Validator) loadSpec(ctx context.Context) error {
// Build action→schema index for O(1) lookup
actionSchemas := v.buildActionIndex(ctx, doc)
if len(actionSchemas) == 0 {
log.Errorf(ctx, fmt.Errorf("no actions indexed"), "No actions indexed from spec — external $refs may not have resolved. Check that IsExternalRefsAllowed=true and the referenced URLs are reachable and return valid YAML/JSON")
}
v.specMutex.Lock()
v.spec = &cachedSpec{
doc: doc,