1mod groups;
2mod library;
3mod search;
4mod settings;
5mod sources;
6mod transport;
7mod user_directory;
8
9use std::{sync::Arc, time::Duration};
10
11use context69_contracts::{AuthMeResponse, HealthResponse};
12use reqwest::{Method, Url, header::USER_AGENT};
13use tokio::sync::RwLock;
14
15use crate::Error;
16
17pub use groups::{
18 GroupApi, GroupChildrenApi, GroupMemberApi, GroupMembersApi, GroupSourceFolderApi,
19 GroupSourceFoldersApi, GroupsApi,
20};
21pub use library::{
22 GroupLibraryApi, GroupLibraryTextsApi, LibraryApi, LibraryFileApi, LibraryFilesApi,
23 LibraryFolderApi, LibraryFoldersApi, LibraryJobApi, LibraryTextsApi,
24};
25pub use search::{DocumentApi, SearchApi};
26pub use settings::{
27 DoclingSettingsApi, ProviderAccountApi, ProviderAccountsApi, RuntimeSettingsApi,
28 SearchSettingsApi, SettingsApi,
29};
30pub use sources::{SourceApi, SourceConnectionApi, SourceConnectionsApi, SourcesApi};
31pub use user_directory::UserDirectoryApi;
32
33pub(crate) const PERSONAL_ACCESS_TOKEN_PREFIX: &str = "ctx_pat_";
34
35#[derive(Debug, Clone, Default)]
36struct SessionState {
37 personal_access_token: Option<String>,
38}
39
40#[derive(Clone)]
41pub struct Context69Client {
42 client: reqwest::Client,
43 base_url: Url,
44 session: Arc<RwLock<SessionState>>,
45}
46
47#[derive(Debug)]
48pub struct Context69ClientBuilder {
49 base_url: Option<Url>,
50 user_agent: Option<String>,
51 timeout: Option<Duration>,
52 personal_access_token: Option<String>,
53}
54
55impl Context69Client {
56 pub fn builder() -> Context69ClientBuilder {
57 Context69ClientBuilder {
58 base_url: None,
59 user_agent: None,
60 timeout: None,
61 personal_access_token: None,
62 }
63 }
64
65 pub fn with_personal_access_token(
66 &self,
67 token: impl Into<String>,
68 ) -> Result<Context69Client, Error> {
69 Ok(Context69Client {
70 client: self.client.clone(),
71 base_url: self.base_url.clone(),
72 session: Arc::new(RwLock::new(SessionState {
73 personal_access_token: Some(transport::validate_personal_access_token(
74 token.into(),
75 )?),
76 })),
77 })
78 }
79
80 pub fn user_directory(&self) -> UserDirectoryApi<'_> {
81 UserDirectoryApi::new(self)
82 }
83
84 pub fn groups(&self) -> GroupsApi<'_> {
85 GroupsApi::new(self)
86 }
87
88 pub fn group(&self, group_path: impl Into<String>) -> GroupApi<'_> {
89 GroupApi::new(self, group_path.into())
90 }
91
92 pub fn sources(&self) -> SourcesApi<'_> {
93 SourcesApi::new(self)
94 }
95
96 pub fn source(&self, source_key: impl Into<String>) -> SourceApi<'_> {
97 SourceApi::new(self, source_key.into())
98 }
99
100 pub fn source_connections(&self) -> SourceConnectionsApi<'_> {
101 SourceConnectionsApi::new(self)
102 }
103
104 pub fn source_connection(&self, name: impl Into<String>) -> SourceConnectionApi<'_> {
105 SourceConnectionApi::new(self, name.into())
106 }
107
108 pub fn library(&self) -> LibraryApi<'_> {
109 LibraryApi::new(self)
110 }
111
112 pub fn settings(&self) -> SettingsApi<'_> {
113 SettingsApi::new(self)
114 }
115
116 pub fn search(&self) -> SearchApi<'_> {
117 SearchApi::new(self)
118 }
119
120 pub fn document(&self, document_id: i64) -> DocumentApi<'_> {
121 DocumentApi::new(self, document_id)
122 }
123
124 pub async fn me(&self) -> Result<AuthMeResponse, Error> {
125 self.execute_json(self.authorized_request(Method::GET, "/v1/auth/me").await?)
126 .await
127 }
128
129 pub async fn healthz(&self) -> Result<HealthResponse, Error> {
130 let response = self.client.get(self.url("/healthz")?).send().await?;
131 self.read_json_response(response).await
132 }
133}
134
135impl Context69ClientBuilder {
136 pub fn base_url(mut self, base_url: &str) -> Result<Self, Error> {
137 let mut url =
138 Url::parse(base_url).map_err(|_| Error::InvalidBaseUrl(base_url.to_string()))?;
139 if !url.path().ends_with('/') {
140 url.set_path(&format!("{}/", url.path()));
141 }
142 self.base_url = Some(url);
143 Ok(self)
144 }
145
146 pub fn user_agent(mut self, user_agent: impl Into<String>) -> Self {
147 self.user_agent = Some(user_agent.into());
148 self
149 }
150
151 pub fn timeout(mut self, timeout: Duration) -> Result<Self, Error> {
152 if timeout.is_zero() {
153 return Err(Error::InvalidTimeout(timeout));
154 }
155 self.timeout = Some(timeout);
156 Ok(self)
157 }
158
159 pub fn with_personal_access_token(mut self, token: impl Into<String>) -> Result<Self, Error> {
160 self.personal_access_token = Some(transport::validate_personal_access_token(token.into())?);
161 Ok(self)
162 }
163
164 pub fn build(self) -> Result<Context69Client, Error> {
165 let base_url = self
166 .base_url
167 .ok_or_else(|| Error::InvalidBaseUrl("missing base_url".to_string()))?;
168 let mut builder = reqwest::Client::builder();
169 if let Some(user_agent) = self.user_agent {
170 builder = builder.default_headers({
171 let mut headers = reqwest::header::HeaderMap::new();
172 headers.insert(
173 USER_AGENT,
174 user_agent
175 .parse()
176 .map_err(|_| Error::InvalidHeader(user_agent.clone()))?,
177 );
178 headers
179 });
180 }
181 if let Some(timeout) = self.timeout {
182 builder = builder.timeout(timeout);
183 }
184 Ok(Context69Client {
185 client: builder.build()?,
186 base_url,
187 session: Arc::new(RwLock::new(SessionState {
188 personal_access_token: self.personal_access_token,
189 })),
190 })
191 }
192}
193
194#[cfg(test)]
195mod tests;