Thank you for being patient! We're working hard on resolving the issue
A condensed dry run of building a hypothetical Strava integration
that surfaces activities at :~strava:{authId}:activities.
tento/
└── tento-strava/
├── Cargo.toml
└── src/
├── lib.rs
├── client.rs # your Strava SDK
├── oauth.rs # StravaOAuthCredentials
└── connector/
├── mod.rs # Strava, StravaClient,
│ StravaBackendDeps, StravaAuth,
│ StravaParams
└── activities/
├── mod.rs # Activities Row impl
└── logs.rs # Logs Row impl
Cargo.toml[package]
name = "tento-strava"
version = "0.1.0"
edition = "2024"
[features]
default = []
connector = ["dep:tento-lona-connector", "dep:tento-chrono", "dep:futures", "dep:uuid"]
sync-plugin = [
"dep:lona-sync", "dep:async-trait",
"dep:tento-lona-connector", "dep:tento-chrono", "dep:futures", "dep:uuid",
]
[dependencies]
tento-lona-connector = { workspace = true, optional = true }
tento-chrono = { workspace = true, optional = true }
futures = { workspace = true, optional = true }
uuid = { workspace = true, optional = true }
lona-sync = { workspace = true, optional = true }
async-trait = { workspace = true, optional = true }
tento-oauth.workspace = true
anyhow.workspace = true
chrono.workspace = true
serde.workspace = true
serde_json.workspace = true
reqwest.workspace = true
src/lib.rs#![deny(missing_docs)]
//! Strava SDK + connector + sync-plugin.
pub mod client;
pub mod oauth;
#[cfg(feature = "connector")]
pub mod connector;
#[cfg(feature = "sync-plugin")]
pub mod sync_plugin;
pub use client::Client;
src/connector/mod.rs//! Strava integration for `tento-lona-connector`.
#![deny(missing_docs)]
pub mod activities;
use std::sync::Arc;
use anyhow::Context;
use futures::future::BoxFuture;
use serde::Deserialize;
use tento_lona_connector::{
AuthProvider, Cell, Integration, Owner, UserAuth, UserAuthExt,
};
use tento_oauth::oauth::Auth;
use crate::oauth::StravaOAuthCredentials;
/// Per-integration grab bag.
#[derive(Clone)]
pub struct StravaClient {
/// HTTP client resolved from the user's Strava OAuth.
pub http: Arc<crate::Client>,
}
/// Strava's deps contract.
pub trait StravaBackendDeps: Send + Sync {
/// Persist a batch of cells onto the named row.
fn persist_cells<'a>(
&'a self,
row_id: ::uuid::Uuid,
cells: Vec<Cell>,
) -> BoxFuture<'a, anyhow::Result<()>>;
/// Read recent log cells for `:logs` rows.
fn recent_log_cells<'a>(
&'a self,
owner: &'a Owner,
lookup_key: &'a str,
) -> BoxFuture<'a, anyhow::Result<Vec<Cell>>>;
}
/// Strava integration marker.
pub struct Strava;
/// Path-level params for `:~strava:{authId}:…` rowkeys.
#[derive(Deserialize, Clone, Debug)]
pub struct StravaParams {
/// Per-account Strava auth identifier.
#[serde(rename = "authId")]
pub auth_id: String,
}
/// AuthProvider marker.
pub struct StravaAuth;
impl AuthProvider for StravaAuth {
const KIND: &'static str = "strava";
type AuthId = str;
type Credentials = Auth<StravaOAuthCredentials>;
}
impl Integration for Strava {
type Deps = dyn StravaBackendDeps;
type Client = StravaClient;
type Params = StravaParams;
fn client<'a>(
user: &'a dyn UserAuth,
_deps: &'a dyn StravaBackendDeps,
p: &'a Self::Params,
) -> BoxFuture<'a, anyhow::Result<Self::Client>> {
Box::pin(async move {
let auth = user
.auth::<StravaAuth>(&p.auth_id)
.await
.context("strava auth lookup")?
.ok_or_else(|| anyhow::anyhow!("no strava auth `{}`", p.auth_id))?;
let handle = auth.acquire_handle().await?;
Ok(StravaClient {
http: Arc::new(crate::Client::from_handle(handle)),
})
})
}
}
src/connector/activities/mod.rs//! Strava activities — `:~strava:{authId}:activities`.
#![deny(missing_docs)]
pub mod logs;
use anyhow::Context;
use futures::future::BoxFuture;
use tento_chrono::{PartialDate, SemanticTime};
use tento_lona_connector::{Cell, Row, SyncCtx, SyncShape, wearable_window_for_stage};
use super::{Strava, StravaParams};
/// Strava activities sync row.
pub struct Activities;
impl Row for Activities {
type Integration = Strava;
type Params = ();
const ROW_LABEL: &'static str = "strava-activities";
fn lookup_key(int: &StravaParams, _: &()) -> String {
format!(":~strava:{}:activities", int.auth_id)
}
fn sync_shape() -> Option<SyncShape> {
Some(SyncShape {
stages: &["viewport", "backfill"],
..Default::default()
})
}
fn sync<'a>(ctx: SyncCtx<'a, Self>) -> BoxFuture<'a, anyhow::Result<usize>> {
Box::pin(async move {
let range = wearable_window_for_stage(ctx.stage);
let activities = ctx
.client
.http
.activities()
.list_range(range)
.await
.context("strava activities.list_range")?;
let cells: Vec<Cell> = activities
.into_iter()
.map(|a| Cell {
id: Some(a.id.to_string()),
date: SemanticTime::from_partial(PartialDate::Ymd(a.start_date.date_naive())),
end_date: None,
data: serde_json::to_value(&a).unwrap(),
})
.collect();
let n = cells.len();
if !cells.is_empty() {
ctx.deps.persist_cells(ctx.row_id, cells).await?;
}
Ok(n)
})
}
}
Implement StravaBackendDeps on your host's deps struct
(mirror lona-so/src/service/rowkey_deps.rs for Garmin's
pattern).
Add a "strava" match arm to your host's UserAuth
credential resolver.
Register the row pattern in your RowKeySchema block:
[integration = ::tento_strava::connector::Strava] {
":~strava:{authId}" {
":activities" { get }
}
}
Register with the orchestrator (if shipping sync-plugin)
via register_row_with_sync::<strava::activities::Activities>(...).
That's the full path. Compare with
tento/tento-garmin/ (workspace) to see the in-tree shape.