@jpgilldev / steid

1//! JSON API.
2//!
3//! Not a milestone of its own — every use case gets a second surface here where that
4//! makes sense. Having two consumers is what keeps the application layer honest about
5//! staying transport-neutral.
6
7use serde::Serialize;
8use topcoat::{
9 Result,
10 context::Cx,
11 router::{
12 content::Json,
13 error::{RouterErrorExt, unauthorized},
14 route,
15 },
16};
17
18use crate::application::list_repos;
19
20use super::{
21 context::{current_actor, identity, memberships, orgs, repos, server_error},
22 profile::handle_param,
23};
24
25/// The authenticated user, as JSON.
26///
27/// A DTO rather than the domain type: serialisation is a wire concern, and deriving
28/// `Serialize` on domain entities lets a field leak into a response the moment someone
29/// adds one.
30#[derive(Debug, Serialize)]
31struct Me {
32 id: String,
33 email: String,
34 handle: String,
35}
36
37/// Who the caller is.
38///
39/// 401 when anonymous, matching the convention that `/me` describes an authenticated
40/// caller and has nothing to say without one.
41#[route(GET "/api/me")]
42async fn me(cx: &Cx) -> Result<Json<Me>> {
43 let identity = identity(cx).await?.ok_or_else(unauthorized)?;
44
45 Ok(Json(Me {
46 id: identity.user_id.to_string(),
47 email: identity.email.to_string(),
48 handle: identity.handle.to_string(),
49 }))
50}
51
52/// A profile, as JSON, for anyone.
53///
54/// Mirrors `PublicProfile` exactly. There is no email field here and there must never
55/// be one — this is served to anonymous callers.
56#[derive(Debug, Serialize)]
57struct Profile {
58 handle: String,
59 label: String,
60 bio: Option<String>,
61 viewer_is_owner: bool,
62}
63
64#[route(GET "/api/users/{handle}")]
65async fn user(cx: &Cx) -> Result<Json<Profile>> {
66 let profile = super::profile::profile_for(cx).await?;
67
68 Ok(Json(Profile {
69 handle: profile.handle.to_string(),
70 label: profile.label,
71 bio: profile.bio,
72 viewer_is_owner: profile.viewer_is_owner,
73 }))
74}
75
76/// A repository in a listing, as JSON.
77///
78/// Mirrors `RepoSummary`. `visibility` is rendered through `Visibility::as_str` rather
79/// than a derived `Serialize` on the domain enum — the wire spelling is this layer's
80/// promise to keep, not something a domain refactor should be free to rename.
81#[derive(Debug, Serialize)]
82struct Repo {
83 name: String,
84 description: Option<String>,
85 visibility: String,
86}
87
88/// The repositories under a handle that the caller may see.
89///
90/// 404 for a handle that does not exist; `[]` for one whose repositories the caller
91/// may not see. That difference is the whole reason `list_repos` answers with an
92/// `Option` — a private repository has to be absent, and absent has to look the same
93/// as owning nothing.
94#[route(GET "/api/users/{handle}/repos")]
95async fn user_repos(cx: &Cx) -> Result<Json<Vec<Repo>>> {
96 let handle = handle_param(cx)?;
97 let actor = current_actor(cx).await?;
98
99 let listed = list_repos(&handle, &actor, &orgs(cx), &memberships(cx), &repos(cx))
100 .await
101 .map_err(server_error)?
102 .ok_or_not_found()?;
103
104 Ok(Json(
105 listed
106 .into_iter()
107 .map(|repo| Repo {
108 name: repo.name.to_string(),
109 description: repo.description,
110 visibility: repo.visibility.as_str().to_owned(),
111 })
112 .collect(),
113 ))
114}