• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

dcdpr / jp / 18901345168

29 Oct 2025 08:15AM UTC coverage: 48.435% (+0.3%) from 48.126%
18901345168

push

github

web-flow
chore(tools): Replace array parameters with `OneOrMany` (#285)

This introduces a new generic `OneOrMany<T>` utility type that accepts
either a single value or a vector through serde's untagged enum
serialization. All tool parameter functions now use `OneOrMany<String>`
instead of `Vec<String>` for list parameters like paths, labels,
assignees, and file_diffs.

The change improves API ergonomics by allowing callers to pass either
`"single_value"` or `["multiple", "values"]` without requiring single
values to be wrapped in arrays. This maintains backward compatibility
while making the tool interfaces more flexible and user-friendly,
particularly for LLMs that tend to sometimes provide single values when
an array is expected.

---------

Signed-off-by: Jean Mertz <git@jeanmertz.com>

99 of 123 new or added lines in 7 files covered. (80.49%)

2 existing lines in 2 files now uncovered.

7658 of 15811 relevant lines covered (48.43%)

14.67 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

0.0
/.config/jp/tools/src/github/pulls.rs
1
use octocrab::{models::repos::DiffEntryStatus, params};
2
use time::OffsetDateTime;
3
use url::Url;
4

5
use super::auth;
6
use crate::{
7
    github::{handle_404, ORG, REPO},
8
    to_xml, to_xml_with_root,
9
    util::OneOrMany,
10
    Result,
11
};
12

13
/// The status of a issue or pull request.
14
#[derive(Debug, Clone, Copy, serde::Serialize, serde::Deserialize)]
15
#[serde(rename_all = "lowercase")]
16
pub enum State {
17
    Open,
18
    Closed,
19
}
20

21
pub(crate) async fn github_pulls(
×
22
    number: Option<u64>,
×
23
    state: Option<State>,
×
NEW
24
    file_diffs: Option<OneOrMany<String>>,
×
25
) -> Result<String> {
×
26
    auth().await?;
×
27

28
    let file_diffs = file_diffs.unwrap_or_default();
×
29

30
    match number {
×
NEW
31
        Some(number) if !file_diffs.is_empty() => diff(number, file_diffs.into_vec()).await,
×
32
        Some(number) => get(number).await,
×
33
        None => list(state).await,
×
34
    }
35
}
×
36

37
async fn get(number: u64) -> Result<String> {
×
38
    #[derive(serde::Serialize)]
39
    struct ChangedFile {
40
        filename: String,
41
        status: DiffEntryStatus,
42
        additions: u64,
43
        deletions: u64,
44
        changes: u64,
45
        previous_filename: Option<String>,
46
    }
47

48
    #[derive(serde::Serialize)]
49
    struct Pull {
50
        number: u64,
51
        title: Option<String>,
52
        body: Option<String>,
53
        url: Option<Url>,
54
        labels: Vec<String>,
55
        author: Option<String>,
56
        #[serde(with = "time::serde::rfc3339::option")]
57
        created_at: Option<OffsetDateTime>,
58
        #[serde(with = "time::serde::rfc3339::option")]
59
        closed_at: Option<OffsetDateTime>,
60
        #[serde(with = "time::serde::rfc3339::option")]
61
        merged_at: Option<OffsetDateTime>,
62
        merge_commit_sha: Option<String>,
63
        changed_files: Vec<ChangedFile>,
64
    }
65

66
    let pull = octocrab::instance()
×
67
        .pulls(ORG, REPO)
×
68
        .get(number)
×
69
        .await
×
70
        .map_err(|e| handle_404(e, format!("Pull #{number} not found in {ORG}/{REPO}")))?;
×
71

72
    let page = octocrab::instance()
×
73
        .pulls(ORG, REPO)
×
74
        .list_files(number)
×
75
        .await
×
76
        .map_err(|e| handle_404(e, format!("Pull #{number} not found in {ORG}/{REPO}")))?;
×
77

78
    let changed_files = octocrab::instance()
×
79
        .all_pages(page)
×
80
        .await?
×
81
        .into_iter()
×
82
        .map(|file| ChangedFile {
×
83
            filename: file.filename,
×
84
            status: file.status,
×
85
            additions: file.additions,
×
86
            deletions: file.deletions,
×
87
            changes: file.changes,
×
88
            previous_filename: file.previous_filename,
×
89
        })
×
90
        .collect();
×
91

92
    to_xml(Pull {
×
93
        number,
×
94
        title: pull.title,
×
95
        body: pull.body,
×
96
        url: pull.html_url,
×
97
        labels: pull
×
98
            .labels
×
99
            .into_iter()
×
100
            .flatten()
×
101
            .map(|label| label.name)
×
102
            .collect(),
×
103
        author: pull.user.map(|user| user.login),
×
104
        created_at: pull
×
105
            .created_at
×
106
            .map(|v| OffsetDateTime::from_unix_timestamp(v.timestamp()))
×
107
            .transpose()?,
×
108
        closed_at: pull
×
109
            .closed_at
×
110
            .map(|v| OffsetDateTime::from_unix_timestamp(v.timestamp()))
×
111
            .transpose()?,
×
112
        merged_at: pull
×
113
            .merged_at
×
114
            .map(|v| OffsetDateTime::from_unix_timestamp(v.timestamp()))
×
115
            .transpose()?,
×
116
        merge_commit_sha: pull.merge_commit_sha,
×
117
        changed_files,
×
118
    })
119
}
×
120

121
async fn diff(number: u64, file_diffs: Vec<String>) -> Result<String> {
×
122
    #[derive(serde::Serialize)]
123
    struct ChangedFile {
124
        filename: String,
125
        status: DiffEntryStatus,
126
        additions: u64,
127
        deletions: u64,
128
        changes: u64,
129
        previous_filename: Option<String>,
130
        patch: Option<String>,
131
    }
132

133
    let page = octocrab::instance()
×
134
        .pulls(ORG, REPO)
×
135
        .list_files(number)
×
136
        .await
×
137
        .map_err(|e| handle_404(e, format!("Pull #{number} not found in {ORG}/{REPO}")))?;
×
138

139
    let changed_files: Vec<_> = octocrab::instance()
×
140
        .all_pages(page)
×
141
        .await?
×
142
        .into_iter()
×
143
        .filter(|file| file_diffs.contains(&file.filename))
×
144
        .map(|file| ChangedFile {
×
145
            patch: file.patch,
×
146
            filename: file.filename,
×
147
            status: file.status,
×
148
            additions: file.additions,
×
149
            deletions: file.deletions,
×
150
            changes: file.changes,
×
151
            previous_filename: file.previous_filename,
×
152
        })
×
153
        .collect();
×
154

155
    to_xml_with_root(&changed_files, "files")
×
156
}
×
157

158
async fn list(state: Option<State>) -> Result<String> {
×
159
    #[derive(serde::Serialize)]
160
    struct Pulls {
161
        pull: Vec<Pull>,
162
    }
163

164
    #[derive(serde::Serialize)]
165
    struct Pull {
166
        number: u64,
167
        title: Option<String>,
168
        url: Option<Url>,
169
        labels: Vec<String>,
170
        author: Option<String>,
171
        #[serde(with = "time::serde::rfc3339::option")]
172
        created_at: Option<OffsetDateTime>,
173
        #[serde(with = "time::serde::rfc3339::option")]
174
        closed_at: Option<OffsetDateTime>,
175
        #[serde(with = "time::serde::rfc3339::option")]
176
        merged_at: Option<OffsetDateTime>,
177
        merge_commit_sha: Option<String>,
178
    }
179

180
    let state = match state {
×
181
        Some(State::Open) => params::State::Open,
×
182
        Some(State::Closed) => params::State::Closed,
×
183
        None => params::State::All,
×
184
    };
185

186
    let page = octocrab::instance()
×
187
        .pulls(ORG, REPO)
×
188
        .list()
×
189
        .state(state)
×
190
        .per_page(100)
×
191
        .send()
×
192
        .await?;
×
193

194
    let pull = octocrab::instance()
×
195
        .all_pages(page)
×
196
        .await?
×
197
        .into_iter()
×
198
        .map(|pull| {
×
199
            Ok(Pull {
200
                number: pull.number,
×
201
                title: pull.title,
×
202
                url: pull.html_url,
×
203
                labels: pull
×
204
                    .labels
×
205
                    .into_iter()
×
206
                    .flatten()
×
207
                    .map(|label| label.name)
×
208
                    .collect(),
×
209
                author: pull.user.map(|user| user.login),
×
210
                created_at: pull
×
211
                    .created_at
×
212
                    .map(|v| OffsetDateTime::from_unix_timestamp(v.timestamp()))
×
213
                    .transpose()?,
×
214
                closed_at: pull
×
215
                    .closed_at
×
216
                    .map(|v| OffsetDateTime::from_unix_timestamp(v.timestamp()))
×
217
                    .transpose()?,
×
218
                merged_at: pull
×
219
                    .merged_at
×
220
                    .map(|v| OffsetDateTime::from_unix_timestamp(v.timestamp()))
×
221
                    .transpose()?,
×
222
                merge_commit_sha: pull.merge_commit_sha,
×
223
            })
224
        })
×
225
        .collect::<Result<_>>()?;
×
226

227
    to_xml(Pulls { pull })
×
228
}
×
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc