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