Merge pull request #640 from beckn/new-context
fix(reqpreprocessor): support camelCase context attributes (bapId, bppId, transactionId, messageId)
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user