fix: make the workspace pass the CI lint/test gate
The new Gitea Actions build gate runs `cargo fmt --check`, `clippy -D warnings`, and `cargo test` — stricter than the old deploy.sh, which only `cargo build`d. That surfaced pre-existing drift that never compiled under the test/clippy profile: - apply rustfmt across the workspace (formatting only, no logic changes) - moments-data: add the missing `prune_events` to the test-only `NoopWriter` stub (the EventWriter trait gained it with the blog-prune feature; a plain `cargo build` never compiles the `#[cfg(test)]` stub, so it went stale) - moments-api: `.max().min()` -> `.clamp()`, and build `usvg::Options` with struct-update syntax instead of post-Default field assignment Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01X7zF7Kf4JqDwa6M8Qgge9M
This commit is contained in:
@@ -6,7 +6,10 @@ pub use sources::{EventSource, PollerState, PollerStateStore, SourceError, run_p
|
||||
|
||||
use async_trait::async_trait;
|
||||
use chrono::NaiveDate;
|
||||
use moments_entities::{DailyCount, Event, EventQuery, HourlyAvg, LanguageDailyCount, ProjectSummary, RepoLanguage, SourceSummary};
|
||||
use moments_entities::{
|
||||
DailyCount, Event, EventQuery, HourlyAvg, LanguageDailyCount, ProjectSummary, RepoLanguage,
|
||||
SourceSummary,
|
||||
};
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum StoreError {
|
||||
@@ -18,11 +21,30 @@ pub enum StoreError {
|
||||
#[async_trait]
|
||||
pub trait EventReader: Send + Sync {
|
||||
async fn list_events(&self, query: &EventQuery) -> Result<Vec<Event>, StoreError>;
|
||||
async fn source_summaries(&self, include_private: bool) -> Result<Vec<SourceSummary>, StoreError>;
|
||||
async fn source_summaries(
|
||||
&self,
|
||||
include_private: bool,
|
||||
) -> Result<Vec<SourceSummary>, StoreError>;
|
||||
async fn list_projects(&self) -> Result<Vec<ProjectSummary>, StoreError>;
|
||||
async fn daily_counts(&self, from: NaiveDate, to: NaiveDate, include_private: bool) -> Result<Vec<DailyCount>, StoreError>;
|
||||
async fn language_daily_counts(&self, from: NaiveDate, to: NaiveDate, include_private: bool) -> Result<Vec<LanguageDailyCount>, StoreError>;
|
||||
async fn hourly_avgs(&self, from: NaiveDate, to: NaiveDate, tz: &str, include_private: bool) -> Result<Vec<HourlyAvg>, StoreError>;
|
||||
async fn daily_counts(
|
||||
&self,
|
||||
from: NaiveDate,
|
||||
to: NaiveDate,
|
||||
include_private: bool,
|
||||
) -> Result<Vec<DailyCount>, StoreError>;
|
||||
async fn language_daily_counts(
|
||||
&self,
|
||||
from: NaiveDate,
|
||||
to: NaiveDate,
|
||||
include_private: bool,
|
||||
) -> Result<Vec<LanguageDailyCount>, StoreError>;
|
||||
async fn hourly_avgs(
|
||||
&self,
|
||||
from: NaiveDate,
|
||||
to: NaiveDate,
|
||||
tz: &str,
|
||||
include_private: bool,
|
||||
) -> Result<Vec<HourlyAvg>, StoreError>;
|
||||
async fn repo_languages(&self) -> Result<Vec<RepoLanguage>, StoreError>;
|
||||
}
|
||||
|
||||
@@ -34,5 +56,9 @@ pub trait EventWriter: Send + Sync {
|
||||
/// Delete events of `source` whose id is not in `keep_ids`. For sources
|
||||
/// whose upstream is authoritative for the full set (e.g. the blog repo),
|
||||
/// this reconciles deletes and renames that upserts alone never would.
|
||||
async fn prune_events(&self, source: moments_entities::Source, keep_ids: &[String]) -> Result<usize, StoreError>;
|
||||
async fn prune_events(
|
||||
&self,
|
||||
source: moments_entities::Source,
|
||||
keep_ids: &[String],
|
||||
) -> Result<usize, StoreError>;
|
||||
}
|
||||
|
||||
@@ -108,7 +108,8 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn excerpt_skips_headings_and_images() {
|
||||
let md = "# title\n\n\n\nfirst real paragraph\nwith a wrapped line\n\nsecond";
|
||||
let md =
|
||||
"# title\n\n\n\nfirst real paragraph\nwith a wrapped line\n\nsecond";
|
||||
assert_eq!(excerpt(md), "first real paragraph with a wrapped line");
|
||||
}
|
||||
|
||||
|
||||
@@ -73,7 +73,9 @@ mod tests {
|
||||
assert!(r.contains("filed bug #1158879 in mozilla.org"), "got: {r}");
|
||||
assert_eq!(
|
||||
item.subtitle.unwrap(),
|
||||
vec![TitleSegment::text("Commit Access (Level 1) for Rob Thijssen")]
|
||||
vec![TitleSegment::text(
|
||||
"Commit Access (Level 1) for Rob Thijssen"
|
||||
)]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,9 +40,7 @@ pub(crate) fn reshape(event: &Event) -> TimelineItem {
|
||||
"create_pull_request" => {
|
||||
pr_action("opened", TimelineIcon::PullRequest, host, repo, content)
|
||||
}
|
||||
"close_pull_request" => {
|
||||
pr_action("closed", TimelineIcon::PullRequest, host, repo, content)
|
||||
}
|
||||
"close_pull_request" => pr_action("closed", TimelineIcon::PullRequest, host, repo, content),
|
||||
"reopen_pull_request" => {
|
||||
pr_action("reopened", TimelineIcon::PullRequest, host, repo, content)
|
||||
}
|
||||
@@ -53,15 +51,13 @@ pub(crate) fn reshape(event: &Event) -> TimelineItem {
|
||||
"approve_pull_request" => {
|
||||
pr_action("approved", TimelineIcon::PullRequest, host, repo, content)
|
||||
}
|
||||
"reject_pull_request" => {
|
||||
pr_action(
|
||||
"requested changes on",
|
||||
TimelineIcon::PullRequest,
|
||||
host,
|
||||
repo,
|
||||
content,
|
||||
)
|
||||
}
|
||||
"reject_pull_request" => pr_action(
|
||||
"requested changes on",
|
||||
TimelineIcon::PullRequest,
|
||||
host,
|
||||
repo,
|
||||
content,
|
||||
),
|
||||
"publish_release" => publish_release(host, repo, content),
|
||||
_ => fallback(host, repo, &event.action),
|
||||
};
|
||||
@@ -303,8 +299,7 @@ fn pr_action(
|
||||
TitleSegment::text(" in "),
|
||||
repo_link(host, repo),
|
||||
];
|
||||
let subtitle =
|
||||
(!pr_title.is_empty()).then(|| vec![TitleSegment::text(pr_title.to_string())]);
|
||||
let subtitle = (!pr_title.is_empty()).then(|| vec![TitleSegment::text(pr_title.to_string())]);
|
||||
(icon, title, subtitle, None)
|
||||
}
|
||||
|
||||
@@ -352,8 +347,7 @@ fn comment_on_pr(
|
||||
TitleSegment::text(" in "),
|
||||
repo_link(host, repo),
|
||||
];
|
||||
let subtitle =
|
||||
(!pr_title.is_empty()).then(|| vec![TitleSegment::text(pr_title.to_string())]);
|
||||
let subtitle = (!pr_title.is_empty()).then(|| vec![TitleSegment::text(pr_title.to_string())]);
|
||||
let body = (!body_text.is_empty()).then(|| TimelineBody::Markdown {
|
||||
text: body_text.to_string(),
|
||||
});
|
||||
@@ -364,7 +358,10 @@ fn publish_release(host: &str, repo: Option<&str>, content: Option<&str>) -> Res
|
||||
let repo = repo.unwrap_or("(unknown repo)");
|
||||
let name = content.unwrap_or("");
|
||||
let title = if name.is_empty() {
|
||||
vec![TitleSegment::text("published a release in "), repo_link(host, repo)]
|
||||
vec![
|
||||
TitleSegment::text("published a release in "),
|
||||
repo_link(host, repo),
|
||||
]
|
||||
} else {
|
||||
vec![
|
||||
TitleSegment::text(format!("released {name} in ")),
|
||||
@@ -452,10 +449,7 @@ mod tests {
|
||||
let item = reshape(&ev("create_issue", raw));
|
||||
assert_eq!(item.icon, TimelineIcon::Issue);
|
||||
let r = render(&item);
|
||||
assert!(
|
||||
r.contains("opened issue #1 in grenade/moments"),
|
||||
"got: {r}"
|
||||
);
|
||||
assert!(r.contains("opened issue #1 in grenade/moments"), "got: {r}");
|
||||
assert_eq!(
|
||||
item.subtitle.unwrap(),
|
||||
vec![TitleSegment::text(
|
||||
|
||||
@@ -13,7 +13,10 @@ pub(crate) fn reshape(event: &Event) -> TimelineItem {
|
||||
}
|
||||
|
||||
let p = &event.payload;
|
||||
let repo_name = p.get("repo").and_then(|r| r.get("name")).and_then(Value::as_str);
|
||||
let repo_name = p
|
||||
.get("repo")
|
||||
.and_then(|r| r.get("name"))
|
||||
.and_then(Value::as_str);
|
||||
let actor_login = p
|
||||
.get("actor")
|
||||
.and_then(|a| a.get("display_login").or_else(|| a.get("login")))
|
||||
@@ -192,8 +195,14 @@ fn pull_request(repo: Option<&str>, p: Option<&Value>) -> Reshaped {
|
||||
.and_then(Value::as_str)
|
||||
.unwrap_or("touched");
|
||||
let pr = p.and_then(|v| v.get("pull_request"));
|
||||
let number = p.and_then(|v| v.get("number")).and_then(Value::as_i64).unwrap_or(0);
|
||||
let pr_title = pr.and_then(|v| v.get("title")).and_then(Value::as_str).unwrap_or("");
|
||||
let number = p
|
||||
.and_then(|v| v.get("number"))
|
||||
.and_then(Value::as_i64)
|
||||
.unwrap_or(0);
|
||||
let pr_title = pr
|
||||
.and_then(|v| v.get("title"))
|
||||
.and_then(Value::as_str)
|
||||
.unwrap_or("");
|
||||
let merged = pr
|
||||
.and_then(|v| v.get("merged"))
|
||||
.and_then(Value::as_bool)
|
||||
@@ -224,8 +233,14 @@ fn pull_request(repo: Option<&str>, p: Option<&Value>) -> Reshaped {
|
||||
fn pull_request_review(repo: Option<&str>, p: Option<&Value>) -> Reshaped {
|
||||
let repo = repo.unwrap_or("(unknown repo)");
|
||||
let pr = p.and_then(|v| v.get("pull_request"));
|
||||
let number = pr.and_then(|v| v.get("number")).and_then(Value::as_i64).unwrap_or(0);
|
||||
let pr_title = pr.and_then(|v| v.get("title")).and_then(Value::as_str).unwrap_or("");
|
||||
let number = pr
|
||||
.and_then(|v| v.get("number"))
|
||||
.and_then(Value::as_i64)
|
||||
.unwrap_or(0);
|
||||
let pr_title = pr
|
||||
.and_then(|v| v.get("title"))
|
||||
.and_then(Value::as_str)
|
||||
.unwrap_or("");
|
||||
let state = p
|
||||
.and_then(|v| v.get("review"))
|
||||
.and_then(|r| r.get("state"))
|
||||
@@ -245,8 +260,14 @@ fn pull_request_review(repo: Option<&str>, p: Option<&Value>) -> Reshaped {
|
||||
fn pull_request_review_comment(repo: Option<&str>, p: Option<&Value>) -> Reshaped {
|
||||
let repo = repo.unwrap_or("(unknown repo)");
|
||||
let pr = p.and_then(|v| v.get("pull_request"));
|
||||
let number = pr.and_then(|v| v.get("number")).and_then(Value::as_i64).unwrap_or(0);
|
||||
let pr_title = pr.and_then(|v| v.get("title")).and_then(Value::as_str).unwrap_or("");
|
||||
let number = pr
|
||||
.and_then(|v| v.get("number"))
|
||||
.and_then(Value::as_i64)
|
||||
.unwrap_or(0);
|
||||
let pr_title = pr
|
||||
.and_then(|v| v.get("title"))
|
||||
.and_then(Value::as_str)
|
||||
.unwrap_or("");
|
||||
let body_text = p
|
||||
.and_then(|v| v.get("comment"))
|
||||
.and_then(|c| c.get("body"))
|
||||
@@ -273,8 +294,14 @@ fn issues(repo: Option<&str>, p: Option<&Value>) -> Reshaped {
|
||||
.and_then(Value::as_str)
|
||||
.unwrap_or("touched");
|
||||
let issue = p.and_then(|v| v.get("issue"));
|
||||
let number = issue.and_then(|v| v.get("number")).and_then(Value::as_i64).unwrap_or(0);
|
||||
let issue_title = issue.and_then(|v| v.get("title")).and_then(Value::as_str).unwrap_or("");
|
||||
let number = issue
|
||||
.and_then(|v| v.get("number"))
|
||||
.and_then(Value::as_i64)
|
||||
.unwrap_or(0);
|
||||
let issue_title = issue
|
||||
.and_then(|v| v.get("title"))
|
||||
.and_then(Value::as_str)
|
||||
.unwrap_or("");
|
||||
|
||||
let title = vec![
|
||||
TitleSegment::text(format!("{action} issue ")),
|
||||
@@ -282,15 +309,22 @@ fn issues(repo: Option<&str>, p: Option<&Value>) -> Reshaped {
|
||||
TitleSegment::text(" in "),
|
||||
repo_link(repo),
|
||||
];
|
||||
let subtitle = (!issue_title.is_empty()).then(|| vec![TitleSegment::text(issue_title.to_string())]);
|
||||
let subtitle =
|
||||
(!issue_title.is_empty()).then(|| vec![TitleSegment::text(issue_title.to_string())]);
|
||||
(TimelineIcon::Issue, title, subtitle, None)
|
||||
}
|
||||
|
||||
fn issue_comment(repo: Option<&str>, p: Option<&Value>) -> Reshaped {
|
||||
let repo = repo.unwrap_or("(unknown repo)");
|
||||
let issue = p.and_then(|v| v.get("issue"));
|
||||
let number = issue.and_then(|v| v.get("number")).and_then(Value::as_i64).unwrap_or(0);
|
||||
let issue_title = issue.and_then(|v| v.get("title")).and_then(Value::as_str).unwrap_or("");
|
||||
let number = issue
|
||||
.and_then(|v| v.get("number"))
|
||||
.and_then(Value::as_i64)
|
||||
.unwrap_or(0);
|
||||
let issue_title = issue
|
||||
.and_then(|v| v.get("title"))
|
||||
.and_then(Value::as_str)
|
||||
.unwrap_or("");
|
||||
let body_text = p
|
||||
.and_then(|v| v.get("comment"))
|
||||
.and_then(|c| c.get("body"))
|
||||
@@ -303,7 +337,8 @@ fn issue_comment(repo: Option<&str>, p: Option<&Value>) -> Reshaped {
|
||||
TitleSegment::text(" in "),
|
||||
repo_link(repo),
|
||||
];
|
||||
let subtitle = (!issue_title.is_empty()).then(|| vec![TitleSegment::text(issue_title.to_string())]);
|
||||
let subtitle =
|
||||
(!issue_title.is_empty()).then(|| vec![TitleSegment::text(issue_title.to_string())]);
|
||||
let body = (!body_text.is_empty()).then(|| TimelineBody::Markdown {
|
||||
text: body_text.to_string(),
|
||||
});
|
||||
@@ -312,7 +347,10 @@ fn issue_comment(repo: Option<&str>, p: Option<&Value>) -> Reshaped {
|
||||
|
||||
fn create(repo: Option<&str>, p: Option<&Value>) -> Reshaped {
|
||||
let repo = repo.unwrap_or("(unknown repo)");
|
||||
let ref_type = p.and_then(|v| v.get("ref_type")).and_then(Value::as_str).unwrap_or("ref");
|
||||
let ref_type = p
|
||||
.and_then(|v| v.get("ref_type"))
|
||||
.and_then(Value::as_str)
|
||||
.unwrap_or("ref");
|
||||
let ref_name = p.and_then(|v| v.get("ref")).and_then(Value::as_str);
|
||||
|
||||
let mut title = vec![TitleSegment::text(format!("created {ref_type} "))];
|
||||
@@ -327,8 +365,14 @@ fn create(repo: Option<&str>, p: Option<&Value>) -> Reshaped {
|
||||
|
||||
fn delete(repo: Option<&str>, p: Option<&Value>) -> Reshaped {
|
||||
let repo = repo.unwrap_or("(unknown repo)");
|
||||
let ref_type = p.and_then(|v| v.get("ref_type")).and_then(Value::as_str).unwrap_or("ref");
|
||||
let ref_name = p.and_then(|v| v.get("ref")).and_then(Value::as_str).unwrap_or("");
|
||||
let ref_type = p
|
||||
.and_then(|v| v.get("ref_type"))
|
||||
.and_then(Value::as_str)
|
||||
.unwrap_or("ref");
|
||||
let ref_name = p
|
||||
.and_then(|v| v.get("ref"))
|
||||
.and_then(Value::as_str)
|
||||
.unwrap_or("");
|
||||
|
||||
let title = vec![
|
||||
TitleSegment::text(format!("deleted {ref_type} {ref_name} in ")),
|
||||
@@ -340,7 +384,9 @@ fn delete(repo: Option<&str>, p: Option<&Value>) -> Reshaped {
|
||||
fn fork(repo: Option<&str>, p: Option<&Value>) -> Reshaped {
|
||||
let repo = repo.unwrap_or("(unknown repo)");
|
||||
let forkee = p.and_then(|v| v.get("forkee"));
|
||||
let forkee_full = forkee.and_then(|f| f.get("full_name")).and_then(Value::as_str);
|
||||
let forkee_full = forkee
|
||||
.and_then(|f| f.get("full_name"))
|
||||
.and_then(Value::as_str);
|
||||
|
||||
let mut title = vec![TitleSegment::text("forked "), repo_link(repo)];
|
||||
if let Some(full) = forkee_full {
|
||||
@@ -366,7 +412,9 @@ fn release(repo: Option<&str>, p: Option<&Value>) -> Reshaped {
|
||||
.and_then(|r| r.get("name").or_else(|| r.get("tag_name")))
|
||||
.and_then(Value::as_str)
|
||||
.unwrap_or("(release)");
|
||||
let url = release.and_then(|r| r.get("html_url")).and_then(Value::as_str);
|
||||
let url = release
|
||||
.and_then(|r| r.get("html_url"))
|
||||
.and_then(Value::as_str);
|
||||
|
||||
let label = if let Some(u) = url {
|
||||
TitleSegment::link(name.to_string(), u.to_string())
|
||||
@@ -389,7 +437,10 @@ fn commit_comment(repo: Option<&str>, p: Option<&Value>) -> Reshaped {
|
||||
.and_then(|c| c.get("body"))
|
||||
.and_then(Value::as_str)
|
||||
.unwrap_or("");
|
||||
let title = vec![TitleSegment::text("commented on a commit in "), repo_link(repo)];
|
||||
let title = vec![
|
||||
TitleSegment::text("commented on a commit in "),
|
||||
repo_link(repo),
|
||||
];
|
||||
let body = (!body_text.is_empty()).then(|| TimelineBody::Markdown {
|
||||
text: body_text.to_string(),
|
||||
});
|
||||
@@ -398,7 +449,11 @@ fn commit_comment(repo: Option<&str>, p: Option<&Value>) -> Reshaped {
|
||||
|
||||
fn public(repo: Option<&str>) -> Reshaped {
|
||||
let repo = repo.unwrap_or("(unknown repo)");
|
||||
let title = vec![TitleSegment::text("made "), repo_link(repo), TitleSegment::text(" public")];
|
||||
let title = vec![
|
||||
TitleSegment::text("made "),
|
||||
repo_link(repo),
|
||||
TitleSegment::text(" public"),
|
||||
];
|
||||
(TimelineIcon::Generic, title, None, None)
|
||||
}
|
||||
|
||||
@@ -444,11 +499,15 @@ fn search_reshape(event: &Event) -> TimelineItem {
|
||||
title.push(TitleSegment::text(" "));
|
||||
}
|
||||
title.push(TitleSegment::text(format!("{verb} {kind} ")));
|
||||
title.push(TitleSegment::link(format!("#{number}"), html_url.to_string()));
|
||||
title.push(TitleSegment::link(
|
||||
format!("#{number}"),
|
||||
html_url.to_string(),
|
||||
));
|
||||
title.push(TitleSegment::text(" in "));
|
||||
title.push(repo_link(&repo));
|
||||
|
||||
let subtitle = (!issue_title.is_empty()).then(|| vec![TitleSegment::text(issue_title.to_string())]);
|
||||
let subtitle =
|
||||
(!issue_title.is_empty()).then(|| vec![TitleSegment::text(issue_title.to_string())]);
|
||||
|
||||
TimelineItem {
|
||||
id: event.id.clone(),
|
||||
@@ -500,8 +559,8 @@ fn commit_reshape(event: &Event) -> TimelineItem {
|
||||
title.push(TitleSegment::text(" in "));
|
||||
title.push(repo_link(repo));
|
||||
|
||||
let subtitle = (!message_first_line.is_empty())
|
||||
.then(|| vec![TitleSegment::text(message_first_line)]);
|
||||
let subtitle =
|
||||
(!message_first_line.is_empty()).then(|| vec![TitleSegment::text(message_first_line)]);
|
||||
|
||||
TimelineItem {
|
||||
id: event.id.clone(),
|
||||
@@ -525,10 +584,7 @@ fn repo_from_url(url: &str) -> Option<String> {
|
||||
|
||||
fn fallback(repo: Option<&str>, action: &str) -> Reshaped {
|
||||
let title = match repo {
|
||||
Some(r) => vec![
|
||||
TitleSegment::text(format!("{action} on ")),
|
||||
repo_link(r),
|
||||
],
|
||||
Some(r) => vec![TitleSegment::text(format!("{action} on ")), repo_link(r)],
|
||||
None => vec![TitleSegment::text(action.to_string())],
|
||||
};
|
||||
(TimelineIcon::Generic, title, None, None)
|
||||
@@ -578,7 +634,10 @@ mod tests {
|
||||
TitleSegment::Link { text, .. } => text.clone(),
|
||||
})
|
||||
.collect();
|
||||
assert!(rendered.contains("pushed 2 commits to grenade/vortex:main"), "got: {rendered}");
|
||||
assert!(
|
||||
rendered.contains("pushed 2 commits to grenade/vortex:main"),
|
||||
"got: {rendered}"
|
||||
);
|
||||
match item.body.unwrap() {
|
||||
TimelineBody::Commits { commits } => {
|
||||
assert_eq!(commits.len(), 2);
|
||||
@@ -640,7 +699,10 @@ mod tests {
|
||||
let item = reshape(&ev("PushEvent", raw));
|
||||
assert_eq!(item.icon, TimelineIcon::GitPush);
|
||||
let r = render(&item);
|
||||
assert!(r.contains("force-pushed 1 commit to grenade/x:main"), "got: {r}");
|
||||
assert!(
|
||||
r.contains("force-pushed 1 commit to grenade/x:main"),
|
||||
"got: {r}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -784,11 +846,16 @@ mod tests {
|
||||
TitleSegment::Link { text, .. } => text.clone(),
|
||||
})
|
||||
.collect();
|
||||
assert!(rendered.contains("committed a6fcefb in faith1337z/Trade"), "got: {rendered}");
|
||||
assert!(
|
||||
rendered.contains("committed a6fcefb in faith1337z/Trade"),
|
||||
"got: {rendered}"
|
||||
);
|
||||
// body of the commit message is dropped; only first line in subtitle
|
||||
assert_eq!(
|
||||
item.subtitle.unwrap(),
|
||||
vec![TitleSegment::text("split multiline message into multiple irc messages")]
|
||||
vec![TitleSegment::text(
|
||||
"split multiline message into multiple irc messages"
|
||||
)]
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -23,10 +23,7 @@ pub(crate) fn reshape(event: &Event) -> TimelineItem {
|
||||
.next()
|
||||
.unwrap_or("")
|
||||
.to_string();
|
||||
let author = p
|
||||
.get("author")
|
||||
.and_then(Value::as_str)
|
||||
.map(author_name);
|
||||
let author = p.get("author").and_then(Value::as_str).map(author_name);
|
||||
|
||||
let mut title = Vec::new();
|
||||
if let Some(name) = author {
|
||||
|
||||
Reference in New Issue
Block a user