Skip to main content

origin_gamedb/
client.rs

1use std::path::Path;
2
3use reqwest::multipart::{Form, Part};
4use serde_json::{json, Value};
5
6use crate::{
7    error::{OriginError, Result},
8    transport::HttpTransport,
9    CognifyOptions, SearchOptions, SearchType, DEFAULT_BASE_URL,
10};
11
12/// Builder for configuring a [`GameDbClient`].
13#[derive(Debug, Clone)]
14pub struct GameDbClientBuilder {
15    token: String,
16    base_url: String,
17    client: Option<reqwest::Client>,
18}
19
20impl GameDbClientBuilder {
21    pub fn new(token: impl Into<String>) -> Self {
22        Self {
23            token: token.into(),
24            base_url: DEFAULT_BASE_URL.to_string(),
25            client: None,
26        }
27    }
28
29    /// Override the GameDB API base URL.
30    pub fn base_url(mut self, url: impl Into<String>) -> Self {
31        self.base_url = url.into();
32        self
33    }
34
35    /// Use a custom `reqwest::Client` (e.g. custom timeout, proxy, TLS settings).
36    pub fn http_client(mut self, client: reqwest::Client) -> Self {
37        self.client = Some(client);
38        self
39    }
40
41    /// Build the [`GameDbClient`].
42    pub fn build(self) -> GameDbClient {
43        let transport = HttpTransport::new(self.base_url, self.token);
44        let transport = if let Some(client) = self.client {
45            transport.with_client(client)
46        } else {
47            transport
48        };
49
50        GameDbClient::from_transport(transport)
51    }
52}
53
54/// Client for the Origin GameDB knowledge graph service.
55#[derive(Debug, Clone)]
56pub struct GameDbClient {
57    pub(crate) transport: HttpTransport,
58}
59
60impl GameDbClient {
61    /// Create a client with the default GameDB API URL.
62    pub fn new(token: impl Into<String>) -> Self {
63        Self::builder(token).build()
64    }
65
66    /// Create a builder for fine-grained configuration.
67    pub fn builder(token: impl Into<String>) -> GameDbClientBuilder {
68        GameDbClientBuilder::new(token)
69    }
70
71    pub fn from_transport(transport: HttpTransport) -> Self {
72        Self { transport }
73    }
74
75    pub async fn health(&self) -> Result<bool> {
76        self.transport.get("/health").await
77    }
78
79    pub async fn health_detailed(&self) -> Result<Value> {
80        self.transport.get("/health/detailed").await
81    }
82
83    pub async fn datasets(&self) -> Result<Value> {
84        self.transport.get("/api/v1/datasets").await
85    }
86
87    pub async fn create_dataset(&self, name: &str) -> Result<Value> {
88        self.transport
89            .post("/api/v1/datasets", &json!({ "name": name }))
90            .await
91    }
92
93    pub async fn delete_dataset(&self, id: &str) -> Result<Value> {
94        self.transport
95            .delete(&format!("/api/v1/datasets/{id}"))
96            .await
97    }
98
99    pub async fn delete_all_datasets(&self) -> Result<Value> {
100        self.transport.delete("/api/v1/datasets").await
101    }
102
103    pub async fn dataset_status(&self) -> Result<Value> {
104        self.transport.get("/api/v1/datasets/status").await
105    }
106
107    pub async fn dataset_graph(&self, id: &str) -> Result<Value> {
108        self.transport
109            .get(&format!("/api/v1/datasets/{id}/graph"))
110            .await
111    }
112
113    pub async fn dataset_data(&self, id: &str) -> Result<Value> {
114        self.transport
115            .get(&format!("/api/v1/datasets/{id}/data"))
116            .await
117    }
118
119    pub async fn delete_data(&self, dataset_id: &str, data_id: &str) -> Result<Value> {
120        self.transport
121            .delete(&format!("/api/v1/datasets/{dataset_id}/data/{data_id}"))
122            .await
123    }
124
125    pub async fn add_text(&self, dataset_name: &str, content: &str) -> Result<Value> {
126        let form = Form::new()
127            .text("datasetName", dataset_name.to_owned())
128            .part(
129                "data",
130                Part::text(content.to_owned())
131                    .file_name("inline.txt")
132                    .mime_str("text/plain")?,
133            );
134
135        self.transport.post_multipart("/api/v1/add", form).await
136    }
137
138    pub async fn add_file(&self, dataset_name: &str, file_path: &str) -> Result<Value> {
139        let bytes = tokio::fs::read(file_path).await?;
140        let file_name = Path::new(file_path)
141            .file_name()
142            .and_then(|name| name.to_str())
143            .ok_or_else(|| OriginError::config(format!("invalid file path: {file_path}")))?
144            .to_owned();
145
146        let form = Form::new()
147            .text("datasetName", dataset_name.to_owned())
148            .part("data", Part::bytes(bytes).file_name(file_name));
149
150        self.transport.post_multipart("/api/v1/add", form).await
151    }
152
153    pub async fn cognify(&self, options: &CognifyOptions) -> Result<Value> {
154        self.transport
155            .post("/api/v1/cognify", &json!(options))
156            .await
157    }
158
159    pub async fn search(&self, query: &str, options: Option<SearchOptions>) -> Result<Value> {
160        let options = options.unwrap_or_default();
161        let mut body = json!({
162            "query": query,
163            "search_type": options.search_type.unwrap_or(SearchType::GraphCompletion),
164            "top_k": options.top_k.unwrap_or(5),
165        });
166
167        if let Some(datasets) = options.datasets.filter(|datasets| !datasets.is_empty()) {
168            body["datasets"] = json!(datasets);
169        }
170
171        self.transport.post("/api/v1/search", &body).await
172    }
173
174    pub async fn search_history(&self) -> Result<Value> {
175        self.transport.get("/api/v1/search").await
176    }
177
178    pub async fn settings(&self) -> Result<Value> {
179        self.transport.get("/api/v1/settings").await
180    }
181
182    pub async fn save_settings(&self, settings: &Value) -> Result<Value> {
183        self.transport.post("/api/v1/settings", settings).await
184    }
185
186    pub async fn ontologies(&self) -> Result<Value> {
187        self.transport.get("/api/v1/ontologies").await
188    }
189}