Skip to content

Commit bbed7b1

Browse files
refactor(client): extract API paths and use typed HTTP headers (#11)
Replace hardcoded API path strings with named constants and path-building helper functions. Replace string-literal HTTP headers with http::header typed constants (AUTHORIZATION, USER_AGENT, CONTENT_TYPE). Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 7e70863 commit bbed7b1

1 file changed

Lines changed: 55 additions & 23 deletions

File tree

crates/dkdc-md-cli/src/client.rs

Lines changed: 55 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,16 @@ use ureq::{Agent, http};
88

99
const BASE_URL: &str = "https://api.motherduck.com";
1010
const TIMEOUT: Duration = Duration::from_secs(10);
11-
const USER_AGENT: &str = concat!("dkdc-md-cli/", env!("CARGO_PKG_VERSION"));
11+
const USER_AGENT_VALUE: &str = concat!("dkdc-md-cli/", env!("CARGO_PKG_VERSION"));
1212
const SUCCESS_STATUS: std::ops::Range<u16> = 200..300;
13+
const CONTENT_TYPE_JSON: &str = "application/json";
14+
15+
// API path segments
16+
const API_V1: &str = "/v1";
17+
const USERS: &str = "users";
18+
const TOKENS: &str = "tokens";
19+
const INSTANCES: &str = "instances";
20+
const ACTIVE_ACCOUNTS: &str = "active_accounts";
1321

1422
/// Characters that must be percent-encoded in a URL path segment.
1523
const PATH_SEGMENT: &AsciiSet = &CONTROLS.add(b' ').add(b'#').add(b'%').add(b'/').add(b'?');
@@ -18,6 +26,34 @@ fn encode_path(s: &str) -> String {
1826
utf8_percent_encode(s, PATH_SEGMENT).to_string()
1927
}
2028

29+
fn users_path() -> String {
30+
format!("{API_V1}/{USERS}")
31+
}
32+
33+
fn user_path(username: &str) -> String {
34+
format!("{API_V1}/{USERS}/{}", encode_path(username))
35+
}
36+
37+
fn user_tokens_path(username: &str) -> String {
38+
format!("{API_V1}/{USERS}/{}/{TOKENS}", encode_path(username))
39+
}
40+
41+
fn user_token_path(username: &str, token_id: &str) -> String {
42+
format!(
43+
"{API_V1}/{USERS}/{}/{TOKENS}/{}",
44+
encode_path(username),
45+
encode_path(token_id),
46+
)
47+
}
48+
49+
fn user_instances_path(username: &str) -> String {
50+
format!("{API_V1}/{USERS}/{}/{INSTANCES}", encode_path(username))
51+
}
52+
53+
fn active_accounts_path() -> String {
54+
format!("{API_V1}/{ACTIVE_ACCOUNTS}")
55+
}
56+
2157
pub struct MotherduckClient {
2258
agent: Agent,
2359
bearer: String,
@@ -60,8 +96,8 @@ impl MotherduckClient {
6096
let resp = self
6197
.agent
6298
.get(&url)
63-
.header("Authorization", &self.bearer)
64-
.header("User-Agent", USER_AGENT)
99+
.header(http::header::AUTHORIZATION, &self.bearer)
100+
.header(http::header::USER_AGENT, USER_AGENT_VALUE)
65101
.call()
66102
.context("request failed")?;
67103
handle_response(resp).with_context(|| format!("GET {path}"))
@@ -72,8 +108,8 @@ impl MotherduckClient {
72108
let resp = self
73109
.agent
74110
.delete(&url)
75-
.header("Authorization", &self.bearer)
76-
.header("User-Agent", USER_AGENT)
111+
.header(http::header::AUTHORIZATION, &self.bearer)
112+
.header(http::header::USER_AGENT, USER_AGENT_VALUE)
77113
.call()
78114
.context("request failed")?;
79115
handle_response(resp).with_context(|| format!("DELETE {path}"))
@@ -85,9 +121,9 @@ impl MotherduckClient {
85121
let resp = self
86122
.agent
87123
.post(&url)
88-
.header("Authorization", &self.bearer)
89-
.header("User-Agent", USER_AGENT)
90-
.header("Content-Type", "application/json")
124+
.header(http::header::AUTHORIZATION, &self.bearer)
125+
.header(http::header::USER_AGENT, USER_AGENT_VALUE)
126+
.header(http::header::CONTENT_TYPE, CONTENT_TYPE_JSON)
91127
.send(&bytes)
92128
.context("request failed")?;
93129
handle_response(resp).with_context(|| format!("POST {path}"))
@@ -99,9 +135,9 @@ impl MotherduckClient {
99135
let resp = self
100136
.agent
101137
.put(&url)
102-
.header("Authorization", &self.bearer)
103-
.header("User-Agent", USER_AGENT)
104-
.header("Content-Type", "application/json")
138+
.header(http::header::AUTHORIZATION, &self.bearer)
139+
.header(http::header::USER_AGENT, USER_AGENT_VALUE)
140+
.header(http::header::CONTENT_TYPE, CONTENT_TYPE_JSON)
105141
.send(&bytes)
106142
.context("request failed")?;
107143
handle_response(resp).with_context(|| format!("PUT {path}"))
@@ -110,17 +146,17 @@ impl MotherduckClient {
110146
// -- Users --
111147

112148
pub fn create_user(&self, username: &str) -> Result<Value> {
113-
self.post_json("/v1/users", &json!({"username": username}))
149+
self.post_json(&users_path(), &json!({"username": username}))
114150
}
115151

116152
pub fn delete_user(&self, username: &str) -> Result<Value> {
117-
self.delete(&format!("/v1/users/{}", encode_path(username)))
153+
self.delete(&user_path(username))
118154
}
119155

120156
// -- Tokens --
121157

122158
pub fn list_tokens(&self, username: &str) -> Result<Value> {
123-
self.get(&format!("/v1/users/{}/tokens", encode_path(username)))
159+
self.get(&user_tokens_path(username))
124160
}
125161

126162
pub fn create_token(
@@ -131,7 +167,7 @@ impl MotherduckClient {
131167
token_type: Option<&str>,
132168
) -> Result<Value> {
133169
self.post_json(
134-
&format!("/v1/users/{}/tokens", encode_path(username)),
170+
&user_tokens_path(username),
135171
&CreateTokenRequest {
136172
name,
137173
ttl,
@@ -141,17 +177,13 @@ impl MotherduckClient {
141177
}
142178

143179
pub fn delete_token(&self, username: &str, token_id: &str) -> Result<Value> {
144-
self.delete(&format!(
145-
"/v1/users/{}/tokens/{}",
146-
encode_path(username),
147-
encode_path(token_id),
148-
))
180+
self.delete(&user_token_path(username, token_id))
149181
}
150182

151183
// -- Ducklings --
152184

153185
pub fn get_duckling_config(&self, username: &str) -> Result<Value> {
154-
self.get(&format!("/v1/users/{}/instances", encode_path(username)))
186+
self.get(&user_instances_path(username))
155187
}
156188

157189
pub fn set_duckling_config(
@@ -162,7 +194,7 @@ impl MotherduckClient {
162194
rs_flock_size: u32,
163195
) -> Result<Value> {
164196
self.put_json(
165-
&format!("/v1/users/{}/instances", encode_path(username)),
197+
&user_instances_path(username),
166198
&json!({
167199
"config": {
168200
"read_write": { "instance_size": rw_size },
@@ -175,7 +207,7 @@ impl MotherduckClient {
175207
// -- Accounts --
176208

177209
pub fn list_active_accounts(&self) -> Result<Value> {
178-
self.get("/v1/active_accounts")
210+
self.get(&active_accounts_path())
179211
}
180212
}
181213

0 commit comments

Comments
 (0)