yuki_cli/client/
contact.rs1use quick_xml::Reader;
2use quick_xml::events::Event;
3
4use crate::error::YukiError;
5
6use super::local_name;
7use super::soap_client::{SoapClient, SoapEnvelope};
8
9const BASE_URL: &str = "https://api.yukiworks.nl/ws/Contact.asmx";
10
11#[derive(Debug, Clone)]
13pub struct Contact {
14 pub id: String,
15 pub name: String,
16 pub contact_type: String,
17 pub country: String,
18 pub is_supplier: bool,
19 pub is_customer: bool,
20}
21
22pub struct ContactClient {
24 soap: SoapClient,
25}
26
27impl ContactClient {
28 pub fn new() -> Self {
29 Self {
30 soap: SoapClient::new(BASE_URL),
31 }
32 }
33
34 fn require_session(&self) -> Result<&str, YukiError> {
35 self.soap.session_id().ok_or_else(|| {
36 YukiError::AuthFailed("not authenticated — call authenticate() first".to_string())
37 })
38 }
39
40 pub async fn authenticate(&mut self, api_key: &str) -> Result<String, YukiError> {
42 self.soap.authenticate(api_key).await
43 }
44
45 pub async fn search_contacts(&self, query: &str) -> Result<Vec<Contact>, YukiError> {
47 let session = self.require_session()?;
48 let envelope = SoapEnvelope::new("SearchContacts")
49 .session(session)
50 .param("searchQuery", query)
51 .build();
52 let body = self.soap.call("SearchContacts", envelope).await?;
53 parse_contacts(&body)
54 }
55
56 pub async fn get_suppliers_and_customers(
58 &self,
59 contact_type: &str,
60 ) -> Result<Vec<Contact>, YukiError> {
61 let session = self.require_session()?;
62 let envelope = SoapEnvelope::new("GetSuppliersAndCustomers")
63 .session(session)
64 .param("contactType", contact_type)
65 .build();
66 let body = self.soap.call("GetSuppliersAndCustomers", envelope).await?;
67 parse_contacts(&body)
68 }
69}
70
71pub fn parse_contacts(xml: &str) -> Result<Vec<Contact>, YukiError> {
76 let mut reader = Reader::from_str(xml);
77 reader.config_mut().trim_text(true);
78
79 let mut contacts = Vec::new();
80 let mut in_contact = false;
81 let mut current_field = String::new();
82 let mut contact = Contact {
83 id: String::new(),
84 name: String::new(),
85 contact_type: String::new(),
86 country: String::new(),
87 is_supplier: false,
88 is_customer: false,
89 };
90 let mut buf = Vec::new();
91
92 loop {
93 match reader.read_event_into(&mut buf) {
94 Ok(Event::Start(ref e)) => {
95 let local = local_name(e.name().as_ref()).to_string();
96 match local.as_str() {
97 "Contact" => {
98 in_contact = true;
99 contact = Contact {
100 id: String::new(),
101 name: String::new(),
102 contact_type: String::new(),
103 country: String::new(),
104 is_supplier: false,
105 is_customer: false,
106 };
107 for attr in e.attributes().flatten() {
108 if attr.key.as_ref() == b"ID" {
109 contact.id = String::from_utf8_lossy(&attr.value).to_string();
110 }
111 }
112 }
113 "Type" | "Name" | "Country" | "IsSupplier" | "IsCustomer" if in_contact => {
114 current_field = local;
115 }
116 _ => {}
117 }
118 }
119 Ok(Event::Text(ref e)) if in_contact && !current_field.is_empty() => {
120 let text = e
121 .unescape()
122 .map_err(|e| YukiError::Xml(e.to_string()))?
123 .trim()
124 .to_string();
125 match current_field.as_str() {
126 "Type" => contact.contact_type = text,
127 "Name" => contact.name = text,
128 "Country" => contact.country = text,
129 "IsSupplier" => contact.is_supplier = text.eq_ignore_ascii_case("true"),
130 "IsCustomer" => contact.is_customer = text.eq_ignore_ascii_case("true"),
131 _ => {}
132 }
133 }
134 Ok(Event::End(ref e)) => {
135 let name = e.name();
136 let local = local_name(name.as_ref());
137 match local {
138 "Type" | "Name" | "Country" | "IsSupplier" | "IsCustomer" => {
139 current_field.clear();
140 }
141 "Contact" => {
142 if !contact.id.is_empty() {
143 contacts.push(contact.clone());
144 }
145 in_contact = false;
146 }
147 _ => {}
148 }
149 }
150 Ok(Event::Eof) => break,
151 Err(e) => return Err(YukiError::Xml(e.to_string())),
152 _ => {}
153 }
154 buf.clear();
155 }
156
157 Ok(contacts)
158}
159
160impl Default for ContactClient {
161 fn default() -> Self {
162 Self::new()
163 }
164}