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 {
62 json!({
63 "server": meta.server_label,
64 "auth": meta.auth_state,
65 "exit_code": exit_code,
66 })
67}
68
69fn value_content_to_json(vc: &ValueContent) -> serde_json::Value {
75 use serde_json::Map;
76 let mut m = Map::new();
77 match vc {
78 ValueContent::Text(s) => {
79 m.insert(
80 "value_type".into(),
81 serde_json::Value::String("text".into()),
82 );
83 m.insert("text".into(), serde_json::Value::String(s.clone()));
84 }
85 ValueContent::Integer(n) => {
86 m.insert(
87 "value_type".into(),
88 serde_json::Value::String("integer".into()),
89 );
90 m.insert("value".into(), serde_json::Value::Number((*n).into()));
91 }
92 ValueContent::Decimal(s) => {
93 m.insert(
94 "value_type".into(),
95 serde_json::Value::String("decimal".into()),
96 );
97 m.insert("value".into(), serde_json::Value::String(s.clone()));
98 }
99 ValueContent::Boolean(b) => {
100 m.insert(
101 "value_type".into(),
102 serde_json::Value::String("boolean".into()),
103 );
104 m.insert("value".into(), serde_json::Value::Bool(*b));
105 }
106 ValueContent::Date(dv) => {
107 m.insert(
108 "value_type".into(),
109 serde_json::Value::String("date".into()),
110 );
111 m.insert(
112 "calendar".into(),
113 serde_json::Value::String(dv.calendar.clone()),
114 );
115 let point_to_json = |p: &DatePoint| {
117 json!({
118 "year": p.year,
119 "month": p.month,
120 "day": p.day,
121 "era": p.era,
122 })
123 };
124 m.insert("start".into(), point_to_json(&dv.start));
125 m.insert("end".into(), point_to_json(&dv.end));
126 }
127 ValueContent::Time(s) => {
128 m.insert(
129 "value_type".into(),
130 serde_json::Value::String("time".into()),
131 );
132 m.insert("value".into(), serde_json::Value::String(s.clone()));
133 }
134 ValueContent::Uri(s) => {
135 m.insert("value_type".into(), serde_json::Value::String("uri".into()));
136 m.insert("value".into(), serde_json::Value::String(s.clone()));
137 }
138 ValueContent::Color(s) => {
139 m.insert(
140 "value_type".into(),
141 serde_json::Value::String("color".into()),
142 );
143 m.insert("value".into(), serde_json::Value::String(s.clone()));
144 }
145 ValueContent::Geoname(s) => {
146 m.insert(
147 "value_type".into(),
148 serde_json::Value::String("geoname".into()),
149 );
150 m.insert("value".into(), serde_json::Value::String(s.clone()));
151 }
152 ValueContent::ListItem { node_iri, label } => {
153 m.insert(
154 "value_type".into(),
155 serde_json::Value::String("list-item".into()),
156 );
157 m.insert(
158 "node_iri".into(),
159 serde_json::Value::String(node_iri.clone()),
160 );
161 let label_val = match label {
162 Some(s) => serde_json::Value::String(s.clone()),
163 None => serde_json::Value::Null,
164 };
165 m.insert("label".into(), label_val);
166 }
167 ValueContent::Link {
168 target_iri,
169 target_label,
170 } => {
171 m.insert(
172 "value_type".into(),
173 serde_json::Value::String("link".into()),
174 );
175 m.insert(
176 "target_iri".into(),
177 serde_json::Value::String(target_iri.clone()),
178 );
179 let tl_val = match target_label {
180 Some(s) => serde_json::Value::String(s.clone()),
181 None => serde_json::Value::Null,
182 };
183 m.insert("target_label".into(), tl_val);
184 }
185 ValueContent::File(fv) => {
186 use crate::model::resource_type::ValueType;
187 let type_token = fv.value_type.as_token().to_string();
188 m.insert("value_type".into(), serde_json::Value::String(type_token));
189 m.insert(
190 "filename".into(),
191 serde_json::Value::String(fv.filename.clone()),
192 );
193 m.insert("url".into(), serde_json::Value::String(fv.url.clone()));
194 match fv.value_type {
196 ValueType::StillImage => {
197 let w_val: serde_json::Value = fv.width.map_or(serde_json::Value::Null, |w| {
198 serde_json::Value::Number(w.into())
199 });
200 let h_val: serde_json::Value = fv.height.map_or(serde_json::Value::Null, |h| {
201 serde_json::Value::Number(h.into())
202 });
203 m.insert("width".into(), w_val);
204 m.insert("height".into(), h_val);
205 }
206 _ => {
207 m.insert("width".into(), serde_json::Value::Null);
208 m.insert("height".into(), serde_json::Value::Null);
209 }
210 }
211 }
212 ValueContent::Raw { value_type, text } => {
213 m.insert(
214 "value_type".into(),
215 serde_json::Value::String(value_type.clone()),
216 );
217 m.insert("text".into(), serde_json::Value::String(text.clone()));
218 }
219 }
220 serde_json::Value::Object(m)
221}
222
223fn diagnostic_kind(diag: &Diagnostic) -> &'static str {
225 match diag {
226 Diagnostic::Usage(_) => "usage",
227 Diagnostic::AuthRequired(_) => "auth_required",
228 Diagnostic::NotFound(_) => "not_found",
229 Diagnostic::ServerError(_) => "server_error",
230 Diagnostic::Network(_) => "network",
231 Diagnostic::Conflict(_) => "conflict",
232 Diagnostic::Io(_) => "io",
233 Diagnostic::Internal(_) | Diagnostic::NotImplemented(_) => "internal",
234 }
235}
236
237impl Renderer for JsonRenderer {
238 fn diagnostic(&mut self, diag: &Diagnostic, meta: &MetaContext) -> Result<(), Diagnostic> {
239 let exit_code = diag.exit_category() as u8;
242 let obj = json!({
243 "_meta": meta_block(meta, exit_code),
244 "error": {
245 "kind": diagnostic_kind(diag),
246 "message": diag.to_string(),
247 },
248 });
249 writeln!(
250 self.out,
251 "{}",
252 serde_json::to_string(&obj)
253 .map_err(|e| Diagnostic::Internal(format!("json serialisation error: {e}")))?
254 )?;
255 Ok(())
256 }
257
258 fn auth_login(
259 &mut self,
260 outcome: &AuthLoginOutcome,
261 meta: &MetaContext,
262 ) -> Result<(), Diagnostic> {
263 let obj = json!({
264 "_meta": meta_block(meta, 0),
265 "data": {
266 "user": outcome.user,
267 "expires_at": outcome.expires_at.map(|dt| dt.to_rfc3339()),
268 "state": "login_success",
269 },
270 });
271 writeln!(
272 self.out,
273 "{}",
274 serde_json::to_string(&obj)
275 .map_err(|e| Diagnostic::Internal(format!("json serialisation error: {e}")))?
276 )?;
277 Ok(())
278 }
279
280 fn auth_status(
281 &mut self,
282 outcome: &AuthStatusOutcome,
283 meta: &MetaContext,
284 ) -> Result<(), Diagnostic> {
285 let obj = match outcome {
286 AuthStatusOutcome::LoggedIn {
287 server: _,
288 user,
289 expires_at,
290 expired,
291 } => json!({
292 "_meta": meta_block(meta, 0),
293 "data": {
294 "user": user,
295 "expires_at": expires_at.map(|dt| dt.to_rfc3339()),
296 "state": if *expired { "expired" } else { "logged_in" },
297 },
298 }),
299 AuthStatusOutcome::AuthenticatedViaEnv {
304 server: _,
305 expires_at,
306 expired,
307 } => json!({
308 "_meta": meta_block(meta, 0),
309 "data": {
310 "user": null,
311 "expires_at": expires_at.map(|dt| dt.to_rfc3339()),
312 "state": if *expired { "expired" } else { "logged_in" },
313 },
314 }),
315 AuthStatusOutcome::NotLoggedIn { server: _ } => json!({
316 "_meta": meta_block(meta, 0),
317 "data": {
318 "user": null,
319 "expires_at": null,
320 "state": "not_logged_in",
321 },
322 }),
323 };
324 writeln!(
325 self.out,
326 "{}",
327 serde_json::to_string(&obj)
328 .map_err(|e| Diagnostic::Internal(format!("json serialisation error: {e}")))?
329 )?;
330 Ok(())
331 }
332
333 fn auth_logout(
334 &mut self,
335 outcome: &AuthLogoutOutcome,
336 meta: &MetaContext,
337 ) -> Result<(), Diagnostic> {
338 let obj = json!({
339 "_meta": meta_block(meta, 0),
340 "data": {
341 "was_cached": outcome.was_cached,
342 "state": if outcome.was_cached { "logout_was_cached" } else { "logout_no_op" },
343 },
344 });
345 writeln!(
346 self.out,
347 "{}",
348 serde_json::to_string(&obj)
349 .map_err(|e| Diagnostic::Internal(format!("json serialisation error: {e}")))?
350 )?;
351 Ok(())
352 }
353
354 fn auth_set_token(
355 &mut self,
356 outcome: &AuthSetTokenOutcome,
357 meta: &MetaContext,
358 ) -> Result<(), Diagnostic> {
359 let obj = json!({
360 "_meta": meta_block(meta, 0),
361 "data": {
362 "user": outcome.user,
363 "expires_at": outcome.expires_at.map(|dt| dt.to_rfc3339()),
364 "state": "token_cached",
365 },
366 });
367 writeln!(
368 self.out,
369 "{}",
370 serde_json::to_string(&obj)
371 .map_err(|e| Diagnostic::Internal(format!("json serialisation error: {e}")))?
372 )?;
373 Ok(())
374 }
375
376 fn project_dump(
377 &mut self,
378 outcome: &DumpOutcome,
379 meta: &MetaContext,
380 ) -> Result<(), Diagnostic> {
381 let obj = json!({
382 "_meta": meta_block(meta, 0),
383 "data": {
384 "path": outcome.path.display().to_string(),
385 "bytes": outcome.bytes,
386 "cleaned_up": outcome.cleaned_up,
387 "reused": outcome.reused,
388 "created_at": outcome.created_at.map(|dt| dt.to_rfc3339()),
389 },
390 });
391 writeln!(
392 self.out,
393 "{}",
394 serde_json::to_string(&obj)
395 .map_err(|e| Diagnostic::Internal(format!("json serialisation error: {e}")))?
396 )?;
397 Ok(())
398 }
399
400 fn project_dump_deleted(
401 &mut self,
402 outcome: &DumpDeleteOutcome,
403 meta: &MetaContext,
404 ) -> Result<(), Diagnostic> {
405 let obj = if let Some(ref note) = outcome.note {
406 json!({
407 "_meta": meta_block(meta, 0),
408 "data": {
409 "deleted": outcome.deleted,
410 "note": note,
411 },
412 })
413 } else {
414 json!({
415 "_meta": meta_block(meta, 0),
416 "data": {
417 "deleted": outcome.deleted,
418 },
419 })
420 };
421 writeln!(
422 self.out,
423 "{}",
424 serde_json::to_string(&obj)
425 .map_err(|e| Diagnostic::Internal(format!("json serialisation error: {e}")))?
426 )?;
427 Ok(())
428 }
429
430 fn projects(&mut self, view: &ProjectListView, meta: &MetaContext) -> Result<(), Diagnostic> {
431 let data: Vec<serde_json::Value> = view
435 .items
436 .iter()
437 .map(|item| {
438 json!({
439 "iri": item.iri,
440 "shortcode": item.shortcode,
441 "shortname": item.shortname,
442 "longname": item.longname,
443 "status": item.status.as_str(),
444 "data_models": item.data_models,
445 })
446 })
447 .collect();
448
449 let obj = json!({
450 "_meta": meta_block(meta, 0),
451 "data": data,
452 });
453 writeln!(
454 self.out,
455 "{}",
456 serde_json::to_string(&obj)
457 .map_err(|e| Diagnostic::Internal(format!("json serialisation error: {e}")))?
458 )?;
459 Ok(())
460 }
461
462 fn project_describe(
463 &mut self,
464 project: &ProjectDetail,
465 meta: &MetaContext,
466 ) -> Result<(), Diagnostic> {
467 let description: Vec<serde_json::Value> = project
470 .description
471 .iter()
472 .map(|d| {
473 json!({
474 "value": d.value,
475 "language": d.language,
476 })
477 })
478 .collect();
479
480 let data_models: Vec<serde_json::Value> = project
481 .data_models
482 .iter()
483 .map(|dm| {
484 json!({
485 "name": dm.name,
486 "iri": dm.iri,
487 })
488 })
489 .collect();
490
491 let obj = json!({
492 "_meta": meta_block(meta, 0),
493 "data": {
494 "iri": project.iri,
495 "shortcode": project.shortcode,
496 "shortname": project.shortname,
497 "longname": project.longname,
498 "status": project.status.as_str(),
499 "description": description,
500 "keywords": project.keywords,
501 "data_models": data_models,
502 },
503 });
504 writeln!(
505 self.out,
506 "{}",
507 serde_json::to_string(&obj)
508 .map_err(|e| Diagnostic::Internal(format!("json serialisation error: {e}")))?
509 )?;
510 Ok(())
511 }
512
513 fn data_model_describe(
514 &mut self,
515 detail: &DataModelDetail,
516 meta: &MetaContext,
517 ) -> Result<(), Diagnostic> {
518 let resource_types: Vec<serde_json::Value> = detail
522 .resource_types
523 .iter()
524 .map(|rt| {
525 json!({
526 "name": rt.name,
527 "iri": rt.iri,
528 "label": rt.label,
529 })
530 })
531 .collect();
532
533 let obj = json!({
534 "_meta": meta_block(meta, 0),
535 "data": {
536 "name": detail.name,
537 "iri": detail.iri,
538 "label": detail.label,
539 "last_modified": detail.last_modified,
540 "resource_types": resource_types,
541 },
542 });
543 writeln!(
544 self.out,
545 "{}",
546 serde_json::to_string(&obj)
547 .map_err(|e| Diagnostic::Internal(format!("json serialisation error: {e}")))?
548 )?;
549 Ok(())
550 }
551
552 fn data_models(
553 &mut self,
554 view: &DataModelListView,
555 meta: &MetaContext,
556 ) -> Result<(), Diagnostic> {
557 let data: Vec<serde_json::Value> = view
561 .items
562 .iter()
563 .map(|item| {
564 json!({
565 "name": item.name,
566 "iri": item.iri,
567 "label": item.label,
568 "last_modified": item.last_modified,
569 "is_builtin": item.is_builtin,
570 })
571 })
572 .collect();
573
574 let obj = json!({
575 "_meta": meta_block(meta, 0),
576 "data": data,
577 });
578 writeln!(
579 self.out,
580 "{}",
581 serde_json::to_string(&obj)
582 .map_err(|e| Diagnostic::Internal(format!("json serialisation error: {e}")))?
583 )?;
584 Ok(())
585 }
586
587 fn resource_types(
588 &mut self,
589 view: &ResourceTypeListView,
590 meta: &MetaContext,
591 ) -> Result<(), Diagnostic> {
592 let data: Vec<serde_json::Value> = view
596 .items
597 .iter()
598 .map(|item| {
599 json!({
600 "name": item.name,
601 "iri": item.iri,
602 "label": item.label,
603 "is_builtin": item.is_builtin,
604 })
605 })
606 .collect();
607
608 let obj = json!({
609 "_meta": meta_block(meta, 0),
610 "data": data,
611 });
612 writeln!(
613 self.out,
614 "{}",
615 serde_json::to_string(&obj)
616 .map_err(|e| Diagnostic::Internal(format!("json serialisation error: {e}")))?
617 )?;
618 Ok(())
619 }
620
621 fn resource_type_describe(
622 &mut self,
623 detail: &crate::model::ResourceTypeDetail,
624 meta: &MetaContext,
625 ) -> Result<(), Diagnostic> {
626 let fields: Vec<serde_json::Value> = detail
630 .fields
631 .iter()
632 .map(|f| {
633 json!({
634 "name": f.name,
635 "iri": f.iri,
636 "label": f.label,
637 "value_type": f.value_type.to_string(),
638 "link_target": f.link_target,
639 "cardinality": f.cardinality.to_string(),
640 "is_builtin": f.is_builtin,
641 "data_model": f.data_model,
642 })
643 })
644 .collect();
645
646 let obj = json!({
647 "_meta": meta_block(meta, 0),
648 "data": {
649 "name": detail.name,
650 "iri": detail.iri,
651 "label": detail.label,
652 "data_model": detail.data_model,
653 "representation": detail.representation.as_ref().map(|r| r.to_string()),
654 "super_types": detail.super_types,
655 "fields": fields,
656 },
657 });
658 writeln!(
659 self.out,
660 "{}",
661 serde_json::to_string(&obj)
662 .map_err(|e| Diagnostic::Internal(format!("json serialisation error: {e}")))?
663 )?;
664 Ok(())
665 }
666
667 fn data_model_structure(
668 &mut self,
669 structure: &DataModelStructure,
670 meta: &MetaContext,
671 ) -> Result<(), Diagnostic> {
672 let data: Vec<serde_json::Value> = structure
676 .relations
677 .iter()
678 .map(|r| {
679 json!({
680 "source": r.source,
681 "target": r.target,
682 "kind": r.kind.to_string(),
683 "field": r.field,
684 "target_data_model": r.target_data_model,
685 })
686 })
687 .collect();
688
689 let obj = json!({
690 "_meta": meta_block(meta, 0),
691 "data": data,
692 });
693 writeln!(
694 self.out,
695 "{}",
696 serde_json::to_string(&obj)
697 .map_err(|e| Diagnostic::Internal(format!("json serialisation error: {e}")))?
698 )?;
699 Ok(())
700 }
701
702 fn resources(&mut self, view: &ResourceListView, meta: &MetaContext) -> Result<(), Diagnostic> {
703 let data: Vec<serde_json::Value> = view
705 .items
706 .iter()
707 .map(|item| {
708 json!({
709 "label": item.label,
710 "iri": item.iri,
711 "ark_url": item.ark_url,
712 "creation_date": item.creation_date,
713 "last_modified": item.last_modified,
714 "resource_type": item.resource_type,
715 })
716 })
717 .collect();
718
719 let mut meta_obj = meta_block(meta, 0);
721 match &view.pagination {
722 ResourceListPagination::SinglePage {
723 page,
724 may_have_more,
725 } => {
726 meta_obj["page"] = serde_json::Value::from(*page);
727 meta_obj["may_have_more_results"] = serde_json::Value::from(*may_have_more);
728 }
729 ResourceListPagination::AllPages { pages_fetched } => {
730 meta_obj["pages_fetched"] = serde_json::Value::from(*pages_fetched);
731 meta_obj["may_have_more_results"] = serde_json::Value::from(false);
733 }
734 }
735
736 if let Some(ref fw) = meta.filter_warning {
738 meta_obj["note"] = serde_json::Value::from(fw.as_str());
739 }
740
741 let obj = json!({
742 "_meta": meta_obj,
743 "data": data,
744 });
745 writeln!(
746 self.out,
747 "{}",
748 serde_json::to_string(&obj)
749 .map_err(|e| Diagnostic::Internal(format!("json serialisation error: {e}")))?
750 )?;
751 Ok(())
752 }
753
754 fn resource_describe(
755 &mut self,
756 detail: &ResourceDetail,
757 meta: &MetaContext,
758 ) -> Result<(), Diagnostic> {
759 let mut meta_obj = meta_block(meta, 0);
763 if let Some(ref fw) = meta.filter_warning {
764 meta_obj["note"] = serde_json::Value::from(fw.as_str());
765 }
766
767 let mut data = serde_json::Map::new();
769 data.insert(
770 "label".into(),
771 serde_json::Value::String(detail.label.clone()),
772 );
773 data.insert("iri".into(), serde_json::Value::String(detail.iri.clone()));
774 data.insert(
775 "resource_type".into(),
776 serde_json::Value::String(detail.resource_type.clone()),
777 );
778 data.insert(
779 "ark_url".into(),
780 detail
781 .ark_url
782 .as_ref()
783 .map_or(serde_json::Value::Null, |s| {
784 serde_json::Value::String(s.clone())
785 }),
786 );
787 data.insert(
788 "creation_date".into(),
789 detail
790 .creation_date
791 .as_ref()
792 .map_or(serde_json::Value::Null, |s| {
793 serde_json::Value::String(s.clone())
794 }),
795 );
796 data.insert(
797 "last_modified".into(),
798 detail
799 .last_modified
800 .as_ref()
801 .map_or(serde_json::Value::Null, |s| {
802 serde_json::Value::String(s.clone())
803 }),
804 );
805 data.insert(
806 "attached_project".into(),
807 detail
808 .attached_project
809 .as_ref()
810 .map_or(serde_json::Value::Null, |s| {
811 serde_json::Value::String(s.clone())
812 }),
813 );
814 data.insert(
815 "owner".into(),
816 detail.owner.as_ref().map_or(serde_json::Value::Null, |s| {
817 serde_json::Value::String(s.clone())
818 }),
819 );
820 data.insert(
821 "visibility".into(),
822 detail
823 .visibility
824 .as_ref()
825 .map_or(serde_json::Value::Null, |v| {
826 serde_json::Value::String(v.as_str().into())
827 }),
828 );
829 data.insert(
830 "your_access".into(),
831 detail
832 .your_access
833 .as_ref()
834 .map_or(serde_json::Value::Null, |a| {
835 serde_json::Value::String(a.as_str().into())
836 }),
837 );
838 if let Some(ref fields) = detail.values {
841 let values_arr: Vec<serde_json::Value> = fields
842 .iter()
843 .map(|fv| {
844 let value_objs: Vec<serde_json::Value> = fv
845 .values
846 .iter()
847 .map(|v| {
848 let mut obj = value_content_to_json(&v.content);
849 if let (Some(c), serde_json::Value::Object(m)) = (&v.comment, &mut obj)
850 {
851 m.insert("comment".into(), serde_json::Value::String(c.clone()));
852 }
853 obj
854 })
855 .collect();
856 let mut fg = serde_json::Map::new();
857 fg.insert("field".into(), serde_json::Value::String(fv.name.clone()));
858 let fl_val = match &fv.label {
859 Some(s) => serde_json::Value::String(s.clone()),
860 None => serde_json::Value::Null,
861 };
862 fg.insert("field_label".into(), fl_val);
863 fg.insert("values".into(), serde_json::Value::Array(value_objs));
864 serde_json::Value::Object(fg)
865 })
866 .collect();
867 data.insert("values".into(), serde_json::Value::Array(values_arr));
868 }
869
870 let obj = json!({
871 "_meta": meta_obj,
872 "data": serde_json::Value::Object(data),
873 });
874 writeln!(
875 self.out,
876 "{}",
877 serde_json::to_string(&obj)
878 .map_err(|e| Diagnostic::Internal(format!("json serialisation error: {e}")))?
879 )?;
880 Ok(())
881 }
882}
883
884#[cfg(test)]
885mod tests {
886 use super::*;
887 use crate::model::{
888 DataModel, DataModelDetail, DataModelSummary, Project, ProjectDescription, ProjectDetail,
889 ProjectStatus, ResourceType, ResourceTypeSummary,
890 };
891 use crate::render::test_support::{SharedBuf, make_meta};
892 use crate::render::{DataModelListView, ResourceTypeListView};
893
894 #[test]
895 fn projects_json_output() {
896 let out = SharedBuf::new();
897 let mut renderer = JsonRenderer::with_writer(out.clone());
898 let items = vec![
899 Project {
900 iri: "http://rdfh.ch/projects/0001".into(),
901 shortcode: "0001".into(),
902 shortname: "anything".into(),
903 longname: Some("Anything Project".into()),
904 status: ProjectStatus::Active,
905 data_models: 2,
906 },
907 Project {
908 iri: "http://rdfh.ch/projects/0002".into(),
909 shortcode: "0002".into(),
910 shortname: "images".into(),
911 longname: None,
912 status: ProjectStatus::Inactive,
913 data_models: 0,
914 },
915 ];
916 let view = ProjectListView {
917 items,
918 total: 2,
919 filter: None,
920 };
921 let meta = make_meta("anonymous", "https://api.test.dasch.swiss");
922 renderer.projects(&view, &meta).unwrap();
923
924 let s = out.string();
925 let parsed: serde_json::Value = serde_json::from_str(s.trim()).unwrap();
926
927 assert_eq!(parsed["_meta"]["auth"], "anonymous");
929 assert_eq!(parsed["_meta"]["server"], "https://api.test.dasch.swiss");
930 assert_eq!(parsed["_meta"]["exit_code"], 0);
931
932 let data = parsed["data"].as_array().unwrap();
934 assert_eq!(data.len(), 2);
935
936 assert_eq!(data[0]["shortcode"], "0001");
938 assert_eq!(data[0]["shortname"], "anything");
939 assert_eq!(data[0]["longname"], "Anything Project");
940 assert_eq!(data[0]["status"], "active");
941 assert_eq!(data[0]["data_models"], 2);
942 assert_eq!(data[0]["iri"], "http://rdfh.ch/projects/0001");
943
944 assert_eq!(data[1]["shortcode"], "0002");
946 assert!(data[1]["longname"].is_null());
947 assert_eq!(data[1]["status"], "inactive");
948 assert_eq!(data[1]["data_models"], 0);
949 }
950
951 #[test]
952 fn projects_json_empty_data_array() {
953 let out = SharedBuf::new();
954 let mut renderer = JsonRenderer::with_writer(out.clone());
955 let view = ProjectListView {
956 items: vec![],
957 total: 0,
958 filter: None,
959 };
960 let meta = make_meta("anonymous", "https://api.test.dasch.swiss");
961 renderer.projects(&view, &meta).unwrap();
962
963 let parsed: serde_json::Value = serde_json::from_str(out.string().trim()).unwrap();
964 assert!(parsed["data"].as_array().unwrap().is_empty());
965 }
966
967 #[test]
974 fn diagnostic_kind_covers_all_variants() {
975 let cases: &[(&Diagnostic, &str)] = &[
976 (&Diagnostic::Usage("x".into()), "usage"),
977 (&Diagnostic::AuthRequired("x".into()), "auth_required"),
978 (&Diagnostic::NotFound("x".into()), "not_found"),
979 (&Diagnostic::ServerError("x".into()), "server_error"),
980 (&Diagnostic::Network("x".into()), "network"),
981 (&Diagnostic::Conflict("x".into()), "conflict"),
982 (&Diagnostic::Io("x".into()), "io"),
983 (&Diagnostic::Internal("x".into()), "internal"),
984 (&Diagnostic::NotImplemented("x".into()), "internal"),
985 ];
986 for (diag, expected_kind) in cases {
987 assert_eq!(
988 diagnostic_kind(diag),
989 *expected_kind,
990 "unexpected kind for {diag:?}"
991 );
992 }
993 }
994
995 #[test]
996 fn conflict_kind_is_conflict() {
997 let d = Diagnostic::Conflict("dump already in progress".into());
998 assert_eq!(diagnostic_kind(&d), "conflict");
999 }
1000
1001 #[test]
1002 fn io_kind_is_io() {
1003 let d = Diagnostic::Io("failed to write /tmp/0001.zip: permission denied".into());
1004 assert_eq!(diagnostic_kind(&d), "io");
1005 }
1006
1007 fn make_beol_detail() -> ProjectDetail {
1008 ProjectDetail {
1012 iri: "http://rdfh.ch/projects/yTerZGyxjZVqFMNNKXCDPF".into(),
1013 shortcode: "0801".into(),
1014 shortname: "beol".into(),
1015 longname: Some("Bernoulli-Euler Online".into()),
1016 status: ProjectStatus::Active,
1017 description: vec![ProjectDescription {
1018 value: "<b>BEOL</b> — early modern mathematics.".into(),
1019 language: Some("en".into()),
1020 }],
1021 keywords: vec!["Bernoulli".into(), "Euler".into(), "Mathematics".into()],
1022 data_models: vec![
1023 DataModelSummary {
1024 name: "beol".into(),
1025 iri: "http://api.dasch.swiss/ontology/0801/beol/v2".into(),
1026 },
1027 DataModelSummary {
1028 name: "biblio".into(),
1029 iri: "http://api.dasch.swiss/ontology/0801/biblio/v2".into(),
1030 },
1031 DataModelSummary {
1032 name: "leibniz".into(),
1033 iri: "http://api.dasch.swiss/ontology/0801/leibniz/v2".into(),
1034 },
1035 DataModelSummary {
1036 name: "newton".into(),
1037 iri: "http://api.dasch.swiss/ontology/0801/newton/v2".into(),
1038 },
1039 ],
1040 }
1041 }
1042
1043 #[test]
1044 fn project_describe_json_full() {
1045 let out = SharedBuf::new();
1046 let mut renderer = JsonRenderer::with_writer(out.clone());
1047 let meta = make_meta("anonymous", "https://api.test.dasch.swiss");
1048 renderer
1049 .project_describe(&make_beol_detail(), &meta)
1050 .unwrap();
1051
1052 let parsed: serde_json::Value = serde_json::from_str(out.string().trim()).unwrap();
1053
1054 assert_eq!(parsed["_meta"]["auth"], "anonymous");
1056 assert_eq!(parsed["_meta"]["exit_code"], 0);
1057
1058 let data = &parsed["data"];
1060 assert!(data.is_object());
1061 assert_eq!(
1062 data["iri"],
1063 "http://rdfh.ch/projects/yTerZGyxjZVqFMNNKXCDPF"
1064 );
1065 assert_eq!(data["shortcode"], "0801");
1066 assert_eq!(data["shortname"], "beol");
1067 assert_eq!(data["longname"], "Bernoulli-Euler Online");
1068 assert_eq!(data["status"], "active");
1069
1070 let desc = data["description"].as_array().unwrap();
1072 assert_eq!(desc.len(), 1);
1073 assert_eq!(desc[0]["value"], "<b>BEOL</b> — early modern mathematics.");
1074 assert_eq!(desc[0]["language"], "en");
1075
1076 let kws = data["keywords"].as_array().unwrap();
1078 assert_eq!(kws.len(), 3);
1079 assert_eq!(kws[0], "Bernoulli");
1080
1081 let dms = data["data_models"].as_array().unwrap();
1084 assert_eq!(dms.len(), 4, "all four data-models must be rendered");
1085 assert_eq!(dms[0]["name"], "beol");
1086 assert_eq!(
1087 dms[0]["iri"],
1088 "http://api.dasch.swiss/ontology/0801/beol/v2"
1089 );
1090 assert_eq!(dms[1]["name"], "biblio");
1091 assert_eq!(dms[2]["name"], "leibniz");
1092 assert_eq!(dms[3]["name"], "newton");
1093 }
1094
1095 #[test]
1096 fn project_describe_json_no_longname() {
1097 let out = SharedBuf::new();
1098 let mut renderer = JsonRenderer::with_writer(out.clone());
1099 let mut detail = make_beol_detail();
1100 detail.longname = None;
1101 let meta = make_meta("anonymous", "https://api.test.dasch.swiss");
1102 renderer.project_describe(&detail, &meta).unwrap();
1103
1104 let parsed: serde_json::Value = serde_json::from_str(out.string().trim()).unwrap();
1105 assert!(parsed["data"]["longname"].is_null());
1107 }
1108
1109 #[test]
1110 fn project_describe_json_empty_fields() {
1111 let out = SharedBuf::new();
1112 let mut renderer = JsonRenderer::with_writer(out.clone());
1113 let detail = ProjectDetail {
1114 iri: "http://rdfh.ch/projects/0000".into(),
1115 shortcode: "0000".into(),
1116 shortname: "minimal".into(),
1117 longname: None,
1118 status: ProjectStatus::Inactive,
1119 description: vec![],
1120 keywords: vec![],
1121 data_models: vec![],
1122 };
1123 let meta = make_meta("anonymous", "https://api.test.dasch.swiss");
1124 renderer.project_describe(&detail, &meta).unwrap();
1125
1126 let parsed: serde_json::Value = serde_json::from_str(out.string().trim()).unwrap();
1127 let data = &parsed["data"];
1128 assert_eq!(data["status"], "inactive");
1129 assert!(data["description"].as_array().unwrap().is_empty());
1130 assert!(data["keywords"].as_array().unwrap().is_empty());
1131 assert!(data["data_models"].as_array().unwrap().is_empty());
1132 }
1133
1134 fn make_data_model_fixture() -> Vec<DataModel> {
1135 vec![
1136 DataModel {
1137 name: "beol".into(),
1138 iri: "http://api.dasch.swiss/ontology/0801/beol/v2".into(),
1139 label: Some("The BEOL data-model".into()),
1140 last_modified: Some("2024-05-27T13:43:26.233048Z".into()),
1141 is_builtin: false,
1142 },
1143 DataModel {
1144 name: "biblio".into(),
1145 iri: "http://api.dasch.swiss/ontology/0801/biblio/v2".into(),
1146 label: None,
1147 last_modified: None,
1148 is_builtin: false,
1149 },
1150 DataModel {
1151 name: "knora-api".into(),
1152 iri: "http://api.knora.org/ontology/knora-api/v2".into(),
1153 label: None,
1154 last_modified: None,
1155 is_builtin: true,
1156 },
1157 ]
1158 }
1159
1160 #[test]
1161 fn data_models_json_output() {
1162 let out = SharedBuf::new();
1163 let mut renderer = JsonRenderer::with_writer(out.clone());
1164 let view = DataModelListView {
1165 items: make_data_model_fixture(),
1166 total: 3,
1167 filter: None,
1168 };
1169 let meta = make_meta("anonymous", "https://api.test.dasch.swiss");
1170 renderer.data_models(&view, &meta).unwrap();
1171
1172 let parsed: serde_json::Value = serde_json::from_str(out.string().trim()).unwrap();
1173
1174 assert_eq!(parsed["_meta"]["auth"], "anonymous");
1176 assert_eq!(parsed["_meta"]["server"], "https://api.test.dasch.swiss");
1177 assert_eq!(parsed["_meta"]["exit_code"], 0);
1178
1179 let data = parsed["data"].as_array().unwrap();
1181 assert_eq!(data.len(), 3);
1182
1183 assert_eq!(data[0]["name"], "beol");
1185 assert_eq!(
1186 data[0]["iri"],
1187 "http://api.dasch.swiss/ontology/0801/beol/v2"
1188 );
1189 assert_eq!(data[0]["label"], "The BEOL data-model");
1190 assert_eq!(data[0]["last_modified"], "2024-05-27T13:43:26.233048Z");
1191 assert_eq!(data[0]["is_builtin"], false);
1192
1193 assert_eq!(data[1]["name"], "biblio");
1195 assert!(data[1]["label"].is_null());
1196 assert!(data[1]["last_modified"].is_null());
1197 assert_eq!(data[1]["is_builtin"], false);
1198
1199 assert_eq!(data[2]["name"], "knora-api");
1201 assert!(data[2]["label"].is_null());
1202 assert!(data[2]["last_modified"].is_null());
1203 assert_eq!(data[2]["is_builtin"], true);
1204 }
1205
1206 #[test]
1207 fn data_models_json_empty_data_array() {
1208 let out = SharedBuf::new();
1209 let mut renderer = JsonRenderer::with_writer(out.clone());
1210 let view = DataModelListView {
1211 items: vec![],
1212 total: 0,
1213 filter: None,
1214 };
1215 let meta = make_meta("anonymous", "https://api.test.dasch.swiss");
1216 renderer.data_models(&view, &meta).unwrap();
1217
1218 let parsed: serde_json::Value = serde_json::from_str(out.string().trim()).unwrap();
1219 assert!(parsed["data"].as_array().unwrap().is_empty());
1220 }
1221
1222 fn make_beol_dm_detail() -> DataModelDetail {
1225 DataModelDetail {
1226 name: "beol".into(),
1227 iri: "http://api.dasch.swiss/ontology/0801/beol/v2".into(),
1228 label: Some("The BEOL data-model".into()),
1229 last_modified: Some("2024-05-27T13:43:26.233048Z".into()),
1230 resource_types: vec![
1231 ResourceTypeSummary {
1232 name: "Archive".into(),
1233 iri: "http://api.dasch.swiss/ontology/0801/beol/v2#Archive".into(),
1234 label: Some("Archive".into()),
1235 },
1236 ResourceTypeSummary {
1237 name: "basicLetter".into(),
1238 iri: "http://api.dasch.swiss/ontology/0801/beol/v2#basicLetter".into(),
1239 label: None,
1240 },
1241 ResourceTypeSummary {
1242 name: "letter".into(),
1243 iri: "http://api.dasch.swiss/ontology/0801/beol/v2#letter".into(),
1244 label: Some("Letter".into()),
1245 },
1246 ],
1247 }
1248 }
1249
1250 #[test]
1251 fn data_model_describe_json_full() {
1252 let out = SharedBuf::new();
1253 let mut renderer = JsonRenderer::with_writer(out.clone());
1254 let meta = make_meta("anonymous", "https://api.test.dasch.swiss");
1255 renderer
1256 .data_model_describe(&make_beol_dm_detail(), &meta)
1257 .unwrap();
1258
1259 let parsed: serde_json::Value = serde_json::from_str(out.string().trim()).unwrap();
1260
1261 assert_eq!(parsed["_meta"]["auth"], "anonymous");
1263 assert_eq!(parsed["_meta"]["server"], "https://api.test.dasch.swiss");
1264 assert_eq!(parsed["_meta"]["exit_code"], 0);
1265
1266 let data = &parsed["data"];
1268 assert!(data.is_object());
1269 assert_eq!(data["name"], "beol");
1270 assert_eq!(data["iri"], "http://api.dasch.swiss/ontology/0801/beol/v2");
1271 assert_eq!(data["label"], "The BEOL data-model");
1272 assert_eq!(data["last_modified"], "2024-05-27T13:43:26.233048Z");
1274
1275 let rts = data["resource_types"].as_array().unwrap();
1277 assert_eq!(rts.len(), 3);
1278 assert_eq!(rts[0]["name"], "Archive");
1279 assert_eq!(
1280 rts[0]["iri"],
1281 "http://api.dasch.swiss/ontology/0801/beol/v2#Archive"
1282 );
1283 assert_eq!(rts[0]["label"], "Archive");
1284 assert_eq!(rts[1]["name"], "basicLetter");
1286 assert!(rts[1]["label"].is_null());
1287 assert_eq!(rts[2]["name"], "letter");
1288 assert_eq!(rts[2]["label"], "Letter");
1289 }
1290
1291 #[test]
1292 fn data_model_describe_json_no_label_no_last_modified() {
1293 let out = SharedBuf::new();
1294 let mut renderer = JsonRenderer::with_writer(out.clone());
1295 let detail = DataModelDetail {
1296 name: "minimal".into(),
1297 iri: "http://api.dasch.swiss/ontology/0000/minimal/v2".into(),
1298 label: None,
1299 last_modified: None,
1300 resource_types: vec![],
1301 };
1302 let meta = make_meta("anonymous", "https://api.test.dasch.swiss");
1303 renderer.data_model_describe(&detail, &meta).unwrap();
1304
1305 let parsed: serde_json::Value = serde_json::from_str(out.string().trim()).unwrap();
1306 let data = &parsed["data"];
1307 assert!(data["label"].is_null());
1309 assert!(data["last_modified"].is_null());
1311 assert!(data["resource_types"].as_array().unwrap().is_empty());
1313 }
1314
1315 fn make_rt_fixture() -> Vec<ResourceType> {
1318 vec![
1319 ResourceType {
1320 name: "Archive".into(),
1321 iri: "http://api.dasch.swiss/ontology/0801/beol/v2#Archive".into(),
1322 label: Some("Archive".into()),
1323 is_builtin: false,
1324 },
1325 ResourceType {
1326 name: "letter".into(),
1327 iri: "http://api.dasch.swiss/ontology/0801/beol/v2#letter".into(),
1328 label: None,
1329 is_builtin: false,
1330 },
1331 ]
1332 }
1333
1334 #[test]
1335 fn resource_types_json_output() {
1336 let out = SharedBuf::new();
1337 let mut renderer = JsonRenderer::with_writer(out.clone());
1338 let view = ResourceTypeListView {
1339 items: make_rt_fixture(),
1340 total: 2,
1341 filter: None,
1342 data_model: "beol".into(),
1343 };
1344 let meta = make_meta("anonymous", "https://api.test.dasch.swiss");
1345 renderer.resource_types(&view, &meta).unwrap();
1346
1347 let parsed: serde_json::Value = serde_json::from_str(out.string().trim()).unwrap();
1348
1349 assert_eq!(parsed["_meta"]["auth"], "anonymous");
1351 assert_eq!(parsed["_meta"]["server"], "https://api.test.dasch.swiss");
1352 assert_eq!(parsed["_meta"]["exit_code"], 0);
1353
1354 let data = parsed["data"].as_array().unwrap();
1356 assert_eq!(data.len(), 2);
1357
1358 assert_eq!(data[0]["name"], "Archive");
1360 assert_eq!(
1361 data[0]["iri"],
1362 "http://api.dasch.swiss/ontology/0801/beol/v2#Archive"
1363 );
1364 assert_eq!(data[0]["label"], "Archive");
1365 assert_eq!(data[0]["is_builtin"], false);
1366
1367 assert_eq!(data[1]["name"], "letter");
1369 assert!(data[1]["label"].is_null());
1370 assert_eq!(data[1]["is_builtin"], false);
1371 }
1372
1373 #[test]
1374 fn resource_types_json_with_builtins() {
1375 let out = SharedBuf::new();
1376 let mut renderer = JsonRenderer::with_writer(out.clone());
1377 let view = ResourceTypeListView {
1378 items: vec![ResourceType {
1379 name: "Region".into(),
1380 iri: "http://api.knora.org/ontology/knora-api/v2#Region".into(),
1381 label: Some("Region".into()),
1382 is_builtin: true,
1383 }],
1384 total: 1,
1385 filter: None,
1386 data_model: "beol".into(),
1387 };
1388 let meta = make_meta("anonymous", "https://api.test.dasch.swiss");
1389 renderer.resource_types(&view, &meta).unwrap();
1390
1391 let parsed: serde_json::Value = serde_json::from_str(out.string().trim()).unwrap();
1392 let data = parsed["data"].as_array().unwrap();
1393 assert_eq!(data.len(), 1);
1394 assert_eq!(data[0]["name"], "Region");
1395 assert_eq!(data[0]["label"], "Region");
1396 assert_eq!(data[0]["is_builtin"], true);
1398 }
1399
1400 #[test]
1401 fn resource_types_json_with_filter() {
1402 let out = SharedBuf::new();
1404 let mut renderer = JsonRenderer::with_writer(out.clone());
1405 let view = ResourceTypeListView {
1406 items: vec![make_rt_fixture().remove(0)], total: 2,
1408 filter: Some("arch".to_string()),
1409 data_model: "beol".into(),
1410 };
1411 let meta = make_meta("anonymous", "https://api.test.dasch.swiss");
1412 renderer.resource_types(&view, &meta).unwrap();
1413
1414 let parsed: serde_json::Value = serde_json::from_str(out.string().trim()).unwrap();
1415 let data = parsed["data"].as_array().unwrap();
1416 assert_eq!(data.len(), 1);
1417 assert_eq!(data[0]["name"], "Archive");
1418 }
1419
1420 #[test]
1421 fn resource_types_json_empty_data_array() {
1422 let out = SharedBuf::new();
1423 let mut renderer = JsonRenderer::with_writer(out.clone());
1424 let view = ResourceTypeListView {
1425 items: vec![],
1426 total: 0,
1427 filter: None,
1428 data_model: "beol".into(),
1429 };
1430 let meta = make_meta("anonymous", "https://api.test.dasch.swiss");
1431 renderer.resource_types(&view, &meta).unwrap();
1432
1433 let parsed: serde_json::Value = serde_json::from_str(out.string().trim()).unwrap();
1434 assert!(parsed["data"].as_array().unwrap().is_empty());
1435 }
1436
1437 use crate::model::resource_type::ValueType;
1440 use crate::model::{
1441 DatePoint, DateValue, FieldValues, FileValue, ResourceAccess, ResourceDetail,
1442 ResourceVisibility, Value, ValueContent,
1443 };
1444
1445 fn make_resource_detail_no_values() -> ResourceDetail {
1446 ResourceDetail {
1447 label: "Test Resource".into(),
1448 iri: "http://rdfh.ch/0803/abc123".into(),
1449 resource_type: "Page".into(),
1450 ark_url: None,
1451 creation_date: None,
1452 last_modified: None,
1453 attached_project: None,
1454 owner: None,
1455 visibility: Some(ResourceVisibility::Public),
1456 your_access: Some(ResourceAccess::View),
1457 values: None,
1458 }
1459 }
1460
1461 #[test]
1462 fn resource_describe_json_no_values_key_absent() {
1463 let out = SharedBuf::new();
1465 let mut renderer = JsonRenderer::with_writer(out.clone());
1466 let meta = make_meta("anonymous", "https://api.test.dasch.swiss");
1467 renderer
1468 .resource_describe(&make_resource_detail_no_values(), &meta)
1469 .unwrap();
1470
1471 let parsed: serde_json::Value = serde_json::from_str(out.string().trim()).unwrap();
1472 let data = &parsed["data"];
1473 assert!(
1474 data["values"].is_null() && !data.as_object().unwrap().contains_key("values"),
1475 "values key must be absent when values is None; got data: {data}"
1476 );
1477 }
1478
1479 #[test]
1480 fn resource_describe_json_some_values_array_present() {
1481 let out = SharedBuf::new();
1483 let mut renderer = JsonRenderer::with_writer(out.clone());
1484 let mut detail = make_resource_detail_no_values();
1485 detail.values = Some(vec![FieldValues {
1486 name: "hasTitle".into(),
1487 label: Some("Title".into()),
1488 values: vec![ValueContent::Text("Hello".into()).into()],
1489 }]);
1490 let meta = make_meta("anonymous", "https://api.test.dasch.swiss");
1491 renderer.resource_describe(&detail, &meta).unwrap();
1492
1493 let parsed: serde_json::Value = serde_json::from_str(out.string().trim()).unwrap();
1494 let data = &parsed["data"];
1495 assert!(
1496 data.as_object().unwrap().contains_key("values"),
1497 "values key must be present when values is Some; got data: {data}"
1498 );
1499 let values = data["values"].as_array().unwrap();
1500 assert_eq!(values.len(), 1);
1501 assert_eq!(values[0]["field"], "hasTitle");
1502 assert_eq!(values[0]["field_label"], "Title");
1503 let val_objs = values[0]["values"].as_array().unwrap();
1504 assert_eq!(val_objs.len(), 1);
1505 assert_eq!(val_objs[0]["value_type"], "text");
1506 assert_eq!(val_objs[0]["text"], "Hello");
1507 }
1508
1509 #[test]
1510 fn resource_describe_json_some_empty_values_array() {
1511 let out = SharedBuf::new();
1513 let mut renderer = JsonRenderer::with_writer(out.clone());
1514 let mut detail = make_resource_detail_no_values();
1515 detail.values = Some(vec![]);
1516 let meta = make_meta("anonymous", "https://api.test.dasch.swiss");
1517 renderer.resource_describe(&detail, &meta).unwrap();
1518
1519 let parsed: serde_json::Value = serde_json::from_str(out.string().trim()).unwrap();
1520 let data = &parsed["data"];
1521 assert!(
1522 data.as_object().unwrap().contains_key("values"),
1523 "values key must be present even for empty Some(vec![]);"
1524 );
1525 assert!(data["values"].as_array().unwrap().is_empty());
1526 }
1527
1528 #[test]
1529 fn resource_describe_json_field_label_null_when_none() {
1530 let out = SharedBuf::new();
1532 let mut renderer = JsonRenderer::with_writer(out.clone());
1533 let mut detail = make_resource_detail_no_values();
1534 detail.values = Some(vec![FieldValues {
1535 name: "seqnum".into(),
1536 label: None,
1537 values: vec![ValueContent::Integer(42).into()],
1538 }]);
1539 let meta = make_meta("anonymous", "https://api.test.dasch.swiss");
1540 renderer.resource_describe(&detail, &meta).unwrap();
1541
1542 let parsed: serde_json::Value = serde_json::from_str(out.string().trim()).unwrap();
1543 let fg = &parsed["data"]["values"][0];
1544 assert!(
1545 fg["field_label"].is_null(),
1546 "field_label must be null when label is None"
1547 );
1548 let val = &fg["values"][0];
1549 assert_eq!(val["value_type"], "integer");
1550 assert_eq!(val["value"], 42);
1551 }
1552
1553 #[test]
1554 fn resource_describe_json_link_value() {
1555 let out = SharedBuf::new();
1556 let mut renderer = JsonRenderer::with_writer(out.clone());
1557 let mut detail = make_resource_detail_no_values();
1558 detail.values = Some(vec![FieldValues {
1559 name: "isPartOf".into(),
1560 label: None,
1561 values: vec![
1562 ValueContent::Link {
1563 target_iri: "http://rdfh.ch/0803/book1".into(),
1564 target_label: Some("My Book".into()),
1565 }
1566 .into(),
1567 ValueContent::Link {
1568 target_iri: "http://rdfh.ch/0803/book2".into(),
1569 target_label: None,
1570 }
1571 .into(),
1572 ],
1573 }]);
1574 let meta = make_meta("anonymous", "https://api.test.dasch.swiss");
1575 renderer.resource_describe(&detail, &meta).unwrap();
1576
1577 let parsed: serde_json::Value = serde_json::from_str(out.string().trim()).unwrap();
1578 let vals = &parsed["data"]["values"][0]["values"];
1579 assert_eq!(vals[0]["value_type"], "link");
1581 assert_eq!(vals[0]["target_iri"], "http://rdfh.ch/0803/book1");
1582 assert_eq!(vals[0]["target_label"], "My Book");
1583 assert_eq!(vals[1]["value_type"], "link");
1585 assert_eq!(vals[1]["target_iri"], "http://rdfh.ch/0803/book2");
1586 assert!(vals[1]["target_label"].is_null());
1587 }
1588
1589 #[test]
1590 fn resource_describe_json_still_image_value() {
1591 let out = SharedBuf::new();
1592 let mut renderer = JsonRenderer::with_writer(out.clone());
1593 let mut detail = make_resource_detail_no_values();
1594 detail.values = Some(vec![FieldValues {
1595 name: "hasStillImageFileValue".into(),
1596 label: None,
1597 values: vec![
1598 ValueContent::File(FileValue {
1599 value_type: ValueType::StillImage,
1600 filename: "image.jp2".into(),
1601 url: "https://iiif.example.com/image.jp2/full/max/0/default.jpg".into(),
1602 width: Some(1200),
1603 height: Some(800),
1604 })
1605 .into(),
1606 ],
1607 }]);
1608 let meta = make_meta("anonymous", "https://api.test.dasch.swiss");
1609 renderer.resource_describe(&detail, &meta).unwrap();
1610
1611 let parsed: serde_json::Value = serde_json::from_str(out.string().trim()).unwrap();
1612 let val = &parsed["data"]["values"][0]["values"][0];
1613 assert_eq!(val["value_type"], "still-image");
1614 assert_eq!(val["filename"], "image.jp2");
1615 assert_eq!(
1616 val["url"],
1617 "https://iiif.example.com/image.jp2/full/max/0/default.jpg"
1618 );
1619 assert_eq!(val["width"], 1200);
1620 assert_eq!(val["height"], 800);
1621 }
1622
1623 #[test]
1624 fn resource_describe_json_date_value() {
1625 let out = SharedBuf::new();
1626 let mut renderer = JsonRenderer::with_writer(out.clone());
1627 let mut detail = make_resource_detail_no_values();
1628 let pt = DatePoint {
1629 year: Some(1489),
1630 month: None,
1631 day: None,
1632 era: Some("CE".into()),
1633 };
1634 detail.values = Some(vec![FieldValues {
1635 name: "hasDate".into(),
1636 label: None,
1637 values: vec![
1638 ValueContent::Date(DateValue {
1639 calendar: "GREGORIAN".into(),
1640 start: pt.clone(),
1641 end: pt.clone(),
1642 })
1643 .into(),
1644 ],
1645 }]);
1646 let meta = make_meta("anonymous", "https://api.test.dasch.swiss");
1647 renderer.resource_describe(&detail, &meta).unwrap();
1648
1649 let parsed: serde_json::Value = serde_json::from_str(out.string().trim()).unwrap();
1650 let val = &parsed["data"]["values"][0]["values"][0];
1651 assert_eq!(val["value_type"], "date");
1652 assert_eq!(val["calendar"], "GREGORIAN");
1653 assert_eq!(val["start"]["year"], 1489);
1654 assert_eq!(val["start"]["era"], "CE");
1655 assert!(val["start"]["month"].is_null());
1656 assert!(val["start"]["day"].is_null());
1657 }
1658
1659 #[test]
1660 fn resource_describe_json_comment_present_when_set() {
1661 let out = SharedBuf::new();
1663 let mut renderer = JsonRenderer::with_writer(out.clone());
1664 let mut detail = make_resource_detail_no_values();
1665 detail.values = Some(vec![FieldValues {
1666 name: "hasTranscription".into(),
1667 label: None,
1668 values: vec![Value {
1669 content: ValueContent::Text("some transcription".into()),
1670 comment: Some("reading uncertain".into()),
1671 }],
1672 }]);
1673 let meta = make_meta("anonymous", "https://api.test.dasch.swiss");
1674 renderer.resource_describe(&detail, &meta).unwrap();
1675
1676 let parsed: serde_json::Value = serde_json::from_str(out.string().trim()).unwrap();
1677 let val = &parsed["data"]["values"][0]["values"][0];
1678 assert_eq!(val["value_type"], "text");
1679 assert_eq!(val["text"], "some transcription");
1680 assert_eq!(val["comment"], "reading uncertain");
1681 }
1682
1683 #[test]
1684 fn resource_describe_json_comment_absent_when_none() {
1685 let out = SharedBuf::new();
1687 let mut renderer = JsonRenderer::with_writer(out.clone());
1688 let mut detail = make_resource_detail_no_values();
1689 detail.values = Some(vec![FieldValues {
1690 name: "hasTranscription".into(),
1691 label: None,
1692 values: vec![ValueContent::Text("plain transcription".into()).into()],
1693 }]);
1694 let meta = make_meta("anonymous", "https://api.test.dasch.swiss");
1695 renderer.resource_describe(&detail, &meta).unwrap();
1696
1697 let parsed: serde_json::Value = serde_json::from_str(out.string().trim()).unwrap();
1698 let val = &parsed["data"]["values"][0]["values"][0];
1699 assert!(
1700 !val.as_object().unwrap().contains_key("comment"),
1701 "comment key must be absent when comment is None; got: {val}"
1702 );
1703 }
1704}