Radish alpha
h
rad:z3gqcJUoA1n9HaHKufZs5FCSGazv5
Radicle Heartwood Protocol & Stack
Radicle
Git
httpd: Allow fetching of patches or issues without defining status
Open did:key:z6MkkfM3...sVz5 opened 2 years ago

Renames the state query variable into status since we define it as e.g. status: "draft" And we allow consumers to get all the issues without querying for a specific status.

4 files changed +137 -65 bd8e0ebc 583592bc
modified radicle-httpd/src/api.rs
@@ -17,8 +17,6 @@ use serde_json::json;
use tokio::sync::RwLock;
use tower_http::cors::{self, CorsLayer};

-
use radicle::cob::issue;
-
use radicle::cob::patch;
use radicle::identity::{DocAt, RepoId};
use radicle::node::policy::Scope;
use radicle::node::routing::Store;
@@ -167,7 +165,7 @@ pub struct RawQuery {
pub struct CobsQuery<T> {
    pub page: Option<usize>,
    pub per_page: Option<usize>,
-
    pub state: Option<T>,
+
    pub status: Option<T>,
}

#[derive(Serialize, Deserialize, Clone)]
@@ -178,44 +176,6 @@ pub struct PoliciesQuery {
    pub scope: Option<Scope>,
}

-
#[derive(Default, Serialize, Deserialize, Clone)]
-
#[serde(rename_all = "camelCase")]
-
pub enum IssueState {
-
    Closed,
-
    #[default]
-
    Open,
-
}
-

-
impl IssueState {
-
    pub fn matches(&self, issue: &issue::State) -> bool {
-
        match self {
-
            Self::Open => matches!(issue, issue::State::Open),
-
            Self::Closed => matches!(issue, issue::State::Closed { .. }),
-
        }
-
    }
-
}
-

-
#[derive(Default, Serialize, Deserialize, Clone)]
-
#[serde(rename_all = "camelCase")]
-
pub enum PatchState {
-
    #[default]
-
    Open,
-
    Draft,
-
    Archived,
-
    Merged,
-
}
-

-
impl PatchState {
-
    pub fn matches(&self, patch: &patch::State) -> bool {
-
        match self {
-
            Self::Open => matches!(patch, patch::State::Open { .. }),
-
            Self::Draft => matches!(patch, patch::State::Draft),
-
            Self::Archived => matches!(patch, patch::State::Archived),
-
            Self::Merged => matches!(patch, patch::State::Merged { .. }),
-
        }
-
    }
-
}
-

mod project {
    use nonempty::NonEmpty;
    use serde::Serialize;
modified radicle-httpd/src/api/v1/projects.rs
@@ -580,26 +580,25 @@ async fn readme_handler(
async fn issues_handler(
    State(ctx): State<Context>,
    Path(project): Path<RepoId>,
-
    Query(qs): Query<CobsQuery<api::IssueState>>,
+
    Query(qs): Query<CobsQuery<issue::State>>,
) -> impl IntoResponse {
    let (repo, _) = ctx.repo(project)?;
    let CobsQuery {
        page,
        per_page,
-
        state,
+
        status,
    } = qs;
    let page = page.unwrap_or(0);
    let per_page = per_page.unwrap_or(10);
-
    let state = state.unwrap_or_default();
    let issues = ctx.profile.issues(&repo)?;
-
    let mut issues: Vec<_> = issues
-
        .list()?
-
        .filter_map(|r| {
-
            let (id, issue) = r.ok()?;
-
            (state.matches(issue.state())).then_some((id, issue))
-
        })
-
        .collect::<Vec<_>>();
-

+
    let mut issues = if let Some(status) = status {
+
        issues
+
            .list_by_status(&status)?
+
            .filter_map(Result::ok)
+
            .collect::<Vec<_>>()
+
    } else {
+
        issues.list()?.filter_map(Result::ok).collect::<Vec<_>>()
+
    };
    issues.sort_by(|(_, a), (_, b)| b.timestamp().cmp(&a.timestamp()));
    let aliases = &ctx.profile.aliases();
    let issues = issues
@@ -932,26 +931,26 @@ async fn patch_update_handler(
/// `GET /projects/:project/patches`
async fn patches_handler(
    State(ctx): State<Context>,
-
    Path(rid): Path<RepoId>,
-
    Query(qs): Query<CobsQuery<api::PatchState>>,
+
    Path(project): Path<RepoId>,
+
    Query(qs): Query<CobsQuery<patch::Status>>,
) -> impl IntoResponse {
-
    let (repo, _) = ctx.repo(rid)?;
+
    let (repo, _) = ctx.repo(project)?;
    let CobsQuery {
        page,
        per_page,
-
        state,
+
        status,
    } = qs;
    let page = page.unwrap_or(0);
    let per_page = per_page.unwrap_or(10);
-
    let state = state.unwrap_or_default();
    let patches = ctx.profile.patches(&repo)?;
-
    let mut patches = patches
-
        .list()?
-
        .filter_map(|r| {
-
            let (id, patch) = r.ok()?;
-
            (state.matches(patch.state())).then_some((id, patch))
-
        })
-
        .collect::<Vec<_>>();
+
    let mut patches = if let Some(status) = status {
+
        patches
+
            .list_by_status(&status)?
+
            .filter_map(Result::ok)
+
            .collect::<Vec<_>>()
+
    } else {
+
        patches.list()?.filter_map(Result::ok).collect::<Vec<_>>()
+
    };
    patches.sort_by(|(_, a), (_, b)| b.timestamp().cmp(&a.timestamp()));
    let aliases = ctx.profile.aliases();
    let patches = patches
modified radicle/src/cob/issue/cache.rs
@@ -32,6 +32,10 @@ pub trait Issues {
    /// List all issues that are in the store.
    fn list(&self) -> Result<Self::Iter<'_>, Self::Error>;

+
    /// List all issues in the store that match the provided
+
    /// `status`.
+
    fn list_by_status(&self, status: &State) -> Result<Self::Iter<'_>, Self::Error>;
+

    /// Get the [`IssueCounts`] of all the issues in the store.
    fn counts(&self) -> Result<IssueCounts, Self::Error>;

@@ -355,6 +359,23 @@ where
            .map_err(super::Error::from)
    }

+
    fn list_by_status(&self, status: &State) -> Result<Self::Iter<'_>, Self::Error> {
+
        let status = *status;
+
        self.store
+
            .all()
+
            .map(move |inner| NoCacheIter {
+
                inner: Box::new(inner.into_iter().filter_map(move |res| {
+
                    match res {
+
                        Ok((id, issue)) => (status == State::from(issue.state))
+
                            .then_some((id, issue))
+
                            .map(Ok),
+
                        Err(e) => Some(Err(e.into())),
+
                    }
+
                })),
+
            })
+
            .map_err(super::Error::from)
+
    }
+

    fn counts(&self) -> Result<IssueCounts, Self::Error> {
        self.store.counts().map_err(super::Error::from)
    }
@@ -412,6 +433,10 @@ where
        query::list(&self.cache.db, &self.rid())
    }

+
    fn list_by_status(&self, status: &State) -> Result<Self::Iter<'_>, Self::Error> {
+
        query::list_by_status(&self.cache.db, &self.rid(), status)
+
    }
+

    fn counts(&self) -> Result<IssueCounts, Self::Error> {
        query::counts(&self.cache.db, &self.rid())
    }
@@ -432,6 +457,10 @@ where
        query::list(&self.cache.db, &self.rid())
    }

+
    fn list_by_status(&self, status: &State) -> Result<Self::Iter<'_>, Self::Error> {
+
        query::list_by_status(&self.cache.db, &self.rid(), status)
+
    }
+

    fn counts(&self) -> Result<IssueCounts, Self::Error> {
        query::counts(&self.cache.db, &self.rid())
    }
@@ -484,6 +513,26 @@ mod query {
        })
    }

+
    pub(super) fn list_by_status<'a>(
+
        db: &'a sql::ConnectionThreadSafe,
+
        rid: &RepoId,
+
        filter: &State,
+
    ) -> Result<IssuesIter<'a>, Error> {
+
        let mut stmt = db.prepare(
+
            "SELECT id, issue
+
             FROM issues
+
             WHERE repo = ?1
+
             AND issue->>'$.state.status' = ?2
+
             ORDER BY id
+
            ",
+
        )?;
+
        stmt.bind((1, rid))?;
+
        stmt.bind((2, sql::Value::String(filter.to_string())))?;
+
        Ok(IssuesIter {
+
            inner: stmt.into_iter(),
+
        })
+
    }
+

    pub(super) fn counts(
        db: &sql::ConnectionThreadSafe,
        rid: &RepoId,
@@ -661,6 +710,69 @@ mod tests {
        assert_eq!(issues, list);
    }

+
    fn create_random_issue_list(state: State) -> Vec<(ObjectId, Issue)> {
+
        let ids = (0..arbitrary::gen::<u8>(1))
+
            .map(|_| IssueId::from(arbitrary::oid()))
+
            .collect::<BTreeSet<IssueId>>();
+

+
        ids.into_iter()
+
            .map(|id| {
+
                (
+
                    id,
+
                    Issue {
+
                        title: id.to_string(),
+
                        state,
+
                        ..Issue::new(Thread::default())
+
                    },
+
                )
+
            })
+
            .collect::<Vec<_>>()
+
    }
+

+
    #[test]
+
    fn test_list_by_status() {
+
        let repo = arbitrary::gen::<MockRepository>(1);
+
        let mut cache = memory(repo);
+
        let issues =
+
            create_random_issue_list(State::Open)
+
                .into_iter()
+
                .zip(create_random_issue_list(State::Closed {
+
                    reason: CloseReason::Solved,
+
                }));
+

+
        issues
+
            .clone()
+
            .for_each(|((open_id, open_issue), (closed_id, closed_issue))| {
+
                let rid = cache.rid();
+
                cache.update(&rid, &open_id, &open_issue).unwrap();
+
                cache.update(&rid, &closed_id, &closed_issue).unwrap();
+
            });
+

+
        let mut open_list = cache
+
            .list_by_status(&State::Open)
+
            .unwrap()
+
            .collect::<Result<Vec<_>, _>>()
+
            .unwrap();
+
        let mut closed_list = cache
+
            .list_by_status(&State::Closed {
+
                reason: CloseReason::Solved,
+
            })
+
            .unwrap()
+
            .collect::<Result<Vec<_>, _>>()
+
            .unwrap();
+

+
        let (mut open_issues, mut closed_issues): (Vec<(ObjectId, Issue)>, Vec<(ObjectId, Issue)>) =
+
            issues.unzip();
+

+
        open_list.sort_by_key(|(id, _)| *id);
+
        open_issues.sort_by_key(|(id, _)| *id);
+
        assert_eq!(open_list, open_issues);
+

+
        closed_list.sort_by_key(|(id, _)| *id);
+
        closed_issues.sort_by_key(|(id, _)| *id);
+
        assert_eq!(closed_list, closed_issues);
+
    }
+

    #[test]
    fn test_remove() {
        let repo = arbitrary::gen::<MockRepository>(1);
modified radicle/src/cob/patch.rs
@@ -1488,7 +1488,8 @@ impl From<&State> for Status {

/// A simplified enumeration of a [`State`] that can be used for
/// filtering purposes.
-
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
+
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
+
#[serde(rename_all = "camelCase", tag = "status")]
pub enum Status {
    Draft,
    #[default]