Skip to content

Commit 4fb9620

Browse files
add dataset handler file
1 parent d681666 commit 4fb9620

1 file changed

Lines changed: 212 additions & 0 deletions

File tree

src/handlers/http/datasets.rs

Lines changed: 212 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,212 @@
1+
/*
2+
* Parseable Server (C) 2022 - 2025 Parseable, Inc.
3+
*
4+
* This program is free software: you can redistribute it and/or modify
5+
* it under the terms of the GNU Affero General Public License as
6+
* published by the Free Software Foundation, either version 3 of the
7+
* License, or (at your option) any later version.
8+
*
9+
* This program is distributed in the hope that it will be useful,
10+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12+
* GNU Affero General Public License for more details.
13+
*
14+
* You should have received a copy of the GNU Affero General Public License
15+
* along with this program. If not, see <http://www.gnu.org/licenses/>.
16+
*
17+
*/
18+
19+
use std::collections::HashSet;
20+
21+
use actix_web::http::StatusCode;
22+
use actix_web::{HttpResponse, web};
23+
use serde::{Deserialize, Serialize};
24+
25+
use crate::{
26+
handlers::DatasetTag,
27+
parseable::PARSEABLE,
28+
storage::{ObjectStorageError, StreamType},
29+
};
30+
31+
#[derive(Debug, Serialize)]
32+
#[serde(rename_all = "camelCase")]
33+
struct CorrelatedDataset {
34+
name: String,
35+
shared_tags: Vec<DatasetTag>,
36+
shared_labels: Vec<String>,
37+
}
38+
39+
/// GET /api/v1/datasets/{name}/correlated
40+
/// Returns all datasets sharing at least one tag or label with the named dataset.
41+
pub async fn get_correlated_datasets(
42+
path: web::Path<String>,
43+
) -> Result<HttpResponse, DatasetsError> {
44+
let dataset_name = path.into_inner();
45+
46+
let stream = PARSEABLE
47+
.get_stream(&dataset_name)
48+
.map_err(|_| DatasetsError::DatasetNotFound(dataset_name.clone()))?;
49+
50+
let target_tags: HashSet<DatasetTag> = stream.get_dataset_tags().into_iter().collect();
51+
let target_labels: HashSet<String> = stream.get_dataset_labels().into_iter().collect();
52+
53+
if target_tags.is_empty() && target_labels.is_empty() {
54+
return Ok(HttpResponse::Ok().json(Vec::<CorrelatedDataset>::new()));
55+
}
56+
57+
let all_streams = PARSEABLE.streams.list();
58+
let mut correlated = Vec::new();
59+
60+
for name in all_streams {
61+
if name == dataset_name {
62+
continue;
63+
}
64+
if let Ok(s) = PARSEABLE.get_stream(&name) {
65+
// Skip internal streams
66+
if s.get_stream_type() == StreamType::Internal {
67+
continue;
68+
}
69+
70+
let s_tags: HashSet<DatasetTag> = s.get_dataset_tags().into_iter().collect();
71+
let s_labels: HashSet<String> = s.get_dataset_labels().into_iter().collect();
72+
73+
let shared_tags: Vec<DatasetTag> = target_tags.intersection(&s_tags).copied().collect();
74+
let shared_labels: Vec<String> =
75+
target_labels.intersection(&s_labels).cloned().collect();
76+
77+
if !shared_tags.is_empty() || !shared_labels.is_empty() {
78+
correlated.push(CorrelatedDataset {
79+
name,
80+
shared_tags,
81+
shared_labels,
82+
});
83+
}
84+
}
85+
}
86+
87+
Ok(HttpResponse::Ok().json(correlated))
88+
}
89+
90+
/// GET /api/v1/datasets/tags/{tag}
91+
/// Returns all datasets that have the specified tag.
92+
pub async fn get_datasets_by_tag(path: web::Path<String>) -> Result<HttpResponse, DatasetsError> {
93+
let tag_str = path.into_inner();
94+
let tag =
95+
DatasetTag::try_from(tag_str.as_str()).map_err(|_| DatasetsError::InvalidTag(tag_str))?;
96+
97+
let all_streams = PARSEABLE.streams.list();
98+
let mut matching = Vec::new();
99+
100+
for name in all_streams {
101+
if let Ok(s) = PARSEABLE.get_stream(&name) {
102+
if s.get_stream_type() == StreamType::Internal {
103+
continue;
104+
}
105+
if s.get_dataset_tags().contains(&tag) {
106+
matching.push(name);
107+
}
108+
}
109+
}
110+
111+
Ok(HttpResponse::Ok().json(matching))
112+
}
113+
114+
#[derive(Debug, Deserialize)]
115+
pub struct PutTagsBody {
116+
pub tags: Vec<DatasetTag>,
117+
}
118+
119+
#[derive(Debug, Deserialize)]
120+
pub struct PutLabelsBody {
121+
pub labels: Vec<String>,
122+
}
123+
124+
/// PUT /api/v1/datasets/{name}/tags
125+
/// Replaces the dataset's tags with the provided list.
126+
pub async fn put_dataset_tags(
127+
path: web::Path<String>,
128+
body: web::Json<PutTagsBody>,
129+
) -> Result<HttpResponse, DatasetsError> {
130+
let dataset_name = path.into_inner();
131+
let new_tags: Vec<DatasetTag> = body
132+
.into_inner()
133+
.tags
134+
.into_iter()
135+
.collect::<HashSet<_>>()
136+
.into_iter()
137+
.collect();
138+
139+
let stream = PARSEABLE
140+
.get_stream(&dataset_name)
141+
.map_err(|_| DatasetsError::DatasetNotFound(dataset_name.clone()))?;
142+
143+
// Update storage first, then in-memory
144+
let storage = PARSEABLE.storage.get_object_store();
145+
let existing_labels = stream.get_dataset_labels();
146+
storage
147+
.update_dataset_tags_and_labels_in_stream(&dataset_name, &new_tags, &existing_labels)
148+
.await
149+
.map_err(DatasetsError::Storage)?;
150+
151+
stream.set_dataset_tags(new_tags.clone());
152+
153+
Ok(HttpResponse::Ok().json(serde_json::json!({ "tags": new_tags })))
154+
}
155+
156+
/// PUT /api/v1/datasets/{name}/labels
157+
/// Replaces the dataset's labels with the provided list.
158+
pub async fn put_dataset_labels(
159+
path: web::Path<String>,
160+
body: web::Json<PutLabelsBody>,
161+
) -> Result<HttpResponse, DatasetsError> {
162+
let dataset_name = path.into_inner();
163+
let new_labels: Vec<String> = body
164+
.into_inner()
165+
.labels
166+
.into_iter()
167+
.collect::<HashSet<_>>()
168+
.into_iter()
169+
.collect();
170+
171+
let stream = PARSEABLE
172+
.get_stream(&dataset_name)
173+
.map_err(|_| DatasetsError::DatasetNotFound(dataset_name.clone()))?;
174+
175+
// Update storage first, then in-memory
176+
let storage = PARSEABLE.storage.get_object_store();
177+
let existing_tags = stream.get_dataset_tags();
178+
storage
179+
.update_dataset_tags_and_labels_in_stream(&dataset_name, &existing_tags, &new_labels)
180+
.await
181+
.map_err(DatasetsError::Storage)?;
182+
183+
stream.set_dataset_labels(new_labels.clone());
184+
185+
Ok(HttpResponse::Ok().json(serde_json::json!({ "labels": new_labels })))
186+
}
187+
188+
#[derive(Debug, thiserror::Error)]
189+
pub enum DatasetsError {
190+
#[error("Dataset not found: {0}")]
191+
DatasetNotFound(String),
192+
#[error("Invalid tag: {0}")]
193+
InvalidTag(String),
194+
#[error("Storage error: {0}")]
195+
Storage(ObjectStorageError),
196+
}
197+
198+
impl actix_web::ResponseError for DatasetsError {
199+
fn status_code(&self) -> StatusCode {
200+
match self {
201+
DatasetsError::DatasetNotFound(_) => StatusCode::NOT_FOUND,
202+
DatasetsError::InvalidTag(_) => StatusCode::BAD_REQUEST,
203+
DatasetsError::Storage(_) => StatusCode::INTERNAL_SERVER_ERROR,
204+
}
205+
}
206+
207+
fn error_response(&self) -> HttpResponse {
208+
HttpResponse::build(self.status_code()).json(serde_json::json!({
209+
"error": self.to_string()
210+
}))
211+
}
212+
}

0 commit comments

Comments
 (0)