use serde_json::Value;
use tail_fin_common::TailFinError;
use crate::types::*;
pub fn parse_search_result(data: &Value) -> Result<SearchResult, TailFinError> {
let query = data
.get("query")
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string();
let page = data.get("page").and_then(|v| v.as_u64()).unwrap_or(1) as u32;
let total_records = data
.get("total_records")
.and_then(|v| v.as_u64())
.unwrap_or(0);
let total_pages = data
.get("total_pages")
.and_then(|v| v.as_u64())
.unwrap_or(0) as u32;
let records = data
.get("records")
.and_then(|v| v.as_array())
.map(|arr| arr.iter().filter_map(parse_record).collect())
.unwrap_or_default();
Ok(SearchResult {
query,
page,
total_records,
total_pages,
records,
})
}
fn parse_record(v: &Value) -> Option<TenderRecord> {
let brief = v.get("brief").unwrap_or(v);
let companies_obj = brief.get("companies").unwrap_or(&Value::Null);
Some(TenderRecord {
date: v
.get("date")
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string(),
filename: v
.get("filename")
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string(),
job_number: v
.get("job_number")
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string(),
unit_id: v
.get("unit_id")
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string(),
unit_name: v
.get("unit_name")
.and_then(|v| v.as_str())
.or_else(|| v.get("機關資料:機關名稱").and_then(|v| v.as_str()))
.unwrap_or("")
.to_string(),
title: brief
.get("title")
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string(),
tender_type: brief
.get("type")
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string(),
companies: CompanyBrief {
ids: companies_obj
.get("ids")
.and_then(|v| v.as_array())
.map(|a| {
a.iter()
.filter_map(|v| v.as_str().map(|s| s.to_string()))
.collect()
})
.unwrap_or_default(),
names: companies_obj
.get("names")
.and_then(|v| v.as_array())
.map(|a| {
a.iter()
.filter_map(|v| v.as_str().map(|s| s.to_string()))
.collect()
})
.unwrap_or_default(),
},
url: v
.get("url")
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string(),
tender_api_url: v
.get("tender_api_url")
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string(),
})
}
pub fn parse_info(data: &Value) -> Result<PccInfo, TailFinError> {
Ok(PccInfo {
latest_date: data
.get("最新資料時間")
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string(),
oldest_date: data
.get("最舊資料時間")
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string(),
total_records: data.get("公告數").and_then(|v| v.as_u64()).unwrap_or(0),
})
}
pub fn parse_units(data: &Value) -> Vec<Unit> {
data.as_array()
.map(|arr| {
arr.iter()
.filter_map(|v| {
Some(Unit {
unit_id: v.get("unit_id").and_then(|v| v.as_str())?.to_string(),
name: v
.get("name")
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string(),
url: v
.get("url")
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string(),
})
})
.collect()
})
.unwrap_or_default()
}
pub fn parse_budgets(data: &Value) -> Vec<SpecialBudget> {
data.get("budgets")
.and_then(|v| v.as_array())
.map(|arr| {
arr.iter()
.filter_map(|v| {
let url = v
.get("search_api_url")
.and_then(|v| v.as_str())?
.to_string();
let name = url
.split("query=")
.nth(1)
.map(|s| urlencoding::decode(s).unwrap_or_default().to_string())
.unwrap_or_default();
Some(SpecialBudget {
name,
search_api_url: url,
})
})
.collect()
})
.unwrap_or_default()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_search_result() {
let data = serde_json::json!({
"query": "電腦",
"page": 1,
"total_records": 100,
"total_pages": 2,
"records": [{
"date": "20240101",
"filename": "BDM-1-123",
"job_number": "abc123",
"unit_id": "3.76",
"unit_name": "彰化縣政府",
"brief": {
"type": "決標公告",
"title": "電腦設備採購",
"companies": {
"ids": ["12345678"],
"names": ["台灣電腦公司"]
}
},
"url": "https://web.pcc.gov.tw/...",
"tender_api_url": "https://pcc.g0v.ronny.tw/api/tender?unit_id=3.76&job_number=abc123"
}]
});
let result = parse_search_result(&data).unwrap();
assert_eq!(result.query, "電腦");
assert_eq!(result.total_records, 100);
assert_eq!(result.records.len(), 1);
assert_eq!(result.records[0].title, "電腦設備採購");
assert_eq!(result.records[0].companies.names, vec!["台灣電腦公司"]);
}
#[test]
fn test_parse_search_empty() {
let data = serde_json::json!({
"query": "xxx",
"page": 1,
"total_records": 0,
"total_pages": 0,
"records": []
});
let result = parse_search_result(&data).unwrap();
assert_eq!(result.total_records, 0);
assert!(result.records.is_empty());
}
#[test]
fn test_parse_info() {
let data = serde_json::json!({
"最新資料時間": "2024-01-01T00:00:00+08:00",
"最舊資料時間": "2010-01-01T00:00:00+08:00",
"公告數": 5000000
});
let info = parse_info(&data).unwrap();
assert_eq!(info.total_records, 5000000);
assert!(info.latest_date.contains("2024"));
}
#[test]
fn test_parse_units() {
let data = serde_json::json!([
{ "unit_id": "3.76", "name": "彰化縣政府", "url": "https://..." },
{ "unit_id": "A.17", "name": "台北市政府", "url": "https://..." }
]);
let units = parse_units(&data);
assert_eq!(units.len(), 2);
assert_eq!(units[0].unit_id, "3.76");
}
#[test]
fn test_parse_budgets() {
let data = serde_json::json!({
"budgets": [
{ "search_api_url": "https://pcc.g0v.ronny.tw/api/searchbyspecialbudget?query=%E5%89%8D%E7%9E%BB" }
]
});
let budgets = parse_budgets(&data);
assert_eq!(budgets.len(), 1);
assert_eq!(budgets[0].name, "前瞻");
}
}