1use crate::adapter::FormatAdapter;
6use crate::error::AdapterError;
7use crate::record::{EdgeRecord, EntityRecord};
8use khive_types::{EdgeRelation, EntityKind};
9use serde_json::Value;
10use std::str::FromStr;
11use uuid::Uuid;
12
13pub struct JsonFormatAdapter {
21 entities: Vec<Result<EntityRecord, AdapterError>>,
22 edges: Vec<Result<EdgeRecord, AdapterError>>,
23 warnings: Vec<String>,
24}
25
26impl JsonFormatAdapter {
27 pub fn new(json_input: &str) -> Result<Self, AdapterError> {
32 let value: Value =
33 serde_json::from_str(json_input).map_err(|e| AdapterError::Parse(e.to_string()))?;
34
35 let array = match value {
36 Value::Array(a) => a,
37 _ => {
38 return Err(AdapterError::Parse(
39 "expected a JSON array at the top level".into(),
40 ))
41 }
42 };
43
44 let mut entities = Vec::new();
45 let mut edges = Vec::new();
46 let mut warnings = Vec::new();
47
48 for (index, item) in array.into_iter().enumerate() {
49 let obj = match item {
50 Value::Object(m) => m,
51 other => {
52 warnings.push(format!(
53 "record {index}: expected an object, got {}; skipped",
54 other.type_str()
55 ));
56 continue;
57 }
58 };
59
60 let has_source = obj.keys().any(|k| {
63 let l = k.to_ascii_lowercase();
64 l == "source" || l == "from"
65 });
66 let has_target = obj.keys().any(|k| {
67 let l = k.to_ascii_lowercase();
68 l == "target" || l == "to"
69 });
70
71 if has_source && has_target {
72 edges.push(parse_edge(index, obj));
73 } else {
74 entities.push(parse_entity(index, obj, &mut warnings));
75 }
76 }
77
78 Ok(Self {
79 entities,
80 edges,
81 warnings,
82 })
83 }
84}
85
86impl FormatAdapter for JsonFormatAdapter {
87 fn name(&self) -> &str {
88 "json"
89 }
90
91 fn entities(&mut self) -> impl Iterator<Item = Result<EntityRecord, AdapterError>> {
92 self.entities.drain(..)
93 }
94
95 fn edges(&mut self) -> impl Iterator<Item = Result<EdgeRecord, AdapterError>> {
96 self.edges.drain(..)
97 }
98
99 fn warnings(&self) -> &[String] {
100 &self.warnings
101 }
102}
103
104fn remove_ci(
113 obj: &mut serde_json::Map<String, Value>,
114 field_lower: &str,
115) -> Option<(String, Value)> {
116 let key = obj
117 .keys()
118 .find(|k| k.to_ascii_lowercase() == field_lower)
119 .cloned()?;
120 let val = obj.remove(&key)?;
121 Some((key, val))
122}
123
124fn extract_required_string(
126 obj: &mut serde_json::Map<String, Value>,
127 index: usize,
128 field: &str,
129) -> Result<String, AdapterError> {
130 match remove_ci(obj, field) {
131 Some((_, Value::String(s))) if !s.is_empty() => Ok(s),
132 Some(_) => Err(AdapterError::InvalidField {
133 index,
134 field: field.into(),
135 reason: "must be a non-empty string".into(),
136 }),
137 None => Err(AdapterError::MissingField {
138 index,
139 field: field.into(),
140 }),
141 }
142}
143
144fn extract_uuid_field(
146 obj: &mut serde_json::Map<String, Value>,
147 index: usize,
148 field: &str,
149) -> Result<Uuid, AdapterError> {
150 match remove_ci(obj, field) {
151 Some((_, Value::String(s))) => s.parse::<Uuid>().map_err(|e| AdapterError::InvalidField {
152 index,
153 field: field.into(),
154 reason: e.to_string(),
155 }),
156 Some(_) => Err(AdapterError::InvalidField {
157 index,
158 field: field.into(),
159 reason: "must be a UUID string".into(),
160 }),
161 None => Ok(Uuid::new_v4()),
162 }
163}
164
165fn extract_weight(
167 obj: &mut serde_json::Map<String, Value>,
168 index: usize,
169) -> Result<f64, AdapterError> {
170 match remove_ci(obj, "weight") {
171 Some((_, Value::Number(n))) => {
172 let w = n.as_f64().ok_or_else(|| AdapterError::InvalidField {
173 index,
174 field: "weight".into(),
175 reason: "weight is not a finite f64".into(),
176 })?;
177 if !w.is_finite() || !(0.0..=1.0).contains(&w) {
178 return Err(AdapterError::InvalidField {
179 index,
180 field: "weight".into(),
181 reason: format!("must be finite and in [0.0, 1.0], got {w}"),
182 });
183 }
184 Ok(w)
185 }
186 Some(_) => Err(AdapterError::InvalidField {
187 index,
188 field: "weight".into(),
189 reason: "must be a number".into(),
190 }),
191 None => Ok(0.7),
192 }
193}
194
195fn parse_entity(
196 index: usize,
197 mut obj: serde_json::Map<String, Value>,
198 warnings: &mut Vec<String>,
199) -> Result<EntityRecord, AdapterError> {
200 let name = extract_required_string(&mut obj, index, "name")?;
201
202 let kind = {
203 let raw = extract_required_string(&mut obj, index, "kind")?;
204 EntityKind::from_str(&raw)
205 .map_err(|_| AdapterError::UnknownKind {
206 index,
207 kind: raw.clone(),
208 })?
209 .name()
210 .to_owned()
211 };
212
213 let id = extract_uuid_field(&mut obj, index, "id")?;
214
215 let description = match remove_ci(&mut obj, "description") {
216 Some((_, Value::String(s))) => Some(s),
217 Some(_) => {
218 warnings.push(format!(
219 "record {index}: 'description' is not a string; ignored"
220 ));
221 None
222 }
223 None => None,
224 };
225
226 let tags: Vec<String> = match remove_ci(&mut obj, "tags") {
227 Some((_, Value::Array(arr))) => arr
228 .into_iter()
229 .filter_map(|v| match v {
230 Value::String(s) => Some(s),
231 _ => {
232 warnings.push(format!("record {index}: non-string tag value ignored"));
233 None
234 }
235 })
236 .collect(),
237 Some(_) => {
238 warnings.push(format!("record {index}: 'tags' is not an array; ignored"));
239 Vec::new()
240 }
241 None => Vec::new(),
242 };
243
244 let mut props_base = match remove_ci(&mut obj, "properties") {
245 Some((_, Value::Object(m))) => m,
246 Some((_, other)) => {
247 warnings.push(format!(
248 "record {index}: 'properties' is not an object (got {}); ignored",
249 other.type_str()
250 ));
251 serde_json::Map::new()
252 }
253 None => serde_json::Map::new(),
254 };
255 for (k, v) in obj {
256 props_base.insert(k, v);
257 }
258
259 Ok(EntityRecord {
260 id,
261 kind,
262 name,
263 description,
264 properties: Value::Object(props_base),
265 tags,
266 })
267}
268
269fn parse_edge(
270 index: usize,
271 mut obj: serde_json::Map<String, Value>,
272) -> Result<EdgeRecord, AdapterError> {
273 let source = remove_ci(&mut obj, "source")
274 .or_else(|| remove_ci(&mut obj, "from"))
275 .and_then(|(_, v)| v.as_str().map(|s| s.to_owned()))
276 .ok_or_else(|| AdapterError::MissingField {
277 index,
278 field: "source".into(),
279 })?;
280
281 let target = remove_ci(&mut obj, "target")
282 .or_else(|| remove_ci(&mut obj, "to"))
283 .and_then(|(_, v)| v.as_str().map(|s| s.to_owned()))
284 .ok_or_else(|| AdapterError::MissingField {
285 index,
286 field: "target".into(),
287 })?;
288
289 let relation = {
290 let raw = extract_required_string(&mut obj, index, "relation")?;
291 EdgeRelation::from_str(&raw)
292 .map_err(|_| AdapterError::UnknownRelation {
293 index,
294 relation: raw.clone(),
295 })?
296 .as_str()
297 .to_owned()
298 };
299
300 let edge_id = match remove_ci(&mut obj, "edge_id").or_else(|| remove_ci(&mut obj, "id")) {
301 Some((_, Value::String(s))) => {
302 s.parse::<Uuid>().map_err(|e| AdapterError::InvalidField {
303 index,
304 field: "edge_id".into(),
305 reason: e.to_string(),
306 })?
307 }
308 Some(_) => {
309 return Err(AdapterError::InvalidField {
310 index,
311 field: "edge_id".into(),
312 reason: "must be a UUID string".into(),
313 })
314 }
315 None => Uuid::new_v4(),
316 };
317
318 let weight = extract_weight(&mut obj, index)?;
319
320 let mut properties = match remove_ci(&mut obj, "properties") {
321 Some((_, Value::Object(m))) => m,
322 Some(_) | None => serde_json::Map::new(),
323 };
324 for (k, v) in obj {
325 properties.insert(k, v);
326 }
327
328 Ok(EdgeRecord {
329 edge_id,
330 source,
331 target,
332 relation,
333 weight,
334 properties: Value::Object(properties),
335 })
336}
337
338trait TypeStr {
343 fn type_str(&self) -> &'static str;
344}
345
346impl TypeStr for Value {
347 fn type_str(&self) -> &'static str {
348 match self {
349 Value::Null => "null",
350 Value::Bool(_) => "bool",
351 Value::Number(_) => "number",
352 Value::String(_) => "string",
353 Value::Array(_) => "array",
354 Value::Object(_) => "object",
355 }
356 }
357}