A data-provider plugin is a WASM component that implements the data-provider interface
defined in wit/data-provider.wit and ships a manifest
declaring its identity and required network access. This crate (fulltime-plugin-api) is
the only dependency required to build one: it provides the canonical schema types, the
generated interface bindings, and the manifest parser.
Neither the host (Apps/rust) nor any plugin's implementation lives in this repo — this
repo only defines the contract. The reference implementation is Plugins/Bundesliga,
wrapping Libs/openligadb.
Every value a plugin returns is typed against the canonical schema, not a provider-specific shape:
Competition— a league, cup, or tournament.Team— a participant, identified consistently across every plugin.Fixture— a scheduled or completed match. The optionalgroupfield distinguishes group-stage fixtures (e.g."Group A") from single-table league fixtures (None).Standings— one or moreStandingsGroups, each a ranked list ofStandingsRow. A single-table league format is one unnamed group; a group-stage tournament is several named groups sharing the same row shape.
See the type definitions in wit/data-provider.wit (interface types) for the authoritative field list — the Rust types in this crate are generated
directly from it, so the two never drift.
Implement the five operations in interface data-provider:
| Function | Returns |
|---|---|
list-competitions |
list<competition> |
fetch-fixtures |
list<fixture> for a competition (scheduled/live) |
fetch-results |
list<fixture> for a competition (finished) |
fetch-standings |
standings for a competition |
fetch-metadata |
competition |
Every function returns a result<T, provider-error>. Use the structured error variants
instead of letting an upstream failure surface as an unhandled trap:
network-failure— the upstream HTTP call failed at the network layer.rate-limited— the upstream source responded with a rate limit, optionally carryingretry-after-seconds.schema-mapping-failure— the upstream response can't be represented in the canonical schema.
Plugins have no direct network access — a WASM component can't open a socket on its own.
Every upstream HTTP call goes through the host interface's fetch import, which
world plugin requires (import host;). Call this crate's host_fetch wrapper rather
than the raw generated binding:
let body: Vec<u8> = fulltime_plugin_api::host_fetch("https://api.openligadb.de/getbltable/bl1/2024")?;host_fetch only links and behaves correctly when compiled as part of a real wasm32
component instantiated by a host implementing host.fetch — it has no behavior of its own
outside that. Two consequences for how you structure a plugin:
- Gate calls to it behind
#[cfg(target_arch = "wasm32")](or an equivalent feature flag). - For native unit/integration tests, define your own small injectable trait (a
Fetchertaking a URL and returning bytes or an error) that yourwasm32build implements by delegating tohost_fetch, and that your tests implement with fixture data. This mirrors the patternPlugins/Bundesligauses.
A non-2xx status or any other transport failure comes back as this crate's
NetworkFailure — the same type your data-provider operations already return inside
ProviderError::NetworkFailure.
Every plugin ships a TOML manifest, parsed at load time by Manifest::parse:
id = "bundesliga"
version = "0.1.0"
schema_version = "1.0"
interface_version = "2.0"
network_hosts = ["api.openligadb.de"]id— unique among plugins the host loads.version— the plugin's own release version, unrelated to the contract versions below.schema_version/interface_version— themajor.minorcontract versions this plugin was built against (seeSCHEMA_VERSIONandINTERFACE_VERSION).network_hosts— every hostname the plugin needs to call. The host runtime scopes the plugin's HTTP fetch capability to exactly this list; a plugin has no network access to anything not declared here. This crate validates the field is present and well-formed, not that the hosts are reachable — that enforcement lives in the host runtime.
The schema and the interface version independently, each as a major.minor pair, because a
schema field addition and an interface signature change are unrelated concerns. A host
accepts a plugin when, for both versions, the major matches and the host's minor is equal
to or greater than the plugin's declared minor — the host being a superset of what the
plugin expects. See Version::accepts.
When either version changes:
- Minor bump: additive only (new optional field, new function that doesn't change existing signatures). Existing plugins keep working against a newer host.
- Major bump: breaking (removed/renamed field, changed function signature). Plugins must be rebuilt and republish their manifest's target version.
This crate re-exports what wit_bindgen::generate! produces for the data-provider
export so you don't regenerate your own (nominally incompatible) copy of the canonical
types from a vendored WIT file:
Guest— the trait to implement, one method per operation in the table above, using this crate's ownTeam/Fixture/Standings/Competition/ProviderErrortypes directly.export!— the macro that exports yourGuestimplementation as the component'sdata-providerinterface. Called from a downstream crate, it needs thewith_types_inform — the single-arg form only resolves inside this crate itself, sinceexport!iswit-bindgen-generated and expects to find its supporting types in the crate that declares them.
struct MyPlugin;
impl fulltime_plugin_api::Guest for MyPlugin {
fn list_competitions() -> Result<Vec<fulltime_plugin_api::Competition>, fulltime_plugin_api::ProviderError> {
// ...
}
// fetch_fixtures, fetch_results, fetch_standings, fetch_metadata ...
}
fulltime_plugin_api::export!(MyPlugin with_types_in fulltime_plugin_api);- Add this crate as a dependency:
[dependencies] fulltime-plugin-api = "0.1"
- Implement
Guestagainst your upstream data source, mapping its response shape into the canonical schema types, and callinghost_fetchfor every upstream request. - Call
export!with thewith_types_inform against your implementation. - Write your
manifest.tomldeclaring the network hosts you call andinterface_version = "2.0". - Build to a WASM component target and load it against the host runtime in
Apps/rust.