fix(router): support camelCase context attributes (bppUri, bapUri) for beckn spec migration

Replace fixed JSON struct tags for bpp_uri and bap_uri with a map-based parse of
the context object. A new getContextString() helper checks the snake_case key first
and falls back to the camelCase key, so routing works transparently for both the
legacy beckn spec and the new camelCase convention.

Also adds a nil-context guard so a missing context field returns a clear error
instead of a panic.

Test coverage:
- Two new cases in TestRouteSuccess for bppUri and bapUri camelCase payloads
- TestGetContextString: 5 table-driven cases covering snake, camel, precedence,
  missing, and empty-snake-fallthrough scenarios
- TestRouteNilContext: confirms clear error on missing context field

Fixes #636

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Mayuresh
2026-03-25 15:41:16 +05:30
parent 6d4f45a632
commit 72593d2ab6
2 changed files with 115 additions and 5 deletions

View File

@@ -199,21 +199,46 @@ func validateRules(rules []routingRule) error {
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.
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 {
Context struct {
Domain string `json:"domain"`
Version string `json:"version"`
BPPURI string `json:"bpp_uri,omitempty"`
BAPURI string `json:"bap_uri,omitempty"`
} `json:"context"`
}
if err := json.Unmarshal(body, &requestBody); err != nil {
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
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
switch route.TargetType {
case targetTypeBPP:
return handleProtocolMapping(route, requestBody.Context.BPPURI, endpoint)
return handleProtocolMapping(route, bppURI, endpoint)
case targetTypeBAP:
return handleProtocolMapping(route, requestBody.Context.BAPURI, endpoint)
return handleProtocolMapping(route, bapURI, endpoint)
}
return route, nil
}