Skip to content

Commit 15b86c0

Browse files
Merge remote-tracking branch 'repoconf-rust-public-lib-template/main'
# Conflicts: # AGENTS.md # Cargo.lock # README.md # README.ts # src/lib.rs
2 parents 554ecfe + 81f9acb commit 15b86c0

29 files changed

Lines changed: 2217 additions & 2731 deletions

.agents/cli-for-type.md

Lines changed: 243 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,243 @@
1+
# Concepts for CLI-for-type
2+
3+
## Target type
4+
5+
The type whose methods are exposed through a CLI and invoked by child commands.
6+
7+
Examples:
8+
9+
- Rust example.
10+
- ```rust
11+
//! In this example, all types are in the same file. In real code, the types must be in their own files according to project guidelines.
12+
13+
use std::io::stdout;
14+
use clap::{Parser, Subcommand};
15+
use derive_new::new;
16+
use errgonomic::handle;
17+
use secrecy::SecretString;
18+
use subtype::{subtype, subtype_string, subtype_u64};
19+
use thiserror::Error;
20+
21+
use FooSubcommand::*;
22+
23+
subtype! {
24+
#[derive(Clone, Debug)]
25+
pub struct FooApiKey(SecretString)
26+
}
27+
28+
impl core::str::FromStr for FooApiKey {
29+
type Err = core::convert::Infallible;
30+
31+
fn from_str(s: &str) -> Result<Self, Self::Err> {
32+
Ok(FooApiKey::new(SecretString::from(s.to_owned())))
33+
}
34+
}
35+
36+
subtype_u64! {
37+
pub struct UserId(u64)
38+
}
39+
40+
impl core::str::FromStr for UserId {
41+
type Err = core::num::ParseIntError;
42+
43+
fn from_str(s: &str) -> Result<Self, Self::Err> {
44+
s.parse::<u64>().map(Self::new)
45+
}
46+
}
47+
48+
subtype_u64! {
49+
pub struct UsersPage(u64)
50+
}
51+
52+
impl core::str::FromStr for UsersPage {
53+
type Err = core::num::ParseIntError;
54+
55+
fn from_str(s: &str) -> Result<Self, Self::Err> {
56+
s.parse::<u64>().map(Self::new)
57+
}
58+
}
59+
60+
subtype_string! {
61+
pub struct UserName(String)
62+
}
63+
64+
impl core::str::FromStr for UserName {
65+
type Err = core::convert::Infallible;
66+
67+
fn from_str(s: &str) -> Result<Self, Self::Err> {
68+
Ok(Self::new(s))
69+
}
70+
}
71+
72+
#[derive(Clone, Debug)]
73+
pub struct User {
74+
pub id: UserId,
75+
pub name: UserName,
76+
}
77+
78+
#[derive(new)]
79+
pub struct FooClient {
80+
api_key: FooApiKey,
81+
}
82+
83+
impl FooClient {
84+
pub fn users(&self, _page: UsersPage) -> Result<Vec<User>, FooClientUsersError> {
85+
use FooClientUsersError::*;
86+
let _ = NotImplemented {};
87+
todo!()
88+
}
89+
90+
pub fn set_user_name(&self, _id: UserId, _name: &UserName) -> Result<(), FooClientSetUserNameError> {
91+
use FooClientSetUserNameError::*;
92+
let _ = NotImplemented {};
93+
todo!()
94+
}
95+
}
96+
97+
98+
#[derive(Error, Debug)]
99+
pub enum FooClientUsersError {
100+
#[error("foo client users operation is not implemented")]
101+
NotImplemented {},
102+
}
103+
104+
#[derive(Error, Debug)]
105+
pub enum FooClientSetUserNameError {
106+
#[error("foo client set user name operation is not implemented")]
107+
NotImplemented {},
108+
}
109+
110+
#[derive(Parser, Debug)]
111+
#[command(author, version, about, propagate_version = true)]
112+
pub struct FooCommand {
113+
#[arg(long, env = "FOO_API_KEY")]
114+
api_key: FooApiKey,
115+
116+
#[command(subcommand)]
117+
subcommand: FooSubcommand,
118+
}
119+
120+
#[derive(Subcommand, Clone, Debug)]
121+
pub enum FooSubcommand {
122+
Users(FooUsersCommand),
123+
SetUserName(FooSetUserNameCommand),
124+
}
125+
126+
impl FooCommand {
127+
pub async fn run(self) -> Result<(), FooCommandRunError> {
128+
use FooCommandRunError::*;
129+
let Self {
130+
api_key,
131+
subcommand,
132+
} = self;
133+
let client = FooClient::new(api_key);
134+
match subcommand {
135+
Users(command) => map_err!(command.run(&client).await, UsersCommandRunFailed),
136+
SetUserName(command) => map_err!(command.run(&client).await, SetUserNameCommandRunFailed),
137+
}
138+
}
139+
}
140+
141+
#[derive(Error, Debug)]
142+
pub enum FooCommandRunError {
143+
#[error("failed to run users command")]
144+
UsersCommandRunFailed { source: FooUsersCommandRunError },
145+
146+
#[error("failed to run set user name command")]
147+
SetUserNameCommandRunFailed { source: FooSetUserNameCommandRunError },
148+
}
149+
150+
#[derive(Parser, Debug)]
151+
pub struct FooUsersCommand {
152+
#[arg(long)]
153+
page: UsersPage,
154+
}
155+
156+
impl FooUsersCommand {
157+
pub async fn run(self, client: &FooClient) -> Result<(), FooUsersCommandRunError> {
158+
use FooUsersCommandRunError::*;
159+
let Self {
160+
page,
161+
} = self;
162+
let mut stdout = stdout();
163+
let users = handle!(client.users(page), UsersFailed, page);
164+
handle!(serde_json::to_writer_pretty(&mut stdout, &users), ToWriterPrettyFailed, users);
165+
Ok(())
166+
}
167+
}
168+
169+
#[derive(Error, Debug)]
170+
pub enum FooUsersCommandRunError {
171+
#[error("failed to fetch users for page '{page}'")]
172+
UsersFailed { source: FooClientUsersError, page: UsersPage },
173+
#[error("failed to write users to stdout")]
174+
ToWriterPrettyFailed { source: serde_json::Error, users: Vec<User> },
175+
}
176+
177+
#[derive(Parser, Debug)]
178+
pub struct FooSetUserNameCommand {
179+
#[arg(long)]
180+
user_id: UserId,
181+
182+
#[arg(long)]
183+
name: UserName,
184+
}
185+
186+
impl FooSetUserNameCommand {
187+
pub async fn run(self, client: &FooClient) -> Result<Vec<User>, FooSetUserNameCommandRunError> {
188+
use FooSetUserNameCommandRunError::*;
189+
let Self {
190+
user_id,
191+
name,
192+
} = self;
193+
handle!(client.set_user_name(user_id, &name), SetUserNameFailed, user_id, name);
194+
Ok(())
195+
}
196+
}
197+
198+
#[derive(Error, Debug)]
199+
pub enum FooSetUserNameCommandRunError {
200+
#[error("failed to set user name '{name}' for user '{user_id}'")]
201+
SetUserNameFailed { source: FooClientSetUserNameError, user_id: UserId, name: UserName },
202+
}
203+
```
204+
205+
Requirements:
206+
207+
- Must be constructible by the parent command.
208+
- Must expose at least one public method that can be invoked by a child command.
209+
210+
Notes:
211+
212+
- The target type should not parse CLI arguments itself.
213+
214+
## Parent command
215+
216+
The command that constructs the target type and delegates execution to the selected child command.
217+
218+
Requirements:
219+
220+
- Must have fields for all inputs needed to construct the target type.
221+
- Must construct an instance of the target type.
222+
- Must pass the constructed instance of the target type to the selected child command.
223+
224+
Notes:
225+
226+
- The parent command isolates CLI parsing from domain logic.
227+
228+
## Child command
229+
230+
A subcommand that wraps one target-type method and maps CLI arguments to that method.
231+
232+
Requirements:
233+
234+
- Must have a field for each argument of the wrapped method.
235+
- Must call the wrapped method in its run implementation.
236+
- Must return an error type that wraps the method error and the relevant argument values.
237+
- Must write the results to stdout in an efficient way:
238+
- Must serialize the results using `serde_json`
239+
- Must use `serde_json::to_writer_pretty`
240+
241+
Notes:
242+
243+
- Child commands should not construct the target type themselves.

.agents/cli.md

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
# CLI guidelines
2+
3+
## Dependencies
4+
5+
- `clap` (features: at least "derive", "env")
6+
- `tokio` (features: at least "macros", "rt", "rt-multi-thread")
7+
- `errgonomic`
8+
- `thiserror`
9+
10+
## File layout and required items
11+
12+
### File `src/main.rs`
13+
14+
- Must define a `main` entrypoint
15+
- Must define a `verify_cli` test for the top-level command exactly as in the example below (with `debug_assert`)
16+
17+
Example:
18+
19+
```rust
20+
use clap::Parser;
21+
use errgonomic::exit_result;
22+
use my_crate_name::Command;
23+
use std::process::ExitCode;
24+
25+
#[tokio::main]
26+
async fn main() -> ExitCode {
27+
let args = Command::parse();
28+
let result = args.run().await;
29+
exit_result(result)
30+
}
31+
32+
#[test]
33+
fn verify_cli() {
34+
use clap::CommandFactory;
35+
Command::command().debug_assert();
36+
}
37+
```
38+
39+
### File `src/command.rs`
40+
41+
- Must define a [command-like struct](#command-like-struct) named `Command`
42+
- Must define a [subcommand-like enum](#subcommand-like-enum) named `Subcommand`
43+
44+
Example:
45+
46+
```rust
47+
use std::process::ExitCode;
48+
use Subcommand::*;
49+
use errgonomic::map_err;
50+
use thiserror::Error;
51+
52+
#[derive(clap::Parser, Debug)]
53+
#[command(author, version, about, propagate_version = true)]
54+
pub struct Command {
55+
#[command(subcommand)]
56+
subcommand: Subcommand,
57+
}
58+
59+
#[derive(clap::Subcommand, Clone, Debug)]
60+
pub enum Subcommand {
61+
Print(PrintCommand),
62+
}
63+
64+
impl Command {
65+
pub async fn run(self) -> Result<ExitCode, CommandRunError> {
66+
use CommandRunError::*;
67+
let Self {
68+
subcommand,
69+
} = self;
70+
match subcommand {
71+
Print(command) => map_err!(command.run().await, PrintCommandRunFailed),
72+
}
73+
}
74+
}
75+
76+
#[derive(Error, Debug)]
77+
pub enum CommandRunError {
78+
#[error("failed to run print command")]
79+
PrintCommandRunFailed { source: PrintCommandRunError },
80+
}
81+
82+
mod print_command;
83+
84+
pub use print_command::*;
85+
```
86+
87+
## Definitions
88+
89+
### Command-like struct
90+
91+
A struct that contains fields for CLI arguments.
92+
93+
- Must have a name that is a concatenation of all command names leading up to and including this command name, and ends with `Command` (see example above)
94+
- Must derive `clap::Parser`
95+
- Must be attached to a parent module: if it's a top-level command: `src/lib.rs`, else: `src/command.rs`
96+
- May contain a `subcommand` field annotated with `#[command(subcommand)]`
97+
- Must have a `pub async fn run`
98+
- Must return a `Result` with `ExitCode`
99+
- If it contains a `subcommand` field: must match on `subcommand` and call `run` of each command
100+
101+
Command example:
102+
103+
- Name: `DbDownloadYcombinatorStartupsCommand`
104+
- File: `src/command/db_download_ycombinator_startups_command.rs` (attached to `src/command.rs`)
105+
- Shell command: `cargo run -- db download ycombinator-startups`
106+
107+
### Subcommand-like enum
108+
109+
An enum that contains variants for CLI subcommands.
110+
111+
- Must have a name that is a concatenation of all command names leading up to and including this command name, and ends with `Subcommand` (see example above)
112+
- Must derive `clap::Subcommand`
113+
- Must be located in the same file as its parent command struct
114+
- Each variant must be a tuple variant containing exactly one command
115+
116+
Subcommand example:
117+
118+
- Name: `DbDownloadSubcommand`
119+
- File: `src/cli/db_command/db_download_command.rs` (same file as its parent `DbDownloadCommand`)
120+
121+
### Proxy command-like struct
122+
123+
A [command-like struct](#command-like-struct) that has a `subcommand` field and calls `run` on each subcommand.
124+
125+
Proxy command example:
126+
127+
- Name: `DbCommand`
128+
- File: `src/command/db_command.rs` (attached to `src/command.rs`)

0 commit comments

Comments
 (0)