Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
366 changes: 366 additions & 0 deletions config/mappings/agmarknet/mandi-price.select.yaml

Large diffs are not rendered by default.

45 changes: 44 additions & 1 deletion config/oan-provider-adapter.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,11 @@
# capability's call plan from the registry, calls the provider, and answers with
# the mapped result. There is no callback -- the answer is the HTTP response.
#
# Two capabilities are served here, weather and mandi, by two domain packages
# in one pipeline. They share this module, the registry client and the mapper,
# and share nothing else: which one answers is decided by the payload, not by
# the URL, the domain or the order they appear in.
#
# Adding a provider is three things, and none of them is a Go change here:
# 1. a registry row binding "<participantId>|<capabilityCode>" to a call plan
# 2. one mapping file per action, published at the URL that row names
Expand Down Expand Up @@ -270,14 +275,52 @@ modules:
#
# maxResponseBytes: 4194304 # default: 4194304 (4 MiB)

# A second capability in the same pipeline, from a different domain
# package. Nothing about it is weather's business: a different
# upstream, a different mapping, a different set of prerequisites --
# and the same two registry rows. This entry, plus "mandi" in steps
# below, is the entire cost of adding it.
- id: mandi
config:
bindingKeys: "agmarknet|openagrinet:MandiPrice"

# Agmarknet's Vistaar API takes its token as a QUERY parameter,
# which is what authScheme query is for. The adapter holds the
# parameter's NAME and the name of the variable carrying the
# value -- never the value -- and redacts it from the URL it
# logs, so a token cannot reach the log by way of the request.
authScheme: query
queryName: token
queryValueEnv: MANDI_TOKEN
Comment thread
ameersohel45 marked this conversation as resolved.

# MANDI_TOKEN MUST BE SET IN THE ADAPTER'S ENVIRONMENT. Without it
# every mandi call fails immediately and permanently: the step
# refuses to call an upstream unauthenticated, and marks the failure
# as one no retry can fix.
#
# There is no default and there cannot be one -- a credential is not
# something this file may hold. So the failure mode is a plugin that
# loads cleanly, registers cleanly, passes startup validation, and
# then fails 100% of requests. Worth knowing before deploying:
#
# docker: -e MANDI_TOKEN=... or an env_file
# kubernetes: a Secret, mounted as an environment variable
# local: MANDI_TOKEN=... ./server --config=...
#
# The error names the SCHEME rather than the variable, deliberately:
# this runs behind a signed network call and a peer has no business
# learning which variables this deployment reads. The variable name
# is in the adapter's own log at error level.

# Declaring a step above is not enough: THIS list is what runs. A step
# that appears under providerSteps but not here never executes, and the
# request falls through to the 404 above -- which looks like a registry
# problem and is not.
steps:
- validateSign # the sender's key, from the registry
- validateSchema # the pinned Beckn v2 spec
- weather # resolve, map out, call, map back
- weather # openagrinet:WeatherObservation, or pass through
- mandi # openagrinet:MandiPrice, or pass through
Comment thread
ameersohel45 marked this conversation as resolved.
- signAck # signs whatever the step answered with

# ----------------------------------------------------------------------------
Expand Down
1 change: 1 addition & 0 deletions install/build-plugins.sh
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ plugins=(
"oanregistry"
"jsonmapper"
"weather"
Comment thread
ameersohel45 marked this conversation as resolved.
"mandi"
"manifestloader"
"reqpreprocessor"
"otelsetup"
Expand Down
104 changes: 104 additions & 0 deletions pkg/plugin/implementation/mandi/cmd/plugin.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
// Command plugin builds the mandi provider step as a loadable plugin.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This whole 104-line file is a verbatim copy of weather/cmd/plugin.go, comments includedparseConfig, New, splitList, the maxResponseBytes parsing and validation, and the Provider/var _ declarations are byte-identical modulo four identifier substitutions.

Since mandi.Config is already = upstream.Config (mandi.go:31), nothing in here is domain-specific. It belongs in a shared internal/upstream helper taking a package name and a New func — otherwise every future domain plugin re-copies it, and any fix to the config parsing has to be applied N times.

This already has a concrete cost: go test reports pkg/plugin/implementation/mandi/cmd [no test files], while weather/cmd/plugin_test.go has five test funcs covering parseConfig — including the maxResponseBytes non-integer and non-positive rejections at lines 47-53. So the copied error-handling path is entirely unexercised in mandi.

Sharing the helper closes the test gap instead of requiring a second copy of the tests.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed on the substance, and holding this one rather than doing it now — flagging so it isn't mistaken for handled. Leaving the thread open.

One correction to the shape of the fix, though. plugin.go can't move to internal: each plugin is built as its own .so from ./pkg/plugin/implementation/<dir>/cmd/plugin.go, and the loader does pgn.Lookup("Provider"), so every domain plugin needs its own package main exporting that symbol. Move it and there's nothing to build a .so from.

What can move is the ~45 lines that aren't domain-specific — parseConfig and splitList. plugin.go would stay put as a ~20-line shim calling a shared upstream.ParseConfig. Domain-specific parts are only the provider type, the New it delegates to, and the exported symbol.

Your concrete-cost point is the reason it's worth doing at all, and I verified it: mandi/cmd has 0 test files against weather/cmd's 5 test funcs, so the maxResponseBytes bounds checking and the auth-scheme validation are untested in this copy. Shared, they'd be tested once and every future plugin would inherit that.

Also confirmed your "verbatim modulo identifiers" claim — after substituting mandiweather the diff is comment-only.

//
// The filename of the built .so is the id a deployment names in providerSteps,
// so this package is mandi's whole public surface: a config map in, a step out.
package main

import (
"context"
"errors"
"fmt"
"strconv"
"strings"

"github.com/beckn-one/beckn-onix/pkg/log"
"github.com/beckn-one/beckn-onix/pkg/plugin/definition"
"github.com/beckn-one/beckn-onix/pkg/plugin/implementation/mandi"
)

// mandiProvider implements definition.ProviderStepProvider.
type mandiProvider struct{}

// newStepFunc creates a new step. Indirected for tests.
var newStepFunc = mandi.New

// parseConfig turns the plugin config map into a typed Config. Anything absent
// is left zero: mandi.New applies the defaults and validates the auth scheme,
// so those rules live in one place.
func (p mandiProvider) parseConfig(config map[string]string) (*mandi.Config, error) {
cfg := &mandi.Config{
BindingKeys: splitList(config["bindingKeys"]),
// Absent means the Beckn v2 convention. See upstream.Config for why
// this is a default rather than something to set.
ProviderIDAt: config["providerIdAt"],
CapabilityCodeAt: config["capabilityCodeAt"],
AuthScheme: config["authScheme"],
UsernameEnv: config["usernameEnv"],
PasswordEnv: config["passwordEnv"],
HeaderName: config["headerName"],
HeaderValueEnv: config["headerValueEnv"],
QueryName: config["queryName"],
QueryValueEnv: config["queryValueEnv"],
}

if raw, exists := config["maxResponseBytes"]; exists && raw != "" {
value, err := strconv.ParseInt(raw, 10, 64)
if err != nil {
return nil, fmt.Errorf("invalid maxResponseBytes value '%s': %w", raw, err)
}
if value <= 0 {
return nil, fmt.Errorf("maxResponseBytes must be positive, got %d", value)
}
cfg.MaxResponseBytes = value
}

return cfg, nil
}

// New creates a new mandi provider step instance.
func (p mandiProvider) New(ctx context.Context, registry definition.ProviderRecordLookup, mapper definition.Mapper, config map[string]string) (definition.Step, func() error, error) {
if ctx == nil {
return nil, nil, errors.New("context cannot be nil")
}

cfg, err := p.parseConfig(config)
if err != nil {
log.Errorf(ctx, err, "Failed to parse mandi configuration")
return nil, nil, fmt.Errorf("failed to parse mandi configuration: %w", err)
}

step, closer, err := newStepFunc(ctx, registry, mapper, cfg)
if err != nil {
log.Errorf(ctx, err, "Failed to create mandi step")
return nil, nil, err
}

log.Infof(ctx, "Mandi step created successfully")
return step, closer, nil
}

// splitList reads a comma-separated config value, which is how a list reaches a
// plugin -- the config is map[string]string. Blanks are dropped and spaces
// trimmed, so a trailing comma or a wrapped line is not a config error.
//
// A comma is unambiguous here: a binding key separates its own halves with a
// pipe.
func splitList(raw string) []string {
if strings.TrimSpace(raw) == "" {
return nil
}
var out []string
for _, part := range strings.Split(raw, ",") {
if trimmed := strings.TrimSpace(part); trimmed != "" {
out = append(out, trimmed)
}
}
return out
}

// Provider is the exported plugin instance.
var Provider = mandiProvider{}

// Compile-time proof the provider satisfies the interface the manager asserts
// against. A mismatch is otherwise a runtime cast failure at startup.
var _ definition.ProviderStepProvider = Provider
Loading
Loading