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 "data_models": item.data_models,
386 })
387 })
388 .collect();
389
390 let obj = json!({
391 "_meta": meta_block(meta, 0),
392 "data": data,
393 });
394 writeln!(
395 self.out,
396 "{}",
397 serde_json::to_string(&obj).map_err(|e| Diagnostic::Internal(format!("json serialisation error: {e}")))?
398 )?;
399 Ok(())
400 }
401
402 fn project_describe(&mut self, project: &ProjectDetail, meta: &MetaContext) -> Result<(), Diagnostic> {
403 let description: Vec<serde_json::Value> = project
406 .description
407 .iter()
408 .map(|d| {
409 json!({
410 "value": d.value,
411 "language": d.language,
412 })
413 })
414 .collect();
415
416 let data_models: Vec<serde_json::Value> = project
417 .data_models
418 .iter()
419 .map(|dm| {
420 json!({
421 "name": dm.name,
422 "iri": dm.iri,
423 })
424 })
425 .collect();
426
427 let obj = json!({
428 "_meta": meta_block(meta, 0),
429 "data": {
430 "iri": project.iri,
431 "shortcode": project.shortcode,
432 "shortname": project.shortname,
433 "longname": project.longname,
434 "description": description,
435 "keywords": project.keywords,
436 "data_models": data_models,
437 },
438 });
439 writeln!(
440 self.out,
441 "{}",
442 serde_json::to_string(&obj).map_err(|e| Diagnostic::Internal(format!("json serialisation error: {e}")))?
443 )?;
444 Ok(())
445 }
446
447 fn data_model_describe(&mut self, detail: &DataModelDetail, meta: &MetaContext) -> Result<(), Diagnostic> {
448 let resource_types: Vec<serde_json::Value> = detail
452 .resource_types
453 .iter()
454 .map(|rt| {
455 json!({
456 "name": rt.name,
457 "iri": rt.iri,
458 "label": rt.label,
459 })
460 })
461 .collect();
462
463 let obj = json!({
464 "_meta": meta_block(meta, 0),
465 "data": {
466 "name": detail.name,
467 "iri": detail.iri,
468 "label": detail.label,
469 "last_modified": detail.last_modified,
470 "resource_types": resource_types,
471 },
472 });
473 writeln!(
474 self.out,
475 "{}",
476 serde_json::to_string(&obj).map_err(|e| Diagnostic::Internal(format!("json serialisation error: {e}")))?
477 )?;
478 Ok(())
479 }
480
481 fn data_models(&mut self, view: &DataModelListView, meta: &MetaContext) -> Result<(), Diagnostic> {
482 let data: Vec<serde_json::Value> = view
486 .items
487 .iter()
488 .map(|item| {
489 json!({
490 "name": item.name,
491 "iri": item.iri,
492 "label": item.label,
493 "last_modified": item.last_modified,
494 "is_builtin": item.is_builtin,
495 })
496 })
497 .collect();
498
499 let obj = json!({
500 "_meta": meta_block(meta, 0),
501 "data": data,
502 });
503 writeln!(
504 self.out,
505 "{}",
506 serde_json::to_string(&obj).map_err(|e| Diagnostic::Internal(format!("json serialisation error: {e}")))?
507 )?;
508 Ok(())
509 }
510
511 fn resource_types(&mut self, view: &ResourceTypeListView, meta: &MetaContext) -> Result<(), Diagnostic> {
512 let data: Vec<serde_json::Value> = view
523 .items
524 .iter()
525 .map(|item| {
526 let mut obj = json!({
527 "name": item.name,
528 "iri": item.iri,
529 "label": item.label,
530 "is_builtin": item.is_builtin,
531 });
532 if let Some(count) = item.count {
533 obj["count"] = serde_json::Value::from(count);
534 }
535 obj
536 })
537 .collect();
538
539 let mut meta_obj = meta_block(meta, 0);
541 if let Some(ref cc) = meta.count_caveat {
542 meta_obj["note"] = serde_json::Value::from(cc.as_str());
543 }
544
545 let obj = json!({
546 "_meta": meta_obj,
547 "data": data,
548 });
549 writeln!(
550 self.out,
551 "{}",
552 serde_json::to_string(&obj).map_err(|e| Diagnostic::Internal(format!("json serialisation error: {e}")))?
553 )?;
554 Ok(())
555 }
556
557 fn resource_type_describe(
558 &mut self,
559 detail: &crate::model::ResourceTypeDetail,
560 meta: &MetaContext,
561 ) -> Result<(), Diagnostic> {
562 let fields: Vec<serde_json::Value> = detail
566 .fields
567 .iter()
568 .map(|f| {
569 json!({
570 "name": f.name,
571 "iri": f.iri,
572 "label": f.label,
573 "value_type": f.value_type.to_string(),
574 "link_target": f.link_target,
575 "cardinality": f.cardinality.to_string(),
576 "is_builtin": f.is_builtin,
577 "data_model": f.data_model,
578 })
579 })
580 .collect();
581
582 let mut meta_obj = meta_block(meta, 0);
584 if let Some(ref cc) = meta.count_caveat {
585 meta_obj["note"] = serde_json::Value::from(cc.as_str());
586 }
587
588 let mut data = json!({
594 "name": detail.name,
595 "iri": detail.iri,
596 "label": detail.label,
597 "data_model": detail.data_model,
598 "representation": detail.representation.as_ref().map(|r| r.to_string()),
599 "super_types": detail.super_types,
600 "fields": fields,
601 });
602 if let Some(count) = detail.count {
603 data["count"] = serde_json::Value::from(count);
604 }
605
606 let obj = json!({
607 "_meta": meta_obj,
608 "data": data,
609 });
610 writeln!(
611 self.out,
612 "{}",
613 serde_json::to_string(&obj).map_err(|e| Diagnostic::Internal(format!("json serialisation error: {e}")))?
614 )?;
615 Ok(())
616 }
617
618 fn data_model_structure(&mut self, structure: &DataModelStructure, meta: &MetaContext) -> Result<(), Diagnostic> {
619 let data: Vec<serde_json::Value> = structure
623 .relations
624 .iter()
625 .map(|r| {
626 json!({
627 "source": r.source,
628 "target": r.target,
629 "kind": r.kind.to_string(),
630 "field": r.field,
631 "target_data_model": r.target_data_model,
632 })
633 })
634 .collect();
635
636 let obj = json!({
637 "_meta": meta_block(meta, 0),
638 "data": data,
639 });
640 writeln!(
641 self.out,
642 "{}",
643 serde_json::to_string(&obj).map_err(|e| Diagnostic::Internal(format!("json serialisation error: {e}")))?
644 )?;
645 Ok(())
646 }
647
648 fn resources(&mut self, view: &ResourceListView, meta: &MetaContext) -> Result<(), Diagnostic> {
649 let data: Vec<serde_json::Value> = view
651 .items
652 .iter()
653 .map(|item| {
654 json!({
655 "label": item.label,
656 "iri": item.iri,
657 "ark_url": item.ark_url,
658 "creation_date": item.creation_date,
659 "last_modified": item.last_modified,
660 "resource_type": item.resource_type,
661 })
662 })
663 .collect();
664
665 let mut meta_obj = meta_block(meta, 0);
667 match &view.pagination {
668 ResourceListPagination::SinglePage { page, may_have_more } => {
669 meta_obj["page"] = serde_json::Value::from(*page);
670 meta_obj["may_have_more_results"] = serde_json::Value::from(*may_have_more);
671 }
672 ResourceListPagination::AllPages { pages_fetched } => {
673 meta_obj["pages_fetched"] = serde_json::Value::from(*pages_fetched);
674 meta_obj["may_have_more_results"] = serde_json::Value::from(false);
676 }
677 }
678
679 if let Some(ref fw) = meta.filter_warning {
681 meta_obj["note"] = serde_json::Value::from(fw.as_str());
682 }
683
684 let obj = json!({
685 "_meta": meta_obj,
686 "data": data,
687 });
688 writeln!(
689 self.out,
690 "{}",
691 serde_json::to_string(&obj).map_err(|e| Diagnostic::Internal(format!("json serialisation error: {e}")))?
692 )?;
693 Ok(())
694 }
695
696 fn resource_describe(&mut self, detail: &ResourceDetail, meta: &MetaContext) -> Result<(), Diagnostic> {
697 let mut meta_obj = meta_block(meta, 0);
701 if let Some(ref fw) = meta.filter_warning {
702 meta_obj["note"] = serde_json::Value::from(fw.as_str());
703 }
704
705 let mut data = serde_json::Map::new();
707 data.insert("label".into(), serde_json::Value::String(detail.label.clone()));
708 data.insert("iri".into(), serde_json::Value::String(detail.iri.clone()));
709 data.insert("resource_type".into(), serde_json::Value::String(detail.resource_type.clone()));
710 data.insert(
711 "ark_url".into(),
712 detail
713 .ark_url
714 .as_ref()
715 .map_or(serde_json::Value::Null, |s| serde_json::Value::String(s.clone())),
716 );
717 data.insert(
718 "creation_date".into(),
719 detail
720 .creation_date
721 .as_ref()
722 .map_or(serde_json::Value::Null, |s| serde_json::Value::String(s.clone())),
723 );
724 data.insert(
725 "last_modified".into(),
726 detail
727 .last_modified
728 .as_ref()
729 .map_or(serde_json::Value::Null, |s| serde_json::Value::String(s.clone())),
730 );
731 data.insert(
732 "attached_project".into(),
733 detail
734 .attached_project
735 .as_ref()
736 .map_or(serde_json::Value::Null, |s| serde_json::Value::String(s.clone())),
737 );
738 data.insert(
739 "owner".into(),
740 detail
741 .owner
742 .as_ref()
743 .map_or(serde_json::Value::Null, |s| serde_json::Value::String(s.clone())),
744 );
745 data.insert(
746 "visibility".into(),
747 detail
748 .visibility
749 .as_ref()
750 .map_or(serde_json::Value::Null, |v| serde_json::Value::String(v.as_str().into())),
751 );
752 data.insert(
753 "your_access".into(),
754 detail
755 .your_access
756 .as_ref()
757 .map_or(serde_json::Value::Null, |a| serde_json::Value::String(a.as_str().into())),
758 );
759 if let Some(ref fields) = detail.values {
762 let values_arr: Vec<serde_json::Value> = fields
763 .iter()
764 .map(|fv| {
765 let value_objs: Vec<serde_json::Value> = fv
766 .values
767 .iter()
768 .map(|v| {
769 let mut obj = value_content_to_json(&v.content);
770 if let (Some(c), serde_json::Value::Object(m)) = (&v.comment, &mut obj) {
771 m.insert("comment".into(), serde_json::Value::String(c.clone()));
772 }
773 obj
774 })
775 .collect();
776 let mut fg = serde_json::Map::new();
777 fg.insert("field".into(), serde_json::Value::String(fv.name.clone()));
778 let fl_val = match &fv.label {
779 Some(s) => serde_json::Value::String(s.clone()),
780 None => serde_json::Value::Null,
781 };
782 fg.insert("field_label".into(), fl_val);
783 fg.insert("values".into(), serde_json::Value::Array(value_objs));
784 serde_json::Value::Object(fg)
785 })
786 .collect();
787 data.insert("values".into(), serde_json::Value::Array(values_arr));
788 }
789
790 let obj = json!({
791 "_meta": meta_obj,
792 "data": serde_json::Value::Object(data),
793 });
794 writeln!(
795 self.out,
796 "{}",
797 serde_json::to_string(&obj).map_err(|e| Diagnostic::Internal(format!("json serialisation error: {e}")))?
798 )?;
799 Ok(())
800 }
801
802 fn vocabularies(&mut self, view: &VocabularyListView, meta: &MetaContext) -> Result<(), Diagnostic> {
803 let data: Vec<serde_json::Value> = view
809 .items
810 .iter()
811 .map(|item| {
812 let labels = localized_text_array(&item.header.labels);
813 let comments = localized_text_array(&item.header.comments);
814 let mut obj = json!({
815 "name": item.header.name,
816 "iri": item.header.iri,
817 "labels": labels,
818 "comments": comments,
819 });
820 if let Some(n) = item.node_count {
821 obj["nodes"] = serde_json::Value::from(n);
822 }
823 if let Some(d) = item.depth {
824 obj["depth"] = serde_json::Value::from(d);
825 }
826 obj
827 })
828 .collect();
829
830 let mut meta_obj = meta_block(meta, 0);
833 if let Some(ref cc) = meta.count_cost {
834 meta_obj["note"] = serde_json::Value::from(cc.as_str());
835 }
836
837 let obj = json!({
838 "_meta": meta_obj,
839 "data": data,
840 });
841 writeln!(
842 self.out,
843 "{}",
844 serde_json::to_string(&obj).map_err(|e| Diagnostic::Internal(format!("json serialisation error: {e}")))?
845 )?;
846 Ok(())
847 }
848
849 fn vocabulary_describe(&mut self, detail: &VocabularyDetail, meta: &MetaContext) -> Result<(), Diagnostic> {
850 fn nested_to_json(node: &NestedVocabularyNode<'_>) -> serde_json::Value {
864 let h = node.header;
865 json!({
866 "node_iri": h.iri,
867 "number": node.number,
868 "name": h.name,
869 "labels": localized_text_array(&h.labels),
870 "comments": localized_text_array(&h.comments),
871 "position": node.position,
872 "children": node.children.iter().map(nested_to_json).collect::<Vec<_>>(),
873 })
874 }
875
876 let root = &detail.tree.root;
877 let children: Vec<serde_json::Value> =
878 nest_vocabulary_detail(detail).iter().map(nested_to_json).collect::<Vec<_>>();
879
880 let mut data = json!({
881 "name": root.name,
882 "iri": root.iri,
883 "labels": localized_text_array(&root.labels),
884 "comments": localized_text_array(&root.comments),
885 "nodes": detail.node_count,
886 "depth": detail.depth,
887 "children": children,
888 });
889 if let Some(ref requested) = detail.tree.requested_node {
893 data["requested_node"] = serde_json::Value::from(requested.as_str());
894 }
895 if let Some(ref subtree_of) = detail.subtree_of {
896 data["subtree_of"] = serde_json::Value::from(subtree_of.as_str());
897 }
898
899 let obj = json!({
900 "_meta": meta_block(meta, 0),
901 "data": data,
902 });
903 writeln!(
904 self.out,
905 "{}",
906 serde_json::to_string(&obj).map_err(|e| Diagnostic::Internal(format!("json serialisation error: {e}")))?
907 )?;
908 Ok(())
909 }
910}
911
912#[cfg(test)]
913mod tests {
914 use super::*;
915 use crate::model::{
916 DataModel, DataModelDetail, DataModelSummary, Project, ProjectDescription, ProjectDetail, ResourceType,
917 ResourceTypeSummary,
918 };
919 use crate::render::test_support::{SharedBuf, make_meta};
920 use crate::render::{DataModelListView, ResourceTypeListView};
921
922 #[test]
923 fn meta_block_full_context_has_all_three_keys() {
924 let meta = MetaContext {
927 server_label: "https://api.test.dasch.swiss".into(),
928 auth_state: "anonymous".into(),
929 filter_warning: None,
930 count_caveat: None,
931 count_cost: None,
932 };
933 let block = meta_block(&meta, 1);
934 let obj = block.as_object().unwrap();
935 assert_eq!(obj["server"], "https://api.test.dasch.swiss");
936 assert_eq!(obj["auth"], "anonymous");
937 assert_eq!(obj["exit_code"], 1);
938 assert_eq!(obj.len(), 3, "expected exactly three keys; got: {block}");
939 }
940
941 #[test]
942 fn meta_block_empty_server_and_auth_omits_both_keys() {
943 let meta = MetaContext {
946 server_label: String::new(),
947 auth_state: String::new(),
948 filter_warning: None,
949 count_caveat: None,
950 count_cost: None,
951 };
952 let block = meta_block(&meta, 2);
953 let obj = block.as_object().unwrap();
954 assert!(
955 !obj.contains_key("server"),
956 "server key must be absent when server_label is empty; got: {block}"
957 );
958 assert!(
959 !obj.contains_key("auth"),
960 "auth key must be absent when auth_state is empty; got: {block}"
961 );
962 assert_eq!(obj["exit_code"], 2);
963 assert_eq!(obj.len(), 1, "expected exactly one key; got: {block}");
964 }
965
966 #[test]
967 fn projects_json_output() {
968 let out = SharedBuf::new();
969 let mut renderer = JsonRenderer::with_writer(out.clone());
970 let items = vec![
971 Project {
972 iri: "http://rdfh.ch/projects/0001".into(),
973 shortcode: "0001".into(),
974 shortname: "anything".into(),
975 longname: Some("Anything Project".into()),
976 data_models: 2,
977 },
978 Project {
979 iri: "http://rdfh.ch/projects/0002".into(),
980 shortcode: "0002".into(),
981 shortname: "images".into(),
982 longname: None,
983 data_models: 0,
984 },
985 ];
986 let view = ProjectListView { items, total: 2, filter: None };
987 let meta = make_meta("anonymous", "https://api.test.dasch.swiss");
988 renderer.projects(&view, &meta).unwrap();
989
990 let s = out.string();
991 let parsed: serde_json::Value = serde_json::from_str(s.trim()).unwrap();
992
993 assert_eq!(parsed["_meta"]["auth"], "anonymous");
995 assert_eq!(parsed["_meta"]["server"], "https://api.test.dasch.swiss");
996 assert_eq!(parsed["_meta"]["exit_code"], 0);
997
998 let data = parsed["data"].as_array().unwrap();
1000 assert_eq!(data.len(), 2);
1001
1002 assert_eq!(data[0]["shortcode"], "0001");
1004 assert_eq!(data[0]["shortname"], "anything");
1005 assert_eq!(data[0]["longname"], "Anything Project");
1006 assert_eq!(data[0]["data_models"], 2);
1007 assert_eq!(data[0]["iri"], "http://rdfh.ch/projects/0001");
1008
1009 assert_eq!(data[1]["shortcode"], "0002");
1011 assert!(data[1]["longname"].is_null());
1012 assert_eq!(data[1]["data_models"], 0);
1013 }
1014
1015 #[test]
1016 fn projects_json_empty_data_array() {
1017 let out = SharedBuf::new();
1018 let mut renderer = JsonRenderer::with_writer(out.clone());
1019 let view = ProjectListView { items: vec![], total: 0, filter: None };
1020 let meta = make_meta("anonymous", "https://api.test.dasch.swiss");
1021 renderer.projects(&view, &meta).unwrap();
1022
1023 let parsed: serde_json::Value = serde_json::from_str(out.string().trim()).unwrap();
1024 assert!(parsed["data"].as_array().unwrap().is_empty());
1025 }
1026
1027 #[test]
1034 fn diagnostic_kind_covers_all_variants() {
1035 let cases: &[(&Diagnostic, &str)] = &[
1036 (&Diagnostic::Usage("x".into()), "usage"),
1037 (&Diagnostic::AuthRequired("x".into()), "auth_required"),
1038 (&Diagnostic::NotFound("x".into()), "not_found"),
1039 (&Diagnostic::ServerError("x".into()), "server_error"),
1040 (&Diagnostic::Network("x".into()), "network"),
1041 (&Diagnostic::Conflict("x".into()), "conflict"),
1042 (&Diagnostic::Io("x".into()), "io"),
1043 (&Diagnostic::Internal("x".into()), "internal"),
1044 (&Diagnostic::NotImplemented("x".into()), "internal"),
1045 ];
1046 for (diag, expected_kind) in cases {
1047 assert_eq!(diagnostic_kind(diag), *expected_kind, "unexpected kind for {diag:?}");
1048 }
1049 }
1050
1051 #[test]
1052 fn conflict_kind_is_conflict() {
1053 let d = Diagnostic::Conflict("dump already in progress".into());
1054 assert_eq!(diagnostic_kind(&d), "conflict");
1055 }
1056
1057 #[test]
1058 fn io_kind_is_io() {
1059 let d = Diagnostic::Io("failed to write /tmp/0001.zip: permission denied".into());
1060 assert_eq!(diagnostic_kind(&d), "io");
1061 }
1062
1063 #[test]
1070 fn diagnostic_not_found_json_output() {
1071 let out = SharedBuf::new();
1072 let mut renderer = JsonRenderer::with_writer(out.clone());
1073 let diag = Diagnostic::NotFound("resource http://rdfh.ch/0001/xyz not found".into());
1074 let meta = make_meta("anonymous", "https://api.test.dasch.swiss");
1075 renderer.diagnostic(&diag, &meta).unwrap();
1076
1077 let parsed: serde_json::Value = serde_json::from_str(out.string().trim()).unwrap();
1078 assert_eq!(parsed["error"]["kind"], "not_found");
1079 assert_eq!(parsed["_meta"]["exit_code"], 1);
1080 let message = parsed["error"]["message"].as_str().expect("error.message must be a string");
1081 assert!(!message.is_empty(), "error.message must be non-empty");
1082 assert_eq!(message, diag.to_string());
1083 }
1084
1085 fn make_beol_detail() -> ProjectDetail {
1086 ProjectDetail {
1090 iri: "http://rdfh.ch/projects/yTerZGyxjZVqFMNNKXCDPF".into(),
1091 shortcode: "0801".into(),
1092 shortname: "beol".into(),
1093 longname: Some("Bernoulli-Euler Online".into()),
1094 description: vec![ProjectDescription {
1095 value: "<b>BEOL</b> — early modern mathematics.".into(),
1096 language: Some("en".into()),
1097 }],
1098 keywords: vec!["Bernoulli".into(), "Euler".into(), "Mathematics".into()],
1099 data_models: vec![
1100 DataModelSummary {
1101 name: "beol".into(),
1102 iri: "http://api.dasch.swiss/ontology/0801/beol/v2".into(),
1103 },
1104 DataModelSummary {
1105 name: "biblio".into(),
1106 iri: "http://api.dasch.swiss/ontology/0801/biblio/v2".into(),
1107 },
1108 DataModelSummary {
1109 name: "leibniz".into(),
1110 iri: "http://api.dasch.swiss/ontology/0801/leibniz/v2".into(),
1111 },
1112 DataModelSummary {
1113 name: "newton".into(),
1114 iri: "http://api.dasch.swiss/ontology/0801/newton/v2".into(),
1115 },
1116 ],
1117 }
1118 }
1119
1120 #[test]
1121 fn project_describe_json_full() {
1122 let out = SharedBuf::new();
1123 let mut renderer = JsonRenderer::with_writer(out.clone());
1124 let meta = make_meta("anonymous", "https://api.test.dasch.swiss");
1125 renderer.project_describe(&make_beol_detail(), &meta).unwrap();
1126
1127 let parsed: serde_json::Value = serde_json::from_str(out.string().trim()).unwrap();
1128
1129 assert_eq!(parsed["_meta"]["auth"], "anonymous");
1131 assert_eq!(parsed["_meta"]["exit_code"], 0);
1132
1133 let data = &parsed["data"];
1135 assert!(data.is_object());
1136 assert_eq!(data["iri"], "http://rdfh.ch/projects/yTerZGyxjZVqFMNNKXCDPF");
1137 assert_eq!(data["shortcode"], "0801");
1138 assert_eq!(data["shortname"], "beol");
1139 assert_eq!(data["longname"], "Bernoulli-Euler Online");
1140
1141 let desc = data["description"].as_array().unwrap();
1143 assert_eq!(desc.len(), 1);
1144 assert_eq!(desc[0]["value"], "<b>BEOL</b> — early modern mathematics.");
1145 assert_eq!(desc[0]["language"], "en");
1146
1147 let kws = data["keywords"].as_array().unwrap();
1149 assert_eq!(kws.len(), 3);
1150 assert_eq!(kws[0], "Bernoulli");
1151
1152 let dms = data["data_models"].as_array().unwrap();
1155 assert_eq!(dms.len(), 4, "all four data-models must be rendered");
1156 assert_eq!(dms[0]["name"], "beol");
1157 assert_eq!(dms[0]["iri"], "http://api.dasch.swiss/ontology/0801/beol/v2");
1158 assert_eq!(dms[1]["name"], "biblio");
1159 assert_eq!(dms[2]["name"], "leibniz");
1160 assert_eq!(dms[3]["name"], "newton");
1161 }
1162
1163 #[test]
1164 fn project_describe_json_no_longname() {
1165 let out = SharedBuf::new();
1166 let mut renderer = JsonRenderer::with_writer(out.clone());
1167 let mut detail = make_beol_detail();
1168 detail.longname = None;
1169 let meta = make_meta("anonymous", "https://api.test.dasch.swiss");
1170 renderer.project_describe(&detail, &meta).unwrap();
1171
1172 let parsed: serde_json::Value = serde_json::from_str(out.string().trim()).unwrap();
1173 assert!(parsed["data"]["longname"].is_null());
1175 }
1176
1177 #[test]
1178 fn project_describe_json_empty_fields() {
1179 let out = SharedBuf::new();
1180 let mut renderer = JsonRenderer::with_writer(out.clone());
1181 let detail = ProjectDetail {
1182 iri: "http://rdfh.ch/projects/0000".into(),
1183 shortcode: "0000".into(),
1184 shortname: "minimal".into(),
1185 longname: None,
1186 description: vec![],
1187 keywords: vec![],
1188 data_models: vec![],
1189 };
1190 let meta = make_meta("anonymous", "https://api.test.dasch.swiss");
1191 renderer.project_describe(&detail, &meta).unwrap();
1192
1193 let parsed: serde_json::Value = serde_json::from_str(out.string().trim()).unwrap();
1194 let data = &parsed["data"];
1195 assert!(data["description"].as_array().unwrap().is_empty());
1196 assert!(data["keywords"].as_array().unwrap().is_empty());
1197 assert!(data["data_models"].as_array().unwrap().is_empty());
1198 }
1199
1200 fn make_data_model_fixture() -> Vec<DataModel> {
1201 vec![
1202 DataModel {
1203 name: "beol".into(),
1204 iri: "http://api.dasch.swiss/ontology/0801/beol/v2".into(),
1205 label: Some("The BEOL data-model".into()),
1206 last_modified: Some("2024-05-27T13:43:26.233048Z".into()),
1207 is_builtin: false,
1208 },
1209 DataModel {
1210 name: "biblio".into(),
1211 iri: "http://api.dasch.swiss/ontology/0801/biblio/v2".into(),
1212 label: None,
1213 last_modified: None,
1214 is_builtin: false,
1215 },
1216 DataModel {
1217 name: "knora-api".into(),
1218 iri: "http://api.knora.org/ontology/knora-api/v2".into(),
1219 label: None,
1220 last_modified: None,
1221 is_builtin: true,
1222 },
1223 ]
1224 }
1225
1226 #[test]
1227 fn data_models_json_output() {
1228 let out = SharedBuf::new();
1229 let mut renderer = JsonRenderer::with_writer(out.clone());
1230 let view = DataModelListView { items: make_data_model_fixture(), total: 3, filter: None };
1231 let meta = make_meta("anonymous", "https://api.test.dasch.swiss");
1232 renderer.data_models(&view, &meta).unwrap();
1233
1234 let parsed: serde_json::Value = serde_json::from_str(out.string().trim()).unwrap();
1235
1236 assert_eq!(parsed["_meta"]["auth"], "anonymous");
1238 assert_eq!(parsed["_meta"]["server"], "https://api.test.dasch.swiss");
1239 assert_eq!(parsed["_meta"]["exit_code"], 0);
1240
1241 let data = parsed["data"].as_array().unwrap();
1243 assert_eq!(data.len(), 3);
1244
1245 assert_eq!(data[0]["name"], "beol");
1247 assert_eq!(data[0]["iri"], "http://api.dasch.swiss/ontology/0801/beol/v2");
1248 assert_eq!(data[0]["label"], "The BEOL data-model");
1249 assert_eq!(data[0]["last_modified"], "2024-05-27T13:43:26.233048Z");
1250 assert_eq!(data[0]["is_builtin"], false);
1251
1252 assert_eq!(data[1]["name"], "biblio");
1254 assert!(data[1]["label"].is_null());
1255 assert!(data[1]["last_modified"].is_null());
1256 assert_eq!(data[1]["is_builtin"], false);
1257
1258 assert_eq!(data[2]["name"], "knora-api");
1260 assert!(data[2]["label"].is_null());
1261 assert!(data[2]["last_modified"].is_null());
1262 assert_eq!(data[2]["is_builtin"], true);
1263 }
1264
1265 #[test]
1266 fn data_models_json_empty_data_array() {
1267 let out = SharedBuf::new();
1268 let mut renderer = JsonRenderer::with_writer(out.clone());
1269 let view = DataModelListView { items: vec![], total: 0, filter: None };
1270 let meta = make_meta("anonymous", "https://api.test.dasch.swiss");
1271 renderer.data_models(&view, &meta).unwrap();
1272
1273 let parsed: serde_json::Value = serde_json::from_str(out.string().trim()).unwrap();
1274 assert!(parsed["data"].as_array().unwrap().is_empty());
1275 }
1276
1277 fn make_beol_dm_detail() -> DataModelDetail {
1280 DataModelDetail {
1281 name: "beol".into(),
1282 iri: "http://api.dasch.swiss/ontology/0801/beol/v2".into(),
1283 label: Some("The BEOL data-model".into()),
1284 last_modified: Some("2024-05-27T13:43:26.233048Z".into()),
1285 resource_types: vec![
1286 ResourceTypeSummary {
1287 name: "Archive".into(),
1288 iri: "http://api.dasch.swiss/ontology/0801/beol/v2#Archive".into(),
1289 label: Some("Archive".into()),
1290 },
1291 ResourceTypeSummary {
1292 name: "basicLetter".into(),
1293 iri: "http://api.dasch.swiss/ontology/0801/beol/v2#basicLetter".into(),
1294 label: None,
1295 },
1296 ResourceTypeSummary {
1297 name: "letter".into(),
1298 iri: "http://api.dasch.swiss/ontology/0801/beol/v2#letter".into(),
1299 label: Some("Letter".into()),
1300 },
1301 ],
1302 }
1303 }
1304
1305 #[test]
1306 fn data_model_describe_json_full() {
1307 let out = SharedBuf::new();
1308 let mut renderer = JsonRenderer::with_writer(out.clone());
1309 let meta = make_meta("anonymous", "https://api.test.dasch.swiss");
1310 renderer.data_model_describe(&make_beol_dm_detail(), &meta).unwrap();
1311
1312 let parsed: serde_json::Value = serde_json::from_str(out.string().trim()).unwrap();
1313
1314 assert_eq!(parsed["_meta"]["auth"], "anonymous");
1316 assert_eq!(parsed["_meta"]["server"], "https://api.test.dasch.swiss");
1317 assert_eq!(parsed["_meta"]["exit_code"], 0);
1318
1319 let data = &parsed["data"];
1321 assert!(data.is_object());
1322 assert_eq!(data["name"], "beol");
1323 assert_eq!(data["iri"], "http://api.dasch.swiss/ontology/0801/beol/v2");
1324 assert_eq!(data["label"], "The BEOL data-model");
1325 assert_eq!(data["last_modified"], "2024-05-27T13:43:26.233048Z");
1327
1328 let rts = data["resource_types"].as_array().unwrap();
1330 assert_eq!(rts.len(), 3);
1331 assert_eq!(rts[0]["name"], "Archive");
1332 assert_eq!(rts[0]["iri"], "http://api.dasch.swiss/ontology/0801/beol/v2#Archive");
1333 assert_eq!(rts[0]["label"], "Archive");
1334 assert_eq!(rts[1]["name"], "basicLetter");
1336 assert!(rts[1]["label"].is_null());
1337 assert_eq!(rts[2]["name"], "letter");
1338 assert_eq!(rts[2]["label"], "Letter");
1339 }
1340
1341 #[test]
1342 fn data_model_describe_json_no_label_no_last_modified() {
1343 let out = SharedBuf::new();
1344 let mut renderer = JsonRenderer::with_writer(out.clone());
1345 let detail = DataModelDetail {
1346 name: "minimal".into(),
1347 iri: "http://api.dasch.swiss/ontology/0000/minimal/v2".into(),
1348 label: None,
1349 last_modified: None,
1350 resource_types: vec![],
1351 };
1352 let meta = make_meta("anonymous", "https://api.test.dasch.swiss");
1353 renderer.data_model_describe(&detail, &meta).unwrap();
1354
1355 let parsed: serde_json::Value = serde_json::from_str(out.string().trim()).unwrap();
1356 let data = &parsed["data"];
1357 assert!(data["label"].is_null());
1359 assert!(data["last_modified"].is_null());
1361 assert!(data["resource_types"].as_array().unwrap().is_empty());
1363 }
1364
1365 fn make_rt_fixture() -> Vec<ResourceType> {
1368 vec![
1369 ResourceType {
1370 name: "Archive".into(),
1371 iri: "http://api.dasch.swiss/ontology/0801/beol/v2#Archive".into(),
1372 label: Some("Archive".into()),
1373 is_builtin: false,
1374 count: None,
1375 },
1376 ResourceType {
1377 name: "letter".into(),
1378 iri: "http://api.dasch.swiss/ontology/0801/beol/v2#letter".into(),
1379 label: None,
1380 is_builtin: false,
1381 count: None,
1382 },
1383 ]
1384 }
1385
1386 #[test]
1387 fn resource_types_json_output() {
1388 let out = SharedBuf::new();
1389 let mut renderer = JsonRenderer::with_writer(out.clone());
1390 let view = ResourceTypeListView {
1391 items: make_rt_fixture(),
1392 total: 2,
1393 filter: None,
1394 data_model: "beol".into(),
1395 };
1396 let meta = make_meta("anonymous", "https://api.test.dasch.swiss");
1397 renderer.resource_types(&view, &meta).unwrap();
1398
1399 let parsed: serde_json::Value = serde_json::from_str(out.string().trim()).unwrap();
1400
1401 assert_eq!(parsed["_meta"]["auth"], "anonymous");
1403 assert_eq!(parsed["_meta"]["server"], "https://api.test.dasch.swiss");
1404 assert_eq!(parsed["_meta"]["exit_code"], 0);
1405
1406 let data = parsed["data"].as_array().unwrap();
1408 assert_eq!(data.len(), 2);
1409
1410 assert_eq!(data[0]["name"], "Archive");
1412 assert_eq!(data[0]["iri"], "http://api.dasch.swiss/ontology/0801/beol/v2#Archive");
1413 assert_eq!(data[0]["label"], "Archive");
1414 assert_eq!(data[0]["is_builtin"], false);
1415
1416 assert_eq!(data[1]["name"], "letter");
1418 assert!(data[1]["label"].is_null());
1419 assert_eq!(data[1]["is_builtin"], false);
1420 }
1421
1422 #[test]
1423 fn resource_types_json_with_builtins() {
1424 let out = SharedBuf::new();
1425 let mut renderer = JsonRenderer::with_writer(out.clone());
1426 let view = ResourceTypeListView {
1427 items: vec![ResourceType {
1428 name: "Region".into(),
1429 iri: "http://api.knora.org/ontology/knora-api/v2#Region".into(),
1430 label: Some("Region".into()),
1431 is_builtin: true,
1432 count: None,
1433 }],
1434 total: 1,
1435 filter: None,
1436 data_model: "beol".into(),
1437 };
1438 let meta = make_meta("anonymous", "https://api.test.dasch.swiss");
1439 renderer.resource_types(&view, &meta).unwrap();
1440
1441 let parsed: serde_json::Value = serde_json::from_str(out.string().trim()).unwrap();
1442 let data = parsed["data"].as_array().unwrap();
1443 assert_eq!(data.len(), 1);
1444 assert_eq!(data[0]["name"], "Region");
1445 assert_eq!(data[0]["label"], "Region");
1446 assert_eq!(data[0]["is_builtin"], true);
1448 }
1449
1450 #[test]
1451 fn resource_types_json_with_filter() {
1452 let out = SharedBuf::new();
1454 let mut renderer = JsonRenderer::with_writer(out.clone());
1455 let view = ResourceTypeListView {
1456 items: vec![make_rt_fixture().remove(0)], total: 2,
1458 filter: Some("arch".to_string()),
1459 data_model: "beol".into(),
1460 };
1461 let meta = make_meta("anonymous", "https://api.test.dasch.swiss");
1462 renderer.resource_types(&view, &meta).unwrap();
1463
1464 let parsed: serde_json::Value = serde_json::from_str(out.string().trim()).unwrap();
1465 let data = parsed["data"].as_array().unwrap();
1466 assert_eq!(data.len(), 1);
1467 assert_eq!(data[0]["name"], "Archive");
1468 }
1469
1470 #[test]
1471 fn resource_types_json_empty_data_array() {
1472 let out = SharedBuf::new();
1473 let mut renderer = JsonRenderer::with_writer(out.clone());
1474 let view = ResourceTypeListView {
1475 items: vec![],
1476 total: 0,
1477 filter: None,
1478 data_model: "beol".into(),
1479 };
1480 let meta = make_meta("anonymous", "https://api.test.dasch.swiss");
1481 renderer.resource_types(&view, &meta).unwrap();
1482
1483 let parsed: serde_json::Value = serde_json::from_str(out.string().trim()).unwrap();
1484 assert!(parsed["data"].as_array().unwrap().is_empty());
1485 }
1486
1487 #[test]
1488 fn resource_types_json_with_count_and_caveat() {
1489 let out = SharedBuf::new();
1492 let mut renderer = JsonRenderer::with_writer(out.clone());
1493 let mut items = make_rt_fixture();
1494 items[0].count = Some(5);
1495 let view = ResourceTypeListView { items, total: 2, filter: None, data_model: "beol".into() };
1496 let meta = crate::render::MetaContext {
1497 server_label: "https://api.test.dasch.swiss".into(),
1498 auth_state: "anonymous".into(),
1499 filter_warning: None,
1500 count_caveat: Some("counts are not permission-filtered".into()),
1501 count_cost: None,
1502 };
1503 renderer.resource_types(&view, &meta).unwrap();
1504
1505 let parsed: serde_json::Value = serde_json::from_str(out.string().trim()).unwrap();
1506 assert_eq!(parsed["data"][0]["count"], 5);
1507 assert!(
1509 !parsed["data"][1].as_object().unwrap().contains_key("count"),
1510 "count key must be absent when None; got: {}",
1511 parsed["data"][1]
1512 );
1513 assert_eq!(
1514 parsed["_meta"]["note"], "counts are not permission-filtered",
1515 "note must carry count_caveat"
1516 );
1517 }
1518
1519 #[test]
1520 fn resource_types_json_no_count_no_note() {
1521 let out = SharedBuf::new();
1524 let mut renderer = JsonRenderer::with_writer(out.clone());
1525 let view = ResourceTypeListView {
1526 items: make_rt_fixture(),
1527 total: 2,
1528 filter: None,
1529 data_model: "beol".into(),
1530 };
1531 let meta = make_meta("anonymous", "https://api.test.dasch.swiss");
1532 renderer.resource_types(&view, &meta).unwrap();
1533
1534 let parsed: serde_json::Value = serde_json::from_str(out.string().trim()).unwrap();
1535 assert!(!parsed["_meta"].as_object().unwrap().contains_key("note"));
1536 assert!(!parsed["data"][0].as_object().unwrap().contains_key("count"));
1537 }
1538
1539 fn make_minimal_rt_detail() -> crate::model::ResourceTypeDetail {
1542 crate::model::ResourceTypeDetail {
1543 name: "manuscript".into(),
1544 iri: "http://api.dasch.swiss/ontology/0801/beol/v2#manuscript".into(),
1545 label: Some("Manuscript".into()),
1546 data_model: "beol".into(),
1547 representation: None,
1548 super_types: vec![],
1549 fields: vec![],
1550 count: None,
1551 }
1552 }
1553
1554 #[test]
1555 fn resource_type_describe_json_with_count_and_caveat() {
1556 let out = SharedBuf::new();
1557 let mut renderer = JsonRenderer::with_writer(out.clone());
1558 let mut detail = make_minimal_rt_detail();
1559 detail.count = Some(99);
1560 let meta = crate::render::MetaContext {
1561 server_label: "https://api.dasch.swiss".into(),
1562 auth_state: "anonymous".into(),
1563 filter_warning: None,
1564 count_caveat: Some("counts exclude deleted resources".into()),
1565 count_cost: None,
1566 };
1567 renderer.resource_type_describe(&detail, &meta).unwrap();
1568
1569 let parsed: serde_json::Value = serde_json::from_str(out.string().trim()).unwrap();
1570 assert_eq!(parsed["data"]["count"], 99);
1571 assert_eq!(
1572 parsed["_meta"]["note"], "counts exclude deleted resources",
1573 "note must carry count_caveat"
1574 );
1575 }
1576
1577 #[test]
1578 fn resource_type_describe_json_no_count_no_note() {
1579 let out = SharedBuf::new();
1582 let mut renderer = JsonRenderer::with_writer(out.clone());
1583 let meta = make_meta("anonymous", "https://api.dasch.swiss");
1584 renderer.resource_type_describe(&make_minimal_rt_detail(), &meta).unwrap();
1585
1586 let parsed: serde_json::Value = serde_json::from_str(out.string().trim()).unwrap();
1587 assert!(!parsed["data"].as_object().unwrap().contains_key("count"));
1588 assert!(!parsed["_meta"].as_object().unwrap().contains_key("note"));
1589 }
1590
1591 use crate::model::resource_type::ValueType;
1594 use crate::model::{
1595 DatePoint, DateValue, FieldValues, FileValue, ResourceAccess, ResourceDetail, ResourceVisibility, Value,
1596 ValueContent,
1597 };
1598
1599 fn make_resource_detail_no_values() -> ResourceDetail {
1600 ResourceDetail {
1601 label: "Test Resource".into(),
1602 iri: "http://rdfh.ch/0803/abc123".into(),
1603 resource_type: "Page".into(),
1604 ark_url: None,
1605 creation_date: None,
1606 last_modified: None,
1607 attached_project: None,
1608 owner: None,
1609 visibility: Some(ResourceVisibility::Public),
1610 your_access: Some(ResourceAccess::View),
1611 values: None,
1612 }
1613 }
1614
1615 #[test]
1616 fn resource_describe_json_no_values_key_absent() {
1617 let out = SharedBuf::new();
1619 let mut renderer = JsonRenderer::with_writer(out.clone());
1620 let meta = make_meta("anonymous", "https://api.test.dasch.swiss");
1621 renderer.resource_describe(&make_resource_detail_no_values(), &meta).unwrap();
1622
1623 let parsed: serde_json::Value = serde_json::from_str(out.string().trim()).unwrap();
1624 let data = &parsed["data"];
1625 assert!(
1626 data["values"].is_null() && !data.as_object().unwrap().contains_key("values"),
1627 "values key must be absent when values is None; got data: {data}"
1628 );
1629 }
1630
1631 #[test]
1632 fn resource_describe_json_some_values_array_present() {
1633 let out = SharedBuf::new();
1635 let mut renderer = JsonRenderer::with_writer(out.clone());
1636 let mut detail = make_resource_detail_no_values();
1637 detail.values = Some(vec![FieldValues {
1638 name: "hasTitle".into(),
1639 label: Some("Title".into()),
1640 values: vec![ValueContent::Text("Hello".into()).into()],
1641 }]);
1642 let meta = make_meta("anonymous", "https://api.test.dasch.swiss");
1643 renderer.resource_describe(&detail, &meta).unwrap();
1644
1645 let parsed: serde_json::Value = serde_json::from_str(out.string().trim()).unwrap();
1646 let data = &parsed["data"];
1647 assert!(
1648 data.as_object().unwrap().contains_key("values"),
1649 "values key must be present when values is Some; got data: {data}"
1650 );
1651 let values = data["values"].as_array().unwrap();
1652 assert_eq!(values.len(), 1);
1653 assert_eq!(values[0]["field"], "hasTitle");
1654 assert_eq!(values[0]["field_label"], "Title");
1655 let val_objs = values[0]["values"].as_array().unwrap();
1656 assert_eq!(val_objs.len(), 1);
1657 assert_eq!(val_objs[0]["value_type"], "text");
1658 assert_eq!(val_objs[0]["text"], "Hello");
1659 }
1660
1661 #[test]
1662 fn resource_describe_json_some_empty_values_array() {
1663 let out = SharedBuf::new();
1665 let mut renderer = JsonRenderer::with_writer(out.clone());
1666 let mut detail = make_resource_detail_no_values();
1667 detail.values = Some(vec![]);
1668 let meta = make_meta("anonymous", "https://api.test.dasch.swiss");
1669 renderer.resource_describe(&detail, &meta).unwrap();
1670
1671 let parsed: serde_json::Value = serde_json::from_str(out.string().trim()).unwrap();
1672 let data = &parsed["data"];
1673 assert!(
1674 data.as_object().unwrap().contains_key("values"),
1675 "values key must be present even for empty Some(vec![]);"
1676 );
1677 assert!(data["values"].as_array().unwrap().is_empty());
1678 }
1679
1680 #[test]
1681 fn resource_describe_json_field_label_null_when_none() {
1682 let out = SharedBuf::new();
1684 let mut renderer = JsonRenderer::with_writer(out.clone());
1685 let mut detail = make_resource_detail_no_values();
1686 detail.values = Some(vec![FieldValues {
1687 name: "seqnum".into(),
1688 label: None,
1689 values: vec![ValueContent::Integer(42).into()],
1690 }]);
1691 let meta = make_meta("anonymous", "https://api.test.dasch.swiss");
1692 renderer.resource_describe(&detail, &meta).unwrap();
1693
1694 let parsed: serde_json::Value = serde_json::from_str(out.string().trim()).unwrap();
1695 let fg = &parsed["data"]["values"][0];
1696 assert!(fg["field_label"].is_null(), "field_label must be null when label is None");
1697 let val = &fg["values"][0];
1698 assert_eq!(val["value_type"], "integer");
1699 assert_eq!(val["value"], 42);
1700 }
1701
1702 #[test]
1703 fn resource_describe_json_link_value() {
1704 let out = SharedBuf::new();
1705 let mut renderer = JsonRenderer::with_writer(out.clone());
1706 let mut detail = make_resource_detail_no_values();
1707 detail.values = Some(vec![FieldValues {
1708 name: "isPartOf".into(),
1709 label: None,
1710 values: vec![
1711 ValueContent::Link {
1712 target_iri: "http://rdfh.ch/0803/book1".into(),
1713 target_label: Some("My Book".into()),
1714 }
1715 .into(),
1716 ValueContent::Link {
1717 target_iri: "http://rdfh.ch/0803/book2".into(),
1718 target_label: None,
1719 }
1720 .into(),
1721 ],
1722 }]);
1723 let meta = make_meta("anonymous", "https://api.test.dasch.swiss");
1724 renderer.resource_describe(&detail, &meta).unwrap();
1725
1726 let parsed: serde_json::Value = serde_json::from_str(out.string().trim()).unwrap();
1727 let vals = &parsed["data"]["values"][0]["values"];
1728 assert_eq!(vals[0]["value_type"], "link");
1730 assert_eq!(vals[0]["target_iri"], "http://rdfh.ch/0803/book1");
1731 assert_eq!(vals[0]["target_label"], "My Book");
1732 assert_eq!(vals[1]["value_type"], "link");
1734 assert_eq!(vals[1]["target_iri"], "http://rdfh.ch/0803/book2");
1735 assert!(vals[1]["target_label"].is_null());
1736 }
1737
1738 #[test]
1739 fn resource_describe_json_still_image_value() {
1740 let out = SharedBuf::new();
1741 let mut renderer = JsonRenderer::with_writer(out.clone());
1742 let mut detail = make_resource_detail_no_values();
1743 detail.values = Some(vec![FieldValues {
1744 name: "hasStillImageFileValue".into(),
1745 label: None,
1746 values: vec![
1747 ValueContent::File(FileValue {
1748 value_type: ValueType::StillImage,
1749 filename: "image.jp2".into(),
1750 url: "https://iiif.example.com/image.jp2/full/max/0/default.jpg".into(),
1751 width: Some(1200),
1752 height: Some(800),
1753 })
1754 .into(),
1755 ],
1756 }]);
1757 let meta = make_meta("anonymous", "https://api.test.dasch.swiss");
1758 renderer.resource_describe(&detail, &meta).unwrap();
1759
1760 let parsed: serde_json::Value = serde_json::from_str(out.string().trim()).unwrap();
1761 let val = &parsed["data"]["values"][0]["values"][0];
1762 assert_eq!(val["value_type"], "still-image");
1763 assert_eq!(val["filename"], "image.jp2");
1764 assert_eq!(val["url"], "https://iiif.example.com/image.jp2/full/max/0/default.jpg");
1765 assert_eq!(val["width"], 1200);
1766 assert_eq!(val["height"], 800);
1767 }
1768
1769 #[test]
1770 fn resource_describe_json_date_value() {
1771 let out = SharedBuf::new();
1772 let mut renderer = JsonRenderer::with_writer(out.clone());
1773 let mut detail = make_resource_detail_no_values();
1774 let pt = DatePoint {
1775 year: Some(1489),
1776 month: None,
1777 day: None,
1778 era: Some("CE".into()),
1779 };
1780 detail.values = Some(vec![FieldValues {
1781 name: "hasDate".into(),
1782 label: None,
1783 values: vec![
1784 ValueContent::Date(DateValue {
1785 calendar: "GREGORIAN".into(),
1786 start: pt.clone(),
1787 end: pt.clone(),
1788 })
1789 .into(),
1790 ],
1791 }]);
1792 let meta = make_meta("anonymous", "https://api.test.dasch.swiss");
1793 renderer.resource_describe(&detail, &meta).unwrap();
1794
1795 let parsed: serde_json::Value = serde_json::from_str(out.string().trim()).unwrap();
1796 let val = &parsed["data"]["values"][0]["values"][0];
1797 assert_eq!(val["value_type"], "date");
1798 assert_eq!(val["calendar"], "GREGORIAN");
1799 assert_eq!(val["start"]["year"], 1489);
1800 assert_eq!(val["start"]["era"], "CE");
1801 assert!(val["start"]["month"].is_null());
1802 assert!(val["start"]["day"].is_null());
1803 }
1804
1805 #[test]
1806 fn resource_describe_json_comment_present_when_set() {
1807 let out = SharedBuf::new();
1809 let mut renderer = JsonRenderer::with_writer(out.clone());
1810 let mut detail = make_resource_detail_no_values();
1811 detail.values = Some(vec![FieldValues {
1812 name: "hasTranscription".into(),
1813 label: None,
1814 values: vec![Value {
1815 content: ValueContent::Text("some transcription".into()),
1816 comment: Some("reading uncertain".into()),
1817 }],
1818 }]);
1819 let meta = make_meta("anonymous", "https://api.test.dasch.swiss");
1820 renderer.resource_describe(&detail, &meta).unwrap();
1821
1822 let parsed: serde_json::Value = serde_json::from_str(out.string().trim()).unwrap();
1823 let val = &parsed["data"]["values"][0]["values"][0];
1824 assert_eq!(val["value_type"], "text");
1825 assert_eq!(val["text"], "some transcription");
1826 assert_eq!(val["comment"], "reading uncertain");
1827 }
1828
1829 #[test]
1830 fn resource_describe_json_comment_absent_when_none() {
1831 let out = SharedBuf::new();
1833 let mut renderer = JsonRenderer::with_writer(out.clone());
1834 let mut detail = make_resource_detail_no_values();
1835 detail.values = Some(vec![FieldValues {
1836 name: "hasTranscription".into(),
1837 label: None,
1838 values: vec![ValueContent::Text("plain transcription".into()).into()],
1839 }]);
1840 let meta = make_meta("anonymous", "https://api.test.dasch.swiss");
1841 renderer.resource_describe(&detail, &meta).unwrap();
1842
1843 let parsed: serde_json::Value = serde_json::from_str(out.string().trim()).unwrap();
1844 let val = &parsed["data"]["values"][0]["values"][0];
1845 assert!(
1846 !val.as_object().unwrap().contains_key("comment"),
1847 "comment key must be absent when comment is None; got: {val}"
1848 );
1849 }
1850}