Skip to main content

source_map_php/
meili.rs

1use anyhow::{Context, Result, anyhow};
2use reqwest::blocking::Client;
3use serde::Serialize;
4use serde_json::{Value, json};
5
6use crate::config::MeiliConnection;
7use crate::models::SearchResponse;
8
9#[derive(Debug, Clone)]
10pub struct MeiliClient {
11    client: Client,
12    connection: MeiliConnection,
13}
14
15impl MeiliClient {
16    pub fn new(connection: MeiliConnection) -> Result<Self> {
17        Ok(Self {
18            client: Client::builder().build()?,
19            connection,
20        })
21    }
22
23    pub fn health(&self) -> Result<Value> {
24        self.get("health")
25    }
26
27    pub fn create_index(&self, name: &str) -> Result<()> {
28        let response = self
29            .client
30            .post(self.url("indexes")?)
31            .bearer_auth(&self.connection.api_key)
32            .json(&json!({ "uid": name }))
33            .send()?;
34        if response.status().is_success() || response.status().as_u16() == 409 {
35            return Ok(());
36        }
37        Err(anyhow!(
38            "failed to create index {name}: {}",
39            response.text()?
40        ))
41    }
42
43    pub fn delete_index(&self, name: &str) -> Result<()> {
44        let response = self
45            .client
46            .delete(self.url(&format!("indexes/{name}"))?)
47            .bearer_auth(&self.connection.api_key)
48            .send()?;
49        if response.status().is_success() || response.status().as_u16() == 404 {
50            return Ok(());
51        }
52        Err(anyhow!(
53            "failed to delete index {name}: {}",
54            response.text()?
55        ))
56    }
57
58    pub fn apply_settings(&self, index: &str, settings: &Value) -> Result<()> {
59        let task = self
60            .client
61            .patch(self.url(&format!("indexes/{index}/settings"))?)
62            .bearer_auth(&self.connection.api_key)
63            .json(settings)
64            .send()?
65            .json::<Value>()?;
66        self.wait_for_task(task_uid(&task)?)?;
67        Ok(())
68    }
69
70    pub fn replace_documents<T: Serialize>(&self, index: &str, documents: &[T]) -> Result<()> {
71        let task = self
72            .client
73            .post(self.url(&format!("indexes/{index}/documents"))?)
74            .bearer_auth(&self.connection.api_key)
75            .json(documents)
76            .send()?
77            .json::<Value>()?;
78        self.wait_for_task(task_uid(&task)?)?;
79        Ok(())
80    }
81
82    pub fn search<T: serde::de::DeserializeOwned>(
83        &self,
84        index: &str,
85        body: Value,
86    ) -> Result<SearchResponse<T>> {
87        Ok(self
88            .client
89            .post(self.url(&format!("indexes/{index}/search"))?)
90            .bearer_auth(&self.connection.api_key)
91            .json(&body)
92            .send()?
93            .json()?)
94    }
95
96    pub fn stats(&self, index: &str) -> Result<Value> {
97        self.get(&format!("indexes/{index}/stats"))
98    }
99
100    pub fn swap_indexes(&self, swaps: Vec<(String, String)>) -> Result<()> {
101        let payload = swaps
102            .into_iter()
103            .map(|(indexes_a, indexes_b)| json!({ "indexes": [indexes_a, indexes_b] }))
104            .collect::<Vec<_>>();
105        let task = self
106            .client
107            .post(self.url("swap-indexes")?)
108            .bearer_auth(&self.connection.api_key)
109            .json(&payload)
110            .send()?
111            .json::<Value>()?;
112        self.wait_for_task(task_uid(&task)?)?;
113        Ok(())
114    }
115
116    pub fn wait_for_task(&self, uid: u64) -> Result<()> {
117        for _ in 0..50 {
118            let task = self.get(&format!("tasks/{uid}"))?;
119            match task.get("status").and_then(Value::as_str) {
120                Some("succeeded") => return Ok(()),
121                Some("failed") => return Err(anyhow!("meilisearch task {uid} failed: {task}")),
122                _ => std::thread::sleep(std::time::Duration::from_millis(100)),
123            }
124        }
125        Err(anyhow!("timed out waiting for meilisearch task {uid}"))
126    }
127
128    fn get(&self, path: &str) -> Result<Value> {
129        Ok(self
130            .client
131            .get(self.url(path)?)
132            .bearer_auth(&self.connection.api_key)
133            .send()?
134            .json()?)
135    }
136
137    fn url(&self, path: &str) -> Result<reqwest::Url> {
138        self.connection
139            .host
140            .join(path)
141            .with_context(|| format!("join meilisearch path {path}"))
142    }
143}
144
145pub fn symbols_settings() -> Value {
146    json!({
147        "searchableAttributes": [
148            "short_name", "fqn", "owner_class", "namespace", "symbol_tokens", "signature",
149            "doc_summary", "doc_description", "param_docs", "return_doc", "throws_docs",
150            "inline_rule_comments", "comment_keywords", "framework_tags", "package_name", "path"
151        ],
152        "filterableAttributes": [
153            "repo", "framework", "kind", "package_name", "is_vendor", "is_project_code", "is_test", "route_ids", "risk_tags"
154        ],
155        "sortableAttributes": ["is_project_code", "related_tests_count", "references_count", "line_start"],
156        "displayedAttributes": [
157            "id", "stable_key", "kind", "fqn", "signature", "doc_summary", "path", "line_start", "package_name", "related_tests", "missing_test_warning"
158        ]
159    })
160}
161
162pub fn routes_settings() -> Value {
163    json!({
164        "searchableAttributes": ["uri", "route_name", "action", "controller", "controller_method", "middleware"],
165        "filterableAttributes": ["repo", "framework", "method"],
166    })
167}
168
169pub fn tests_settings() -> Value {
170    json!({
171        "searchableAttributes": ["fqn", "covered_symbols", "referenced_symbols", "routes_called", "command"],
172        "filterableAttributes": ["repo", "framework"],
173    })
174}
175
176pub fn packages_settings() -> Value {
177    json!({
178        "searchableAttributes": ["name", "description", "keywords"],
179        "filterableAttributes": ["repo", "type"]
180    })
181}
182
183pub fn schema_settings() -> Value {
184    json!({
185        "searchableAttributes": ["migration", "table", "operation", "path"],
186        "filterableAttributes": ["repo", "operation"]
187    })
188}
189
190pub fn runs_settings() -> Value {
191    json!({
192        "searchableAttributes": ["run_id", "framework", "mode"],
193        "filterableAttributes": ["framework", "mode", "index_prefix"]
194    })
195}
196
197fn task_uid(value: &Value) -> Result<u64> {
198    value
199        .get("taskUid")
200        .or_else(|| value.get("uid"))
201        .and_then(Value::as_u64)
202        .ok_or_else(|| anyhow!("meilisearch response missing task uid: {value}"))
203}