pr_01m47d15m3e54sn21z27rpy5n9/crates/contracts/src/search.rs

345 lines10,885 bytesCodeBlame
1//! The search service: one search across all of g1t, and Explore.
2//!
3//! It covers everything public, and private content the viewer is a member
4//! of: repositories (name, description, topics, README), code on default
5//! branches, issues and pull requests, people and workspaces. Who may see
6//! what is decided when the query runs, against the viewer's current
7//! memberships and each repository's current visibility, never only when
8//! something was indexed.
9//!
10//! Each `*Args` struct is the argument of the method of the same name,
11//! served at `POST /rpc/<method>`. Mirrors `packages/contracts/src/search.ts`.
12
13use serde::{Deserialize, Serialize};
14
15use crate::Viewer;
16
17/// What a search looks through: one tab of results.
18#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
19#[serde(rename_all = "lowercase")]
20pub enum SearchType {
21 Repositories,
22 Code,
23 Issues,
24 Pulls,
25 People,
26}
27
28impl SearchType {
29 pub const ALL: [SearchType; 5] = [
30 SearchType::Repositories,
31 SearchType::Code,
32 SearchType::Issues,
33 SearchType::Pulls,
34 SearchType::People,
35 ];
36
37 pub fn as_str(self) -> &'static str {
38 match self {
39 SearchType::Repositories => "repositories",
40 SearchType::Code => "code",
41 SearchType::Issues => "issues",
42 SearchType::Pulls => "pulls",
43 SearchType::People => "people",
44 }
45 }
46
47 /// Reads a type as people write it: `repositories`, `repos`, `code`,
48 /// `issues`, `pulls`, `prs`, `people`, `users`.
49 pub fn parse(text: &str) -> Option<SearchType> {
50 match text.trim().to_lowercase().as_str() {
51 "repositories" | "repository" | "repos" | "repo" | "projects" | "project" => Some(SearchType::Repositories),
52 "code" => Some(SearchType::Code),
53 "issues" | "issue" => Some(SearchType::Issues),
54 "pulls" | "pull" | "prs" | "pr" | "pull_requests" | "pull-requests" => Some(SearchType::Pulls),
55 "people" | "users" | "user" | "workspaces" | "workspace" => Some(SearchType::People),
56 _ => None,
57 }
58 }
59}
60
61/// The most results on one page.
62pub const MAX_PER_PAGE: u32 = 50;
63/// Results on a page when none is asked for.
64pub const DEFAULT_PER_PAGE: u32 = 20;
65/// Counts stop here: past it a tab says "1,000+".
66pub const COUNT_CAP: u32 = 1000;
67/// The longest query read; the rest is ignored.
68pub const MAX_QUERY_CHARS: usize = 256;
69
70/// `search`. Returns `Outcome<SearchResults>`.
71#[derive(Debug, Default, Serialize, Deserialize)]
72#[serde(rename_all = "camelCase")]
73pub struct SearchArgs {
74 pub viewer: Viewer,
75 /// Words, quoted phrases and qualifiers, such as
76 /// `parse_query repo:acme/web language:rust`.
77 pub query: String,
78 /// Which tab of results. Worked out from the qualifiers when absent:
79 /// `path:` means code, `is:pr` pull requests, and so on.
80 #[serde(default, rename = "type")]
81 pub kind: Option<SearchType>,
82 /// From 1.
83 #[serde(default)]
84 pub page: Option<u32>,
85 #[serde(default)]
86 pub per_page: Option<u32>,
87}
88
89/// One piece of a snippet, highlighted where it matched the query.
90#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
91pub struct Segment {
92 pub text: String,
93 #[serde(default)]
94 pub highlight: bool,
95}
96
97/// A line of code in a result, numbered from 1.
98#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
99pub struct CodeLine {
100 pub number: u32,
101 pub parts: Vec<Segment>,
102}
103
104/// What a result is.
105#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
106#[serde(rename_all = "lowercase")]
107pub enum HitKind {
108 Repository,
109 Code,
110 Issue,
111 Pull,
112 User,
113 Workspace,
114}
115
116/// One result. Which fields are set depends on `kind`.
117#[derive(Clone, Debug, Serialize, Deserialize)]
118#[serde(rename_all = "camelCase")]
119pub struct Hit {
120 pub kind: HitKind,
121 /// What to show as the result's name: `acme/web`, a file's path,
122 /// `Fix the login redirect`, a person's name.
123 pub title: String,
124 /// Where it is on g1t.sh, as a path: `/acme/web/blob/main/src/app.rs#L12`.
125 pub url: String,
126 /// The repository, as `owner/name`, for everything but people.
127 #[serde(default)]
128 pub repo: Option<String>,
129 /// Whether only members of its workspace can see it.
130 #[serde(default)]
131 pub private: bool,
132 /// A repository's or workspace's description, a person's bio.
133 #[serde(default)]
134 pub description: Option<String>,
135 /// The part of the text that matched, highlighted. Empty for code.
136 #[serde(default)]
137 pub snippet: Vec<Segment>,
138 /// For code: the lines that matched, with a line around each.
139 #[serde(default)]
140 pub lines: Vec<CodeLine>,
141 /// For code: the file's path in its repository.
142 #[serde(default)]
143 pub path: Option<String>,
144 /// For code: the file's language. For a repository: the language most
145 /// of its code is in.
146 #[serde(default)]
147 pub language: Option<String>,
148 /// For code: the branch it was read from.
149 #[serde(default, rename = "ref")]
150 pub git_ref: Option<String>,
151 /// For issues and pull requests.
152 #[serde(default)]
153 pub number: Option<u32>,
154 /// `open` or `closed` for an issue; `draft`, `open`, `merged` or
155 /// `closed` for a pull request.
156 #[serde(default)]
157 pub state: Option<String>,
158 /// Who opened the issue or pull request.
159 #[serde(default)]
160 pub author: Option<String>,
161 #[serde(default)]
162 pub labels: Vec<String>,
163 /// A repository's topics.
164 #[serde(default)]
165 pub topics: Vec<String>,
166 /// A user's username or a workspace's slug.
167 #[serde(default)]
168 pub slug: Option<String>,
169 /// The uploaded avatar's hash, for people and workspaces.
170 #[serde(default)]
171 pub avatar: Option<String>,
172 /// RFC 3339: when it last changed, or a repository's last push.
173 #[serde(default)]
174 pub updated_at: Option<String>,
175}
176
177impl Hit {
178 pub fn new(kind: HitKind, title: impl Into<String>, url: impl Into<String>) -> Hit {
179 Hit {
180 kind,
181 title: title.into(),
182 url: url.into(),
183 repo: None,
184 private: false,
185 description: None,
186 snippet: Vec::new(),
187 lines: Vec::new(),
188 path: None,
189 language: None,
190 git_ref: None,
191 number: None,
192 state: None,
193 author: None,
194 labels: Vec::new(),
195 topics: Vec::new(),
196 slug: None,
197 avatar: None,
198 updated_at: None,
199 }
200 }
201}
202
203/// How many results each tab has, up to [`COUNT_CAP`].
204#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
205pub struct SearchCounts {
206 pub repositories: u32,
207 pub code: u32,
208 pub issues: u32,
209 pub pulls: u32,
210 pub people: u32,
211}
212
213impl SearchCounts {
214 pub fn get(&self, kind: SearchType) -> u32 {
215 match kind {
216 SearchType::Repositories => self.repositories,
217 SearchType::Code => self.code,
218 SearchType::Issues => self.issues,
219 SearchType::Pulls => self.pulls,
220 SearchType::People => self.people,
221 }
222 }
223
224 pub fn set(&mut self, kind: SearchType, count: u32) {
225 let slot = match kind {
226 SearchType::Repositories => &mut self.repositories,
227 SearchType::Code => &mut self.code,
228 SearchType::Issues => &mut self.issues,
229 SearchType::Pulls => &mut self.pulls,
230 SearchType::People => &mut self.people,
231 };
232 *slot = count;
233 }
234}
235
236#[derive(Clone, Debug, Serialize, Deserialize)]
237#[serde(rename_all = "camelCase")]
238pub struct SearchResults {
239 /// The query as it was read.
240 pub query: String,
241 /// The tab these results are for.
242 #[serde(rename = "type")]
243 pub kind: SearchType,
244 pub counts: SearchCounts,
245 pub page: u32,
246 pub per_page: u32,
247 /// Whether there is a page after this one.
248 pub more: bool,
249 pub hits: Vec<Hit>,
250 /// What the query could not do, such as a code search with no word of
251 /// three characters or more.
252 #[serde(default)]
253 pub notes: Vec<String>,
254}
255
256/// `suggest`: a few results of every kind but code, for the command
257/// palette as someone types. Returns `Vec<Hit>`.
258#[derive(Debug, Default, Serialize, Deserialize)]
259pub struct SuggestArgs {
260 pub viewer: Viewer,
261 pub query: String,
262}
263
264/// `explore`: public repositories, recently active or newly created,
265/// optionally of one language or topic. Returns `Explore`.
266#[derive(Debug, Default, Serialize, Deserialize)]
267#[serde(rename_all = "camelCase")]
268pub struct ExploreArgs {
269 pub viewer: Viewer,
270 /// `active` (the default) or `new`.
271 #[serde(default)]
272 pub sort: Option<String>,
273 #[serde(default)]
274 pub language: Option<String>,
275 #[serde(default)]
276 pub topic: Option<String>,
277 #[serde(default)]
278 pub page: Option<u32>,
279}
280
281/// A public repository as Explore lists it.
282#[derive(Clone, Debug, Serialize, Deserialize)]
283#[serde(rename_all = "camelCase")]
284pub struct ExploreRepo {
285 pub namespace: String,
286 pub name: String,
287 pub description: Option<String>,
288 #[serde(default)]
289 pub topics: Vec<String>,
290 pub language: Option<String>,
291 /// RFC 3339.
292 pub created_at: String,
293 /// RFC 3339: the last push to its default branch, if any.
294 pub pushed_at: Option<String>,
295 /// Whether it is archived: read-only, kept for reference.
296 #[serde(default)]
297 pub archived: bool,
298}
299
300/// A language or topic, with how many public repositories have it.
301#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
302pub struct Facet {
303 pub name: String,
304 pub count: u32,
305}
306
307#[derive(Clone, Debug, Serialize, Deserialize)]
308#[serde(rename_all = "camelCase")]
309pub struct Explore {
310 pub repos: Vec<ExploreRepo>,
311 pub languages: Vec<Facet>,
312 pub topics: Vec<Facet>,
313 pub page: u32,
314 pub more: bool,
315}
316
317#[cfg(test)]
318mod tests {
319 use super::*;
320
321 #[test]
322 fn types_read_as_people_write_them() {
323 assert_eq!(SearchType::parse("prs"), Some(SearchType::Pulls));
324 assert_eq!(SearchType::parse("Repos"), Some(SearchType::Repositories));
325 assert_eq!(SearchType::parse("users"), Some(SearchType::People));
326 assert_eq!(SearchType::parse("wiki"), None);
327 }
328
329 #[test]
330 fn the_type_is_sent_as_type() {
331 let results = SearchResults {
332 query: "x".into(),
333 kind: SearchType::Code,
334 counts: SearchCounts::default(),
335 page: 1,
336 per_page: 20,
337 more: false,
338 hits: vec![],
339 notes: vec![],
340 };
341 let value = serde_json::to_value(&results).unwrap();
342 assert_eq!(value["type"], "code");
343 assert_eq!(value["perPage"], 20);
344 }
345}