- 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>
151 lines
4.3 KiB
Go
151 lines
4.3 KiB
Go
package reqpreprocessor
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"strings"
|
|
|
|
"github.com/beckn-one/beckn-onix/pkg/log"
|
|
"github.com/beckn-one/beckn-onix/pkg/model"
|
|
)
|
|
|
|
// Config represents the configuration for the request preprocessor middleware.
|
|
type Config struct {
|
|
Role string
|
|
ContextKeys []string
|
|
ParentID string
|
|
}
|
|
|
|
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) {
|
|
if err := validateConfig(cfg); err != nil {
|
|
return nil, err
|
|
}
|
|
return func(next http.Handler) http.Handler {
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
body, err := io.ReadAll(r.Body)
|
|
if err != nil {
|
|
http.Error(w, "Failed to read request body", http.StatusBadRequest)
|
|
return
|
|
}
|
|
var req map[string]interface{}
|
|
ctx := r.Context()
|
|
if err := json.Unmarshal(body, &req); err != nil {
|
|
http.Error(w, "Failed to decode request body", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
// Extract context from request.
|
|
reqContext, ok := req["context"].(map[string]interface{})
|
|
if !ok {
|
|
http.Error(w, fmt.Sprintf("%s field not found or invalid.", contextKey), http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
// Resolve subscriber ID — checks snake_case key first, falls back to camelCase.
|
|
var subID any
|
|
switch cfg.Role {
|
|
case "bap":
|
|
subID = firstNonNil(reqContext["bap_id"], reqContext["bapId"])
|
|
case "bpp":
|
|
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 = firstNonNil(reqContext["bpp_id"], reqContext["bppId"])
|
|
case "bpp":
|
|
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)
|
|
}
|
|
|
|
if cfg.ParentID != "" {
|
|
log.Debugf(ctx, "adding parentID to request:%s, %v", model.ContextKeyParentID, cfg.ParentID)
|
|
ctx = context.WithValue(ctx, model.ContextKeyParentID, cfg.ParentID)
|
|
}
|
|
|
|
if callerID != nil {
|
|
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))
|
|
r.ContentLength = int64(len(body))
|
|
r = r.WithContext(ctx)
|
|
next.ServeHTTP(w, r)
|
|
})
|
|
}, nil
|
|
}
|
|
|
|
func validateConfig(cfg *Config) error {
|
|
if cfg == nil {
|
|
return errors.New("config cannot be nil")
|
|
}
|
|
|
|
if cfg.Role != "bap" && cfg.Role != "bpp" {
|
|
return errors.New("role must be either 'bap' or 'bpp'")
|
|
}
|
|
|
|
for _, key := range cfg.ContextKeys {
|
|
if _, err := model.ParseContextKey(key); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|