Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
68 commits
Select commit Hold shift + click to select a range
a7258fb
feat: add the OAN registry plugin [OpenAgriNet/engineering-tracker#46]
ameersohel45 Aug 31, 2026
b29da0f
feat: add the JSONata mapper plugin [OpenAgriNet/engineering-tracker#66]
ameersohel45 Aug 31, 2026
f4ef51a
feat: add the Mausamgram provider plugin [#1]
ameersohel45 Aug 31, 2026
497c908
fix: read the updated registry schema contract [OpenAgriNet/engineeri…
ameersohel45 Aug 31, 2026
5e57bdf
fix: hand mappings only what a party sent [OpenAgriNet/engineering-tr…
ameersohel45 Aug 31, 2026
8d05338
fix: refuse an unanswered request in a provider module [#1]
ameersohel45 Aug 31, 2026
5ccf772
fix: quote the resource the request selected [#1]
ameersohel45 Aug 31, 2026
343da4f
fix: follow the WeatherObservation schema pack [#1]
ameersohel45 Sep 1, 2026
72c3f09
refactor: extract the provider's request in the mapping [#1]
ameersohel45 Sep 1, 2026
7da3fc4
feat: let a mapping declare what it requires of a payload [#1]
ameersohel45 Sep 1, 2026
bf7dcde
feat: let one provider step serve several capabilities [#1]
ameersohel45 Sep 1, 2026
5c033d8
refactor: split the provider step into machinery and domain [#1]
ameersohel45 Sep 1, 2026
395a701
feat: let a deployment say where a binding key lives [#1]
ameersohel45 Sep 1, 2026
92cd529
fix: quote one resource per forecast day [#1]
ameersohel45 Sep 2, 2026
b7187c6
fix: refuse a published path with an empty segment [#1]
ameersohel45 Sep 2, 2026
52768f0
fix: stop asserting network identity from a mapping [#1]
ameersohel45 Sep 2, 2026
ff2c61d
test: pin the registry shape after the key and role changes [#1]
ameersohel45 Sep 2, 2026
573dc1d
fix: make the on_select answer validate against the Beckn v2 spec [#1]
ameersohel45 Sep 2, 2026
22141a5
feat: let a provider present its credential in the query string [#1]
ameersohel45 Sep 3, 2026
ed79587
feat: log the upstream request and its outcome at info [#1]
ameersohel45 Sep 3, 2026
d08717d
fix: address the review findings on PR #2 [#1]
ameersohel45 Sep 3, 2026
f506c0a
fix: report an unservable payload as a bad request [#1]
ameersohel45 Sep 3, 2026
3f54ab7
fix: accept any 2xx, and say what the provider said [#1]
ameersohel45 Sep 3, 2026
5aa1882
docs(config): document every plugin option, and stop annotating upstr…
ameersohel45 Sep 6, 2026
c945812
refactor(mappings): echo the caller's @context instead of restating i…
ameersohel45 Sep 7, 2026
85adfa9
chore(config): trim the extended-schema allowlist to the host in use …
ameersohel45 Sep 7, 2026
23e88c2
fix(upstream): redact the credential in the form that reaches the URL…
ameersohel45 Sep 7, 2026
1759712
fix(upstream): keep the provider's body and the env-var names off the…
ameersohel45 Sep 7, 2026
3342a88
fix(upstream): bound the registry's retry budget, and stop backoff ov…
ameersohel45 Sep 7, 2026
2e5e1ad
fix(oanregistry): bound the search response read [#1]
ameersohel45 Sep 7, 2026
b6283d0
fix(upstream): keep the error chain matchable through redaction [#1]
ameersohel45 Sep 7, 2026
7428a05
fix(upstream): send the method in the case the RFC gives it [#1]
ameersohel45 Sep 7, 2026
32e84c5
fix(upstream): validate the base url, and refuse dot segments and fra…
ameersohel45 Sep 7, 2026
936d037
fix(oanbinding): count commitments, not the values they happen to yie…
ameersohel45 Sep 7, 2026
8ee889d
refactor(model): hoist servedActions onto ProviderRecord [#1]
ameersohel45 Sep 7, 2026
26ff4ef
perf(jsonmapper): collapse concurrent misses into one fetch [#1]
ameersohel45 Sep 7, 2026
b21fdf4
fix(handler): refuse to sign a response that cannot carry a message [#1]
ameersohel45 Sep 7, 2026
8a17ba6
docs(mappings): correct two claims about the pack that were wrong [#1]
ameersohel45 Sep 7, 2026
3806f16
docs(config): placeholder the registry url [#1]
ameersohel45 Sep 7, 2026
2be8ce6
fix(jsonmapper): serialise mapping evaluation across the package, not…
ameersohel45 Sep 8, 2026
72039b1
fix(handler): refuse a non-envelope answer before the signer runs, no…
ameersohel45 Sep 8, 2026
e402f96
fix(upstream): redact the credential for every auth scheme, not just …
ameersohel45 Sep 8, 2026
0d62b52
test(handler): make the duplicate-provider-step test reach the guard …
ameersohel45 Sep 8, 2026
2978c93
docs: correct four claims that contradict the code, and drop two dead…
ameersohel45 Sep 8, 2026
82a5f1e
fix(jsonmapper): release the evaluation lock even if the library pani…
ameersohel45 Sep 8, 2026
214a793
docs: say that a mapping reads _local, because it does [#1]
ameersohel45 Sep 8, 2026
6ae1ba6
feat: add the Mandi provider plugin [#8]
ameersohel45 Sep 3, 2026
8fc6104
docs(config): serve both capabilities from the reference config [#8]
ameersohel45 Sep 6, 2026
b6e954c
refactor(mappings): echo the caller's @context in the mandi mapping t…
ameersohel45 Sep 7, 2026
ba5f922
fix(mappings): guard the price cast on shape, not just presence [#8]
ameersohel45 Sep 7, 2026
ebc1c91
fix(mappings): emit only records that can produce a conformant resour…
ameersohel45 Sep 7, 2026
aaac1b4
fix(mappings): state what the answer knows instead of echoing the req…
ameersohel45 Sep 7, 2026
3ffe2e8
fix(mappings): distinguish colliding rows, and answer only what was a…
ameersohel45 Sep 7, 2026
9714b24
docs: document MANDI_TOKEN, and correct two claims about the pack [#8]
ameersohel45 Sep 7, 2026
6ae0b0d
test(mandi): cover the plugin entry point, which had no tests at all …
ameersohel45 Sep 8, 2026
75f87f0
docs(mappings): put each comment above the code it describes [#8]
ameersohel45 Sep 8, 2026
a4337f3
fix(schemav2validator): keep the JSON-LD keys a schema declares [#16]
ameersohel45 Sep 7, 2026
e524cf7
refactor(plugins): name the capability plugins after their capabiliti…
ameersohel45 Sep 7, 2026
586ed23
feat(config): validate resource attributes against their schema packs…
ameersohel45 Sep 7, 2026
4fdf394
refactor(config): resolve capability schemas from the payload's @cont…
ameersohel45 Sep 7, 2026
4d18317
refactor: name the registry and binding packages for what they are [#16]
ameersohel45 Sep 7, 2026
78d42b9
docs(config): drop the network name from the reference config's comme…
ameersohel45 Sep 7, 2026
64a1de2
fix(schemav2validator): read only http and https for a payload-chosen…
ameersohel45 Sep 7, 2026
9a1594f
fix(schemav2validator): read the list form of @type, and reject what …
ameersohel45 Sep 7, 2026
006dec8
Merge pull request #13 from OpenAgriNet/feat/8-mandi-plugin
manjudr Sep 8, 2026
5132832
fix(schemav2validator): enforce the host allowlist on every schema re…
ameersohel45 Sep 8, 2026
cca7ff2
docs(MandiPrice): follow the rename in a cross-reference [#16]
ameersohel45 Sep 8, 2026
bcd10b3
Merge pull request #17 from OpenAgriNet/feat/16-capability-schema-con…
manjudr Sep 8, 2026
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
10 changes: 10 additions & 0 deletions cmd/adapter/main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,16 @@ func (m *MockPluginManager) Cache(ctx context.Context, cfg *plugin.Config) (defi
return nil, nil
}

// Mapper returns a mock implementation of the Mapper interface.
func (m *MockPluginManager) Mapper(ctx context.Context, cfg *plugin.Config) (definition.Mapper, error) {
return nil, nil
}

// ProviderStep returns a mock implementation of the provider Step interface.
func (m *MockPluginManager) ProviderStep(ctx context.Context, registry definition.ProviderRecordLookup, mapper definition.Mapper, cfg *plugin.Config) (definition.Step, error) {
return nil, nil
}

// Registry returns a mock implementation of the RegistryLookup interface.
func (m *MockPluginManager) Registry(ctx context.Context, cache definition.Cache, cfg *plugin.Config) (definition.RegistryLookup, error) {
return nil, nil
Expand Down
367 changes: 367 additions & 0 deletions config/mappings/agmarknet/mandi-price.select.yaml

Large diffs are not rendered by default.

311 changes: 311 additions & 0 deletions config/mappings/mausamgram/weather-observation.select.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,311 @@
# Mausamgram, openagrinet:WeatherObservation, select. Both directions, one file.
#
# One file per binding-action rather than one per direction, because both legs of
# one upstream call are one unit of configuration: they are published, reviewed
# and retired together, and a reference to one is a reference to the other.
#
# The registry entry pointing here decides which action this serves, so nothing
# in the file names it. The filename's action segment must match that entry --
# a mismatch would apply a correct mapping to the wrong call, silently.
#
# Both halves read:
# beckn the inbound Beckn payload -- the context to echo, the offer to
# quote against
# and the response half additionally reads:
# response the provider's answer, in its own shape
#
# and both halves also read:
# _local whatever the step's prerequisites resolved before the call -- a
# code looked up from a name, a point resolved to a market. EMPTY
# here: this plugin declares no prerequisites, so nothing in this
# file reads it.
#
# Nothing else is in scope. In particular, values the step already holds and
# merely used to make the call are not in _local: reading those back would be a
# second name for the same data. Where the answer needs one, it takes it from
# what the provider echoed.

# The response half follows the openagrinet:WeatherObservation v0.1 schema pack,
# Direct mode. The pack lives in OpenAgriNet/network-specs; it is referred to
# here by name and version rather than by a path, because a path pins a branch
# and a branch moves.
#
# @context is not stated here at all. The response echoes whatever the request
# declared, so this file never has to know which pack URL is current and cannot
# contradict the caller.
#
# Direct mode requires observationType, source, location, generatedAt and
# parameters. informationMode is what selects those requirements: a catalog
# resource advertising this capability is OnDemand instead, and carries
# supportedParameters rather than values.
#
# The answer returns ONE RESOURCE PER FORECAST DAY, each with its own id derived
# from its date. That is the shape the pack describes: every WeatherObservation
# example carries a single validity and a flat parameters array, so a period is a
# resource and there is no form for several in one.
#
# The ids are therefore new -- the request named an abstract point forecast, the
# answer returns the concrete days that satisfy it. Which is why the offer's
# resourceIds are rewritten below rather than echoed: the offer arrives naming
# the id that was asked for, and leaving it would point the offer at something
# that appears nowhere in the answer.
#
# ONE FIELD HERE IS NOT IN THE PACK, and this does NOT validate against it.
# An earlier version of this comment said the pack set no additionalProperties.
# It does: WeatherObservation v0.1 closes parameters.items with
# `additionalProperties: false`, so the field below is refused, not merely
# ungoverned.
#
# aggregation The pack's parameter entry is parameter/value/unit only, and
# its `parameter` enum has no minimum or maximum variants. This
# provider reports a minimum AND a maximum for temperature and
# humidity every day, so without this field the two arrive as
# two identical Temperature readings and a consumer cannot tell
# which is which.
#
# It is kept because dropping it loses information the pack cannot express any
# other way -- removing it would make the answer conformant and useless. The
# resolution is a pack change, an aggregation field or enum variants, and this
# mapping follows once that lands. Nothing validates a response today, so it
# does not bite until a consumer checks.
#
# Fields that are the same for every day -- the point, the source, the
# observation type -- sit once at the top. Only what varies per day repeats.

# What this capability requires of a payload, checked before either half runs.
# A predicate that is false refuses the request with the message beside it, so
# the caller is told what is wrong with their payload rather than that an
# expression somewhere returned false.
#
# This rule used to be Go: the step read the geometry and required a Point, which
# meant a capability with a different rule needed a different build. It is here
# now, beside the extraction it guards.
#
# NOTE the consequence: nothing in the adapter enforces a geometry any more. A
# mapping that declares no preconditions accepts whatever arrives and hands it to
# the request half, which is exactly the configurability that was asked for --
# and exactly why the responsibility sits in this file.
#
# One check, because there is one thing to say. $exists guards the type test, so
# a request carrying no location and a request carrying a Polygon both land here
# and both learn what this capability needs -- splitting them would be two
# entries repeating the same sentence.
#
# Each check is its own expression and binds $ra for itself; there is no shared
# scope with the halves below. Where several checks say genuinely different
# things, they are separate entries and the first failure is the one reported.
required:
- check: |
(
$ra := beckn.message.contract.commitments[0].resources[0].resourceAttributes;
$exists($ra.location) and $ra.location.type = "Point"
Comment thread
ameersohel45 marked this conversation as resolved.
)
message: "this capability needs a Point location; the provider forecasts one point at a time"

# NOTE, and it is not a small one: the pack and this check disagree today.
# WeatherObservation v0.1's OnDemand branch does not leave `location`
# optional -- it EXCLUDES it, alongside observationType, source, generatedAt,
# observedAt, modelRunAt, validity and parameters. So a request conforming to
# the pack carries no location and this check refuses it, while a request
# satisfying this check does not conform.
#
# Nothing breaks in practice: the exclusion sits under if/then, which the
# validator parses and never evaluates. That is an accident to rely on rather
# than a design.
#
# The pack has no third mode for a question -- OnDemand describes what a
# provider can obtain, Direct describes an obtained reading, and a request is
# neither. Resolving it is a pack change. `coverageAreas` is inherited from
# AgricultureResource, is NOT excluded in OnDemand, and accepts a GeoJSON
# geometry, so it is the likely home for the point once that is settled.

# The request half decides what the provider is asked for. Whatever it produces
# IS the request: query parameters for a method with no body, a body for one that
# takes it.
#
# This is where the extraction lives, deliberately. The step reads only the
# geometry's type -- enough to refuse a Polygon with a clear error, because a
# mapping cannot refuse -- and nothing else. So when this provider wants another
# parameter, it is an edit here and nothing else: no Go, no rebuild, live on the
# next cache expiry.
#
# A date range, for instance, is already in the payload and would be two lines:
#
# "from": $ra.validity.startsAt,
# "to": $ra.validity.endsAt
#
# $ra is bound once so the rest reads as plain field access rather than four
# repetitions of the same path.
#
# GeoJSON is [lon, lat] -- longitude first. Reading them the other way round
# gives a point in the wrong hemisphere that is still a valid request, so it
# fails as wrong data rather than as an error.
request: |
(
$ra := beckn.message.contract.commitments[0].resources[0].resourceAttributes;
{
"lat": $ra.location.coordinates[1],
"lon": $ra.location.coordinates[0]
}
)

# Keyed by direction, not by the action it produces: a select is answered by an
# on_select over the same HTTP round trip, so the callback is this half rather
# than an action of its own.
response: |
(
$lat := response.location.lat;
$lon := response.location.lon;
/* However many days the provider sent. It answers fcstday1..fcstdayN and N
is whatever the forecast ran to, so naming five would truncate a ten-day
answer and mis-handle a one-day one.

Sorted on the numeric suffix, not the key: the keys sort lexically as
fcstday1, fcstday10, fcstday2, and a ten-day forecast delivered in that
order would be wrong in a way nothing downstream could detect. */
$days := $each(response, function($v, $k) {
$contains($k, "fcstday") ? {
"n": $number($substringAfter($k, "fcstday")),
"day": $v
}
})^(n).day;

$reading := function($name, $aggregation, $unit, $value) {
$exists($value) ? {
"parameter": $name,
"aggregation": $aggregation,
"unit": $unit,
"value": $value
}
};

/* A warning is a parameter, not a field of its own: the pack has no
advisory property but does have an Alert parameter. Unit "1" is what it
prescribes for a value that has no unit. */
$alert := function($value) {
$exists($value) ? {
"parameter": "Alert",
"unit": "1",
"value": $value
}
};

$selected := beckn.message.contract.commitments[0];

/* The caller's own @context, echoed back rather than restated here. A
mapping that hardcodes it has to be reissued whenever the pack URL
moves, and it can disagree with what the request actually declared.
Backticks because @ is an operator in JSONata. */
$ctx := beckn.message.contract.commitments[0].resources[0].resourceAttributes.`@context`;
Comment thread
ameersohel45 marked this conversation as resolved.

/* Bound once because it is used twice -- for a resource's own id and for the
offer's reference to it. Two copies of the same expression is how a
dangling reference gets reintroduced. */
$resourceId := function($day) { "res:mausamgram:forecast:" & $day.date };

{
/* Correlation only: the ids that tie this answer to the request that
asked for it, and nothing that asserts who anybody is.

bapId, bapUri, bppId and bppUri are deliberately absent. A mapping is
a payload transformation -- it has no business asserting network
identity, and the two Uri fields it could copy are whatever the caller
happened to send, which in a deployed stack is a container-internal
address that means nothing to anyone outside it. Echoing them would
republish another party's routing details as if they were ours.

Identity on the wire is the adapter's own: it signs what it answers
with, using the key the registry publishes for it. */
"context": {
"version": beckn.context.version,
"action": "on_select",
"networkId": beckn.context.networkId,
"transactionId": beckn.context.transactionId,
"messageId": beckn.context.messageId,
"timestamp": $now()
},
"message": {
"contract": {
"commitments": [
{
/* DRAFT, not QUOTED. The Beckn v2 status enum is DRAFT, ACTIVE
and CLOSED, and a quote is still a draft: nothing is committed
until init and confirm. QUOTED read better and validated
nowhere -- base schema validation refuses it. */
"status": {
"descriptor": { "code": "DRAFT", "name": "Draft" }
},
/* The offer is echoed, but its references are not: the request
named the abstract point forecast, and the answer returns the
concrete days. Leaving resourceIds as they arrived would point
the offer at an id that appears nowhere in the answer.

$merge keeps everything else the request offered -- the id, the
descriptor, the provider -- and replaces one key. */
"offer": $merge([
$selected.offer,
{ "resourceIds": [$map($days, function($day) { $resourceId($day) })] }
]),
/* One resource per forecast day, which is what the pack describes:
every WeatherObservation example carries a single validity and a
flat parameters array, so a period is a resource and there is no
form for several in one.

Wrapped for the same reason as the resourceIds above: JSONata
collapses a one-element sequence to a bare value, so a one-day
forecast would answer with an object where every other N answers
with a list. */
"resources": [$map($days, function($day) {
{
"id": $resourceId($day),
/* Required by Commitment.resources in the spec, which defines
no quantity property and no Quantity schema anywhere -- a
defect upstream. One resource is one day's observation, so
one. Omitting it makes every answer fail validation for a
consumer who validates. */
"quantity": 1,
"resourceAttributes": {
"@context": $ctx,
"@type": "openagrinet:WeatherObservation",
"informationMode": "Direct",
"observationType": "Forecast",
"subjectCategories": ["Weather"],
"source": {
"sourceId": "mausamgram",
"sourceName": "IMD Mausamgram NWP"
},
/* Emitted only when the provider echoed both coordinates.
JSONata drops undefined values inside an array, so a
provider that answered without its location echo would
otherwise produce "coordinates": [] -- an invalid Point,
signed and delivered. Absent is honest; empty is a lie
in the shape of an answer. */
"location": $exists($lat) and $exists($lon) ? {
"type": "Point",
"coordinates": [$lon, $lat]
},
"generatedAt": $now(),
/* This resource reports one day, so its validity opens and
closes on it. */
"validity": {
"startsAt": $day.date,
"endsAt": $day.date
},
"parameters": [
$reading("Rainfall", "Total", "mm", $day.rain),
$reading("Temperature", "Minimum", "Cel", $day.tmin),
$reading("Temperature", "Maximum", "Cel", $day.tmax),
$reading("Humidity", "Minimum", "%", $day.rhmin),
$reading("Humidity", "Maximum", "%", $day.rhmax),
$reading("WindSpeed", "Average", "m/s", $day.wspd),
$alert($day.weather_warning ? $day.weather_warning : $day.cloud_message)
]
}
}
})]
}
]
}
}
}
)
Loading
Loading