1use std::io::{self, Write};
19
20use serde_json::json;
21
22use crate::diagnostic::Diagnostic;
23use crate::model::{
24 DataModelDetail, DataModelStructure, DatePoint, ProjectDetail, ResourceDetail, ValueContent, VocabularyDetail,
25};
26use crate::render::auth::{AuthLoginOutcome, AuthLogoutOutcome, AuthSetTokenOutcome, AuthStatusOutcome};
27use crate::render::dump::{DumpDeleteOutcome, DumpOutcome};
28use crate::render::vocabulary::{NestedVocabularyNode, nest_vocabulary_detail};
29use crate::render::{
30 DataModelListView, MetaContext, ProjectListView, Renderer, ResourceListPagination, ResourceListView,
31 ResourceTypeListView, VocabularyListView,
32};
33
34pub struct JsonRenderer {
36 out: Box<dyn Write>,
37}
38
39impl JsonRenderer {
40 pub fn new() -> Self {
45 Self {
46 out: Box::new(crate::util::BrokenPipeWriter::new(io::stdout())),
47 }
48 }
49
50 pub fn with_writer(w: impl Write + 'static) -> Self {
52 Self { out: Box::new(w) }
53 }
54}
55
56impl Default for JsonRenderer {
57 fn default() -> Self {
58 Self::new()
59 }
60}
61
62fn meta_block(meta: &MetaContext, exit_code: u8) -> serde_json::Value {
70 use serde_json::Map;
71 let mut m = Map::new();
72 if !meta.server_label.is_empty() {
73 m.insert("server".into(), serde_json::Value::String(meta.server_label.clone()));
74 }
75 if !meta.auth_state.is_empty() {
76 m.insert("auth".into(), serde_json::Value::String(meta.auth_state.clone()));
77 }
78 m.insert("exit_code".into(), serde_json::Value::from(exit_code));
79 serde_json::Value::Object(m)
80}
81
82fn localized_text_array(items: &[crate::model::LocalizedText]) -> Vec<serde_json::Value> {
88 items
89 .iter()
90 .map(|l| {
91 json!({
92 "value": l.value,
93 "language": l.language,
94 })
95 })
96 .collect()
97}
98
99fn value_content_to_json(vc: &ValueContent) -> serde_json::Value {
105 use serde_json::Map;
106 let mut m = Map::new();
107 match vc {
108 ValueContent::Text(s) => {
109 m.insert("value_type".into(), serde_json::Value::String("text".into()));
110 m.insert("text".into(), serde_json::Value::String(s.clone()));
111 }
112 ValueContent::Integer(n) => {
113 m.insert("value_type".into(), serde_json::Value::String("integer".into()));
114 m.insert("value".into(), serde_json::Value::Number((*n).into()));
115 }
116 ValueContent::Decimal(s) => {
117 m.insert("value_type".into(), serde_json::Value::String("decimal".into()));
118 m.insert("value".into(), serde_json::Value::String(s.clone()));
119 }
120 ValueContent::Boolean(b) => {
121 m.insert("value_type".into(), serde_json::Value::String("boolean".into()));
122 m.insert("value".into(), serde_json::Value::Bool(*b));
123 }
124 ValueContent::Date(dv) => {
125 m.insert("value_type".into(), serde_json::Value::String("date".into()));
126 m.insert("calendar".into(), serde_json::Value::String(dv.calendar.clone()));
127 let point_to_json = |p: &DatePoint| {
129 json!({
130 "year": p.year,
131 "month": p.month,
132 "day": p.day,
133 "era": p.era,
134 })
135 };
136 m.insert("start".into(), point_to_json(&dv.start));
137 m.insert("end".into(), point_to_json(&dv.end));
138 }
139 ValueContent::Time(s) => {
140 m.insert("value_type".into(), serde_json::Value::String("time".into()));
141 m.insert("value".into(), serde_json::Value::String(s.clone()));
142 }
143 ValueContent::Uri(s) => {
144 m.insert("value_type".into(), serde_json::Value::String("uri".into()));
145 m.insert("value".into(), serde_json::Value::String(s.clone()));
146 }
147 ValueContent::Color(s) => {
148 m.insert("value_type".into(), serde_json::Value::String("color".into()));
149 m.insert("value".into(), serde_json::Value::String(s.clone()));
150 }
151 ValueContent::Geoname(s) => {
152 m.insert("value_type".into(), serde_json::Value::String("geoname".into()));
153 m.insert("value".into(), serde_json::Value::String(s.clone()));
154 }
155 ValueContent::VocabularyItem { node_iri, label } => {
156 m.insert("value_type".into(), serde_json::Value::String("vocabulary-item".into()));
157 m.insert("node_iri".into(), serde_json::Value::String(node_iri.clone()));
158 let label_val = match label {
159 Some(s) => serde_json::Value::String(s.clone()),
160 None => serde_json::Value::Null,
161 };
162 m.insert("label".into(), label_val);
163 }
164 ValueContent::Link { target_iri, target_label } => {
165 m.insert("value_type".into(), serde_json::Value::String("link".into()));
166 m.insert("target_iri".into(), serde_json::Value::String(target_iri.clone()));
167 let tl_val = match target_label {
168 Some(s) => serde_json::Value::String(s.clone()),
169 None => serde_json::Value::Null,
170 };
171 m.insert("target_label".into(), tl_val);
172 }
173 ValueContent::File(fv) => {
174 use crate::model::resource_type::ValueType;
175 let type_token = fv.value_type.as_token().to_string();
176 m.insert("value_type".into(), serde_json::Value::String(type_token));
177 m.insert("filename".into(), serde_json::Value::String(fv.filename.clone()));
178 m.insert("url".into(), serde_json::Value::String(fv.url.clone()));
179 match fv.value_type {
181 ValueType::StillImage => {
182 let w_val: serde_json::Value = fv
183 .width
184 .map_or(serde_json::Value::Null, |w| serde_json::Value::Number(w.into()));
185 let h_val: serde_json::Value = fv
186 .height
187 .map_or(serde_json::Value::Null, |h| serde_json::Value::Number(h.into()));
188 m.insert("width".into(), w_val);
189 m.insert("height".into(), h_val);
190 }
191 _ => {
192 m.insert("width".into(), serde_json::Value::Null);
193 m.insert("height".into(), serde_json::Value::Null);
194 }
195 }
196 }
197 ValueContent::Raw { value_type, text } => {
198 m.insert("value_type".into(), serde_json::Value::String(value_type.clone()));
199 m.insert("text".into(), serde_json::Value::String(text.clone()));
200 }
201 }
202 serde_json::Value::Object(m)
203}
204
205fn diagnostic_kind(diag: &Diagnostic) -> &'static str {
207 match diag {
208 Diagnostic::Usage(_) => "usage",
209 Diagnostic::AuthRequired(_) => "auth_required",
210 Diagnostic::NotFound(_) => "not_found",
211 Diagnostic::ServerError(_) => "server_error",
212 Diagnostic::Network(_) => "network",
213 Diagnostic::Conflict(_) => "conflict",
214 Diagnostic::Io(_) => "io",
215 Diagnostic::Internal(_) | Diagnostic::NotImplemented(_) => "internal",
216 }
217}
218
219impl Renderer for JsonRenderer {
220 fn diagnostic(&mut self, diag: &Diagnostic, meta: &MetaContext) -> Result<(), Diagnostic> {
221 let exit_code = diag.exit_category() as u8;
224 let obj = json!({
225 "_meta": meta_block(meta, exit_code),
226 "error": {
227 "kind": diagnostic_kind(diag),
228 "message": diag.to_string(),
229 },
230 });
231 writeln!(
232 self.out,
233 "{}",
234 serde_json::to_string(&obj).map_err(|e| Diagnostic::Internal(format!("json serialisation error: {e}")))?
235 )?;
236 Ok(())
237 }
238
239 fn auth_login(&mut self, outcome: &AuthLoginOutcome, meta: &MetaContext) -> Result<(), Diagnostic> {
240 let obj = json!({
241 "_meta": meta_block(meta, 0),
242 "data": {
243 "user": outcome.user,
244 "expires_at": outcome.expires_at.map(|dt| dt.to_rfc3339()),
245 "state": "login_success",
246 },
247 });
248 writeln!(
249 self.out,
250 "{}",
251 serde_json::to_string(&obj).map_err(|e| Diagnostic::Internal(format!("json serialisation error: {e}")))?
252 )?;
253 Ok(())
254 }
255
256 fn auth_status(&mut self, outcome: &AuthStatusOutcome, meta: &MetaContext) -> Result<(), Diagnostic> {
257 let obj = match outcome {
258 AuthStatusOutcome::LoggedIn { server: _, user, expires_at, expired } => json!({
259 "_meta": meta_block(meta, 0),
260 "data": {
261 "user": user,
262 "expires_at": expires_at.map(|dt| dt.to_rfc3339()),
263 "state": if *expired { "expired" } else { "logged_in" },
264 },
265 }),
266 AuthStatusOutcome::AuthenticatedViaEnv { server: _, expires_at, expired } => json!({
271 "_meta": meta_block(meta, 0),
272 "data": {
273 "user": null,
274 "expires_at": expires_at.map(|dt| dt.to_rfc3339()),
275 "state": if *expired { "expired" } else { "logged_in" },
276 },
277 }),
278 AuthStatusOutcome::NotLoggedIn { server: _ } => json!({
279 "_meta": meta_block(meta, 0),
280 "data": {
281 "user": null,
282 "expires_at": null,
283 "state": "not_logged_in",
284 },
285 }),
286 };
287 writeln!(
288 self.out,
289 "{}",
290 serde_json::to_string(&obj).map_err(|e| Diagnostic::Internal(format!("json serialisation error: {e}")))?
291 )?;
292 Ok(())
293 }
294
295 fn auth_logout(&mut self, outcome: &AuthLogoutOutcome, meta: &MetaContext) -> Result<(), Diagnostic> {
296 let obj = json!({
297 "_meta": meta_block(meta, 0),
298 "data": {
299 "was_cached": outcome.was_cached,
300 "state": if outcome.was_cached { "logout_was_cached" } else { "logout_no_op" },
301 },
302 });
303 writeln!(
304 self.out,
305 "{}",
306 serde_json::to_string(&obj).map_err(|e| Diagnostic::Internal(format!("json serialisation error: {e}")))?
307 )?;
308 Ok(())
309 }
310
311 fn auth_set_token(&mut self, outcome: &AuthSetTokenOutcome, meta: &MetaContext) -> Result<(), Diagnostic> {
312 let obj = json!({
313 "_meta": meta_block(meta, 0),
314 "data": {
315 "user": outcome.user,
316 "expires_at": outcome.expires_at.map(|dt| dt.to_rfc3339()),
317 "state": "token_cached",
318 },
319 });
320 writeln!(
321 self.out,
322 "{}",
323 serde_json::to_string(&obj).map_err(|e| Diagnostic::Internal(format!("json serialisation error: {e}")))?
324 )?;
325 Ok(())
326 }
327
328 fn project_dump(&mut self, outcome: &DumpOutcome, meta: &MetaContext) -> Result<(), Diagnostic> {
329 let obj = json!({
330 "_meta": meta_block(meta, 0),
331 "data": {
332 "path": outcome.path.display().to_string(),
333 "bytes": outcome.bytes,
334 "cleaned_up": outcome.cleaned_up,
335 "reused": outcome.reused,
336 "created_at": outcome.created_at.map(|dt| dt.to_rfc3339()),
337 },
338 });
339 writeln!(
340 self.out,
341 "{}",
342 serde_json::to_string(&obj).map_err(|e| Diagnostic::Internal(format!("json serialisation error: {e}")))?
343 )?;
344 Ok(())
345 }
346
347 fn project_dump_deleted(&mut self, outcome: &DumpDeleteOutcome, meta: &MetaContext) -> Result<(), Diagnostic> {
348 let obj = if let Some(ref note) = outcome.note {
349 json!({
350 "_meta": meta_block(meta, 0),
351 "data": {
352 "deleted": outcome.deleted,
353 "note": note,
354 },
355 })
356 } else {
357 json!({
358 "_meta": meta_block(meta, 0),
359 "data": {
360 "deleted": outcome.deleted,
361 },
362 })
363 };
364 writeln!(
365 self.out,
366 "{}",
367 serde_json::to_string(&obj).map_err(|e| Diagnostic::Internal(format!("json serialisation error: {e}")))?
368 )?;
369 Ok(())
370 }
371
372 fn projects(&mut self, view: &ProjectListView, meta: &MetaContext) -> Result<(), Diagnostic> {
373 let data: Vec<serde_json::Value> = view
377 .items
378 .iter()
379 .map(|item| {
380 json!({
381 "iri": item.iri,
382 "shortcode": item.shortcode,
383 "shortname": item.shortname,
384 "longname": item.longname,
385 "status": item.status.as_str(),
386 "data_models": item.data_models,
387 })
388 })
389 .collect();
390
391 let obj = json!({
392 "_meta": meta_block(meta, 0),
393 "data": data,
394 });
395 writeln!(
396 self.out,
397 "{}",
398 serde_json::to_string(&obj).map_err(|e| Diagnostic::Internal(format!("json serialisation error: {e}")))?
399 )?;
400 Ok(())
401 }
402
403 fn project_describe(&mut self, project: &ProjectDetail, meta: &MetaContext) -> Result<(), Diagnostic> {
404 let description: Vec<serde_json::Value> = project
407 .description
408 .iter()
409 .map(|d| {
410 json!({
411 "value": d.value,
412 "language": d.language,
413 })
414 })
415 .collect();
416
417 let data_models: Vec<serde_json::Value> = project
418 .data_models
419 .iter()
420 .map(|dm| {
421 json!({
422 "name": dm.name,
423 "iri": dm.iri,
424 })
425 })
426 .collect();
427
428 let obj = json!({
429 "_meta": meta_block(meta, 0),
430 "data": {
431 "iri": project.iri,
432 "shortcode": project.shortcode,
433 "shortname": project.shortname,
434 "longname": project.longname,
435 "status": project.status.as_str(),
436 "description": description,
437 "keywords": project.keywords,
438 "data_models": data_models,
439 },
440 });
441 writeln!(
442 self.out,
443 "{}",
444 serde_json::to_string(&obj).map_err(|e| Diagnostic::Internal(format!("json serialisation error: {e}")))?
445 )?;
446 Ok(())
447 }
448
449 fn data_model_describe(&mut self, detail: &DataModelDetail, meta: &MetaContext) -> Result<(), Diagnostic> {
450 let resource_types: Vec<serde_json::Value> = detail
454 .resource_types
455 .iter()
456 .map(|rt| {
457 json!({
458 "name": rt.name,
459 "iri": rt.iri,
460 "label": rt.label,
461 })
462 })
463 .collect();
464
465 let obj = json!({
466 "_meta": meta_block(meta, 0),
467 "data": {
468 "name": detail.name,
469 "iri": detail.iri,
470 "label": detail.label,
471 "last_modified": detail.last_modified,
472 "resource_types": resource_types,
473 },
474 });
475 writeln!(
476 self.out,
477 "{}",
478 serde_json::to_string(&obj).map_err(|e| Diagnostic::Internal(format!("json serialisation error: {e}")))?
479 )?;
480 Ok(())
481 }
482
483 fn data_models(&mut self, view: &DataModelListView, meta: &MetaContext) -> Result<(), Diagnostic> {
484 let data: Vec<serde_json::Value> = view
488 .items
489 .iter()
490 .map(|item| {
491 json!({
492 "name": item.name,
493 "iri": item.iri,
494 "label": item.label,
495 "last_modified": item.last_modified,
496 "is_builtin": item.is_builtin,
497 })
498 })
499 .collect();
500
501 let obj = json!({
502 "_meta": meta_block(meta, 0),
503 "data": data,
504 });
505 writeln!(
506 self.out,
507 "{}",
508 serde_json::to_string(&obj).map_err(|e| Diagnostic::Internal(format!("json serialisation error: {e}")))?
509 )?;
510 Ok(())
511 }
512
513 fn resource_types(&mut self, view: &ResourceTypeListView, meta: &MetaContext) -> Result<(), Diagnostic> {
514 let data: Vec<serde_json::Value> = view
525 .items
526 .iter()
527 .map(|item| {
528 let mut obj = json!({
529 "name": item.name,
530 "iri": item.iri,
531 "label": item.label,
532 "is_builtin": item.is_builtin,
533 });
534 if let Some(count) = item.count {
535 obj["count"] = serde_json::Value::from(count);
536 }
537 obj
538 })
539 .collect();
540
541 let mut meta_obj = meta_block(meta, 0);
543 if let Some(ref cc) = meta.count_caveat {
544 meta_obj["note"] = serde_json::Value::from(cc.as_str());
545 }
546
547 let obj = json!({
548 "_meta": meta_obj,
549 "data": data,
550 });
551 writeln!(
552 self.out,
553 "{}",
554 serde_json::to_string(&obj).map_err(|e| Diagnostic::Internal(format!("json serialisation error: {e}")))?
555 )?;
556 Ok(())
557 }
558
559 fn resource_type_describe(
560 &mut self,
561 detail: &crate::model::ResourceTypeDetail,
562 meta: &MetaContext,
563 ) -> Result<(), Diagnostic> {
564 let fields: Vec<serde_json::Value> = detail
568 .fields
569 .iter()
570 .map(|f| {
571 json!({
572 "name": f.name,
573 "iri": f.iri,
574 "label": f.label,
575 "value_type": f.value_type.to_string(),
576 "link_target": f.link_target,
577 "cardinality": f.cardinality.to_string(),
578 "is_builtin": f.is_builtin,
579 "data_model": f.data_model,
580 })
581 })
582 .collect();
583
584 let mut meta_obj = meta_block(meta, 0);
586 if let Some(ref cc) = meta.count_caveat {
587 meta_obj["note"] = serde_json::Value::from(cc.as_str());
588 }
589
590 let mut data = json!({
596 "name": detail.name,
597 "iri": detail.iri,
598 "label": detail.label,
599 "data_model": detail.data_model,
600 "representation": detail.representation.as_ref().map(|r| r.to_string()),
601 "super_types": detail.super_types,
602 "fields": fields,
603 });
604 if let Some(count) = detail.count {
605 data["count"] = serde_json::Value::from(count);
606 }
607
608 let obj = json!({
609 "_meta": meta_obj,
610 "data": data,
611 });
612 writeln!(
613 self.out,
614 "{}",
615 serde_json::to_string(&obj).map_err(|e| Diagnostic::Internal(format!("json serialisation error: {e}")))?
616 )?;
617 Ok(())
618 }
619
620 fn data_model_structure(&mut self, structure: &DataModelStructure, meta: &MetaContext) -> Result<(), Diagnostic> {
621 let data: Vec<serde_json::Value> = structure
625 .relations
626 .iter()
627 .map(|r| {
628 json!({
629 "source": r.source,
630 "target": r.target,
631 "kind": r.kind.to_string(),
632 "field": r.field,
633 "target_data_model": r.target_data_model,
634 })
635 })
636 .collect();
637
638 let obj = json!({
639 "_meta": meta_block(meta, 0),
640 "data": data,
641 });
642 writeln!(
643 self.out,
644 "{}",
645 serde_json::to_string(&obj).map_err(|e| Diagnostic::Internal(format!("json serialisation error: {e}")))?
646 )?;
647 Ok(())
648 }
649
650 fn resources(&mut self, view: &ResourceListView, meta: &MetaContext) -> Result<(), Diagnostic> {
651 let data: Vec<serde_json::Value> = view
653 .items
654 .iter()
655 .map(|item| {
656 json!({
657 "label": item.label,
658 "iri": item.iri,
659 "ark_url": item.ark_url,
660 "creation_date": item.creation_date,
661 "last_modified": item.last_modified,
662 "resource_type": item.resource_type,
663 })
664 })
665 .collect();
666
667 let mut meta_obj = meta_block(meta, 0);
669 match &view.pagination {
670 ResourceListPagination::SinglePage { page, may_have_more } => {
671 meta_obj["page"] = serde_json::Value::from(*page);
672 meta_obj["may_have_more_results"] = serde_json::Value::from(*may_have_more);
673 }
674 ResourceListPagination::AllPages { pages_fetched } => {
675 meta_obj["pages_fetched"] = serde_json::Value::from(*pages_fetched);
676 meta_obj["may_have_more_results"] = serde_json::Value::from(false);
678 }
679 }
680
681 if let Some(ref fw) = meta.filter_warning {
683 meta_obj["note"] = serde_json::Value::from(fw.as_str());
684 }
685
686 let obj = json!({
687 "_meta": meta_obj,
688 "data": data,
689 });
690 writeln!(
691 self.out,
692 "{}",
693 serde_json::to_string(&obj).map_err(|e| Diagnostic::Internal(format!("json serialisation error: {e}")))?
694 )?;
695 Ok(())
696 }
697
698 fn resource_describe(&mut self, detail: &ResourceDetail, meta: &MetaContext) -> Result<(), Diagnostic> {
699 let mut meta_obj = meta_block(meta, 0);
703 if let Some(ref fw) = meta.filter_warning {
704 meta_obj["note"] = serde_json::Value::from(fw.as_str());
705 }
706
707 let mut data = serde_json::Map::new();
709 data.insert("label".into(), serde_json::Value::String(detail.label.clone()));
710 data.insert("iri".into(), serde_json::Value::String(detail.iri.clone()));
711 data.insert("resource_type".into(), serde_json::Value::String(detail.resource_type.clone()));
712 data.insert(
713 "ark_url".into(),
714 detail
715 .ark_url
716 .as_ref()
717 .map_or(serde_json::Value::Null, |s| serde_json::Value::String(s.clone())),
718 );
719 data.insert(
720 "creation_date".into(),
721 detail
722 .creation_date
723 .as_ref()
724 .map_or(serde_json::Value::Null, |s| serde_json::Value::String(s.clone())),
725 );
726 data.insert(
727 "last_modified".into(),
728 detail
729 .last_modified
730 .as_ref()
731 .map_or(serde_json::Value::Null, |s| serde_json::Value::String(s.clone())),
732 );
733 data.insert(
734 "attached_project".into(),
735 detail
736 .attached_project
737 .as_ref()
738 .map_or(serde_json::Value::Null, |s| serde_json::Value::String(s.clone())),
739 );
740 data.insert(
741 "owner".into(),
742 detail
743 .owner
744 .as_ref()
745 .map_or(serde_json::Value::Null, |s| serde_json::Value::String(s.clone())),
746 );
747 data.insert(
748 "visibility".into(),
749 detail
750 .visibility
751 .as_ref()
752 .map_or(serde_json::Value::Null, |v| serde_json::Value::String(v.as_str().into())),
753 );
754 data.insert(
755 "your_access".into(),
756 detail
757 .your_access
758 .as_ref()
759 .map_or(serde_json::Value::Null, |a| serde_json::Value::String(a.as_str().into())),
760 );
761 if let Some(ref fields) = detail.values {
764 let values_arr: Vec<serde_json::Value> = fields
765 .iter()
766 .map(|fv| {
767 let value_objs: Vec<serde_json::Value> = fv
768 .values
769 .iter()
770 .map(|v| {
771 let mut obj = value_content_to_json(&v.content);
772 if let (Some(c), serde_json::Value::Object(m)) = (&v.comment, &mut obj) {
773 m.insert("comment".into(), serde_json::Value::String(c.clone()));
774 }
775 obj
776 })
777 .collect();
778 let mut fg = serde_json::Map::new();
779 fg.insert("field".into(), serde_json::Value::String(fv.name.clone()));
780 let fl_val = match &fv.label {
781 Some(s) => serde_json::Value::String(s.clone()),
782 None => serde_json::Value::Null,
783 };
784 fg.insert("field_label".into(), fl_val);
785 fg.insert("values".into(), serde_json::Value::Array(value_objs));
786 serde_json::Value::Object(fg)
787 })
788 .collect();
789 data.insert("values".into(), serde_json::Value::Array(values_arr));
790 }
791
792 let obj = json!({
793 "_meta": meta_obj,
794 "data": serde_json::Value::Object(data),
795 });
796 writeln!(
797 self.out,
798 "{}",
799 serde_json::to_string(&obj).map_err(|e| Diagnostic::Internal(format!("json serialisation error: {e}")))?
800 )?;
801 Ok(())
802 }
803
804 fn vocabularies(&mut self, view: &VocabularyListView, meta: &MetaContext) -> Result<(), Diagnostic> {
805 let data: Vec<serde_json::Value> = view
811 .items
812 .iter()
813 .map(|item| {
814 let labels = localized_text_array(&item.header.labels);
815 let comments = localized_text_array(&item.header.comments);
816 let mut obj = json!({
817 "name": item.header.name,
818 "iri": item.header.iri,
819 "labels": labels,
820 "comments": comments,
821 });
822 if let Some(n) = item.node_count {
823 obj["nodes"] = serde_json::Value::from(n);
824 }
825 if let Some(d) = item.depth {
826 obj["depth"] = serde_json::Value::from(d);
827 }
828 obj
829 })
830 .collect();
831
832 let mut meta_obj = meta_block(meta, 0);
835 if let Some(ref cc) = meta.count_cost {
836 meta_obj["note"] = serde_json::Value::from(cc.as_str());
837 }
838
839 let obj = json!({
840 "_meta": meta_obj,
841 "data": data,
842 });
843 writeln!(
844 self.out,
845 "{}",
846 serde_json::to_string(&obj).map_err(|e| Diagnostic::Internal(format!("json serialisation error: {e}")))?
847 )?;
848 Ok(())
849 }
850
851 fn vocabulary_describe(&mut self, detail: &VocabularyDetail, meta: &MetaContext) -> Result<(), Diagnostic> {
852 fn nested_to_json(node: &NestedVocabularyNode<'_>) -> serde_json::Value {
866 let h = node.header;
867 json!({
868 "node_iri": h.iri,
869 "number": node.number,
870 "name": h.name,
871 "labels": localized_text_array(&h.labels),
872 "comments": localized_text_array(&h.comments),
873 "position": node.position,
874 "children": node.children.iter().map(nested_to_json).collect::<Vec<_>>(),
875 })
876 }
877
878 let root = &detail.tree.root;
879 let children: Vec<serde_json::Value> =
880 nest_vocabulary_detail(detail).iter().map(nested_to_json).collect::<Vec<_>>();
881
882 let mut data = json!({
883 "name": root.name,
884 "iri": root.iri,
885 "labels": localized_text_array(&root.labels),
886 "comments": localized_text_array(&root.comments),
887 "nodes": detail.node_count,
888 "depth": detail.depth,
889 "children": children,
890 });
891 if let Some(ref requested) = detail.tree.requested_node {
895 data["requested_node"] = serde_json::Value::from(requested.as_str());
896 }
897 if let Some(ref subtree_of) = detail.subtree_of {
898 data["subtree_of"] = serde_json::Value::from(subtree_of.as_str());
899 }
900
901 let obj = json!({
902 "_meta": meta_block(meta, 0),
903 "data": data,
904 });
905 writeln!(
906 self.out,
907 "{}",
908 serde_json::to_string(&obj).map_err(|e| Diagnostic::Internal(format!("json serialisation error: {e}")))?
909 )?;
910 Ok(())
911 }
912}
913
914#[cfg(test)]
915mod tests {
916 use super::*;
917 use crate::model::{
918 DataModel, DataModelDetail, DataModelSummary, Project, ProjectDescription, ProjectDetail, ProjectStatus,
919 ResourceType, ResourceTypeSummary,
920 };
921 use crate::render::test_support::{SharedBuf, make_meta};
922 use crate::render::{DataModelListView, ResourceTypeListView};
923
924 #[test]
925 fn meta_block_full_context_has_all_three_keys() {
926 let meta = MetaContext {
929 server_label: "https://api.test.dasch.swiss".into(),
930 auth_state: "anonymous".into(),
931 filter_warning: None,
932 count_caveat: None,
933 count_cost: None,
934 };
935 let block = meta_block(&meta, 1);
936 let obj = block.as_object().unwrap();
937 assert_eq!(obj["server"], "https://api.test.dasch.swiss");
938 assert_eq!(obj["auth"], "anonymous");
939 assert_eq!(obj["exit_code"], 1);
940 assert_eq!(obj.len(), 3, "expected exactly three keys; got: {block}");
941 }
942
943 #[test]
944 fn meta_block_empty_server_and_auth_omits_both_keys() {
945 let meta = MetaContext {
948 server_label: String::new(),
949 auth_state: String::new(),
950 filter_warning: None,
951 count_caveat: None,
952 count_cost: None,
953 };
954 let block = meta_block(&meta, 2);
955 let obj = block.as_object().unwrap();
956 assert!(
957 !obj.contains_key("server"),
958 "server key must be absent when server_label is empty; got: {block}"
959 );
960 assert!(
961 !obj.contains_key("auth"),
962 "auth key must be absent when auth_state is empty; got: {block}"
963 );
964 assert_eq!(obj["exit_code"], 2);
965 assert_eq!(obj.len(), 1, "expected exactly one key; got: {block}");
966 }
967
968 #[test]
969 fn projects_json_output() {
970 let out = SharedBuf::new();
971 let mut renderer = JsonRenderer::with_writer(out.clone());
972 let items = vec![
973 Project {
974 iri: "http://rdfh.ch/projects/0001".into(),
975 shortcode: "0001".into(),
976 shortname: "anything".into(),
977 longname: Some("Anything Project".into()),
978 status: ProjectStatus::Active,
979 data_models: 2,
980 },
981 Project {
982 iri: "http://rdfh.ch/projects/0002".into(),
983 shortcode: "0002".into(),
984 shortname: "images".into(),
985 longname: None,
986 status: ProjectStatus::Inactive,
987 data_models: 0,
988 },
989 ];
990 let view = ProjectListView { items, total: 2, filter: None };
991 let meta = make_meta("anonymous", "https://api.test.dasch.swiss");
992 renderer.projects(&view, &meta).unwrap();
993
994 let s = out.string();
995 let parsed: serde_json::Value = serde_json::from_str(s.trim()).unwrap();
996
997 assert_eq!(parsed["_meta"]["auth"], "anonymous");
999 assert_eq!(parsed["_meta"]["server"], "https://api.test.dasch.swiss");
1000 assert_eq!(parsed["_meta"]["exit_code"], 0);
1001
1002 let data = parsed["data"].as_array().unwrap();
1004 assert_eq!(data.len(), 2);
1005
1006 assert_eq!(data[0]["shortcode"], "0001");
1008 assert_eq!(data[0]["shortname"], "anything");
1009 assert_eq!(data[0]["longname"], "Anything Project");
1010 assert_eq!(data[0]["status"], "active");
1011 assert_eq!(data[0]["data_models"], 2);
1012 assert_eq!(data[0]["iri"], "http://rdfh.ch/projects/0001");
1013
1014 assert_eq!(data[1]["shortcode"], "0002");
1016 assert!(data[1]["longname"].is_null());
1017 assert_eq!(data[1]["status"], "inactive");
1018 assert_eq!(data[1]["data_models"], 0);
1019 }
1020
1021 #[test]
1022 fn projects_json_empty_data_array() {
1023 let out = SharedBuf::new();
1024 let mut renderer = JsonRenderer::with_writer(out.clone());
1025 let view = ProjectListView { items: vec![], total: 0, filter: None };
1026 let meta = make_meta("anonymous", "https://api.test.dasch.swiss");
1027 renderer.projects(&view, &meta).unwrap();
1028
1029 let parsed: serde_json::Value = serde_json::from_str(out.string().trim()).unwrap();
1030 assert!(parsed["data"].as_array().unwrap().is_empty());
1031 }
1032
1033 #[test]
1040 fn diagnostic_kind_covers_all_variants() {
1041 let cases: &[(&Diagnostic, &str)] = &[
1042 (&Diagnostic::Usage("x".into()), "usage"),
1043 (&Diagnostic::AuthRequired("x".into()), "auth_required"),
1044 (&Diagnostic::NotFound("x".into()), "not_found"),
1045 (&Diagnostic::ServerError("x".into()), "server_error"),
1046 (&Diagnostic::Network("x".into()), "network"),
1047 (&Diagnostic::Conflict("x".into()), "conflict"),
1048 (&Diagnostic::Io("x".into()), "io"),
1049 (&Diagnostic::Internal("x".into()), "internal"),
1050 (&Diagnostic::NotImplemented("x".into()), "internal"),
1051 ];
1052 for (diag, expected_kind) in cases {
1053 assert_eq!(diagnostic_kind(diag), *expected_kind, "unexpected kind for {diag:?}");
1054 }
1055 }
1056
1057 #[test]
1058 fn conflict_kind_is_conflict() {
1059 let d = Diagnostic::Conflict("dump already in progress".into());
1060 assert_eq!(diagnostic_kind(&d), "conflict");
1061 }
1062
1063 #[test]
1064 fn io_kind_is_io() {
1065 let d = Diagnostic::Io("failed to write /tmp/0001.zip: permission denied".into());
1066 assert_eq!(diagnostic_kind(&d), "io");
1067 }
1068
1069 #[test]
1076 fn diagnostic_not_found_json_output() {
1077 let out = SharedBuf::new();
1078 let mut renderer = JsonRenderer::with_writer(out.clone());
1079 let diag = Diagnostic::NotFound("resource http://rdfh.ch/0001/xyz not found".into());
1080 let meta = make_meta("anonymous", "https://api.test.dasch.swiss");
1081 renderer.diagnostic(&diag, &meta).unwrap();
1082
1083 let parsed: serde_json::Value = serde_json::from_str(out.string().trim()).unwrap();
1084 assert_eq!(parsed["error"]["kind"], "not_found");
1085 assert_eq!(parsed["_meta"]["exit_code"], 1);
1086 let message = parsed["error"]["message"].as_str().expect("error.message must be a string");
1087 assert!(!message.is_empty(), "error.message must be non-empty");
1088 assert_eq!(message, diag.to_string());
1089 }
1090
1091 fn make_beol_detail() -> ProjectDetail {
1092 ProjectDetail {
1096 iri: "http://rdfh.ch/projects/yTerZGyxjZVqFMNNKXCDPF".into(),
1097 shortcode: "0801".into(),
1098 shortname: "beol".into(),
1099 longname: Some("Bernoulli-Euler Online".into()),
1100 status: ProjectStatus::Active,
1101 description: vec![ProjectDescription {
1102 value: "<b>BEOL</b> — early modern mathematics.".into(),
1103 language: Some("en".into()),
1104 }],
1105 keywords: vec!["Bernoulli".into(), "Euler".into(), "Mathematics".into()],
1106 data_models: vec![
1107 DataModelSummary {
1108 name: "beol".into(),
1109 iri: "http://api.dasch.swiss/ontology/0801/beol/v2".into(),
1110 },
1111 DataModelSummary {
1112 name: "biblio".into(),
1113 iri: "http://api.dasch.swiss/ontology/0801/biblio/v2".into(),
1114 },
1115 DataModelSummary {
1116 name: "leibniz".into(),
1117 iri: "http://api.dasch.swiss/ontology/0801/leibniz/v2".into(),
1118 },
1119 DataModelSummary {
1120 name: "newton".into(),
1121 iri: "http://api.dasch.swiss/ontology/0801/newton/v2".into(),
1122 },
1123 ],
1124 }
1125 }
1126
1127 #[test]
1128 fn project_describe_json_full() {
1129 let out = SharedBuf::new();
1130 let mut renderer = JsonRenderer::with_writer(out.clone());
1131 let meta = make_meta("anonymous", "https://api.test.dasch.swiss");
1132 renderer.project_describe(&make_beol_detail(), &meta).unwrap();
1133
1134 let parsed: serde_json::Value = serde_json::from_str(out.string().trim()).unwrap();
1135
1136 assert_eq!(parsed["_meta"]["auth"], "anonymous");
1138 assert_eq!(parsed["_meta"]["exit_code"], 0);
1139
1140 let data = &parsed["data"];
1142 assert!(data.is_object());
1143 assert_eq!(data["iri"], "http://rdfh.ch/projects/yTerZGyxjZVqFMNNKXCDPF");
1144 assert_eq!(data["shortcode"], "0801");
1145 assert_eq!(data["shortname"], "beol");
1146 assert_eq!(data["longname"], "Bernoulli-Euler Online");
1147 assert_eq!(data["status"], "active");
1148
1149 let desc = data["description"].as_array().unwrap();
1151 assert_eq!(desc.len(), 1);
1152 assert_eq!(desc[0]["value"], "<b>BEOL</b> — early modern mathematics.");
1153 assert_eq!(desc[0]["language"], "en");
1154
1155 let kws = data["keywords"].as_array().unwrap();
1157 assert_eq!(kws.len(), 3);
1158 assert_eq!(kws[0], "Bernoulli");
1159
1160 let dms = data["data_models"].as_array().unwrap();
1163 assert_eq!(dms.len(), 4, "all four data-models must be rendered");
1164 assert_eq!(dms[0]["name"], "beol");
1165 assert_eq!(dms[0]["iri"], "http://api.dasch.swiss/ontology/0801/beol/v2");
1166 assert_eq!(dms[1]["name"], "biblio");
1167 assert_eq!(dms[2]["name"], "leibniz");
1168 assert_eq!(dms[3]["name"], "newton");
1169 }
1170
1171 #[test]
1172 fn project_describe_json_no_longname() {
1173 let out = SharedBuf::new();
1174 let mut renderer = JsonRenderer::with_writer(out.clone());
1175 let mut detail = make_beol_detail();
1176 detail.longname = None;
1177 let meta = make_meta("anonymous", "https://api.test.dasch.swiss");
1178 renderer.project_describe(&detail, &meta).unwrap();
1179
1180 let parsed: serde_json::Value = serde_json::from_str(out.string().trim()).unwrap();
1181 assert!(parsed["data"]["longname"].is_null());
1183 }
1184
1185 #[test]
1186 fn project_describe_json_empty_fields() {
1187 let out = SharedBuf::new();
1188 let mut renderer = JsonRenderer::with_writer(out.clone());
1189 let detail = ProjectDetail {
1190 iri: "http://rdfh.ch/projects/0000".into(),
1191 shortcode: "0000".into(),
1192 shortname: "minimal".into(),
1193 longname: None,
1194 status: ProjectStatus::Inactive,
1195 description: vec![],
1196 keywords: vec![],
1197 data_models: vec![],
1198 };
1199 let meta = make_meta("anonymous", "https://api.test.dasch.swiss");
1200 renderer.project_describe(&detail, &meta).unwrap();
1201
1202 let parsed: serde_json::Value = serde_json::from_str(out.string().trim()).unwrap();
1203 let data = &parsed["data"];
1204 assert_eq!(data["status"], "inactive");
1205 assert!(data["description"].as_array().unwrap().is_empty());
1206 assert!(data["keywords"].as_array().unwrap().is_empty());
1207 assert!(data["data_models"].as_array().unwrap().is_empty());
1208 }
1209
1210 fn make_data_model_fixture() -> Vec<DataModel> {
1211 vec![
1212 DataModel {
1213 name: "beol".into(),
1214 iri: "http://api.dasch.swiss/ontology/0801/beol/v2".into(),
1215 label: Some("The BEOL data-model".into()),
1216 last_modified: Some("2024-05-27T13:43:26.233048Z".into()),
1217 is_builtin: false,
1218 },
1219 DataModel {
1220 name: "biblio".into(),
1221 iri: "http://api.dasch.swiss/ontology/0801/biblio/v2".into(),
1222 label: None,
1223 last_modified: None,
1224 is_builtin: false,
1225 },
1226 DataModel {
1227 name: "knora-api".into(),
1228 iri: "http://api.knora.org/ontology/knora-api/v2".into(),
1229 label: None,
1230 last_modified: None,
1231 is_builtin: true,
1232 },
1233 ]
1234 }
1235
1236 #[test]
1237 fn data_models_json_output() {
1238 let out = SharedBuf::new();
1239 let mut renderer = JsonRenderer::with_writer(out.clone());
1240 let view = DataModelListView { items: make_data_model_fixture(), total: 3, filter: None };
1241 let meta = make_meta("anonymous", "https://api.test.dasch.swiss");
1242 renderer.data_models(&view, &meta).unwrap();
1243
1244 let parsed: serde_json::Value = serde_json::from_str(out.string().trim()).unwrap();
1245
1246 assert_eq!(parsed["_meta"]["auth"], "anonymous");
1248 assert_eq!(parsed["_meta"]["server"], "https://api.test.dasch.swiss");
1249 assert_eq!(parsed["_meta"]["exit_code"], 0);
1250
1251 let data = parsed["data"].as_array().unwrap();
1253 assert_eq!(data.len(), 3);
1254
1255 assert_eq!(data[0]["name"], "beol");
1257 assert_eq!(data[0]["iri"], "http://api.dasch.swiss/ontology/0801/beol/v2");
1258 assert_eq!(data[0]["label"], "The BEOL data-model");
1259 assert_eq!(data[0]["last_modified"], "2024-05-27T13:43:26.233048Z");
1260 assert_eq!(data[0]["is_builtin"], false);
1261
1262 assert_eq!(data[1]["name"], "biblio");
1264 assert!(data[1]["label"].is_null());
1265 assert!(data[1]["last_modified"].is_null());
1266 assert_eq!(data[1]["is_builtin"], false);
1267
1268 assert_eq!(data[2]["name"], "knora-api");
1270 assert!(data[2]["label"].is_null());
1271 assert!(data[2]["last_modified"].is_null());
1272 assert_eq!(data[2]["is_builtin"], true);
1273 }
1274
1275 #[test]
1276 fn data_models_json_empty_data_array() {
1277 let out = SharedBuf::new();
1278 let mut renderer = JsonRenderer::with_writer(out.clone());
1279 let view = DataModelListView { items: vec![], total: 0, filter: None };
1280 let meta = make_meta("anonymous", "https://api.test.dasch.swiss");
1281 renderer.data_models(&view, &meta).unwrap();
1282
1283 let parsed: serde_json::Value = serde_json::from_str(out.string().trim()).unwrap();
1284 assert!(parsed["data"].as_array().unwrap().is_empty());
1285 }
1286
1287 fn make_beol_dm_detail() -> DataModelDetail {
1290 DataModelDetail {
1291 name: "beol".into(),
1292 iri: "http://api.dasch.swiss/ontology/0801/beol/v2".into(),
1293 label: Some("The BEOL data-model".into()),
1294 last_modified: Some("2024-05-27T13:43:26.233048Z".into()),
1295 resource_types: vec![
1296 ResourceTypeSummary {
1297 name: "Archive".into(),
1298 iri: "http://api.dasch.swiss/ontology/0801/beol/v2#Archive".into(),
1299 label: Some("Archive".into()),
1300 },
1301 ResourceTypeSummary {
1302 name: "basicLetter".into(),
1303 iri: "http://api.dasch.swiss/ontology/0801/beol/v2#basicLetter".into(),
1304 label: None,
1305 },
1306 ResourceTypeSummary {
1307 name: "letter".into(),
1308 iri: "http://api.dasch.swiss/ontology/0801/beol/v2#letter".into(),
1309 label: Some("Letter".into()),
1310 },
1311 ],
1312 }
1313 }
1314
1315 #[test]
1316 fn data_model_describe_json_full() {
1317 let out = SharedBuf::new();
1318 let mut renderer = JsonRenderer::with_writer(out.clone());
1319 let meta = make_meta("anonymous", "https://api.test.dasch.swiss");
1320 renderer.data_model_describe(&make_beol_dm_detail(), &meta).unwrap();
1321
1322 let parsed: serde_json::Value = serde_json::from_str(out.string().trim()).unwrap();
1323
1324 assert_eq!(parsed["_meta"]["auth"], "anonymous");
1326 assert_eq!(parsed["_meta"]["server"], "https://api.test.dasch.swiss");
1327 assert_eq!(parsed["_meta"]["exit_code"], 0);
1328
1329 let data = &parsed["data"];
1331 assert!(data.is_object());
1332 assert_eq!(data["name"], "beol");
1333 assert_eq!(data["iri"], "http://api.dasch.swiss/ontology/0801/beol/v2");
1334 assert_eq!(data["label"], "The BEOL data-model");
1335 assert_eq!(data["last_modified"], "2024-05-27T13:43:26.233048Z");
1337
1338 let rts = data["resource_types"].as_array().unwrap();
1340 assert_eq!(rts.len(), 3);
1341 assert_eq!(rts[0]["name"], "Archive");
1342 assert_eq!(rts[0]["iri"], "http://api.dasch.swiss/ontology/0801/beol/v2#Archive");
1343 assert_eq!(rts[0]["label"], "Archive");
1344 assert_eq!(rts[1]["name"], "basicLetter");
1346 assert!(rts[1]["label"].is_null());
1347 assert_eq!(rts[2]["name"], "letter");
1348 assert_eq!(rts[2]["label"], "Letter");
1349 }
1350
1351 #[test]
1352 fn data_model_describe_json_no_label_no_last_modified() {
1353 let out = SharedBuf::new();
1354 let mut renderer = JsonRenderer::with_writer(out.clone());
1355 let detail = DataModelDetail {
1356 name: "minimal".into(),
1357 iri: "http://api.dasch.swiss/ontology/0000/minimal/v2".into(),
1358 label: None,
1359 last_modified: None,
1360 resource_types: vec![],
1361 };
1362 let meta = make_meta("anonymous", "https://api.test.dasch.swiss");
1363 renderer.data_model_describe(&detail, &meta).unwrap();
1364
1365 let parsed: serde_json::Value = serde_json::from_str(out.string().trim()).unwrap();
1366 let data = &parsed["data"];
1367 assert!(data["label"].is_null());
1369 assert!(data["last_modified"].is_null());
1371 assert!(data["resource_types"].as_array().unwrap().is_empty());
1373 }
1374
1375 fn make_rt_fixture() -> Vec<ResourceType> {
1378 vec![
1379 ResourceType {
1380 name: "Archive".into(),
1381 iri: "http://api.dasch.swiss/ontology/0801/beol/v2#Archive".into(),
1382 label: Some("Archive".into()),
1383 is_builtin: false,
1384 count: None,
1385 },
1386 ResourceType {
1387 name: "letter".into(),
1388 iri: "http://api.dasch.swiss/ontology/0801/beol/v2#letter".into(),
1389 label: None,
1390 is_builtin: false,
1391 count: None,
1392 },
1393 ]
1394 }
1395
1396 #[test]
1397 fn resource_types_json_output() {
1398 let out = SharedBuf::new();
1399 let mut renderer = JsonRenderer::with_writer(out.clone());
1400 let view = ResourceTypeListView {
1401 items: make_rt_fixture(),
1402 total: 2,
1403 filter: None,
1404 data_model: "beol".into(),
1405 };
1406 let meta = make_meta("anonymous", "https://api.test.dasch.swiss");
1407 renderer.resource_types(&view, &meta).unwrap();
1408
1409 let parsed: serde_json::Value = serde_json::from_str(out.string().trim()).unwrap();
1410
1411 assert_eq!(parsed["_meta"]["auth"], "anonymous");
1413 assert_eq!(parsed["_meta"]["server"], "https://api.test.dasch.swiss");
1414 assert_eq!(parsed["_meta"]["exit_code"], 0);
1415
1416 let data = parsed["data"].as_array().unwrap();
1418 assert_eq!(data.len(), 2);
1419
1420 assert_eq!(data[0]["name"], "Archive");
1422 assert_eq!(data[0]["iri"], "http://api.dasch.swiss/ontology/0801/beol/v2#Archive");
1423 assert_eq!(data[0]["label"], "Archive");
1424 assert_eq!(data[0]["is_builtin"], false);
1425
1426 assert_eq!(data[1]["name"], "letter");
1428 assert!(data[1]["label"].is_null());
1429 assert_eq!(data[1]["is_builtin"], false);
1430 }
1431
1432 #[test]
1433 fn resource_types_json_with_builtins() {
1434 let out = SharedBuf::new();
1435 let mut renderer = JsonRenderer::with_writer(out.clone());
1436 let view = ResourceTypeListView {
1437 items: vec![ResourceType {
1438 name: "Region".into(),
1439 iri: "http://api.knora.org/ontology/knora-api/v2#Region".into(),
1440 label: Some("Region".into()),
1441 is_builtin: true,
1442 count: None,
1443 }],
1444 total: 1,
1445 filter: None,
1446 data_model: "beol".into(),
1447 };
1448 let meta = make_meta("anonymous", "https://api.test.dasch.swiss");
1449 renderer.resource_types(&view, &meta).unwrap();
1450
1451 let parsed: serde_json::Value = serde_json::from_str(out.string().trim()).unwrap();
1452 let data = parsed["data"].as_array().unwrap();
1453 assert_eq!(data.len(), 1);
1454 assert_eq!(data[0]["name"], "Region");
1455 assert_eq!(data[0]["label"], "Region");
1456 assert_eq!(data[0]["is_builtin"], true);
1458 }
1459
1460 #[test]
1461 fn resource_types_json_with_filter() {
1462 let out = SharedBuf::new();
1464 let mut renderer = JsonRenderer::with_writer(out.clone());
1465 let view = ResourceTypeListView {
1466 items: vec![make_rt_fixture().remove(0)], total: 2,
1468 filter: Some("arch".to_string()),
1469 data_model: "beol".into(),
1470 };
1471 let meta = make_meta("anonymous", "https://api.test.dasch.swiss");
1472 renderer.resource_types(&view, &meta).unwrap();
1473
1474 let parsed: serde_json::Value = serde_json::from_str(out.string().trim()).unwrap();
1475 let data = parsed["data"].as_array().unwrap();
1476 assert_eq!(data.len(), 1);
1477 assert_eq!(data[0]["name"], "Archive");
1478 }
1479
1480 #[test]
1481 fn resource_types_json_empty_data_array() {
1482 let out = SharedBuf::new();
1483 let mut renderer = JsonRenderer::with_writer(out.clone());
1484 let view = ResourceTypeListView {
1485 items: vec![],
1486 total: 0,
1487 filter: None,
1488 data_model: "beol".into(),
1489 };
1490 let meta = make_meta("anonymous", "https://api.test.dasch.swiss");
1491 renderer.resource_types(&view, &meta).unwrap();
1492
1493 let parsed: serde_json::Value = serde_json::from_str(out.string().trim()).unwrap();
1494 assert!(parsed["data"].as_array().unwrap().is_empty());
1495 }
1496
1497 #[test]
1498 fn resource_types_json_with_count_and_caveat() {
1499 let out = SharedBuf::new();
1502 let mut renderer = JsonRenderer::with_writer(out.clone());
1503 let mut items = make_rt_fixture();
1504 items[0].count = Some(5);
1505 let view = ResourceTypeListView { items, total: 2, filter: None, data_model: "beol".into() };
1506 let meta = crate::render::MetaContext {
1507 server_label: "https://api.test.dasch.swiss".into(),
1508 auth_state: "anonymous".into(),
1509 filter_warning: None,
1510 count_caveat: Some("counts are not permission-filtered".into()),
1511 count_cost: None,
1512 };
1513 renderer.resource_types(&view, &meta).unwrap();
1514
1515 let parsed: serde_json::Value = serde_json::from_str(out.string().trim()).unwrap();
1516 assert_eq!(parsed["data"][0]["count"], 5);
1517 assert!(
1519 !parsed["data"][1].as_object().unwrap().contains_key("count"),
1520 "count key must be absent when None; got: {}",
1521 parsed["data"][1]
1522 );
1523 assert_eq!(
1524 parsed["_meta"]["note"], "counts are not permission-filtered",
1525 "note must carry count_caveat"
1526 );
1527 }
1528
1529 #[test]
1530 fn resource_types_json_no_count_no_note() {
1531 let out = SharedBuf::new();
1534 let mut renderer = JsonRenderer::with_writer(out.clone());
1535 let view = ResourceTypeListView {
1536 items: make_rt_fixture(),
1537 total: 2,
1538 filter: None,
1539 data_model: "beol".into(),
1540 };
1541 let meta = make_meta("anonymous", "https://api.test.dasch.swiss");
1542 renderer.resource_types(&view, &meta).unwrap();
1543
1544 let parsed: serde_json::Value = serde_json::from_str(out.string().trim()).unwrap();
1545 assert!(!parsed["_meta"].as_object().unwrap().contains_key("note"));
1546 assert!(!parsed["data"][0].as_object().unwrap().contains_key("count"));
1547 }
1548
1549 fn make_minimal_rt_detail() -> crate::model::ResourceTypeDetail {
1552 crate::model::ResourceTypeDetail {
1553 name: "manuscript".into(),
1554 iri: "http://api.dasch.swiss/ontology/0801/beol/v2#manuscript".into(),
1555 label: Some("Manuscript".into()),
1556 data_model: "beol".into(),
1557 representation: None,
1558 super_types: vec![],
1559 fields: vec![],
1560 count: None,
1561 }
1562 }
1563
1564 #[test]
1565 fn resource_type_describe_json_with_count_and_caveat() {
1566 let out = SharedBuf::new();
1567 let mut renderer = JsonRenderer::with_writer(out.clone());
1568 let mut detail = make_minimal_rt_detail();
1569 detail.count = Some(99);
1570 let meta = crate::render::MetaContext {
1571 server_label: "https://api.dasch.swiss".into(),
1572 auth_state: "anonymous".into(),
1573 filter_warning: None,
1574 count_caveat: Some("counts exclude deleted resources".into()),
1575 count_cost: None,
1576 };
1577 renderer.resource_type_describe(&detail, &meta).unwrap();
1578
1579 let parsed: serde_json::Value = serde_json::from_str(out.string().trim()).unwrap();
1580 assert_eq!(parsed["data"]["count"], 99);
1581 assert_eq!(
1582 parsed["_meta"]["note"], "counts exclude deleted resources",
1583 "note must carry count_caveat"
1584 );
1585 }
1586
1587 #[test]
1588 fn resource_type_describe_json_no_count_no_note() {
1589 let out = SharedBuf::new();
1592 let mut renderer = JsonRenderer::with_writer(out.clone());
1593 let meta = make_meta("anonymous", "https://api.dasch.swiss");
1594 renderer.resource_type_describe(&make_minimal_rt_detail(), &meta).unwrap();
1595
1596 let parsed: serde_json::Value = serde_json::from_str(out.string().trim()).unwrap();
1597 assert!(!parsed["data"].as_object().unwrap().contains_key("count"));
1598 assert!(!parsed["_meta"].as_object().unwrap().contains_key("note"));
1599 }
1600
1601 use crate::model::resource_type::ValueType;
1604 use crate::model::{
1605 DatePoint, DateValue, FieldValues, FileValue, ResourceAccess, ResourceDetail, ResourceVisibility, Value,
1606 ValueContent,
1607 };
1608
1609 fn make_resource_detail_no_values() -> ResourceDetail {
1610 ResourceDetail {
1611 label: "Test Resource".into(),
1612 iri: "http://rdfh.ch/0803/abc123".into(),
1613 resource_type: "Page".into(),
1614 ark_url: None,
1615 creation_date: None,
1616 last_modified: None,
1617 attached_project: None,
1618 owner: None,
1619 visibility: Some(ResourceVisibility::Public),
1620 your_access: Some(ResourceAccess::View),
1621 values: None,
1622 }
1623 }
1624
1625 #[test]
1626 fn resource_describe_json_no_values_key_absent() {
1627 let out = SharedBuf::new();
1629 let mut renderer = JsonRenderer::with_writer(out.clone());
1630 let meta = make_meta("anonymous", "https://api.test.dasch.swiss");
1631 renderer.resource_describe(&make_resource_detail_no_values(), &meta).unwrap();
1632
1633 let parsed: serde_json::Value = serde_json::from_str(out.string().trim()).unwrap();
1634 let data = &parsed["data"];
1635 assert!(
1636 data["values"].is_null() && !data.as_object().unwrap().contains_key("values"),
1637 "values key must be absent when values is None; got data: {data}"
1638 );
1639 }
1640
1641 #[test]
1642 fn resource_describe_json_some_values_array_present() {
1643 let out = SharedBuf::new();
1645 let mut renderer = JsonRenderer::with_writer(out.clone());
1646 let mut detail = make_resource_detail_no_values();
1647 detail.values = Some(vec![FieldValues {
1648 name: "hasTitle".into(),
1649 label: Some("Title".into()),
1650 values: vec![ValueContent::Text("Hello".into()).into()],
1651 }]);
1652 let meta = make_meta("anonymous", "https://api.test.dasch.swiss");
1653 renderer.resource_describe(&detail, &meta).unwrap();
1654
1655 let parsed: serde_json::Value = serde_json::from_str(out.string().trim()).unwrap();
1656 let data = &parsed["data"];
1657 assert!(
1658 data.as_object().unwrap().contains_key("values"),
1659 "values key must be present when values is Some; got data: {data}"
1660 );
1661 let values = data["values"].as_array().unwrap();
1662 assert_eq!(values.len(), 1);
1663 assert_eq!(values[0]["field"], "hasTitle");
1664 assert_eq!(values[0]["field_label"], "Title");
1665 let val_objs = values[0]["values"].as_array().unwrap();
1666 assert_eq!(val_objs.len(), 1);
1667 assert_eq!(val_objs[0]["value_type"], "text");
1668 assert_eq!(val_objs[0]["text"], "Hello");
1669 }
1670
1671 #[test]
1672 fn resource_describe_json_some_empty_values_array() {
1673 let out = SharedBuf::new();
1675 let mut renderer = JsonRenderer::with_writer(out.clone());
1676 let mut detail = make_resource_detail_no_values();
1677 detail.values = Some(vec![]);
1678 let meta = make_meta("anonymous", "https://api.test.dasch.swiss");
1679 renderer.resource_describe(&detail, &meta).unwrap();
1680
1681 let parsed: serde_json::Value = serde_json::from_str(out.string().trim()).unwrap();
1682 let data = &parsed["data"];
1683 assert!(
1684 data.as_object().unwrap().contains_key("values"),
1685 "values key must be present even for empty Some(vec![]);"
1686 );
1687 assert!(data["values"].as_array().unwrap().is_empty());
1688 }
1689
1690 #[test]
1691 fn resource_describe_json_field_label_null_when_none() {
1692 let out = SharedBuf::new();
1694 let mut renderer = JsonRenderer::with_writer(out.clone());
1695 let mut detail = make_resource_detail_no_values();
1696 detail.values = Some(vec![FieldValues {
1697 name: "seqnum".into(),
1698 label: None,
1699 values: vec![ValueContent::Integer(42).into()],
1700 }]);
1701 let meta = make_meta("anonymous", "https://api.test.dasch.swiss");
1702 renderer.resource_describe(&detail, &meta).unwrap();
1703
1704 let parsed: serde_json::Value = serde_json::from_str(out.string().trim()).unwrap();
1705 let fg = &parsed["data"]["values"][0];
1706 assert!(fg["field_label"].is_null(), "field_label must be null when label is None");
1707 let val = &fg["values"][0];
1708 assert_eq!(val["value_type"], "integer");
1709 assert_eq!(val["value"], 42);
1710 }
1711
1712 #[test]
1713 fn resource_describe_json_link_value() {
1714 let out = SharedBuf::new();
1715 let mut renderer = JsonRenderer::with_writer(out.clone());
1716 let mut detail = make_resource_detail_no_values();
1717 detail.values = Some(vec![FieldValues {
1718 name: "isPartOf".into(),
1719 label: None,
1720 values: vec![
1721 ValueContent::Link {
1722 target_iri: "http://rdfh.ch/0803/book1".into(),
1723 target_label: Some("My Book".into()),
1724 }
1725 .into(),
1726 ValueContent::Link {
1727 target_iri: "http://rdfh.ch/0803/book2".into(),
1728 target_label: None,
1729 }
1730 .into(),
1731 ],
1732 }]);
1733 let meta = make_meta("anonymous", "https://api.test.dasch.swiss");
1734 renderer.resource_describe(&detail, &meta).unwrap();
1735
1736 let parsed: serde_json::Value = serde_json::from_str(out.string().trim()).unwrap();
1737 let vals = &parsed["data"]["values"][0]["values"];
1738 assert_eq!(vals[0]["value_type"], "link");
1740 assert_eq!(vals[0]["target_iri"], "http://rdfh.ch/0803/book1");
1741 assert_eq!(vals[0]["target_label"], "My Book");
1742 assert_eq!(vals[1]["value_type"], "link");
1744 assert_eq!(vals[1]["target_iri"], "http://rdfh.ch/0803/book2");
1745 assert!(vals[1]["target_label"].is_null());
1746 }
1747
1748 #[test]
1749 fn resource_describe_json_still_image_value() {
1750 let out = SharedBuf::new();
1751 let mut renderer = JsonRenderer::with_writer(out.clone());
1752 let mut detail = make_resource_detail_no_values();
1753 detail.values = Some(vec![FieldValues {
1754 name: "hasStillImageFileValue".into(),
1755 label: None,
1756 values: vec![
1757 ValueContent::File(FileValue {
1758 value_type: ValueType::StillImage,
1759 filename: "image.jp2".into(),
1760 url: "https://iiif.example.com/image.jp2/full/max/0/default.jpg".into(),
1761 width: Some(1200),
1762 height: Some(800),
1763 })
1764 .into(),
1765 ],
1766 }]);
1767 let meta = make_meta("anonymous", "https://api.test.dasch.swiss");
1768 renderer.resource_describe(&detail, &meta).unwrap();
1769
1770 let parsed: serde_json::Value = serde_json::from_str(out.string().trim()).unwrap();
1771 let val = &parsed["data"]["values"][0]["values"][0];
1772 assert_eq!(val["value_type"], "still-image");
1773 assert_eq!(val["filename"], "image.jp2");
1774 assert_eq!(val["url"], "https://iiif.example.com/image.jp2/full/max/0/default.jpg");
1775 assert_eq!(val["width"], 1200);
1776 assert_eq!(val["height"], 800);
1777 }
1778
1779 #[test]
1780 fn resource_describe_json_date_value() {
1781 let out = SharedBuf::new();
1782 let mut renderer = JsonRenderer::with_writer(out.clone());
1783 let mut detail = make_resource_detail_no_values();
1784 let pt = DatePoint {
1785 year: Some(1489),
1786 month: None,
1787 day: None,
1788 era: Some("CE".into()),
1789 };
1790 detail.values = Some(vec![FieldValues {
1791 name: "hasDate".into(),
1792 label: None,
1793 values: vec![
1794 ValueContent::Date(DateValue {
1795 calendar: "GREGORIAN".into(),
1796 start: pt.clone(),
1797 end: pt.clone(),
1798 })
1799 .into(),
1800 ],
1801 }]);
1802 let meta = make_meta("anonymous", "https://api.test.dasch.swiss");
1803 renderer.resource_describe(&detail, &meta).unwrap();
1804
1805 let parsed: serde_json::Value = serde_json::from_str(out.string().trim()).unwrap();
1806 let val = &parsed["data"]["values"][0]["values"][0];
1807 assert_eq!(val["value_type"], "date");
1808 assert_eq!(val["calendar"], "GREGORIAN");
1809 assert_eq!(val["start"]["year"], 1489);
1810 assert_eq!(val["start"]["era"], "CE");
1811 assert!(val["start"]["month"].is_null());
1812 assert!(val["start"]["day"].is_null());
1813 }
1814
1815 #[test]
1816 fn resource_describe_json_comment_present_when_set() {
1817 let out = SharedBuf::new();
1819 let mut renderer = JsonRenderer::with_writer(out.clone());
1820 let mut detail = make_resource_detail_no_values();
1821 detail.values = Some(vec![FieldValues {
1822 name: "hasTranscription".into(),
1823 label: None,
1824 values: vec![Value {
1825 content: ValueContent::Text("some transcription".into()),
1826 comment: Some("reading uncertain".into()),
1827 }],
1828 }]);
1829 let meta = make_meta("anonymous", "https://api.test.dasch.swiss");
1830 renderer.resource_describe(&detail, &meta).unwrap();
1831
1832 let parsed: serde_json::Value = serde_json::from_str(out.string().trim()).unwrap();
1833 let val = &parsed["data"]["values"][0]["values"][0];
1834 assert_eq!(val["value_type"], "text");
1835 assert_eq!(val["text"], "some transcription");
1836 assert_eq!(val["comment"], "reading uncertain");
1837 }
1838
1839 #[test]
1840 fn resource_describe_json_comment_absent_when_none() {
1841 let out = SharedBuf::new();
1843 let mut renderer = JsonRenderer::with_writer(out.clone());
1844 let mut detail = make_resource_detail_no_values();
1845 detail.values = Some(vec![FieldValues {
1846 name: "hasTranscription".into(),
1847 label: None,
1848 values: vec![ValueContent::Text("plain transcription".into()).into()],
1849 }]);
1850 let meta = make_meta("anonymous", "https://api.test.dasch.swiss");
1851 renderer.resource_describe(&detail, &meta).unwrap();
1852
1853 let parsed: serde_json::Value = serde_json::from_str(out.string().trim()).unwrap();
1854 let val = &parsed["data"]["values"][0]["values"][0];
1855 assert!(
1856 !val.as_object().unwrap().contains_key("comment"),
1857 "comment key must be absent when comment is None; got: {val}"
1858 );
1859 }
1860}