Merge pull request #639 from beckn/camelcase
fix(router): support camelCase context attributes (bppUri, bapUri) fo…
This commit is contained in:
@@ -199,21 +199,46 @@ func validateRules(rules []routingRule) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// getContextString returns the value for a context field, checking the snake_case
|
||||||
|
// key first and falling back to the camelCase key. This supports both the legacy
|
||||||
|
// beckn spec (snake_case) and the new camelCase convention transparently.
|
||||||
|
func getContextString(ctx map[string]interface{}, snakeKey, camelKey string) string {
|
||||||
|
if v, ok := ctx[snakeKey].(string); ok && v != "" {
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
if v, ok := ctx[camelKey].(string); ok && v != "" {
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
// Route determines the routing destination based on the request context.
|
// Route determines the routing destination based on the request context.
|
||||||
func (r *Router) Route(ctx context.Context, url *url.URL, body []byte) (*model.Route, error) {
|
func (r *Router) Route(ctx context.Context, url *url.URL, body []byte) (*model.Route, error) {
|
||||||
// Parse the body to extract domain and version
|
// Parse domain and version via typed struct — unchanged from original.
|
||||||
var requestBody struct {
|
var requestBody struct {
|
||||||
Context struct {
|
Context struct {
|
||||||
Domain string `json:"domain"`
|
Domain string `json:"domain"`
|
||||||
Version string `json:"version"`
|
Version string `json:"version"`
|
||||||
BPPURI string `json:"bpp_uri,omitempty"`
|
|
||||||
BAPURI string `json:"bap_uri,omitempty"`
|
|
||||||
} `json:"context"`
|
} `json:"context"`
|
||||||
}
|
}
|
||||||
if err := json.Unmarshal(body, &requestBody); err != nil {
|
if err := json.Unmarshal(body, &requestBody); err != nil {
|
||||||
return nil, fmt.Errorf("error parsing request body: %w", err)
|
return nil, fmt.Errorf("error parsing request body: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Parse context as a map solely to resolve URI fields that have both
|
||||||
|
// snake_case (bpp_uri, bap_uri) and camelCase (bppUri, bapUri) variants.
|
||||||
|
var uriBody struct {
|
||||||
|
Context map[string]interface{} `json:"context"`
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal(body, &uriBody); err != nil {
|
||||||
|
return nil, fmt.Errorf("error parsing request body: %w", err)
|
||||||
|
}
|
||||||
|
if uriBody.Context == nil {
|
||||||
|
return nil, fmt.Errorf("context field not found or invalid in request body")
|
||||||
|
}
|
||||||
|
bppURI := getContextString(uriBody.Context, "bpp_uri", "bppUri")
|
||||||
|
bapURI := getContextString(uriBody.Context, "bap_uri", "bapUri")
|
||||||
|
|
||||||
// Extract the endpoint from the URL
|
// Extract the endpoint from the URL
|
||||||
endpoint := path.Base(url.Path)
|
endpoint := path.Base(url.Path)
|
||||||
|
|
||||||
@@ -251,9 +276,9 @@ func (r *Router) Route(ctx context.Context, url *url.URL, body []byte) (*model.R
|
|||||||
// Handle BPP/BAP routing with request URIs
|
// Handle BPP/BAP routing with request URIs
|
||||||
switch route.TargetType {
|
switch route.TargetType {
|
||||||
case targetTypeBPP:
|
case targetTypeBPP:
|
||||||
return handleProtocolMapping(route, requestBody.Context.BPPURI, endpoint)
|
return handleProtocolMapping(route, bppURI, endpoint)
|
||||||
case targetTypeBAP:
|
case targetTypeBAP:
|
||||||
return handleProtocolMapping(route, requestBody.Context.BAPURI, endpoint)
|
return handleProtocolMapping(route, bapURI, endpoint)
|
||||||
}
|
}
|
||||||
return route, nil
|
return route, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -470,6 +470,19 @@ func TestRouteSuccess(t *testing.T) {
|
|||||||
url: "https://example.com/v1/ondc/on_select",
|
url: "https://example.com/v1/ondc/on_select",
|
||||||
body: `{"context": {"domain": "ONDC:TRV10", "version": "1.1.0", "bpp_uri": "https://bpp1.example.com"}}`,
|
body: `{"context": {"domain": "ONDC:TRV10", "version": "1.1.0", "bpp_uri": "https://bpp1.example.com"}}`,
|
||||||
},
|
},
|
||||||
|
// camelCase variants (beckn spec camelCase migration)
|
||||||
|
{
|
||||||
|
name: "camelCase: bppUri in context is resolved for bpp routing",
|
||||||
|
configFile: "bap_caller.yaml",
|
||||||
|
url: "https://example.com/v1/ondc/select",
|
||||||
|
body: `{"context": {"domain": "ONDC:TRV10", "version": "1.1.0", "bppUri": "https://bpp1.example.com"}}`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "camelCase: bapUri in context is resolved for bap routing",
|
||||||
|
configFile: "bpp_caller.yaml",
|
||||||
|
url: "https://example.com/v1/ondc/on_select",
|
||||||
|
body: `{"context": {"domain": "ONDC:TRV10", "version": "1.1.0", "bapUri": "https://bap1.example.com"}}`,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
@@ -762,6 +775,78 @@ func TestV2ConflictingRules(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestGetContextString tests the dual-key lookup helper used to support both
|
||||||
|
// snake_case (legacy) and camelCase (new beckn spec) context attribute names.
|
||||||
|
func TestGetContextString(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
ctx map[string]interface{}
|
||||||
|
snakeKey string
|
||||||
|
camelKey string
|
||||||
|
want string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "snake_case key present",
|
||||||
|
ctx: map[string]interface{}{"bpp_uri": "https://bpp.example.com"},
|
||||||
|
snakeKey: "bpp_uri",
|
||||||
|
camelKey: "bppUri",
|
||||||
|
want: "https://bpp.example.com",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "camelCase key present",
|
||||||
|
ctx: map[string]interface{}{"bppUri": "https://bpp.example.com"},
|
||||||
|
snakeKey: "bpp_uri",
|
||||||
|
camelKey: "bppUri",
|
||||||
|
want: "https://bpp.example.com",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "snake_case takes precedence when both present",
|
||||||
|
ctx: map[string]interface{}{"bpp_uri": "https://snake.example.com", "bppUri": "https://camel.example.com"},
|
||||||
|
snakeKey: "bpp_uri",
|
||||||
|
camelKey: "bppUri",
|
||||||
|
want: "https://snake.example.com",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "neither key present returns empty string",
|
||||||
|
ctx: map[string]interface{}{"domain": "ONDC:TRV10"},
|
||||||
|
snakeKey: "bpp_uri",
|
||||||
|
camelKey: "bppUri",
|
||||||
|
want: "",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "empty snake_case value falls through to camelCase",
|
||||||
|
ctx: map[string]interface{}{"bpp_uri": "", "bppUri": "https://bpp.example.com"},
|
||||||
|
snakeKey: "bpp_uri",
|
||||||
|
camelKey: "bppUri",
|
||||||
|
want: "https://bpp.example.com",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
got := getContextString(tt.ctx, tt.snakeKey, tt.camelKey)
|
||||||
|
if got != tt.want {
|
||||||
|
t.Errorf("getContextString(%v, %q, %q) = %q, want %q", tt.ctx, tt.snakeKey, tt.camelKey, got, tt.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestRouteNilContext tests that Route returns a clear error when the context
|
||||||
|
// field is absent from the request body.
|
||||||
|
func TestRouteNilContext(t *testing.T) {
|
||||||
|
ctx := context.Background()
|
||||||
|
router, _, rulesFilePath := setupRouter(t, "bap_caller.yaml")
|
||||||
|
defer os.RemoveAll(filepath.Dir(rulesFilePath))
|
||||||
|
|
||||||
|
parsedURL, _ := url.Parse("https://example.com/v1/ondc/select")
|
||||||
|
_, err := router.Route(ctx, parsedURL, []byte(`{"message": {}}`))
|
||||||
|
|
||||||
|
if err == nil || !strings.Contains(err.Error(), "context field not found or invalid") {
|
||||||
|
t.Errorf("Route() with missing context = %v, want error containing 'context field not found or invalid'", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// TestV1DomainRequired tests that domain is required for v1 configs
|
// TestV1DomainRequired tests that domain is required for v1 configs
|
||||||
func TestV1DomainRequired(t *testing.T) {
|
func TestV1DomainRequired(t *testing.T) {
|
||||||
router := &Router{
|
router := &Router{
|
||||||
|
|||||||
Reference in New Issue
Block a user