use std::borrow::Cow;
use serde::Deserialize;
use super::BugzillaClient;
use crate::error::{BzrError, Result};
use crate::types::FieldValue;
pub(crate) const FIELD_ALIASES: &[(&str, &str)] = &[
("file_loc", "bug_file_loc"),
("group", "bug_group"),
("id", "bug_id"),
("severity", "bug_severity"),
("status", "bug_status"),
("type", "bug_type"),
];
#[derive(Deserialize)]
struct FieldBugResponse {
fields: Vec<FieldEntry>,
}
#[derive(Deserialize)]
struct FieldEntry {
values: Vec<FieldValue>,
}
fn resolve_field_alias(name: &str) -> Cow<'_, str> {
let lower = name.to_ascii_lowercase();
for &(alias, api_name) in FIELD_ALIASES {
if lower == alias {
return Cow::Borrowed(api_name);
}
}
Cow::Borrowed(name)
}
impl BugzillaClient {
pub async fn get_field_values(&self, field_name: &str) -> Result<Vec<FieldValue>> {
let resolved = resolve_field_alias(field_name);
let data: FieldBugResponse = self.get_json(&format!("field/bug/{resolved}")).await?;
let field = data
.fields
.into_iter()
.next()
.ok_or_else(|| BzrError::NotFound {
resource: "field",
id: field_name.to_string(),
})?;
Ok(field.values)
}
}
#[cfg(test)]
#[expect(clippy::unwrap_used)]
mod tests {
use wiremock::matchers::{method, path};
use wiremock::{Mock, MockServer, ResponseTemplate};
use crate::client::test_helpers::test_client;
use crate::error::BzrError;
#[test]
fn resolve_field_alias_maps_status() {
assert_eq!(super::resolve_field_alias("status").as_ref(), "bug_status");
}
#[test]
fn resolve_field_alias_maps_severity() {
assert_eq!(
super::resolve_field_alias("severity").as_ref(),
"bug_severity"
);
}
#[test]
fn resolve_field_alias_maps_id() {
assert_eq!(super::resolve_field_alias("id").as_ref(), "bug_id");
}
#[test]
fn resolve_field_alias_maps_type() {
assert_eq!(super::resolve_field_alias("type").as_ref(), "bug_type");
}
#[test]
fn resolve_field_alias_maps_group() {
assert_eq!(super::resolve_field_alias("group").as_ref(), "bug_group");
}
#[test]
fn resolve_field_alias_maps_file_loc() {
assert_eq!(
super::resolve_field_alias("file_loc").as_ref(),
"bug_file_loc"
);
}
#[test]
fn resolve_field_alias_passes_through_unknown() {
assert_eq!(super::resolve_field_alias("priority").as_ref(), "priority");
}
#[test]
fn resolve_field_alias_passes_through_already_prefixed() {
assert_eq!(
super::resolve_field_alias("bug_status").as_ref(),
"bug_status"
);
}
#[test]
fn resolve_field_alias_is_case_insensitive() {
assert_eq!(super::resolve_field_alias("Status").as_ref(), "bug_status");
assert_eq!(
super::resolve_field_alias("SEVERITY").as_ref(),
"bug_severity"
);
}
#[tokio::test]
async fn get_field_values_returns_values() {
let mock = MockServer::start().await;
Mock::given(method("GET"))
.and(path("/rest/field/bug/bug_status"))
.respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
"fields": [{
"values": [
{"name": "NEW", "sort_key": 100, "is_active": true, "can_change_to": [{"name": "ASSIGNED"}, {"name": "RESOLVED"}]},
{"name": "RESOLVED", "sort_key": 500, "is_active": true}
]
}]
})))
.mount(&mock)
.await;
let client = test_client(&mock.uri());
let values = client.get_field_values("status").await.unwrap();
assert_eq!(values.len(), 2);
assert_eq!(values[0].name, "NEW");
let transitions = values[0].can_change_to.as_ref().unwrap();
assert_eq!(transitions.len(), 2);
assert_eq!(transitions[0].name, "ASSIGNED");
}
#[tokio::test]
async fn get_field_values_resolves_severity_alias() {
let mock = MockServer::start().await;
Mock::given(method("GET"))
.and(path("/rest/field/bug/bug_severity"))
.respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
"fields": [{
"values": [
{"name": "blocker", "sort_key": 100, "is_active": true},
{"name": "normal", "sort_key": 200, "is_active": true}
]
}]
})))
.mount(&mock)
.await;
let client = test_client(&mock.uri());
let values = client.get_field_values("severity").await.unwrap();
assert_eq!(values.len(), 2);
assert_eq!(values[0].name, "blocker");
}
#[tokio::test]
async fn get_field_values_unrecognized_field_returns_not_found() {
let mock = MockServer::start().await;
Mock::given(method("GET"))
.and(path("/rest/field/bug/nonexistent"))
.respond_with(
ResponseTemplate::new(200).set_body_json(serde_json::json!({"fields": []})),
)
.mount(&mock)
.await;
let client = test_client(&mock.uri());
let err = client.get_field_values("nonexistent").await.unwrap_err();
assert!(
matches!(
err,
BzrError::NotFound {
resource: "field",
..
}
),
"expected NotFound, got: {err}"
);
}
}