diff --git a/app/src/RepositoriesList.tsx b/app/src/RepositoriesList.tsx index 43942e6..25b5a62 100644 --- a/app/src/RepositoriesList.tsx +++ b/app/src/RepositoriesList.tsx @@ -1,7 +1,36 @@ +import { For, Show } from "solid-js"; +import { newXrpcKnottyClient } from "./lib/atproto"; +import { createQuery, queryOptions } from "@tanstack/solid-query"; +import { DidChip } from "./components/DidChip"; + +async function resolveRepositories() { + const rpc = newXrpcKnottyClient(); + const res = await rpc.get("dev.drawbu.knotty.list"); + if (!res.ok) return null; + return res.data; +} + +const repositoriesQueryOptions = () => + queryOptions({ + queryKey: ["mirrors", "list"], + queryFn: () => resolveRepositories(), + }); + export function RepositoriesList() { + const query = createQuery(() => repositoriesQueryOptions()); + return (

mirrored repositories:

+ loading...

}> + {(data) => ( + + )} +
); } diff --git a/app/src/components/DidChip.tsx b/app/src/components/DidChip.tsx index 2b3bf3d..eddfdb3 100644 --- a/app/src/components/DidChip.tsx +++ b/app/src/components/DidChip.tsx @@ -3,6 +3,7 @@ import { createSignal } from "solid-js"; type Props = { did: Did; + full?: boolean; class?: string; }; @@ -27,7 +28,9 @@ export function DidChip(props: Props) { onClick={copy} class={`group flex items-center gap-1 font-mono text-xs text-zinc-500 hover:text-zinc-800 dark:hover:text-zinc-300 transition-colors ${props.class ?? ""}`} > - {shortDid(props.did)} + + {props.full ? props.did : shortDid(props.did)} + {} + +export interface $params {} +export interface $output extends v.InferXRPCBodyInput {} + +declare module "@atcute/lexicons/ambient" { + interface XRPCQueries { + "dev.drawbu.knotty.list": mainSchema; + } +} diff --git a/crates/knotty-knot/src/xrpc/knotty/create.rs b/crates/knotty-knot/src/xrpc/knotty/create.rs index 458e997..48b79cd 100644 --- a/crates/knotty-knot/src/xrpc/knotty/create.rs +++ b/crates/knotty-knot/src/xrpc/knotty/create.rs @@ -8,7 +8,7 @@ use knotty_lexicons::dev_drawbu::knotty::create::{CreateOutput, CreateRequest}; use crate::{error::KnotError, repodid, state::KnotState}; -#[axum::debug_handler(state = KnotState)] +#[axum::debug_handler] pub async fn handler( State(state): State, ExtractServiceAuth(auth): ExtractServiceAuth, diff --git a/crates/knotty-knot/src/xrpc/knotty/list.rs b/crates/knotty-knot/src/xrpc/knotty/list.rs index 1bb2f86..8ba2cd8 100644 --- a/crates/knotty-knot/src/xrpc/knotty/list.rs +++ b/crates/knotty-knot/src/xrpc/knotty/list.rs @@ -1,7 +1,37 @@ use crate::{error::KnotError, state::KnotState}; -use axum::extract::State; +use axum::{Json, extract::State}; +use jacquard::types::did::Did; +use knotty_lexicons::dev_drawbu::knotty::list::{ListOutput, Repo}; -#[axum::debug_handler(state = KnotState)] -pub async fn handler(State(_state): State) -> Result<(), KnotError> { - Err(KnotError::not_implemented()) +#[axum::debug_handler] +pub async fn handler( + State(state): State, +) -> Result>, KnotError> { + let dir = match std::fs::read_dir(state.storage_path) { + Ok(dir) => dir, + Err(err) => { + tracing::info!(error = %err, "storage path is unreachable"); + return Ok(Json(ListOutput { + repos: Vec::new(), + extra_data: Default::default(), + })); + } + }; + + let repos: Vec<_> = dir + .into_iter() + .flatten() + .filter(|entry| entry.file_type().map_or(false, |ft| ft.is_dir())) + .map(|entry| entry.file_name().to_string_lossy().to_string()) + .filter_map(|entry| Did::new_owned(entry).ok()) + .map(|repo_did| Repo { + repo_did, + extra_data: Default::default(), + }) + .collect(); + + Ok(Json(ListOutput { + repos, + extra_data: Default::default(), + })) } diff --git a/crates/knotty-knot/src/xrpc/knotty/mod.rs b/crates/knotty-knot/src/xrpc/knotty/mod.rs index e240028..2e6a6b0 100644 --- a/crates/knotty-knot/src/xrpc/knotty/mod.rs +++ b/crates/knotty-knot/src/xrpc/knotty/mod.rs @@ -1,6 +1,6 @@ -use axum::{Router, routing::post}; +use axum::Router; use jacquard_axum::IntoRouter; -use knotty_lexicons::dev_drawbu::knotty::create::CreateRequest; +use knotty_lexicons::dev_drawbu::knotty::{create::CreateRequest, list::ListRequest}; use crate::state::KnotState; @@ -10,5 +10,5 @@ mod list; pub fn router() -> Router { Router::new() .merge(CreateRequest::into_router(create::handler)) - .route("/xrpc/dev.drawbu.knotty.list", post(list::handler)) + .merge(ListRequest::into_router(list::handler)) } diff --git a/crates/knotty-lexicons/src/dev_drawbu/knotty.rs b/crates/knotty-lexicons/src/dev_drawbu/knotty.rs index 5cb8c20..fb83c3b 100644 --- a/crates/knotty-lexicons/src/dev_drawbu/knotty.rs +++ b/crates/knotty-lexicons/src/dev_drawbu/knotty.rs @@ -4,3 +4,4 @@ // Any manual changes will be overwritten on the next regeneration. pub mod create; +pub mod list; diff --git a/crates/knotty-lexicons/src/dev_drawbu/knotty/list.rs b/crates/knotty-lexicons/src/dev_drawbu/knotty/list.rs new file mode 100644 index 0000000..8f64a8c --- /dev/null +++ b/crates/knotty-lexicons/src/dev_drawbu/knotty/list.rs @@ -0,0 +1,236 @@ +// @generated by jacquard-lexicon. DO NOT EDIT. +// +// Lexicon: dev.drawbu.knotty.list +// +// This file was automatically generated from Lexicon schemas. +// Any manual changes will be overwritten on the next regeneration. + +#[allow(unused_imports)] +use alloc::collections::BTreeMap; + +#[allow(unused_imports)] +use core::marker::PhantomData; + +#[allow(unused_imports)] +use jacquard_common::deps::codegen::unicode_segmentation::UnicodeSegmentation; +use jacquard_common::types::string::Did; +use jacquard_derive::{IntoStatic, lexicon}; +use jacquard_lexicon::lexicon::LexiconDoc; +use jacquard_lexicon::schema::LexiconSchema; + +use crate::dev_drawbu::knotty::list; +#[allow(unused_imports)] +use jacquard_lexicon::validation::{ConstraintError, ValidationPath}; +use serde::{Deserialize, Serialize}; + +#[lexicon] +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, IntoStatic)] +#[serde(rename_all = "camelCase")] +pub struct ListOutput<'a> { + ///List of mirrored repositories. + #[serde(borrow)] + pub repos: Vec>, +} + +/// A mirrored repository. + +#[lexicon] +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, IntoStatic)] +#[serde(rename_all = "camelCase")] +pub struct Repo<'a> { + ///DID of the mirrored repository. + #[serde(borrow)] + pub repo_did: Did<'a>, +} + +/// XRPC request marker type. + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, IntoStatic, Copy)] +pub struct List; +/// Response type for dev.drawbu.knotty.list +pub struct ListResponse; +impl jacquard_common::xrpc::XrpcResp for ListResponse { + const NSID: &'static str = "dev.drawbu.knotty.list"; + const ENCODING: &'static str = "application/json"; + type Output<'de> = ListOutput<'de>; + type Err<'de> = jacquard_common::xrpc::GenericError<'de>; +} + +impl jacquard_common::xrpc::XrpcRequest for List { + const NSID: &'static str = "dev.drawbu.knotty.list"; + const METHOD: jacquard_common::xrpc::XrpcMethod = jacquard_common::xrpc::XrpcMethod::Query; + type Response = ListResponse; +} + +/// Endpoint type for dev.drawbu.knotty.list +pub struct ListRequest; +impl jacquard_common::xrpc::XrpcEndpoint for ListRequest { + const PATH: &'static str = "/xrpc/dev.drawbu.knotty.list"; + const METHOD: jacquard_common::xrpc::XrpcMethod = jacquard_common::xrpc::XrpcMethod::Query; + type Request<'de> = List; + type Response = ListResponse; +} + +impl<'a> LexiconSchema for Repo<'a> { + fn nsid() -> &'static str { + "dev.drawbu.knotty.list" + } + fn def_name() -> &'static str { + "repo" + } + fn lexicon_doc() -> LexiconDoc<'static> { + lexicon_doc_dev_drawbu_knotty_list() + } + fn validate(&self) -> Result<(), ConstraintError> { + Ok(()) + } +} + +pub mod repo_state { + + pub use crate::builder_types::{IsSet, IsUnset, Set, Unset}; + #[allow(unused)] + use core::marker::PhantomData; + mod sealed { + pub trait Sealed {} + } + /// State trait tracking which required fields have been set + pub trait State: sealed::Sealed { + type RepoDid; + } + /// Empty state - all required fields are unset + pub struct Empty(()); + impl sealed::Sealed for Empty {} + impl State for Empty { + type RepoDid = Unset; + } + ///State transition - sets the `repo_did` field to Set + pub struct SetRepoDid(PhantomData S>); + impl sealed::Sealed for SetRepoDid {} + impl State for SetRepoDid { + type RepoDid = Set; + } + /// Marker types for field names + #[allow(non_camel_case_types)] + pub mod members { + ///Marker type for the `repo_did` field + pub struct repo_did(()); + } +} + +/// Builder for constructing an instance of this type +pub struct RepoBuilder<'a, S: repo_state::State> { + _state: PhantomData S>, + _fields: (Option>,), + _lifetime: PhantomData<&'a ()>, +} + +impl<'a> Repo<'a> { + /// Create a new builder for this type + pub fn new() -> RepoBuilder<'a, repo_state::Empty> { + RepoBuilder::new() + } +} + +impl<'a> RepoBuilder<'a, repo_state::Empty> { + /// Create a new builder with all fields unset + pub fn new() -> Self { + RepoBuilder { + _state: PhantomData, + _fields: (None,), + _lifetime: PhantomData, + } + } +} + +impl<'a, S> RepoBuilder<'a, S> +where + S: repo_state::State, + S::RepoDid: repo_state::IsUnset, +{ + /// Set the `repoDid` field (required) + pub fn repo_did( + mut self, + value: impl Into>, + ) -> RepoBuilder<'a, repo_state::SetRepoDid> { + self._fields.0 = Option::Some(value.into()); + RepoBuilder { + _state: PhantomData, + _fields: self._fields, + _lifetime: PhantomData, + } + } +} + +impl<'a, S> RepoBuilder<'a, S> +where + S: repo_state::State, + S::RepoDid: repo_state::IsSet, +{ + /// Build the final struct + pub fn build(self) -> Repo<'a> { + Repo { + repo_did: self._fields.0.unwrap(), + extra_data: Default::default(), + } + } + /// Build the final struct with custom extra_data + pub fn build_with_data( + self, + extra_data: BTreeMap< + jacquard_common::deps::smol_str::SmolStr, + jacquard_common::types::value::Data<'a>, + >, + ) -> Repo<'a> { + Repo { + repo_did: self._fields.0.unwrap(), + extra_data: Some(extra_data), + } + } +} + +fn lexicon_doc_dev_drawbu_knotty_list() -> LexiconDoc<'static> { + use alloc::collections::BTreeMap; + #[allow(unused_imports)] + use jacquard_common::{CowStr, deps::smol_str::SmolStr, types::blob::MimeType}; + use jacquard_lexicon::lexicon::*; + LexiconDoc { + lexicon: Lexicon::Lexicon1, + id: CowStr::new_static("dev.drawbu.knotty.list"), + defs: { + let mut map = BTreeMap::new(); + map.insert( + SmolStr::new_static("main"), + LexUserType::XrpcQuery(LexXrpcQuery { + parameters: None, + ..Default::default() + }), + ); + map.insert( + SmolStr::new_static("repo"), + LexUserType::Object(LexObject { + description: Some(CowStr::new_static("A mirrored repository.")), + required: Some(vec![SmolStr::new_static("repoDid")]), + properties: { + #[allow(unused_mut)] + let mut map = BTreeMap::new(); + map.insert( + SmolStr::new_static("repoDid"), + LexObjectProperty::String(LexString { + description: Some(CowStr::new_static( + "DID of the mirrored repository.", + )), + format: Some(LexStringFormat::Did), + ..Default::default() + }), + ); + map + }, + ..Default::default() + }), + ); + map + }, + ..Default::default() + } +} diff --git a/lexicons/dev/drawbu/knotty/create.json b/lexicons/dev/drawbu/knotty/create.json index a3d4be8..dde73bb 100644 --- a/lexicons/dev/drawbu/knotty/create.json +++ b/lexicons/dev/drawbu/knotty/create.json @@ -1,4 +1,5 @@ { + "$schema": "https://unpkg.com/@atcute/lexicon-doc/schema/lexicon-doc.schema.json", "lexicon": 1, "id": "dev.drawbu.knotty.create", "defs": { diff --git a/lexicons/dev/drawbu/knotty/list.json b/lexicons/dev/drawbu/knotty/list.json new file mode 100644 index 0000000..42bf667 --- /dev/null +++ b/lexicons/dev/drawbu/knotty/list.json @@ -0,0 +1,40 @@ +{ + "$schema": "https://unpkg.com/@atcute/lexicon-doc/schema/lexicon-doc.schema.json", + "lexicon": 1, + "id": "dev.drawbu.knotty.list", + "defs": { + "main": { + "type": "query", + "description": "List all mirrored repositories hosted on this server.", + "output": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": ["repos"], + "properties": { + "repos": { + "type": "array", + "description": "List of mirrored repositories.", + "items": { + "type": "ref", + "ref": "#repo" + } + } + } + } + } + }, + "repo": { + "type": "object", + "description": "A mirrored repository.", + "required": ["repoDid"], + "properties": { + "repoDid": { + "type": "string", + "format": "did", + "description": "DID of the mirrored repository." + } + } + } + } +}