feat: add the Mandi provider plugin - #13
Conversation
ede4859 to
033807d
Compare
manjudr
left a comment
There was a problem hiding this comment.
Requesting changes
Small, focused PR and the mapping test is the strongest part of it — it asserts real output rather than just that the mapping compiles: outbound query params, on_select, DRAFT status, resource count, id shape, the six Direct-required fields, string→number price coercion, ISO arrival date, and the absent-not-zero partial-record case. That's the right kind of test.
Its blind spot is that every fixture is well-formed and code-bearing, which is exactly why the findings below survive it. None of them need much code — they need adversarial fixtures.
Verified before reviewing: go build ./..., go vet, gofmt -l, go test ./pkg/plugin/implementation/mandi/... and go test -race all pass. Findings 1, 2, 3 and 5 were confirmed empirically against the real mapper and the shipped mapping, and the observed outputs are quoted inline.
Should fix before merge
mandi-price.select.yaml:120—$resourceIdomitsVariety/Grade, so rows that differ only by those collide on one id. The PR's own fixture has such a pair, saved only by differing dates. District-wide queries collapse every market onto one id too.:130—$pricedchecks existence but not castability. Agmarknet'sNR/-markers make$number()throwD3030, so one unreported cell 500s the entire multi-row answer. Line 193's bare$numberonmodalis the same hazard and contradicts the file's stated rule.:37— the guard's message says "governed state and district codes" but the check is bare$exists, and the pack permits names. Names get sent as codes, Agmarknet returns nothing, and the caller gets a signed "no prices" for a market that had prices — verbatim the failure lines 20-26 say the guard prevents.:68— onlysupportedCommodities[0]is ever read, so a three-commodity request is answered for one with no complaint. Same class as theoanbindingrefusal, without the guard.:189— a missingArrival DateproducesarrivalDate: "--"and a degenerate resource id. Weather guards this exact case atweather-observation.select.yaml:248("Absent is honest; empty is a lie in the shape of an answer") — an accidental divergence between siblings.:171—subjectCategoriesechoed from the caller rather than stated from the pack, where weather hardcodes["Weather"]. Unvalidated, so a caller can make the adapter republish the wrong category under its own signature.
Lower severity
:185 mixes request scope with record identity (marketCode from the request, everything else from the record), so district-wide answers name several markets with no code. :191 ignores supportedPriceFields entirely.
Reuse
mandi/cmd/plugin.go is a verbatim 104-line copy of weather/cmd/plugin.go. Since mandi.Config = upstream.Config, none of it is domain-specific. It has already cost something concrete: mandi/cmd ships with no tests while weather/cmd has five covering parseConfig's rejection paths, so the copied error handling is unexercised. Sharing the helper closes the gap rather than requiring a second copy of the tests.
Scope
Clean. The +25/-1 in oan-provider-adapter.yaml is ~6 lines of real config plus ~19 of comment prose matching the file's house style, and the steps addition is required. install/build-plugins.sh +1 is the only other registration point — I checked every place weather appears for a missed one. No dead code, no unrelated edits, no speculative knobs.
One cross-PR note inline: registering a second provider step is what makes PR #2's "arity refused before ownership" issue live rather than theoretical. Not a defect here, but it should be fixed on #2 first.
Sequencing
This targets feat/41-oan-adapter-plugins, so PR #2 needs to land first — and #2 has two blockers of its own. Worth resolving those before merging this.
| @@ -0,0 +1,104 @@ | |||
| // Command plugin builds the mandi provider step as a loadable plugin. | |||
There was a problem hiding this comment.
This whole 104-line file is a verbatim copy of weather/cmd/plugin.go, comments included — parseConfig, 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.
There was a problem hiding this comment.
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 mandi→weather the diff is comment-only.
manjudr
left a comment
There was a problem hiding this comment.
Follow-up review: the three standing checks
Second pass against the defaults — no hardcodes (placeholder + example, read from config), proper logs, no unused methods or logic. Additions to my earlier review; the findings there still stand.
1. No hardcodes — one real operability gap
oan-provider-adapter.yaml:257 — MANDI_TOKEN appears exactly once in the entire repository outside tests: the line that references it. No .env.example, no README section, no deploy manifest, nothing in install/build-plugins.sh.
The result is a plugin that loads, registers, and validates cleanly, then fails every request permanently (doNotRetry), with the only clue being a runtime string naming a variable nobody was told to set. Detail and a suggested .env.example entry inline.
There is an irony worth fixing while you are there: the only env vars this config documents (MAUSAMGRAM_X_API_KEY, MAUSAMGRAM_TOKEN at lines 228/232) are commented-out examples that nothing reads, while the one variable actually required at runtime is undocumented.
Otherwise the hardcode picture here is good. The secret itself is correctly read from the environment rather than committed, and the pack-level constants (on_select, DRAFT, the @type values) are legitimately literal. One judgement call inline at mandi-price.select.yaml:194 about "currency": "INR".
2. Proper logs — nothing to fix in this PR
mandi and mandi/cmd follow the same convention as their weather counterparts (log.Debugf/Infof/Warnf(ctx, ...), no fmt.Println, no payload bodies logged), and all the request-path logging lives in internal/upstream, which this PR does not touch.
One thing this PR inherits rather than causes: I found a credential-redaction bug in internal/upstream that writes the auth token to the log in recoverable form at info level on every successful call, because the redaction string-matches the raw env value while the URL carries it percent-encoded. It affects MANDI_TOKEN specifically — mandi is the only step configured with the query auth scheme, so this PR is what makes the bug reachable in production. I have filed the detail on PR #2, where the code lives. Worth tracking as a dependency of this PR rather than fixing here.
3. No unused method or logic — clean
Swept the mandi packages for unexported functions with no production caller, config fields never read, and consts never referenced. Nothing. mandi.Config is a type alias to upstream.Config so it has no unread fields of its own, and every helper in mandi/cmd/plugin.go is called.
The related point stands from my earlier review: that file is not unused, but it is a verbatim 104-line copy of weather/cmd/plugin.go, and the duplication is why mandi/cmd ships with no tests while weather/cmd has five.
manjudr
left a comment
There was a problem hiding this comment.
Spec-alignment review: MandiPrice v0.1
Reviewed the mapping against the actual pack in OpenAgriNet/network-specs (branch schema-packs-v0.1, schema/MandiPrice/v0.1/), and validated the mapping's real output shapes against the pack's own subschemas with a JSON Schema validator. The response half is in good shape; the request half is built on fields the pack forbids.
1. Blocker: the OnDemand branch forbids market and validity, and two guards require them
The comment at :19-23 reads the pack as leaving market and validity optional. It excludes them — not/anyOf over required is an exclusion list, covering source, commodity, commodityGroup, grade, variety, market, arrivalDate, prices, generatedAt and validity.
payload satisfying all three of this file's guards -> VIOLATES PACK
the pack's OWN OnDemand example (coverageAreas) -> CONFORMS
The adapter refuses the pack's own example, and every payload it accepts is spec-invalid. This is structural: statecode, districtcode, from_date, to_date and marketcode all come from those fields.
The pack's OnDemand scope mechanism is coverageAreas ({codeScheme: ISO-3166-2, areaCode: IN-TS, areaLevel: State}). Mapping that onto Agmarknet's numeric codes needs a gazetteer lookup rather than a JSONata expression — which is presumably why the shortcut was taken. Detail and a suggested guard inline at :19 and :37.
2. A real gap in the pack, not in this PR
The date-range guard cannot be made conformant today. validity is forbidden in OnDemand, and the available OnDemand fields (historicalDataAvailable, historyPeriod, updateFrequency) all describe provider capability, not caller intent. Since this upstream requires from_date/to_date, MandiPrice v0.1 has no way to express what this integration needs.
That is worth taking to the spec repo — a requestedPeriod, or permitting validity in OnDemand as the requested window. I would not hold this PR for the pack change, but the file should say the guard is knowingly non-conformant rather than let it read as satisfied. Inline at :43.
3. Validation failures confirmed against the pack
| Line | Case | Result |
|---|---|---|
:195 |
upstream omits Price Unit |
INVALID: 'unit' is a required property |
:189 |
record has no Arrival Date |
INVALID: '--' is not a 'date' |
:171 |
caller echoes ["Weather"] |
VALID — but asserts the wrong subject |
:171 |
caller echoes [] |
INVALID: [] should be non-empty |
These sharpen three findings from my earlier review from "looks wrong" to "fails the pack".
4. Hardcodes — you asked for fewer; two here should stay, one should be added
Applying the rule that makes this decidable — pack constants belong in the mapping, deployment values belong in config — this file is mostly on the right side:
"currency": "INR"(:194) — keep. Pack requires^[A-Z]{3}$, Agmarknet reports only INR, file is Agmarknet-specific. This supersedes my earlier suggestion to move it."@type"(:169) — keep. Pack constant.subjectCategories(:171) — should become a hardcode:["Market"]. It is a closed-enum pack value that both of the pack's own examples state, and the weather mapping already hardcodes its equivalent. Echoing it is what lets a caller push the wrong subject through the adapter's signature.
The deployment values worth moving to configuration are the ones I flagged on PR #2 (subscriberId, registry URL) and the undocumented MANDI_TOKEN — not these.
5. Where the mapping is right
The response half checks out field-for-field against the pack: every key it emits is declared, correctly placed, and inside the closed market and prices property sets. variety and grade are populated (:181-182) where a lazier mapping would have dropped them — which is also why using them in $resourceId is a small change, since the data is already in hand. Two optional refinements inline at :13 (sourceId prefix convention, missing sourceUri).
Verification
Schema facts come from schema/MandiPrice/v0.1/attributes.yaml and schema/AgricultureResource/v0.1/attributes.yaml at 200d963, checked with jsonschema (Draft 2020-12) against the pack's own conditional clauses and its self-contained prices / market / arrivalDate / subjectCategories subschemas. No external $ref was stubbed for any result quoted here.
033807d to
284ce62
Compare
da6480d to
2be8ce6
Compare
983affb to
ed76651
Compare
🛡️ Trivy security scan (CRITICAL,HIGH,MEDIUM,LOW)Go dependenciesNo findings at CRITICAL,HIGH,MEDIUM,LOW. Container imageNo findings at CRITICAL,HIGH,MEDIUM,LOW. |
|
📊 Test Coverage: ✅ Passed — 100% of changed lines covered, min 80% |
manjudr
left a comment
There was a problem hiding this comment.
@ameersohel45 - Please don't use the OAN in the file names
Code reviewFound 1 issue:
network-adapter/config/mappings/agmarknet/mandi-price.select.yaml Lines 144 to 160 in c59783c The orphaned network-adapter/config/mappings/agmarknet/mandi-price.select.yaml Lines 188 to 193 in c59783c 🤖 Generated with Claude Code - If this code review was useful, please react with 👍. Otherwise, react with 👎. |
c59783c to
2767495
Compare
|
Agreed, and already done in #17 — which targets this branch, so it lands here on merge. Nothing needed on this PR. This branch has 11 such paths; after #17 the count is zero:
Each is named for what it is rather than for the network it was written for: the registry client speaks SunbirdRC, and the binding package resolves a capability binding. Same reasoning as the capability plugins in your other comment. The comments were done too, not just the paths — nothing in the Go file or directory names carries the network name, and #17 has a commit for the wording left in the reference config. |
|
Fixed in 80dd82d — and your reasoning about why it matters is the part I'd have missed. Confirmed all three placements. Your point about the history is exactly the risk: a resource-id collision had to be fixed in this file once already, so someone editing One judgement call beyond a straight move. The market-code paragraph — "without it the query widened to the whole district, so the district code is what identifies the scope" — went to No expression changed. The shipped-mapping tests serve this exact file over HTTP, so they'd have caught it if one had. |
Serves openagrinet:MandiPrice against Agmarknet's Vistaar select, as a sibling to weather: a domain package of 58 lines wrapping internal/upstream, which needed no change for it. That was the test of whether the machinery and domain split from 2b3cab1 actually held, and it did. The package has NO prerequisites, and the reason is the pack rather than luck. A MandiPrice select names the market it wants -- market.marketCode, market.district, market.state -- and carries a commodity code and a validity window, which is every parameter the upstream takes. There is no top-level location in the pack, so nothing has to turn a point into a market, which is the one thing the provider backend needs a spatial SQL query for and the one thing this adapter may not do. The mapping carries the whole contract. Three things in it are not obvious: the upstream's records use Title Case keys WITH SPACES -- `Modal Price` -- so they need backticks, and its prices are STRINGS, so they need $number before they satisfy the pack's numeric types. Both are pinned by a verbatim capture from the provider backend's own documentation. dates convert twice. The pack speaks ISO, the upstream speaks dd-MM-yyyy, so the request half converts out and the response half converts back. the pack requires none of the fields the upstream needs -- an OnDemand select requires only supportedCommodities and supportedPriceFields, leaving market and validity optional. So a spec-valid select can be unanswerable, and the mapping's required: block refuses those with its own message rather than earning a 400 or, worse, an empty result that reads as "no prices". Resource ids are built from codes rather than the names the upstream reports: "Kasdol APMC" and "Paddy(Common)" carry spaces and brackets, and an id a consumer may put in a URL should not. Verified: the shipped mapping run through the real mapper and the real step answers two records as two Direct resources with their prices converted, the offer's references rewritten to match, and absent min/max left absent rather than zeroed. Both directions validate -- the select against beckn.yaml and MandiPrice v0.1 in OnDemand mode, the on_select against beckn.yaml and the pack in Direct mode, with no errors.
Adds mandi alongside weather in config/oan-provider-adapter.yaml, so the reference shows the thing that is actually interesting about this design: two domain packages in one pipeline, sharing the module, the registry client and the mapper, and sharing nothing else. The whole cost of the second capability is one providerSteps entry and one line in steps. No routing table, no new module, no new port. Which one answers is decided by the payload -- each step builds a binding key from it, serves the request if the key is its own, and passes it through untouched if not -- so the order they appear in does not matter either. mandi uses authScheme query, because Agmarknet's Vistaar API takes its token as a query parameter. The adapter holds the parameter's name and the name of the environment 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. Verified by booting this config in an image that has both plugins: both ProviderStep plugins load, the pipeline initialises as [validateSign validateSchema weather mandi signAck], and the module registers at /. Worth noting the published adapter image does NOT yet carry mandi.so, so against that image this config fails at startup with "plugin mandi not found" until it is rebuilt from this branch.
…oo [#8] Same change as the weather mapping: the response half read a hardcoded pack URL and now takes it off the incoming select, so the file never has to know which URL is current and cannot disagree with the caller.
Agmarknet writes an unreported price as a marker rather than omitting the field -- "NR", "-", "". $exists() is true for all of them, so the existence-only guard handed them to $number() and it threw: D3030: unable to cast value to a number: "NR" (argument 1) That failed the WHOLE response half. One unreported cell in one row turned a good multi-row answer into an adapter error -- a 500 where the honest answer is "this row has no minimum", which is exactly the distinction the comment above the function says it exists to preserve. modal had no guard at all: it went through a bare $number while minimum and maximum went through $priced. It uses $priced now, so the three agree. The number branch in the new guard is not redundant, and I checked rather than assumed: $match throws T0410 on a non-string, so testing the regex first would break the day this upstream sends a real number instead of a quoted one. Verified every input shape -- absent, "1441", "1441.50", 1441, "NR", "-", "", "1,441" -- and only the numeric ones survive as numbers. Test drives the shipped mapping with markers in all three price fields of one row and real prices in another. It asserts the good row still arrives with numbers AND that the markers are absent rather than zero. Confirmed it fails against the old guard, reporting the D3030 above verbatim. runShipped is split so a test can vary what the upstream returns; the existing callers are unchanged.
…ce [#8] Three findings, one rule. A record that cannot be made conformant is dropped rather than emitted with a degenerate value -- absent is honest, and present-and-wrong is a lie in the shape of an answer that this adapter then signs. no Arrival Date $iso is substring-and-concatenate and JSONata casts undefined to "", so an absent date became the string "--". The pack declares format: date, so that is invalid, not merely ugly -- and arrivalDate is on the Direct required list, so it cannot be omitted either. The resource id degraded with it: res:agmarknet:2056:2:-- no Price Unit prices.required is [currency, unit], and JSONata drops an absent key rather than emitting null, so the unit silently vanished and took the resource's validity with it. NOT defaulted: Rs./Qtl and Rs./Kg are both real, so inventing one would misreport a price by a factor of a hundred. no usable price prices carries anyOf [minimum, maximum, modal], so a row whose three prices are all unreported markers has nothing to report and cannot satisfy it. Dropped rather than refused, because the other rows in the same answer are good and failing the request would discard them too -- the mistake the price guard in 55ca67d just fixed. The offer's resourceIds are filtered with them, so dropping a record cannot leave a dangling reference; that is asserted. TWO THINGS I GOT WRONG AND THE TESTS CAUGHT, both worth recording. The first version used $exists($match(...)) for the date. This engine returns an EMPTY ARRAY from $match when nothing matches, and $exists([]) is true -- so the predicate was constant-true and the filter did nothing. It reads correctly and does nothing at all, which is the worst combination. Worse, I had verified it against the WRONG ENGINE. My probes used the JavaScript jsonata package; the adapter uses github.com/jsonata-go/jsonata, where $exists([]) differs. Verified in the real engine this time: bare $match in a ternary and $count(...) > 0 both behave, only the $exists form does not. And the filter made the previous commit's test vacuous -- its all-markers row is now dropped, so the loop asserting "markers are absent" never ran. That test now uses a row with one real price and two markers, so it still asserts the distinction, plus a third row that is dropped for having none.
…uest [#8] Two echoes, both of them claims this adapter signs. subjectCategories was $ra.subjectCategories. It is a closed enum on AgricultureResource and "Market" is what a MandiPrice resource IS -- the pack states it in both of its own examples, and the sibling weather mapping has always stated ["Weather"]. Echoing meant a caller sending ["Weather"] on a MandiPrice select got it faithfully republished over this adapter's signature, and nothing caught it: ["Weather"] is enum-legal, so it validates. Verified that -- it comes back VALID against the pack, which is what made the echo dangerous rather than merely untidy. market took marketName, district and state from the record but marketCode from the REQUEST. The review suggested taking the code from the record too; there is nothing to take. This upstream TAKES a market code as a query parameter and reports none back -- there is no such field anywhere in its response, which I checked against the PR's own fixture. So the code is dropped rather than sourced differently: - restating the requested code against a returned row asserts something unverified, and would misreport a provider that answered about a different market instead of surfacing it; - district-wide there is no requested code at all, while the rows come from several markets, so the single code was wrong for most of them. The pack requires only marketName and calls marketCode "when available". Here it is not available, and absent is the honest answer. One test covering both, driven by a request that states the WRONG category, so it fails on an echo of either field. Confirmed it fails against both old forms.
…sked [#8] Four findings in the mandi mapping. RESOURCE IDS COLLIDED. The id was scope:commodity:date, and Agmarknet reports several rows for the same market, commodity and date differing only by Variety and Grade -- this package's own fixture is exactly that pair, saved from collision only by having different arrival dates. Two distinct resources then shared one id and the offer referenced it twice, so a consumer resolving resourceIds could not tell which price it had. Market is in the id too: district-wide $scope is the district and the rows come from several markets inside it. Built with $join over a list, so an absent Variety or Grade drops out rather than leaving an empty segment. ONE COMMODITY, NOT THE FIRST OF SEVERAL. The guard, the outbound query and the commodity stamped on each resource all read supportedCommodities[0], so a caller sending three passed validation, was queried for one, and got a confident signed answer to a third of what it asked. Refused now, with the message saying to send them separately -- the same answer oanbinding gives at the commitment level. THE MARKET GUARD CHECKS WHAT IT PROMISED. Its message said "governed codes" while the check was $exists, and the pack describes district and state as "name or governed code" -- so the names form is pack-legal, passed, and went to Agmarknet verbatim. Agmarknet answered nothing and the caller received a signed, spec-valid "no prices" for a market that had prices. Now checks shape: a numeric district and a short alphabetic state, which is what this upstream takes. supportedPriceFields IS HONOURED. It was validated on the way in and ignored on the way out, so a caller asking for Modal alone got all three. Emitted conditionally now, and the conformance filter judges usability over the REQUESTED fields -- a row whose Modal is a marker cannot serve a request for Modal alone even though its Minimum is fine. Every construct was checked in the engine that actually runs it, github.com/jsonata-go/jsonata, after the $exists([]) surprise in a77b70f: $replace, $join over a list with gaps, `in`, a ternary with no else dropping its key, and $match discriminating 96 from Balodabazar and CG from Chattisgarh. Four tests, each confirmed to fail against the old form -- the id one reports both rows sharing res:agmarknet:2056:2:2025-08-20, and the price-fields one reports minimum and maximum arriving unasked. One existing expectation moved from "state and district" to "codes in market", which the new message carries.
MANDI_TOKEN was documented nowhere. It appeared exactly once outside tests -- on the config line that names it -- with no README, no env example, no manifest. The failure mode is a plugin that loads cleanly, registers cleanly, passes startup validation, and then fails 100% of requests, because the step refuses to call an upstream unauthenticated and marks that as permanent. There cannot be a default: a credential is not something this file may hold. So the note sits at the point of use and says how to supply it under docker, kubernetes and a local run. This got MORE important since the review, not less. The error used to name the variable; 5bd4021 moved that name off the wire, because a network peer has no business learning which variables this deployment reads. The name is in the adapter's own log at error level now, so the config is the only place a deployer will find it in advance. TWO CLAIMS ABOUT THE PACK WERE WRONG, and both were ours rather than the pack's. The header said "NOTHING HERE IS OUTSIDE THE PACK". True of the response half -- every field it sets is declared and correctly placed, inside the closed market and prices sets -- and not true of the request half, which reads market and validity. Split, and the request side now says why: OnDemand describes what a provider can obtain, Direct describes an obtained reading, and a request is neither. The guard note said the pack "leaves market and validity optional". It excludes them. Corrected, with the consequence spelled out -- a payload that satisfies these guards cannot validate, and one that validates cannot satisfy them -- and with the reason nothing breaks today: the exclusion sits under if/then, which the validator parses and never evaluates. An accident to rely on rather than a design, now written down as such. Also notes which constants are the pack's and which are this provider's, since INR is stated while the unit beside it is read: Agmarknet reports a unit and no currency, and the pack requires currency, so it cannot be read or omitted.
] The rebase onto development brought a changed-file coverage gate, and this branch failed it: mandi/cmd/plugin.go was the only changed non-test Go file and sat at 0%, putting the diff at 3% against an 80% minimum. The gap is the one the review already identified. mandi/cmd is a near-verbatim copy of weather/cmd, but weather/cmd has five test functions and this had none -- so the response cap's bounds checking and the auth-scheme rejection were untested in this copy while passing in the other. Not a copy of weather's tests, because the two files are not identical where it counts: mandi carries queryName and queryValueEnv, and query auth is the whole reason this capability needs its own entry -- Agmarknet's Vistaar API takes its token as a query parameter. That case is covered explicitly. Beyond weather's set: splitList is tested directly rather than only through bindingKeys, an empty maxResponseBytes is asserted to read as unset rather than as malformed (a rendered config with an unset variable produces exactly that), and New's success path asserts the closer it returns actually reaches the step's own closer. parseConfig, New and splitList are now at 100%; the changed-line gate reports 100%. This does not deduplicate the two cmd packages, which is the standing review suggestion and is deliberately still open.
Three blocks had drifted from what they document, and the file's own history is the reason it matters: a resource-id collision had to be fixed here once already, so a future edit to $resourceId reading the comment above it would have found a date-conversion rationale instead. $resourceId's two blocks -- "bound once because it is used twice" and "built from CODES, not the names the upstream reports" -- sat above $iso, which converts dd-MM-yyyy to ISO and has nothing to do with either. They now join the comment $resourceId already had. $iso's own comment was orphaned further down with no code beneath it at all. It is back above $iso. The market-code paragraph moved to $scope rather than travelling with the rest: "without it the query widened to the whole district, so the district code is what identifies the scope" describes the $scope binding specifically, not how the id is composed. No expression changed -- the shipped-mapping tests serve this exact file over HTTP and still pass.
80dd82d to
75f87f0
Compare
Merge-readiness re-check (head
|
Targets
feat/41-oan-adapter-pluginsrather thandevelopment, because it builds on the machinery that PR #2 introduces. Merge that one first.What
A second provider capability,
openagrinet:MandiPrice, served against Agmarknet's Vistaar select. Additive: six new files, one line added to the plugin build list, and nothing existing changed.The domain package —
pkg/plugin/implementation/mandi/38 lines, plus a 20-line prerequisites file that is deliberately empty. It supplies its name and nothing else; recognising the capability, resolving the call plan, authenticating, calling with the registry's budget and translating in both directions are all
internal/upstream's.internal/upstreamneeded no change for it. That was the test of whether the machinery and domain split in PR #2 actually held, and it is the reason this PR is as small as it is.No prerequisites, and not by luck
A
MandiPriceselect names the market it wants —market.marketCode,market.district,market.state— and carries a commodity code and a validity window, which is every parameter the upstream takes. The pack has no top-level location, so nothing has to turn a point into a market.That matters because turning a point into a market is exactly what the provider backend needs a PostGIS query for, and the adapter may not talk to a database. The pack's shape and the upstream's contract happen to agree, so the plugin is data only.
The mapping —
config/mappings/agmarknet/mandi-price.select.yamlWhere the actual work is. Three things in it are not obvious:
`Modal Price`— so they need backticks, and its prices are strings, so they need$number()before they satisfy the pack's numeric types. A verbatim capture from the provider backend's own documentation is pinned as a fixture, so neither can drift unnoticed.dd-MM-yyyy, so the request half converts out and the response half converts back.OnDemandselect requires onlysupportedCommoditiesandsupportedPriceFields, leavingmarketandvalidityoptional, and definesmarket.districtandmarket.stateas "name or governed code". So a spec-valid select can still be unanswerable, and therequired:block refuses those with its own message rather than earning a 400 from Agmarknet — or worse, an empty result that reads as "no prices".Resource ids are built from codes rather than the names the upstream reports:
Kasdol APMCandPaddy(Common)carry spaces and brackets, and an id a consumer may put in a URL should not.Query-string auth
Agmarknet takes its token as a query parameter.
authScheme: queryfor that arrives in PR #2, since it is machinery rather than anything mandi-specific. The config here holds the parameter's name and the name of the environment variable carrying the value, never the value.Why
Two capabilities from two domain packages in one pipeline, dispatched by binding key read from the payload. Which one answers is decided by the request, not by a routing table, a URL or a domain — each step recognises its own key and passes through anything else.
It also settles a claim PR #2 could only assert: that adding a domain costs a small package and a mapping. It cost 58 lines of Go and one line in the build script.
Testing
go vet ./...clean andgo test ./...green across 63 packages.The shipped mapping is run through the real mapper and the real provider step against a captured Agmarknet response, rather than asserted about. Both directions validate with
jsonschema— the select againstbeckn.yamlandMandiPrice v0.1inOnDemandmode, theon_selectagainstbeckn.yamland the pack inDirectmode — with zero errors.End to end on the local stack, both capabilities loaded into one provider adapter:
Directresource per price record, prices converted from strings, dates in ISO, the offer's references rewritten to what was returned, and a record that reported no minimum or maximum coming back with those absent rather than zeroedOnDemandwhile carrying no pricesThe local Postman collection covers all of that: 8 requests, 39 assertions, no failures.
Notes for review
dd-MM-yyyy, and a query token it rejects when absent. The real API's credentials, host and exact record set are untested.marketcode, widening to the district. Our step makes one call, so an empty result stays empty. Left alone until the real API's behaviour is observed, along with the question of whether Agmarknet reports "no data" as an HTTP 400.Closes #8