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