Thank you for your interest in contributing to Relais! This guide covers how to write and submit adapters, as well as general contribution guidelines.
mkdir -p crates/adapters/mysite
cd crates/adapters/mysite
cargo init --lib --name relais-adapter-mysiteAdd relais-core as a dependency in your new crate's Cargo.toml:
[dependencies]
relais-core = { path = "../../core" }
async-trait = "0.1"
reqwest = { version = "0.12", features = ["json"] }
serde_json = "1"
tracing = "0.1"use async_trait::async_trait;
use relais_core::{
Action, Adapter, AdapterError, AuthType, ExecContext,
Method, Resource, Response, ResponseMeta, SiteManifest,
};
use reqwest::Client;
use serde_json::json;
pub struct MySiteAdapter {
client: Client,
}
impl MySiteAdapter {
pub fn new() -> Self {
Self {
client: Client::new(),
}
}
}
impl Default for MySiteAdapter {
fn default() -> Self {
Self::new()
}
}
#[async_trait]
impl Adapter for MySiteAdapter {
fn manifest(&self) -> SiteManifest {
SiteManifest {
id: "mysite".into(),
name: "My Site".into(),
base_url: "https://api.mysite.com".into(),
auth_type: AuthType::APIKey,
}
}
fn resources(&self) -> Vec<Resource> {
vec![Resource {
id: "items".into(),
description: "Items on My Site".into(),
actions: vec![Action {
id: "list".into(),
method: Method::Read,
description: "List all items".into(),
params: json!({}),
returns: json!({"type": "array", "items": {"type": "object"}}),
pagination: None,
}],
children: vec![],
}]
}
async fn exec(&self, ctx: &ExecContext) -> Result<Response, AdapterError> {
match (ctx.resource.as_str(), ctx.action.as_str()) {
("items", "list") => {
// Fetch from the upstream API and return structured data
todo!("implement")
}
_ => Err(AdapterError::Unsupported(format!(
"{}.{}",
ctx.resource, ctx.action
))),
}
}
}- All public methods must have tests
- Resource tree structure tests (manifest, resources, actions)
exec()error handling tests- Run with:
cargo test -p relais-adapter-mysite
Example test:
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn manifest_has_correct_id() {
let adapter = MySiteAdapter::new();
assert_eq!(adapter.manifest().id, "mysite");
}
#[test]
fn resources_are_not_empty() {
let adapter = MySiteAdapter::new();
assert!(!adapter.resources().is_empty());
}
#[tokio::test]
async fn exec_unsupported_action_returns_error() {
let adapter = MySiteAdapter::new();
let ctx = ExecContext {
site: "mysite".into(),
resource: "nonexistent".into(),
action: "nope".into(),
params: serde_json::json!({}),
credentials: None,
};
let result = adapter.exec(&ctx).await;
assert!(result.is_err());
}
}Before submitting a PR:
- Adapter implements all required trait methods
- Resource tree accurately represents the site's capabilities
- Error handling maps to appropriate
AdapterErrorvariants - No hardcoded credentials
- User-Agent header set appropriately
- Rate limiting respected
- Tests pass with
cargo test - No clippy warnings with
cargo clippy
- Fork the repo
- Create a branch:
feat/adapter-mysite - Add your crate to workspace members in root
Cargo.toml - Submit a PR with a description of what the adapter covers
rustfmtandclippyare mandatory- Follow existing adapter patterns (see the
githubandhackernewsadapters incrates/adapters/) - Accept interfaces, return structs
- Wrap errors with context
By contributing, you agree that your contributions will be dual-licensed under MIT and Apache 2.0.