~ knotless Search/
↑↓ move↵ open⇧↵ all results

chadtmiller.com/knotless

git that lives in your Atmosphere account

knotless.dev
1 collaborator

knotless 0.13.0 Latest 2 packages, 1 image

1
10

Records in a space are read by their URI in full, as proposal 0016 names them

Merged chadtmiller.com asked to merge into main from chadtmiller.com/knotless:space/aturi
.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)
}
crates/knotless-serve/src/web/preview.rs+3−5
crates/knotless-serve/src/web/repo.rs+3−5
crates/knotless-serve/src/web/steps.rs+4−4
docs/design/access.md+9−0