flagon-io/g1t

public

Where people and agents ship software together. The open-source git platform for the whole job: issues, agents, checks and deploys to the edge.

g1t/crates/contracts/src/repos.rs

639 lines20,281 bytesCodeBlame

Pick any line to see why it is the way it is: the commit, the pull request and issue it came from, and what the agent was thinking.

Rust repos service with shipping; pull requests kept in the model1//! The repos service: repository metadata, contents, forks and git access.
2//!
3//! Each `*Args` struct is the argument of the method of the same name,
4//! served at `POST /rpc/<method>`.
5
6use serde::{Deserialize, Serialize};
7
8use crate::{User, Viewer};
9
10#[derive(Clone, Debug, Serialize, Deserialize)]
11#[serde(rename_all = "camelCase")]
12pub struct Repo {
13 pub id: String,
Workspaces own repositories14 /// The slug of the workspace that owns it: the first URL segment.
Rust repos service with shipping; pull requests kept in the model15 pub namespace: String,
16 pub name: String,
17 pub description: Option<String>,
18 pub is_private: bool,
19 pub owner_id: String,
20 pub default_branch: String,
Issues and pull requests replace intents and attempts21 /// Set when this repo is a pull request's working copy of another repo.
Rust repos service with shipping; pull requests kept in the model22 pub fork_of: Option<String>,
Agents as a team: lifecycle, merge queue, billing and a new shell23 /// Whether the default branch is protected: it changes only by merging
24 /// a pull request, and pushes to it are refused.
25 #[serde(default)]
26 pub protected: bool,
RFC 3339 timestamps in identity and repos27 /// RFC 3339.
28 pub created_at: String,
Search across all of g1t, Explore, and a command palette29 /// Words that say what it is about, for search and Explore: lowercase
30 /// letters, digits and hyphens. See [`clean_topics`].
31 #[serde(default)]
32 pub topics: Vec<String>,
33}
34
35/// The most topics a repository has.
36pub const MAX_TOPICS: usize = 20;
37/// The longest topic.
38pub const MAX_TOPIC_CHARS: usize = 35;
39
40/// Topics as they are kept: lowercase, spaces and underscores made
41/// hyphens, each of letters, digits and hyphens, starting with a letter or
42/// digit, without repeats, at most [`MAX_TOPICS`]. Anything else is the
43/// first topic that could not be read.
44pub fn clean_topics(topics: &[String]) -> Result<Vec<String>, String> {
45 let mut kept: Vec<String> = Vec::new();
46 for topic in topics {
47 let topic: String = topic
48 .trim()
49 .to_lowercase()
50 .chars()
51 .map(|c| if c == ' ' || c == '_' { '-' } else { c })
52 .collect();
53 if topic.is_empty() {
54 continue;
55 }
56 let valid = topic.chars().count() <= MAX_TOPIC_CHARS
57 && topic.chars().all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '-')
58 && topic.chars().next().is_some_and(|c| c.is_ascii_alphanumeric());
59 if !valid {
60 return Err(format!(
61 "\"{topic}\" is not a topic: use letters, digits and hyphens, at most {MAX_TOPIC_CHARS} characters."
62 ));
63 }
64 if !kept.contains(&topic) {
65 kept.push(topic);
66 }
67 }
68 if kept.len() > MAX_TOPICS {
69 return Err(format!("A repository has at most {MAX_TOPICS} topics."));
70 }
71 Ok(kept)
Rust repos service with shipping; pull requests kept in the model72}
73
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API74#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
Rust repos service with shipping; pull requests kept in the model75pub struct RepoPath {
76 pub namespace: String,
77 pub name: String,
78}
79
80#[derive(Clone, Debug, Serialize, Deserialize)]
81pub struct Signature {
82 pub name: String,
83 pub email: String,
84}
85
86#[derive(Clone, Debug, Serialize, Deserialize)]
87#[serde(rename_all = "camelCase")]
88pub struct Commit {
89 pub hash: String,
90 pub tree_hash: String,
91 pub message: String,
92 pub author: Signature,
93 pub parents: Vec<String>,
RFC 3339 timestamps in identity and repos94 /// RFC 3339.
95 pub authored_at: String,
Rust repos service with shipping; pull requests kept in the model96}
97
98#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
99#[serde(rename_all = "lowercase")]
100pub enum EntryKind {
101 Tree,
102 Blob,
103 Symlink,
104 Gitlink,
105 Exec,
106}
107
108#[derive(Clone, Debug, Serialize, Deserialize)]
109pub struct TreeEntry {
110 pub name: String,
111 pub hash: String,
112 pub kind: EntryKind,
113}
114
115#[derive(Clone, Debug, Serialize, Deserialize)]
116pub struct Readme {
117 pub name: String,
118 /// Null when the file is binary or too large to show.
119 pub text: Option<String>,
120}
121
122#[derive(Clone, Debug, Serialize, Deserialize)]
123pub struct TreeView {
124 pub repo: Repo,
125 #[serde(rename = "ref")]
126 pub git_ref: String,
127 pub path: String,
128 /// Null when the repo has no commits yet.
129 pub head: Option<Commit>,
130 pub entries: Vec<TreeEntry>,
131 pub readme: Option<Readme>,
132}
133
134#[derive(Clone, Debug, Serialize, Deserialize)]
135pub struct BlobView {
136 pub repo: Repo,
137 #[serde(rename = "ref")]
138 pub git_ref: String,
139 pub path: String,
140 pub size: u64,
141 /// Null when the file is binary or too large to show.
142 pub text: Option<String>,
143}
144
145/// A git remote and a short-lived credential for it.
146#[derive(Clone, Debug, Serialize, Deserialize)]
147pub struct GitAccess {
148 pub remote: String,
149 pub token: String,
150}
151
152#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
153pub enum GitService {
154 #[serde(rename = "git-upload-pack")]
155 UploadPack,
156 #[serde(rename = "git-receive-pack")]
157 ReceivePack,
158}
159
Issues and pull requests replace intents and attempts160/// The result of landing a pull request.
Rust repos service with shipping; pull requests kept in the model161#[derive(Clone, Debug, Serialize, Deserialize)]
162pub struct Landed {
Diffs on attempts; hosted agent presented as the g1t agent163 /// The commit the branch points to now.
Rust repos service with shipping; pull requests kept in the model164 pub commit: String,
Diffs on attempts; hosted agent presented as the g1t agent165 /// The commit it pointed to before, if it had one. Comparing against
Issues and pull requests replace intents and attempts166 /// this shows what the pull request changed.
Diffs on attempts; hosted agent presented as the g1t agent167 pub previous: Option<String>,
Rust repos service with shipping; pull requests kept in the model168}
169
170/// `get`. Returns `Outcome<Repo>`.
171#[derive(Debug, Serialize, Deserialize)]
172pub struct GetArgs {
173 pub path: RepoPath,
174 pub viewer: Viewer,
175}
176
Automations: rules in .g1t/automations that act when something happens177/// `path_by_id`: where a repository is, whoever may see it. For g1t's own
178/// services, which hold a repository's id from an event and act for its
179/// workspace; nothing outside reaches it. Returns `Option<RepoPath>`, null
180/// for a fork or an unknown id.
181#[derive(Debug, Serialize, Deserialize)]
182pub struct PathByIdArgs {
183 pub id: String,
184}
185
Rust repos service with shipping; pull requests kept in the model186/// `get_by_id`. Returns `Outcome<Repo>`.
187#[derive(Debug, Serialize, Deserialize)]
188pub struct GetByIdArgs {
189 pub id: String,
190 pub viewer: Viewer,
191}
192
193/// `list`: repos the viewer may see, newest first. Returns `Vec<Repo>`.
194#[derive(Debug, Default, Serialize, Deserialize)]
Workspaces own repositories195#[serde(rename_all = "camelCase")]
Rust repos service with shipping; pull requests kept in the model196pub struct ListArgs {
197 pub viewer: Viewer,
198 #[serde(default)]
199 pub query: Option<String>,
Workspaces own repositories200 /// Only repos in this workspace.
Rust repos service with shipping; pull requests kept in the model201 #[serde(default)]
202 pub namespace: Option<String>,
Workspaces own repositories203 /// Only repos in workspaces the viewer belongs to.
204 #[serde(default)]
205 pub member_only: bool,
Rust repos service with shipping; pull requests kept in the model206}
207
208/// `create`. Returns `Outcome<Repo>`.
209#[derive(Debug, Serialize, Deserialize)]
210#[serde(rename_all = "camelCase")]
211pub struct CreateArgs {
Workspaces own repositories212 /// Who is creating it; they must belong to the workspace.
Rust repos service with shipping; pull requests kept in the model213 pub owner: User,
Workspaces own repositories214 /// The workspace it is created in.
215 pub namespace: String,
Rust repos service with shipping; pull requests kept in the model216 pub name: String,
217 #[serde(default)]
218 pub description: Option<String>,
219 #[serde(default)]
220 pub is_private: bool,
Agents as a team: lifecycle, merge queue, billing and a new shell221 /// The https address of a public git repository to copy the default
222 /// branch of, such as `https://github.com/owner/repo`.
223 #[serde(default)]
224 pub import_url: Option<String>,
Rust repos service with shipping; pull requests kept in the model225}
226
Agents as a team: lifecycle, merge queue, billing and a new shell227/// `update`: changes whichever of a repository's details are given.
228/// Members of its workspace only. Returns `Outcome<Repo>`.
229#[derive(Debug, Serialize, Deserialize)]
230#[serde(rename_all = "camelCase")]
231pub struct UpdateArgs {
232 pub actor: User,
233 pub path: RepoPath,
234 /// An empty description clears it.
235 #[serde(default)]
236 pub description: Option<String>,
237 #[serde(default)]
238 pub is_private: Option<bool>,
239 #[serde(default)]
240 pub protected: Option<bool>,
Search across all of g1t, Explore, and a command palette241 /// Replaces its topics; an empty list clears them.
242 #[serde(default)]
243 pub topics: Option<Vec<String>>,
Agents as a team: lifecycle, merge queue, billing and a new shell244}
245
Rust repos service with shipping; pull requests kept in the model246/// `tree`. Returns `Outcome<TreeView>`.
247#[derive(Debug, Serialize, Deserialize)]
248#[serde(rename_all = "camelCase")]
249pub struct TreeArgs {
250 pub path: RepoPath,
251 pub viewer: Viewer,
252 /// The default branch when absent.
253 #[serde(default, rename = "ref")]
254 pub git_ref: Option<String>,
255 #[serde(default)]
256 pub tree_path: String,
257}
258
259/// `blob`. Returns `Outcome<BlobView>`.
260#[derive(Debug, Serialize, Deserialize)]
261#[serde(rename_all = "camelCase")]
262pub struct BlobArgs {
263 pub path: RepoPath,
264 pub viewer: Viewer,
265 #[serde(rename = "ref")]
266 pub git_ref: String,
267 pub file_path: String,
268}
269
270/// `log`. Returns `Outcome<Vec<Commit>>`.
271#[derive(Debug, Serialize, Deserialize)]
272pub struct LogArgs {
273 pub path: RepoPath,
274 pub viewer: Viewer,
275 #[serde(default, rename = "ref")]
276 pub git_ref: Option<String>,
277 pub limit: u32,
278}
279
Issues and pull requests replace intents and attempts280/// `fork_for_pull`: a copy-on-write copy of the source repo, hidden from
281/// listings, for one pull request to be made in. Returns `Outcome<Repo>`.
Rust repos service with shipping; pull requests kept in the model282#[derive(Debug, Serialize, Deserialize)]
283#[serde(rename_all = "camelCase")]
284pub struct ForkArgs {
285 pub source_id: String,
Issues and pull requests replace intents and attempts286 pub pull_id: String,
Rust repos service with shipping; pull requests kept in the model287 pub actor: User,
288}
289
290/// `git_access`: authorizes a git operation and says where to send it.
291/// Pushing to a repo that does not exist creates it in the pusher's own
292/// namespace. Returns `Outcome<GitAccess>`.
293#[derive(Debug, Serialize, Deserialize)]
294pub struct GitAccessArgs {
295 pub path: RepoPath,
296 pub viewer: Viewer,
297 pub service: GitService,
298}
299
Pull requests from branches300/// `land`: moves a repository's default branch to the head of a pull
301/// request's source. Refused with `conflict` when the source is behind,
302/// since that would discard commits. Returns `Outcome<Landed>`.
Rust repos service with shipping; pull requests kept in the model303#[derive(Debug, Serialize, Deserialize)]
304#[serde(rename_all = "camelCase")]
305pub struct LandArgs {
Pull requests from branches306 /// The repository holding the commits: a pull request's fork, or the
307 /// target itself when landing one of its own branches.
308 pub source_id: String,
309 /// The branch of the source to land. Required when the source is the
310 /// target; a fork lands its default branch.
311 #[serde(default)]
312 pub branch: Option<String>,
Rust repos service with shipping; pull requests kept in the model313 pub actor: User,
314}
Diffs on attempts; hosted agent presented as the g1t agent315
316#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
317#[serde(rename_all = "lowercase")]
318pub enum FileStatus {
319 Added,
320 Modified,
321 Deleted,
322}
323
324#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
325#[serde(rename_all = "lowercase")]
326pub enum LineKind {
327 /// Unchanged, shown for context.
328 Context,
329 Add,
330 Delete,
331}
332
333#[derive(Clone, Debug, Serialize, Deserialize)]
334pub struct DiffLine {
335 pub kind: LineKind,
336 /// Line number in the old file; absent for added lines.
337 pub old: Option<u32>,
338 /// Line number in the new file; absent for deleted lines.
339 pub new: Option<u32>,
340 pub text: String,
341}
342
343/// A run of changed lines with their surrounding context.
344#[derive(Clone, Debug, Serialize, Deserialize)]
345pub struct Hunk {
346 pub lines: Vec<DiffLine>,
347}
348
349#[derive(Clone, Debug, Serialize, Deserialize)]
350pub struct FileDiff {
351 pub path: String,
352 pub status: FileStatus,
353 pub additions: u32,
354 pub deletions: u32,
355 /// True when the file is binary or too large, so no lines are shown.
356 pub binary: bool,
357 pub hunks: Vec<Hunk>,
358}
359
360/// What changed between two commits.
361#[derive(Clone, Debug, Serialize, Deserialize)]
362pub struct Comparison {
363 /// Null when the head has no earlier commit to compare against.
364 pub base: Option<String>,
365 pub head: String,
366 pub files: Vec<FileDiff>,
367 /// True when the change was too large to return in full.
368 pub truncated: bool,
369}
370
Pull requests from branches371/// `compare`: what `head` changes relative to `base`.
Diffs on attempts; hosted agent presented as the g1t agent372///
Pull requests from branches373/// `head` is a branch or a commit, and defaults to the default branch.
374/// With no `base`, a fork is compared against the point where it and the
375/// repository it came from last agreed; a branch against the point where it
376/// left the default branch; and the default branch against its head's
377/// parent. Returns `Outcome<Comparison>`.
Diffs on attempts; hosted agent presented as the g1t agent378#[derive(Debug, Serialize, Deserialize)]
379#[serde(rename_all = "camelCase")]
380pub struct CompareArgs {
381 pub repo_id: String,
382 pub viewer: Viewer,
383 #[serde(default)]
384 pub base: Option<String>,
Pull requests from branches385 #[serde(default)]
386 pub head: Option<String>,
387}
388
Agents as a team: lifecycle, merge queue, billing and a new shell389/// Lines `start` to `end` of a file, inclusive and counted from 1, last
390/// changed by `commit`.
391#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
392pub struct BlameRange {
393 pub start: u32,
394 pub end: u32,
395 pub commit: String,
396}
397
398/// Who last changed each line of a file.
399#[derive(Clone, Debug, Serialize, Deserialize)]
400pub struct Blame {
401 /// The commit the file was read at.
402 pub head: String,
403 /// Every line, in order, in runs that share a commit.
404 pub ranges: Vec<BlameRange>,
405 /// The commits the ranges name, each once.
406 pub commits: Vec<Commit>,
407 /// True when the history was too long to read in full, so the oldest
408 /// lines are given to the oldest commit read.
409 pub partial: bool,
410}
411
412/// `blame`: who last changed each line of `path` as of `ref` (the default
413/// branch if absent). Returns `Outcome<Blame>`; not found when the file is
414/// missing or is not text.
415#[derive(Debug, Serialize, Deserialize)]
416pub struct BlameArgs {
417 pub path: RepoPath,
418 pub viewer: Viewer,
419 #[serde(default, rename = "ref")]
420 pub git_ref: Option<String>,
421 #[serde(rename = "filePath")]
422 pub file_path: String,
423}
424
Pull requests from branches425/// A branch and the commit it points to.
426#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
427pub struct Branch {
428 pub name: String,
429 pub hash: String,
430}
431
432/// `branches`: the repository's branches, default branch first.
433/// Returns `Outcome<Vec<Branch>>`.
434#[derive(Debug, Serialize, Deserialize)]
435pub struct BranchesArgs {
436 pub path: RepoPath,
437 pub viewer: Viewer,
438}
439
Agents as a team: lifecycle, merge queue, billing and a new shell440/// `behind`: whether the default branch of the repository a pull request
441/// would merge into has commits its source does not. For services that
442/// have already decided the caller may see the pull request; it reveals
443/// one bit. Returns `bool`.
444#[derive(Debug, Serialize, Deserialize)]
445#[serde(rename_all = "camelCase")]
446pub struct BehindArgs {
447 /// The pull request's fork, or the repository itself for a branch.
448 pub source_id: String,
449 /// The branch of the source. A fork is compared on its default branch.
450 #[serde(default)]
451 pub branch: Option<String>,
452}
453
Agents and memory, checks and conflicts, profiles, slug renames, custom domains454/// `divergence`: how a pull request's source and the default branch it
455/// would merge into have moved apart since they last agreed: the files each
456/// side changed. Takes `BehindArgs`. For services that have already decided
457/// the caller may see the pull request; it reveals paths, not contents.
458/// Returns `Option<Divergence>`, null when either side has no commits.
459#[derive(Clone, Debug, Default, Serialize, Deserialize)]
460#[serde(rename_all = "camelCase")]
461pub struct Divergence {
462 /// The source's commit.
463 pub head: String,
464 /// The default branch's commit.
465 pub base: String,
466 /// Where they last agreed, if that could be found.
467 pub merge_base: Option<String>,
468 /// Whether the default branch has commits the source does not.
469 pub behind: bool,
470 /// The files the source changed since the merge base.
471 pub ours: Vec<String>,
472 /// The files the default branch changed since the merge base. Empty
473 /// when it is not behind.
474 pub theirs: Vec<String>,
475 /// Whether either list was cut short.
476 pub truncated: bool,
477}
478
Pull requests from branches479/// `head`: the commit a branch points to, or null. For services reacting
480/// to a push, which have no viewer; it reveals nothing but a commit hash.
481/// Returns `Option<String>`.
482#[derive(Debug, Serialize, Deserialize)]
483#[serde(rename_all = "camelCase")]
484pub struct HeadArgs {
485 pub repo_id: String,
Workflows run when an agent's pull request is marked ready486 /// Empty for the repository's default branch.
Pull requests from branches487 pub branch: String,
Diffs on attempts; hosted agent presented as the g1t agent488}
Merge queue: tested states are deleted once their entry leaves489
490/// Where g1t keeps branches of its own in a repository, such as the merge
491/// queue's tested states. Only these can be removed with `delete_branch`.
492pub const G1T_BRANCH_PREFIX: &str = "g1t-";
493
494/// `delete_branch`: removes a branch g1t made for itself once it is done
495/// with it, never one of people's: the name must start with
496/// [`G1T_BRANCH_PREFIX`]. For services, which have no viewer. Returns
497/// `Outcome<bool>`: whether there was such a branch.
498#[derive(Debug, Serialize, Deserialize)]
499#[serde(rename_all = "camelCase")]
500pub struct DeleteBranchArgs {
501 pub repo_id: String,
502 pub branch: String,
503}
Agents and memory, checks and conflicts, profiles, slug renames, custom domains504
505/// `readable`: of these repository ids, the repositories the viewer may
506/// read, as `get_by_id` decides; forks and unknown ids are left out. For
507/// services that hold ids and must show only what the viewer could open.
508/// At most [`MAX_READABLE`] ids are looked at. Returns `Vec<Repo>`.
509#[derive(Debug, Serialize, Deserialize)]
510pub struct ReadableArgs {
511 pub ids: Vec<String>,
512 pub viewer: Viewer,
513}
514
515/// The most ids one `readable` call looks at.
516pub const MAX_READABLE: usize = 500;
517
518/// `public_namespaces`: the workspaces in which this account made a public
519/// repository, and so a public project, which anyone can see on its page.
520/// Returns `Vec<String>` of workspace slugs.
521#[derive(Debug, Serialize, Deserialize)]
522#[serde(rename_all = "camelCase")]
523pub struct PublicNamespacesArgs {
524 pub owner_id: String,
525}
Search across all of g1t, Explore, and a command palette526
527/// One file on a branch, or one a change touched: its path and blob.
528#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
529pub struct FileEntry {
530 pub path: String,
531 /// The blob it holds now; null when the change deleted it.
532 pub hash: Option<String>,
533}
534
535/// Files, and whether there were more than were listed.
536#[derive(Clone, Debug, Default, Serialize, Deserialize)]
537#[serde(rename_all = "camelCase")]
538pub struct FileList {
539 /// The commit the files were read at; null for an empty repository.
540 pub commit: Option<String>,
541 pub files: Vec<FileEntry>,
542 pub truncated: bool,
543}
544
545/// `list_files`: every file on a branch (the default branch when absent),
546/// path order by level, never descending into a directory named in
547/// `skip_dirs`. For services that index a repository; no viewer, since it
548/// is only reached by g1t's own services. Returns `FileList`.
549#[derive(Debug, Default, Serialize, Deserialize)]
550#[serde(rename_all = "camelCase")]
551pub struct ListFilesArgs {
552 pub repo_id: String,
553 #[serde(default, rename = "ref")]
554 pub git_ref: Option<String>,
555 #[serde(default)]
556 pub skip_dirs: Vec<String>,
557 /// At most this many files; capped at [`MAX_LISTED_FILES`].
558 pub limit: u32,
559}
560
561/// `changed_files`: the files that differ between two commits, as
562/// `list_files` reads them. With no `base`, every file at `head`. Returns
563/// `FileList`.
564#[derive(Debug, Default, Serialize, Deserialize)]
565#[serde(rename_all = "camelCase")]
566pub struct ChangedFilesArgs {
567 pub repo_id: String,
568 #[serde(default)]
569 pub base: Option<String>,
570 pub head: String,
571 #[serde(default)]
572 pub skip_dirs: Vec<String>,
573 pub limit: u32,
574}
575
576/// The most files one `list_files` or `changed_files` call lists.
577pub const MAX_LISTED_FILES: u32 = 10_000;
578
579/// `read_blobs`: the text of these blobs of a repository, for services
580/// that index it. A blob larger than `max_bytes`, or binary, comes back
581/// with no text. Returns `Vec<BlobText>`, in the order asked.
582#[derive(Debug, Default, Serialize, Deserialize)]
583#[serde(rename_all = "camelCase")]
584pub struct ReadBlobsArgs {
585 pub repo_id: String,
586 pub hashes: Vec<String>,
587 pub max_bytes: u32,
588}
589
590/// The most blobs one `read_blobs` call reads.
591pub const MAX_READ_BLOBS: usize = 100;
592
593#[derive(Clone, Debug, Serialize, Deserialize)]
594pub struct BlobText {
595 pub hash: String,
596 pub size: u64,
597 /// Null when the blob is missing, binary or larger than asked.
598 pub text: Option<String>,
599}
600
601/// `all_ids`: every repository that is not a fork, by id, a page at a
602/// time, for services that index all of them. Returns `IdPage`.
603#[derive(Debug, Default, Serialize, Deserialize)]
604pub struct AllIdsArgs {
605 /// Ids after this one.
606 #[serde(default)]
607 pub after: Option<String>,
608 pub limit: u32,
609}
610
611#[derive(Clone, Debug, Default, Serialize, Deserialize)]
612pub struct IdPage {
613 pub ids: Vec<String>,
614 /// Where the next page starts; null on the last.
615 pub next: Option<String>,
616}
617
618#[cfg(test)]
619mod topic_tests {
620 use super::*;
621
622 fn topics(list: &[&str]) -> Result<Vec<String>, String> {
623 clean_topics(&list.iter().map(|t| t.to_string()).collect::<Vec<_>>())
624 }
625
626 #[test]
627 fn topics_are_tidied() {
628 assert_eq!(topics(&["Rust", " web_server ", "rust", ""]).unwrap(), vec!["rust", "web-server"]);
629 }
630
631 #[test]
632 fn odd_topics_are_refused() {
633 assert!(topics(&["c++"]).is_err());
634 assert!(topics(&["-lead"]).is_err());
635 assert!(topics(&[&"a".repeat(36)]).is_err());
636 let many: Vec<String> = (0..21).map(|i| format!("t{i}")).collect();
637 assert!(clean_topics(&many).is_err());
638 }
639}