Merge pull request #640 from beckn/new-context

fix(reqpreprocessor): support camelCase context attributes (bapId, bppId, transactionId, messageId)
This commit is contained in:
Mayuresh A Nirhali
2026-03-25 20:27:47 +05:30
committed by GitHub
3 changed files with 186 additions and 4 deletions

View File

@@ -62,12 +62,16 @@ const (
)
var contextKeys = map[string]ContextKey{
// snake_case keys (legacy beckn spec)
"transaction_id": ContextKeyTxnID,
"message_id": ContextKeyMsgID,
"subscriber_id": ContextKeySubscriberID,
"module_id": ContextKeyModuleID,
"parent_id": ContextKeyParentID,
"remote_id": ContextKeyRemoteID,
// camelCase aliases (new beckn spec — map to the same internal constants)
"transactionId": ContextKeyTxnID,
"messageId": ContextKeyMsgID,
}
// ParseContextKey converts a string into a valid ContextKey.

View File

@@ -8,6 +8,7 @@ import (
"fmt"
"io"
"net/http"
"strings"
"github.com/beckn-one/beckn-onix/pkg/log"
"github.com/beckn-one/beckn-onix/pkg/model"
@@ -22,6 +23,34 @@ type Config struct {
const contextKey = "context"
// firstNonNil returns the first non-nil value from the provided list.
// Used to resolve context fields that may appear under different key names
// (e.g. bap_id or bapId) depending on the beckn spec version in use.
func firstNonNil(values ...any) any {
for _, v := range values {
if v != nil {
return v
}
}
return nil
}
// snakeToCamel converts a snake_case string to camelCase.
// For example: "transaction_id" -> "transactionId".
// Returns the input unchanged if it contains no underscores.
func snakeToCamel(s string) string {
parts := strings.Split(s, "_")
if len(parts) == 1 {
return s
}
for i := 1; i < len(parts); i++ {
if len(parts[i]) > 0 {
parts[i] = strings.ToUpper(parts[i][:1]) + parts[i][1:]
}
}
return strings.Join(parts, "")
}
// NewPreProcessor returns a middleware that processes the incoming request,
// extracts the context field from the body, and adds relevant values (like subscriber ID).
func NewPreProcessor(cfg *Config) (func(http.Handler) http.Handler, error) {
@@ -49,21 +78,24 @@ func NewPreProcessor(cfg *Config) (func(http.Handler) http.Handler, error) {
return
}
// Resolve subscriber ID — checks snake_case key first, falls back to camelCase.
var subID any
switch cfg.Role {
case "bap":
subID = reqContext["bap_id"]
subID = firstNonNil(reqContext["bap_id"], reqContext["bapId"])
case "bpp":
subID = reqContext["bpp_id"]
subID = firstNonNil(reqContext["bpp_id"], reqContext["bppId"])
}
// Resolve caller ID — same dual-key pattern, opposite role.
var callerID any
switch cfg.Role {
case "bap":
callerID = reqContext["bpp_id"]
callerID = firstNonNil(reqContext["bpp_id"], reqContext["bppId"])
case "bpp":
callerID = reqContext["bap_id"]
callerID = firstNonNil(reqContext["bap_id"], reqContext["bapId"])
}
if subID != nil {
log.Debugf(ctx, "adding subscriberId to request:%s, %v", model.ContextKeySubscriberID, subID)
ctx = context.WithValue(ctx, model.ContextKeySubscriberID, subID)
@@ -78,10 +110,18 @@ func NewPreProcessor(cfg *Config) (func(http.Handler) http.Handler, error) {
log.Debugf(ctx, "adding callerID to request:%s, %v", model.ContextKeyRemoteID, callerID)
ctx = context.WithValue(ctx, model.ContextKeyRemoteID, callerID)
}
// Extract generic context keys (e.g. transaction_id, message_id).
// For each configured snake_case key, also try its camelCase equivalent
// so that a single config entry covers both beckn spec versions.
for _, key := range cfg.ContextKeys {
ctxKey, _ := model.ParseContextKey(key)
if v, ok := reqContext[key]; ok {
ctx = context.WithValue(ctx, ctxKey, v)
} else if camelKey := snakeToCamel(key); camelKey != key {
if v, ok := reqContext[camelKey]; ok {
ctx = context.WithValue(ctx, ctxKey, v)
}
}
}
r.Body = io.NopCloser(bytes.NewBuffer(body))

View File

@@ -235,6 +235,144 @@ func TestNewPreProcessorErrorCases(t *testing.T) {
}
}
// TestSnakeToCamel tests the snakeToCamel conversion helper.
func TestSnakeToCamel(t *testing.T) {
tests := []struct {
input string
want string
}{
{"transaction_id", "transactionId"},
{"message_id", "messageId"},
{"bap_id", "bapId"},
{"bpp_id", "bppId"},
{"bap_uri", "bapUri"},
{"bpp_uri", "bppUri"},
{"domain", "domain"}, // no underscore — unchanged
{"version", "version"}, // no underscore — unchanged
}
for _, tt := range tests {
t.Run(tt.input, func(t *testing.T) {
got := snakeToCamel(tt.input)
if got != tt.want {
t.Errorf("snakeToCamel(%q) = %q, want %q", tt.input, got, tt.want)
}
})
}
}
// TestCamelCaseSubscriberID tests that bapId / bppId are resolved when the payload
// uses camelCase context attribute names (new beckn spec).
func TestCamelCaseSubscriberID(t *testing.T) {
tests := []struct {
name string
role string
contextBody map[string]interface{}
wantSubID string
wantCaller string
}{
{
name: "BAP role — camelCase bapId resolved as subscriber",
role: "bap",
contextBody: map[string]interface{}{
"bapId": "bap.example.com",
"bppId": "bpp.example.com",
},
wantSubID: "bap.example.com",
wantCaller: "bpp.example.com",
},
{
name: "BPP role — camelCase bppId resolved as subscriber",
role: "bpp",
contextBody: map[string]interface{}{
"bapId": "bap.example.com",
"bppId": "bpp.example.com",
},
wantSubID: "bpp.example.com",
wantCaller: "bap.example.com",
},
{
name: "snake_case still takes precedence over camelCase",
role: "bap",
contextBody: map[string]interface{}{
"bap_id": "bap-snake.example.com",
"bapId": "bap-camel.example.com",
"bpp_id": "bpp-snake.example.com",
"bppId": "bpp-camel.example.com",
},
wantSubID: "bap-snake.example.com",
wantCaller: "bpp-snake.example.com",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
cfg := &Config{Role: tt.role}
middleware, err := NewPreProcessor(cfg)
if err != nil {
t.Fatalf("NewPreProcessor() error = %v", err)
}
body, _ := json.Marshal(map[string]interface{}{"context": tt.contextBody})
req := httptest.NewRequest(http.MethodPost, "/", bytes.NewReader(body))
var gotSubID, gotCaller interface{}
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
gotSubID = r.Context().Value(model.ContextKeySubscriberID)
gotCaller = r.Context().Value(model.ContextKeyRemoteID)
w.WriteHeader(http.StatusOK)
})
middleware(handler).ServeHTTP(httptest.NewRecorder(), req)
if gotSubID != tt.wantSubID {
t.Errorf("subscriber ID: got %v, want %v", gotSubID, tt.wantSubID)
}
if gotCaller != tt.wantCaller {
t.Errorf("caller ID: got %v, want %v", gotCaller, tt.wantCaller)
}
})
}
}
// TestCamelCaseContextKeys tests that generic context keys (e.g. transaction_id)
// are resolved from their camelCase equivalents (transactionId) when the
// snake_case key is absent from the payload.
func TestCamelCaseContextKeys(t *testing.T) {
cfg := &Config{
Role: "bap",
ContextKeys: []string{"transaction_id", "message_id"},
}
middleware, err := NewPreProcessor(cfg)
if err != nil {
t.Fatalf("NewPreProcessor() error = %v", err)
}
body, _ := json.Marshal(map[string]interface{}{
"context": map[string]interface{}{
"bapId": "bap.example.com",
"transactionId": "txn-abc",
"messageId": "msg-xyz",
},
})
req := httptest.NewRequest(http.MethodPost, "/", bytes.NewReader(body))
var gotTxnID, gotMsgID interface{}
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
gotTxnID = r.Context().Value(model.ContextKeyTxnID)
gotMsgID = r.Context().Value(model.ContextKeyMsgID)
w.WriteHeader(http.StatusOK)
})
middleware(handler).ServeHTTP(httptest.NewRecorder(), req)
if gotTxnID != "txn-abc" {
t.Errorf("transaction_id: got %v, want txn-abc", gotTxnID)
}
if gotMsgID != "msg-xyz" {
t.Errorf("message_id: got %v, want msg-xyz", gotMsgID)
}
}
func TestNewPreProcessorAddsSubscriberIDToContext(t *testing.T) {
cfg := &Config{Role: "bap"}
middleware, err := NewPreProcessor(cfg)