chadtmiller.com/knotless
git that lives in your Atmosphere account
1
10
Records in a space are read by their URI in full, as proposal 0016 names them
.changeset/space-record-uris-read.mdnew+7−0
| @@ -0,0 +1,7 @@ | ||
--- | ||
default: patch | ||
--- | ||
| ||
# A record in a private repository's space can be named by its full URI | ||
| ||
Wherever knotless takes a pull's, an issue's or a comment's `at://` URI, as `knotless show`, `comment`, `merge`, `checks --pull` and a pull's `Fixes`, it reads a private repository's record by its URI in the space too, `at://<owner>/space/dev.knotless.space/<name>/<author>/<collection>/<rkey>`, as atproto's permissioned data proposal names it, as well as by `at://<author>/<collection>/<rkey>`. | ||
crates/knotless/src/aturi.rsnew+341−0
| @@ -0,0 +1,341 @@ | ||
//! `at://` URIs of records, public or in a space, as atproto's | ||
//! permissioned data proposal (0016) addresses them: | ||
//! | ||
//! ```text | ||
//! Public: at://{authorDid}/{collection}/{rkey} | ||
//! Space: at://{spaceDid}/space/{spaceType}/{skey} | ||
//! Record: at://{spaceDid}/space/{spaceType}/{skey}/{authorDid}/{collection}/{rkey} | ||
//! ``` | ||
//! | ||
//! A public collection is an NSID, which has dots, and the `space` marker | ||
//! has none, so the two forms never read as each other. A record in a | ||
//! space is named in full only with every segment through its rkey: the | ||
//! public form of its author, collection and rkey names it only among the | ||
//! records of one space, which [`AtUri::resolved`] turns into its full | ||
//! name once that space is known. | ||
| ||
use anyhow::{Result, bail}; | ||
use std::fmt; | ||
use std::str::FromStr; | ||
| ||
/// The path segment that marks a URI as naming a space or a record in one. | ||
pub const SPACE_MARKER: &str = "space"; | ||
| ||
/// A space, as distinct from a record in one: | ||
/// `at://{spaceDid}/space/{spaceType}/{skey}`. | ||
#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)] | ||
pub struct SpaceRef { | ||
/// The space's authority. | ||
pub space_did: String, | ||
/// The space type, an NSID. | ||
pub space_type: String, | ||
/// The space key, which tells spaces of one type and authority apart. | ||
pub skey: String, | ||
} | ||
| ||
impl SpaceRef { | ||
pub fn new(space_did: &str, space_type: &str, skey: &str) -> SpaceRef { | ||
SpaceRef { | ||
space_did: space_did.to_string(), | ||
space_type: space_type.to_string(), | ||
skey: skey.to_string(), | ||
} | ||
} | ||
| ||
/// The space `uri` names: only the space's own URI, not a record's in | ||
/// it. | ||
pub fn parse(uri: &str) -> Result<SpaceRef> { | ||
match segments(uri)?[..] { | ||
[did, SPACE_MARKER, space_type, skey] => space(uri, did, space_type, skey), | ||
_ => bail!("{uri} is not at://<did>/{SPACE_MARKER}/<type>/<key>"), | ||
} | ||
} | ||
} | ||
| ||
impl fmt::Display for SpaceRef { | ||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { | ||
let SpaceRef { | ||
space_did, | ||
space_type, | ||
skey, | ||
} = self; | ||
write!(f, "at://{space_did}/{SPACE_MARKER}/{space_type}/{skey}") | ||
} | ||
} | ||
| ||
impl FromStr for SpaceRef { | ||
type Err = anyhow::Error; | ||
| ||
fn from_str(uri: &str) -> Result<SpaceRef> { | ||
SpaceRef::parse(uri) | ||
} | ||
} | ||
| ||
/// A record's URI: in public records, or in a space. | ||
#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)] | ||
pub struct AtUri { | ||
/// The space the record is in; none for a public record. | ||
pub space: Option<SpaceRef>, | ||
/// Whose record it is. A public URI's authority, which may be a | ||
/// handle; in a space, always a DID. | ||
pub author: String, | ||
pub collection: String, | ||
pub rkey: String, | ||
} | ||
| ||
impl AtUri { | ||
/// `author`'s public record. | ||
pub fn public(author: &str, collection: &str, rkey: &str) -> AtUri { | ||
AtUri { | ||
space: None, | ||
author: author.to_string(), | ||
collection: collection.to_string(), | ||
rkey: rkey.to_string(), | ||
} | ||
} | ||
| ||
/// `author`'s record in `space`. | ||
pub fn in_space(space: &SpaceRef, author: &str, collection: &str, rkey: &str) -> AtUri { | ||
AtUri { | ||
space: Some(space.clone()), | ||
..AtUri::public(author, collection, rkey) | ||
} | ||
} | ||
| ||
/// The record `uri` names, in either form. | ||
pub fn parse(uri: &str) -> Result<AtUri> { | ||
match segments(uri)?[..] { | ||
[ | ||
did, | ||
SPACE_MARKER, | ||
space_type, | ||
skey, | ||
author, | ||
collection, | ||
rkey, | ||
] => { | ||
let space = space(uri, did, space_type, skey)?; | ||
if !author.starts_with("did:") { | ||
bail!("{uri}: a record in a space is named by its author's DID"); | ||
} | ||
Ok(AtUri::in_space(&space, author, collection, rkey)) | ||
} | ||
[_, SPACE_MARKER, ..] => { | ||
bail!( | ||
"{uri} is not at://<did>/{SPACE_MARKER}/<type>/<key>/<did>/<collection>/<rkey>" | ||
) | ||
} | ||
[author, collection, rkey] => Ok(AtUri::public(author, collection, rkey)), | ||
_ => bail!("{uri} is not at://<did>/<collection>/<rkey>"), | ||
} | ||
} | ||
| ||
/// The record, named in full: one named in the public form takes | ||
/// `space`, when the URI was found where that is the space its records | ||
/// are in. One already in a space, or with none given, is as it was. | ||
pub fn resolved(mut self, space: Option<&SpaceRef>) -> AtUri { | ||
if self.space.is_none() { | ||
self.space = space.cloned(); | ||
} | ||
self | ||
} | ||
| ||
/// The record's author, collection and rkey, in the public form: what | ||
/// names it among the records of its space, and what a key made from | ||
/// a record's URI is made from, so it is the same in either form. | ||
pub fn short(&self) -> String { | ||
format!("at://{}/{}/{}", self.author, self.collection, self.rkey) | ||
} | ||
} | ||
| ||
impl fmt::Display for AtUri { | ||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { | ||
let AtUri { | ||
author, | ||
collection, | ||
rkey, | ||
.. | ||
} = self; | ||
match &self.space { | ||
Some(space) => write!(f, "{space}/{author}/{collection}/{rkey}"), | ||
None => write!(f, "at://{author}/{collection}/{rkey}"), | ||
} | ||
} | ||
} | ||
| ||
impl FromStr for AtUri { | ||
type Err = anyhow::Error; | ||
| ||
fn from_str(uri: &str) -> Result<AtUri> { | ||
AtUri::parse(uri) | ||
} | ||
} | ||
| ||
/// `uri`'s author, collection and rkey, in either form, borrowed from it. | ||
pub fn parts(uri: &str) -> Result<(&str, &str, &str)> { | ||
match segments(uri)?[..] { | ||
[_, SPACE_MARKER, _, _, author, collection, rkey] if author.starts_with("did:") => { | ||
Ok((author, collection, rkey)) | ||
} | ||
[_, SPACE_MARKER, ..] => bail!("{uri} is not a record in a space"), | ||
[author, collection, rkey] => Ok((author, collection, rkey)), | ||
_ => bail!("{uri} is not at://<did>/<collection>/<rkey>"), | ||
} | ||
} | ||
| ||
/// `uri` as [`AtUri::resolved`] names it in `space`; what does not parse | ||
/// as a record's URI is as it was. | ||
pub fn resolve(uri: &str, space: Option<&SpaceRef>) -> String { | ||
match AtUri::parse(uri) { | ||
Ok(at) => at.resolved(space).to_string(), | ||
Err(_) => uri.to_string(), | ||
} | ||
} | ||
| ||
/// [`AtUri::short`] of `uri`; what does not parse as a record's URI is as | ||
/// it was. | ||
pub fn short(uri: &str) -> String { | ||
AtUri::parse(uri).map_or_else(|_| uri.to_string(), |at| at.short()) | ||
} | ||
| ||
/// Whether `a` and `b` name the same record: the same author, collection | ||
/// and rkey, and the same space, unless one names none, as a URI typed or | ||
/// written in the public form for a record in a space does. | ||
pub fn same(a: &str, b: &str) -> bool { | ||
match (AtUri::parse(a), AtUri::parse(b)) { | ||
(Ok(a), Ok(b)) => { | ||
let spaces = match (&a.space, &b.space) { | ||
(Some(x), Some(y)) => x == y, | ||
_ => true, | ||
}; | ||
spaces && a.short() == b.short() | ||
} | ||
_ => a == b, | ||
} | ||
} | ||
| ||
/// The path segments after `at://`, the authority first, none empty. | ||
fn segments(uri: &str) -> Result<Vec<&str>> { | ||
let Some(rest) = uri.strip_prefix("at://") else { | ||
bail!("{uri} is not an at:// URI"); | ||
}; | ||
let parts: Vec<&str> = rest.split('/').collect(); | ||
if parts.iter().any(|p| p.is_empty()) { | ||
bail!("{uri} has an empty segment"); | ||
} | ||
Ok(parts) | ||
} | ||
| ||
fn space(uri: &str, did: &str, space_type: &str, skey: &str) -> Result<SpaceRef> { | ||
if !did.starts_with("did:") { | ||
bail!("{uri}: a space's authority is a DID"); | ||
} | ||
if !space_type.contains('.') { | ||
bail!("{uri}: {space_type} is no space type"); | ||
} | ||
Ok(SpaceRef::new(did, space_type, skey)) | ||
} | ||
| ||
#[cfg(test)] | ||
mod tests { | ||
use super::*; | ||
| ||
const SPACE: &str = "at://did:plc:o/space/dev.knotless.space/r"; | ||
const RECORD: &str = "at://did:plc:o/space/dev.knotless.space/r/did:plc:a/dev.knotless.pull/3k"; | ||
const PUBLIC: &str = "at://did:plc:a/dev.knotless.pull/3k"; | ||
| ||
#[test] | ||
fn a_record_in_a_space_reads_and_writes_back_whole() { | ||
let at = AtUri::parse(RECORD).unwrap(); | ||
let space = SpaceRef::new("did:plc:o", "dev.knotless.space", "r"); | ||
assert_eq!(at.space, Some(space.clone())); | ||
assert_eq!( | ||
(at.author.as_str(), at.collection.as_str(), at.rkey.as_str()), | ||
("did:plc:a", "dev.knotless.pull", "3k") | ||
); | ||
assert_eq!(at.to_string(), RECORD); | ||
assert_eq!(at.short(), PUBLIC); | ||
let made = AtUri::in_space(&space, "did:plc:a", "dev.knotless.pull", "3k"); | ||
assert_eq!(made, at); | ||
assert_eq!(RECORD.parse::<AtUri>().unwrap(), at); | ||
} | ||
| ||
#[test] | ||
fn a_public_record_has_no_space() { | ||
let at = AtUri::parse(PUBLIC).unwrap(); | ||
assert_eq!(at.space, None); | ||
assert_eq!(at.to_string(), PUBLIC); | ||
assert_eq!(at, AtUri::public("did:plc:a", "dev.knotless.pull", "3k")); | ||
// A public URI's authority may be a handle. | ||
let named = AtUri::parse("at://alice.test/dev.knotless.repo/r").unwrap(); | ||
assert_eq!(named.author, "alice.test"); | ||
} | ||
| ||
#[test] | ||
fn a_space_reads_and_writes_back_whole() { | ||
let space = SpaceRef::parse(SPACE).unwrap(); | ||
assert_eq!(space.to_string(), SPACE); | ||
assert_eq!(SPACE.parse::<SpaceRef>().unwrap(), space); | ||
assert!(SpaceRef::parse(RECORD).is_err()); | ||
assert!(SpaceRef::parse(PUBLIC).is_err()); | ||
} | ||
| ||
#[test] | ||
fn what_is_not_either_form_is_refused() { | ||
for bad in [ | ||
"https://did:plc:a/c/r", | ||
"at://did:plc:a/c", | ||
"at://did:plc:a//r", | ||
"at://did:plc:a/c/r/x", | ||
SPACE, | ||
"at://did:plc:o/space/dev.knotless.space/r/did:plc:a/c", | ||
"at://alice.test/space/dev.knotless.space/r/did:plc:a/c/k", | ||
"at://did:plc:o/space/notype/r/did:plc:a/c/k", | ||
"at://did:plc:o/space/dev.knotless.space/r/alice.test/c/k", | ||
] { | ||
assert!(AtUri::parse(bad).is_err(), "{bad}"); | ||
} | ||
for bad in [ | ||
"did:plc:o/space/dev.knotless.space/r", | ||
"at://alice.test/space/dev.knotless.space/r", | ||
"at://did:plc:o/space/notype/r", | ||
] { | ||
assert!(SpaceRef::parse(bad).is_err(), "{bad}"); | ||
} | ||
assert!(parts(SPACE).is_err()); | ||
assert!(parts("at://did:plc:a/c").is_err()); | ||
assert!(parts("at://did:plc:o/space/t.t/r/alice.test/c/k").is_err()); | ||
} | ||
| ||
#[test] | ||
fn parts_are_the_author_collection_and_rkey_of_either_form() { | ||
let want = ("did:plc:a", "dev.knotless.pull", "3k"); | ||
assert_eq!(parts(RECORD).unwrap(), want); | ||
assert_eq!(parts(PUBLIC).unwrap(), want); | ||
} | ||
| ||
#[test] | ||
fn a_public_form_resolves_into_the_space_it_was_found_in() { | ||
let space = SpaceRef::parse(SPACE).unwrap(); | ||
assert_eq!(resolve(PUBLIC, Some(&space)), RECORD); | ||
assert_eq!(resolve(PUBLIC, None), PUBLIC); | ||
// Already in a space, it stays in its own. | ||
let other = SpaceRef::new("did:plc:x", "dev.knotless.space", "y"); | ||
assert_eq!(resolve(RECORD, Some(&other)), RECORD); | ||
assert_eq!(resolve("not a uri", Some(&space)), "not a uri"); | ||
assert_eq!(short(RECORD), PUBLIC); | ||
assert_eq!(short("not a uri"), "not a uri"); | ||
} | ||
| ||
#[test] | ||
fn the_same_record_is_the_same_in_either_form() { | ||
assert!(same(RECORD, PUBLIC)); | ||
assert!(same(PUBLIC, RECORD)); | ||
assert!(same(RECORD, RECORD)); | ||
let elsewhere = "at://did:plc:x/space/dev.knotless.space/r/did:plc:a/dev.knotless.pull/3k"; | ||
assert!(!same(RECORD, elsewhere)); | ||
assert!(!same(PUBLIC, "at://did:plc:a/dev.knotless.pull/4k")); | ||
assert!(same("x", "x")); | ||
assert!(!same("x", PUBLIC)); | ||
} | ||
} | ||
crates/knotless/src/closing.rs+44−8
| @@ -5,7 +5,8 @@ | ||
//! | ||
//! Issues have no short numbers, so a pull names one by its page, | ||
//! `https://<appview>/<owner>/<repo>/issue/<did>/<rkey>`, or by its | ||
//! `at://<did>/dev.knotless.issue/<rkey>` URI, either maybe as a Markdown | ||
//! `at://<did>/dev.knotless.issue/<rkey>` URI (or, for a private | ||
//! repository's, its URI in full, in the repository's space), either maybe as a Markdown | ||
//! link's target: `Fixes [the crash](https://...)`. Only issues on the pull's | ||
//! own repository count: others are passed over. The merge closes each | ||
//! that is open, and that whoever merges may close, with a | ||
| @@ -14,6 +15,7 @@ | ||
//! A pull that names an issue the same way with no keyword before it | ||
//! mentions it: the issue's page lists it, as it lists those that close it. | ||
| ||
use crate::aturi::{self, AtUri}; | ||
use crate::forge::{self, Item, Target}; | ||
use crate::keys; | ||
use crate::records::{self, RepoRecord, StateRecord, nsid}; | ||
| @@ -90,10 +92,13 @@ fn issue(word: &str) -> Option<String> { | ||
let word = word.trim_start_matches(['<', '(']); | ||
let word = word.trim_end_matches(['>', ')', '.', ',', ';', ':', '!', '?']); | ||
let (did, rkey) = match word.strip_prefix("at://") { | ||
Some(_) => match forge::parse_uri(word).ok()? { | ||
(did, nsid::ISSUE, rkey) => (did.to_string(), rkey), | ||
_ => return None, | ||
}, | ||
Some(_) => { | ||
// Named in a space, it is kept so; in the public form, it | ||
// names an issue among those of the pull's own repository. | ||
let at = AtUri::parse(word).ok()?; | ||
let whole = at.collection == nsid::ISSUE && at.author.starts_with("did:"); | ||
return whole.then(|| at.to_string()); | ||
} | ||
None => { | ||
let page = word | ||
.strip_prefix("https://") | ||
| @@ -126,7 +131,7 @@ pub fn closes<'a>(pull: &Item, items: &'a [Item]) -> Vec<&'a Item> { | ||
let named = named_issues(&text); | ||
let found = named | ||
.iter() | ||
.filter_map(|uri| items.iter().find(|i| &i.uri == uri)); | ||
.filter_map(|uri| items.iter().find(|i| aturi::same(&i.uri, uri))); | ||
found.collect() | ||
} | ||
| ||
| @@ -144,8 +149,8 @@ pub fn mentioning<'a>(issue: &Item, items: &'a [Item]) -> Vec<(&'a Item, bool)> | ||
pull.title, | ||
pull.body.as_deref().unwrap_or_default() | ||
); | ||
let names = mentioned_issues(&text).contains(&issue.uri); | ||
names.then(|| (pull, named_issues(&text).contains(&issue.uri))) | ||
let names = |uris: Vec<String>| uris.iter().any(|u| aturi::same(u, &issue.uri)); | ||
names(mentioned_issues(&text)).then(|| (pull, names(named_issues(&text)))) | ||
}; | ||
pulls.filter_map(said).collect() | ||
} | ||
| @@ -233,6 +238,9 @@ mod tests { | ||
] { | ||
assert!(named_issues(&text).is_empty(), "{text}"); | ||
} | ||
let spaced = "at://did:plc:o/space/dev.knotless.space/r/did:plc:a/dev.knotless.issue/3abc"; | ||
assert_eq!(named_issues(&format!("Fixes {spaced}")), [spaced]); | ||
assert!(named_issues("Fixes at://did:plc:o/space/dev.knotless.space/r").is_empty()); | ||
let two = format!("Fixes {I}, fixes {page} and closes {I}2"); | ||
assert_eq!(named_issues(&two), [I.to_string(), format!("{I}2")]); | ||
} | ||
| @@ -346,4 +354,32 @@ mod tests { | ||
let issue = item(open, "issue", "open", "did:plc:a", Some(&body)); | ||
assert!(closes(&issue, &items).is_empty()); | ||
} | ||
| ||
/// A private repository's issue is named in full, in its space; a | ||
/// pull that names it in the public form, as one written before did, | ||
/// closes and mentions it still. | ||
#[test] | ||
fn an_issue_in_a_space_is_named_either_way() { | ||
let space = "at://did:plc:o/space/dev.knotless.space/r"; | ||
let spaced = format!("{space}/did:plc:a/dev.knotless.issue/1"); | ||
let issue = item(&spaced, "issue", "open", "did:plc:a", None); | ||
for named in ["at://did:plc:a/dev.knotless.issue/1", spaced.as_str()] { | ||
let body = format!("Fixes {named}"); | ||
let pull = item( | ||
&format!("{space}/did:plc:b/dev.knotless.pull/p"), | ||
"pull", | ||
"open", | ||
"did:plc:b", | ||
Some(&body), | ||
); | ||
let items = vec![issue.clone(), pull.clone()]; | ||
assert_eq!(closes(&pull, &items).len(), 1, "{named}"); | ||
assert_eq!(mentioning(&issue, &items).len(), 1, "{named}"); | ||
} | ||
// The same author's and rkey's issue in another space is not it. | ||
let other = "at://did:plc:x/space/dev.knotless.space/r/did:plc:a/dev.knotless.issue/1"; | ||
let body = format!("Fixes {other}"); | ||
let pull = item(&spaced, "pull", "open", "did:plc:b", Some(&body)); | ||
assert!(closes(&pull, &[issue, pull.clone()]).is_empty()); | ||
} | ||
} | ||
crates/knotless/src/forge.rs+11−8
| @@ -53,15 +53,11 @@ pub fn repo_uri(did: &str, name: &str) -> String { | ||
format!("at://{did}/{}/{name}", nsid::REPO) | ||
} | ||
| ||
/// An `at://did/collection/rkey` URI's parts. | ||
/// A record's author, collection and rkey, from its URI in either form | ||
/// ([`crate::aturi`]), its author by DID. | ||
pub fn parse_uri(uri: &str) -> Result<(&str, &str, &str)> { | ||
let parts: Vec<&str> = uri | ||
.strip_prefix("at://") | ||
.context("not an at:// URI")? | ||
.split('/') | ||
.collect(); | ||
match parts[..] { | ||
[did, collection, rkey] if did.starts_with("did:") => Ok((did, collection, rkey)), | ||
match crate::aturi::parts(uri)? { | ||
(did, collection, rkey) if did.starts_with("did:") => Ok((did, collection, rkey)), | ||
_ => bail!("{uri} is not at://<did>/<collection>/<rkey>"), | ||
} | ||
} | ||
| @@ -3289,6 +3285,13 @@ mod tests { | ||
assert!(parse_uri("https://x").is_err()); | ||
assert!(parse_uri("at://did:plc:a/c").is_err()); | ||
assert!(parse_uri("at://handle/c/r").is_err()); | ||
let spaced = "at://did:plc:a/space/dev.knotless.space/r/did:plc:b/dev.knotless.pull/p"; | ||
assert_eq!( | ||
parse_uri(spaced).unwrap(), | ||
("did:plc:b", "dev.knotless.pull", "p") | ||
); | ||
let repo = "at://did:plc:a/space/dev.knotless.space/r/did:plc:a/dev.knotless.repo/r"; | ||
assert_eq!(repo_url(repo).unwrap().name, "r"); | ||
} | ||
| ||
#[test] | ||
crates/knotless/src/keys.rs+12−3
| @@ -61,9 +61,9 @@ pub fn release_rkey(key: &str, package: &str, version: &str) -> String { | ||
} | ||
| ||
/// The label record an account keeps for `subject`, a pull's or issue's | ||
/// URI, on the repository `key`. | ||
/// URI, on the repository `key`: the same for either form of the URI. | ||
pub fn label_rkey(key: &str, subject: &str) -> String { | ||
format!("{key}.{}", ref_hash(subject)) | ||
format!("{key}.{}", ref_hash(&crate::aturi::short(subject))) | ||
} | ||
| ||
/// The claim a push creates to move `name` off generation `generation`. | ||
| @@ -81,7 +81,8 @@ pub fn check_prefix(owner: &str, key: &str) -> String { | ||
/// What the keys of every check of `name`, a ref or a pull's AT-URI, in | ||
/// `owner`'s repository `key` start with: a scan of one pull's checks. | ||
pub fn checks_of(owner: &str, key: &str, name: &str) -> String { | ||
format!("{}.{}", check_prefix(owner, key), ref_hash(name)) | ||
let name = crate::aturi::short(name); | ||
format!("{}.{}", check_prefix(owner, key), ref_hash(&name)) | ||
} | ||
| ||
/// The name a runner keys the checks of `branch`'s merge queue by, as a | ||
| @@ -163,6 +164,14 @@ mod tests { | ||
); | ||
let issue = "at://did:plc:a/dev.knotless.issue/3kabc"; | ||
assert_eq!(label_rkey(key, issue), format!("{key}.{}", ref_hash(issue))); | ||
// A record in a space is keyed as its public form is. | ||
let spaced = "at://did:plc:o/space/dev.knotless.space/r/did:plc:a/dev.knotless.issue/3kabc"; | ||
assert_eq!(label_rkey(key, spaced), label_rkey(key, issue)); | ||
assert_eq!(checks_of("o", key, spaced), checks_of("o", key, issue)); | ||
assert_ne!( | ||
checks_of("o", key, "refs/heads/main"), | ||
checks_of("o", key, issue) | ||
); | ||
let pack = pack_rkey(key); | ||
assert!(tid_micros(suffix(key, &pack).unwrap()).is_some()); | ||
assert_eq!(suffix(key, "other.x"), None); | ||
crates/knotless/src/lib.rs+1−0
| @@ -12,6 +12,7 @@ | ||
| ||
pub mod account; | ||
pub mod approvals; | ||
pub mod aturi; | ||
pub mod auth; | ||
pub mod base32; | ||
pub mod checks; | ||
crates/knotless/src/org.rs+1−1
| @@ -63,7 +63,7 @@ pub const GET_PROFILE: &str = "dev.knotless.getProfile"; | ||
| ||
/// The space `org` keeps its invitations and hidden members in. | ||
pub fn space_uri(org: &str) -> String { | ||
format!("at://{org}/space/{SPACE_TYPE}/{RKEY}") | ||
crate::aturi::SpaceRef::new(org, SPACE_TYPE, RKEY).to_string() | ||
} | ||
| ||
/// What a member may do. | ||
crates/knotless/src/records.rs+12−1
| @@ -445,8 +445,11 @@ impl RepoRef { | ||
} | ||
} | ||
| ||
/// The owner's DID, from the URI. | ||
/// The owner's DID, from the URI: its record's author, in a space too. | ||
pub fn did(&self) -> &str { | ||
if let Ok((author, _, _)) = crate::aturi::parts(&self.uri) { | ||
return author; | ||
} | ||
let rest = self.uri.strip_prefix("at://").unwrap_or_default(); | ||
rest.split('/').next().unwrap_or_default() | ||
} | ||
| @@ -1456,5 +1459,13 @@ mod tests { | ||
assert!(r.is("did:plc:a", "3kabc")); | ||
assert!(!r.is("did:plc:a", "3kxyz") && !r.is("did:plc:b", "3kabc")); | ||
assert_eq!(RepoRef::default().did(), ""); | ||
// Its record in a space is named for its author, not the space's. | ||
let spaced = RepoRef { | ||
uri: "at://did:plc:o/space/dev.knotless.space/proj/did:plc:a/dev.knotless.repo/proj" | ||
.into(), | ||
key: "3kabc".into(), | ||
}; | ||
assert_eq!((spaced.did(), spaced.name()), ("did:plc:a", "proj")); | ||
assert!(spaced.is("did:plc:a", "3kabc")); | ||
} | ||
} | ||
crates/knotless/src/space.rs+2−2
| @@ -37,7 +37,7 @@ pub const DELEGATION_HEADER: &str = "atproto-space-delegation"; | ||
| ||
/// The space holding `did`'s private repository `name`. | ||
pub fn uri(did: &str, name: &str) -> String { | ||
format!("at://{did}/space/{SPACE_TYPE}/{name}") | ||
crate::aturi::SpaceRef::new(did, SPACE_TYPE, name).to_string() | ||
} | ||
| ||
/// A space, and what lets requests read its owner's repo in it. Its clones | ||
| @@ -602,7 +602,7 @@ impl Admin<'_> { | ||
} | ||
Err(err) => return Err(err), | ||
}; | ||
let uri = format!("at://{}/space/{space_type}/{skey}", self.session.did); | ||
let uri = crate::aturi::SpaceRef::new(&self.session.did, space_type, skey).to_string(); | ||
Ok((uri, made)) | ||
} | ||
| ||
crates/knotless-serve/src/web/landing.rs+5−1
| @@ -451,7 +451,7 @@ pub async fn remove_runner( | ||
/// Names `input.runner` among the repository's runners, or with `add` | ||
/// false no longer, and answers with those it names after. | ||
async fn naming( | ||
app: &App, | ||
app: &Arc<App>, | ||
viewer: Viewer, | ||
headers: &HeaderMap, | ||
input: RunnerInput, | ||
| @@ -461,6 +461,10 @@ async fn naming( | ||
let url = url_of(&repo); | ||
let named = manage::set_runner(&ctx, &url, &input.runner, add).await; | ||
let warning = named.map_err(refused)?; | ||
// What waits for its checks reads the runners from its record as | ||
// synced: synced again as it is next read, as after any other write | ||
// from the pages. | ||
app.touch(&repo.did); | ||
let runners = runners(app, &ctx, &url).await?; | ||
Ok(runners_answer(app, &repo, runners, warning).await) | ||
} | ||