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
602 .items
603 .iter()
604 .map(|item| {
605 let mut obj = json!({
606 "name": item.name,
607 "iri": item.iri,
608 "label": item.label,
609 "is_builtin": item.is_builtin,
610 });
611 if let Some(count) = item.count {
612 obj["count"] = serde_json::Value::from(count);
613 }
614 obj
615 })
616 .collect();
617
618 let mut meta_obj = meta_block(meta, 0);
620 if let Some(ref cc) = meta.count_caveat {
621 meta_obj["note"] = serde_json::Value::from(cc.as_str());
622 }
623
624 let obj = json!({
625 "_meta": meta_obj,
626 "data": data,
627 });
628 writeln!(
629 self.out,
630 "{}",
631 serde_json::to_string(&obj)
632 .map_err(|e| Diagnostic::Internal(format!("json serialisation error: {e}")))?
633 )?;
634 Ok(())
635 }
636
637 fn resource_type_describe(
638 &mut self,
639 detail: &crate::model::ResourceTypeDetail,
640 meta: &MetaContext,
641 ) -> Result<(), Diagnostic> {
642 let fields: Vec<serde_json::Value> = detail
646 .fields
647 .iter()
648 .map(|f| {
649 json!({
650 "name": f.name,
651 "iri": f.iri,
652 "label": f.label,
653 "value_type": f.value_type.to_string(),
654 "link_target": f.link_target,
655 "cardinality": f.cardinality.to_string(),
656 "is_builtin": f.is_builtin,
657 "data_model": f.data_model,
658 })
659 })
660 .collect();
661
662 let mut meta_obj = meta_block(meta, 0);
664 if let Some(ref cc) = meta.count_caveat {
665 meta_obj["note"] = serde_json::Value::from(cc.as_str());
666 }
667
668 let mut data = json!({
673 "name": detail.name,
674 "iri": detail.iri,
675 "label": detail.label,
676 "data_model": detail.data_model,
677 "representation": detail.representation.as_ref().map(|r| r.to_string()),
678 "super_types": detail.super_types,
679 "fields": fields,
680 });
681 if let Some(count) = detail.count {
682 data["count"] = serde_json::Value::from(count);
683 }
684
685 let obj = json!({
686 "_meta": meta_obj,
687 "data": data,
688 });
689 writeln!(
690 self.out,
691 "{}",
692 serde_json::to_string(&obj)
693 .map_err(|e| Diagnostic::Internal(format!("json serialisation error: {e}")))?
694 )?;
695 Ok(())
696 }
697
698 fn data_model_structure(
699 &mut self,
700 structure: &DataModelStructure,
701 meta: &MetaContext,
702 ) -> Result<(), Diagnostic> {
703 let data: Vec<serde_json::Value> = structure
707 .relations
708 .iter()
709 .map(|r| {
710 json!({
711 "source": r.source,
712 "target": r.target,
713 "kind": r.kind.to_string(),
714 "field": r.field,
715 "target_data_model": r.target_data_model,
716 })
717 })
718 .collect();
719
720 let obj = json!({
721 "_meta": meta_block(meta, 0),
722 "data": data,
723 });
724 writeln!(
725 self.out,
726 "{}",
727 serde_json::to_string(&obj)
728 .map_err(|e| Diagnostic::Internal(format!("json serialisation error: {e}")))?
729 )?;
730 Ok(())
731 }
732
733 fn resources(&mut self, view: &ResourceListView, meta: &MetaContext) -> Result<(), Diagnostic> {
734 let data: Vec<serde_json::Value> = view
736 .items
737 .iter()
738 .map(|item| {
739 json!({
740 "label": item.label,
741 "iri": item.iri,
742 "ark_url": item.ark_url,
743 "creation_date": item.creation_date,
744 "last_modified": item.last_modified,
745 "resource_type": item.resource_type,
746 })
747 })
748 .collect();
749
750 let mut meta_obj = meta_block(meta, 0);
752 match &view.pagination {
753 ResourceListPagination::SinglePage {
754 page,
755 may_have_more,
756 } => {
757 meta_obj["page"] = serde_json::Value::from(*page);
758 meta_obj["may_have_more_results"] = serde_json::Value::from(*may_have_more);
759 }
760 ResourceListPagination::AllPages { pages_fetched } => {
761 meta_obj["pages_fetched"] = serde_json::Value::from(*pages_fetched);
762 meta_obj["may_have_more_results"] = serde_json::Value::from(false);
764 }
765 }
766
767 if let Some(ref fw) = meta.filter_warning {
769 meta_obj["note"] = serde_json::Value::from(fw.as_str());
770 }
771
772 let obj = json!({
773 "_meta": meta_obj,
774 "data": data,
775 });
776 writeln!(
777 self.out,
778 "{}",
779 serde_json::to_string(&obj)
780 .map_err(|e| Diagnostic::Internal(format!("json serialisation error: {e}")))?
781 )?;
782 Ok(())
783 }
784
785 fn resource_describe(
786 &mut self,
787 detail: &ResourceDetail,
788 meta: &MetaContext,
789 ) -> Result<(), Diagnostic> {
790 let mut meta_obj = meta_block(meta, 0);
794 if let Some(ref fw) = meta.filter_warning {
795 meta_obj["note"] = serde_json::Value::from(fw.as_str());
796 }
797
798 let mut data = serde_json::Map::new();
800 data.insert(
801 "label".into(),
802 serde_json::Value::String(detail.label.clone()),
803 );
804 data.insert("iri".into(), serde_json::Value::String(detail.iri.clone()));
805 data.insert(
806 "resource_type".into(),
807 serde_json::Value::String(detail.resource_type.clone()),
808 );
809 data.insert(
810 "ark_url".into(),
811 detail
812 .ark_url
813 .as_ref()
814 .map_or(serde_json::Value::Null, |s| {
815 serde_json::Value::String(s.clone())
816 }),
817 );
818 data.insert(
819 "creation_date".into(),
820 detail
821 .creation_date
822 .as_ref()
823 .map_or(serde_json::Value::Null, |s| {
824 serde_json::Value::String(s.clone())
825 }),
826 );
827 data.insert(
828 "last_modified".into(),
829 detail
830 .last_modified
831 .as_ref()
832 .map_or(serde_json::Value::Null, |s| {
833 serde_json::Value::String(s.clone())
834 }),
835 );
836 data.insert(
837 "attached_project".into(),
838 detail
839 .attached_project
840 .as_ref()
841 .map_or(serde_json::Value::Null, |s| {
842 serde_json::Value::String(s.clone())
843 }),
844 );
845 data.insert(
846 "owner".into(),
847 detail.owner.as_ref().map_or(serde_json::Value::Null, |s| {
848 serde_json::Value::String(s.clone())
849 }),
850 );
851 data.insert(
852 "visibility".into(),
853 detail
854 .visibility
855 .as_ref()
856 .map_or(serde_json::Value::Null, |v| {
857 serde_json::Value::String(v.as_str().into())
858 }),
859 );
860 data.insert(
861 "your_access".into(),
862 detail
863 .your_access
864 .as_ref()
865 .map_or(serde_json::Value::Null, |a| {
866 serde_json::Value::String(a.as_str().into())
867 }),
868 );
869 if let Some(ref fields) = detail.values {
872 let values_arr: Vec<serde_json::Value> = fields
873 .iter()
874 .map(|fv| {
875 let value_objs: Vec<serde_json::Value> = fv
876 .values
877 .iter()
878 .map(|v| {
879 let mut obj = value_content_to_json(&v.content);
880 if let (Some(c), serde_json::Value::Object(m)) = (&v.comment, &mut obj)
881 {
882 m.insert("comment".into(), serde_json::Value::String(c.clone()));
883 }
884 obj
885 })
886 .collect();
887 let mut fg = serde_json::Map::new();
888 fg.insert("field".into(), serde_json::Value::String(fv.name.clone()));
889 let fl_val = match &fv.label {
890 Some(s) => serde_json::Value::String(s.clone()),
891 None => serde_json::Value::Null,
892 };
893 fg.insert("field_label".into(), fl_val);
894 fg.insert("values".into(), serde_json::Value::Array(value_objs));
895 serde_json::Value::Object(fg)
896 })
897 .collect();
898 data.insert("values".into(), serde_json::Value::Array(values_arr));
899 }
900
901 let obj = json!({
902 "_meta": meta_obj,
903 "data": serde_json::Value::Object(data),
904 });
905 writeln!(
906 self.out,
907 "{}",
908 serde_json::to_string(&obj)
909 .map_err(|e| Diagnostic::Internal(format!("json serialisation error: {e}")))?
910 )?;
911 Ok(())
912 }
913}
914
915#[cfg(test)]
916mod tests {
917 use super::*;
918 use crate::model::{
919 DataModel, DataModelDetail, DataModelSummary, Project, ProjectDescription, ProjectDetail,
920 ProjectStatus, ResourceType, ResourceTypeSummary,
921 };
922 use crate::render::test_support::{SharedBuf, make_meta};
923 use crate::render::{DataModelListView, ResourceTypeListView};
924
925 #[test]
926 fn projects_json_output() {
927 let out = SharedBuf::new();
928 let mut renderer = JsonRenderer::with_writer(out.clone());
929 let items = vec![
930 Project {
931 iri: "http://rdfh.ch/projects/0001".into(),
932 shortcode: "0001".into(),
933 shortname: "anything".into(),
934 longname: Some("Anything Project".into()),
935 status: ProjectStatus::Active,
936 data_models: 2,
937 },
938 Project {
939 iri: "http://rdfh.ch/projects/0002".into(),
940 shortcode: "0002".into(),
941 shortname: "images".into(),
942 longname: None,
943 status: ProjectStatus::Inactive,
944 data_models: 0,
945 },
946 ];
947 let view = ProjectListView {
948 items,
949 total: 2,
950 filter: None,
951 };
952 let meta = make_meta("anonymous", "https://api.test.dasch.swiss");
953 renderer.projects(&view, &meta).unwrap();
954
955 let s = out.string();
956 let parsed: serde_json::Value = serde_json::from_str(s.trim()).unwrap();
957
958 assert_eq!(parsed["_meta"]["auth"], "anonymous");
960 assert_eq!(parsed["_meta"]["server"], "https://api.test.dasch.swiss");
961 assert_eq!(parsed["_meta"]["exit_code"], 0);
962
963 let data = parsed["data"].as_array().unwrap();
965 assert_eq!(data.len(), 2);
966
967 assert_eq!(data[0]["shortcode"], "0001");
969 assert_eq!(data[0]["shortname"], "anything");
970 assert_eq!(data[0]["longname"], "Anything Project");
971 assert_eq!(data[0]["status"], "active");
972 assert_eq!(data[0]["data_models"], 2);
973 assert_eq!(data[0]["iri"], "http://rdfh.ch/projects/0001");
974
975 assert_eq!(data[1]["shortcode"], "0002");
977 assert!(data[1]["longname"].is_null());
978 assert_eq!(data[1]["status"], "inactive");
979 assert_eq!(data[1]["data_models"], 0);
980 }
981
982 #[test]
983 fn projects_json_empty_data_array() {
984 let out = SharedBuf::new();
985 let mut renderer = JsonRenderer::with_writer(out.clone());
986 let view = ProjectListView {
987 items: vec![],
988 total: 0,
989 filter: None,
990 };
991 let meta = make_meta("anonymous", "https://api.test.dasch.swiss");
992 renderer.projects(&view, &meta).unwrap();
993
994 let parsed: serde_json::Value = serde_json::from_str(out.string().trim()).unwrap();
995 assert!(parsed["data"].as_array().unwrap().is_empty());
996 }
997
998 #[test]
1005 fn diagnostic_kind_covers_all_variants() {
1006 let cases: &[(&Diagnostic, &str)] = &[
1007 (&Diagnostic::Usage("x".into()), "usage"),
1008 (&Diagnostic::AuthRequired("x".into()), "auth_required"),
1009 (&Diagnostic::NotFound("x".into()), "not_found"),
1010 (&Diagnostic::ServerError("x".into()), "server_error"),
1011 (&Diagnostic::Network("x".into()), "network"),
1012 (&Diagnostic::Conflict("x".into()), "conflict"),
1013 (&Diagnostic::Io("x".into()), "io"),
1014 (&Diagnostic::Internal("x".into()), "internal"),
1015 (&Diagnostic::NotImplemented("x".into()), "internal"),
1016 ];
1017 for (diag, expected_kind) in cases {
1018 assert_eq!(
1019 diagnostic_kind(diag),
1020 *expected_kind,
1021 "unexpected kind for {diag:?}"
1022 );
1023 }
1024 }
1025
1026 #[test]
1027 fn conflict_kind_is_conflict() {
1028 let d = Diagnostic::Conflict("dump already in progress".into());
1029 assert_eq!(diagnostic_kind(&d), "conflict");
1030 }
1031
1032 #[test]
1033 fn io_kind_is_io() {
1034 let d = Diagnostic::Io("failed to write /tmp/0001.zip: permission denied".into());
1035 assert_eq!(diagnostic_kind(&d), "io");
1036 }
1037
1038 fn make_beol_detail() -> ProjectDetail {
1039 ProjectDetail {
1043 iri: "http://rdfh.ch/projects/yTerZGyxjZVqFMNNKXCDPF".into(),
1044 shortcode: "0801".into(),
1045 shortname: "beol".into(),
1046 longname: Some("Bernoulli-Euler Online".into()),
1047 status: ProjectStatus::Active,
1048 description: vec![ProjectDescription {
1049 value: "<b>BEOL</b> — early modern mathematics.".into(),
1050 language: Some("en".into()),
1051 }],
1052 keywords: vec!["Bernoulli".into(), "Euler".into(), "Mathematics".into()],
1053 data_models: vec![
1054 DataModelSummary {
1055 name: "beol".into(),
1056 iri: "http://api.dasch.swiss/ontology/0801/beol/v2".into(),
1057 },
1058 DataModelSummary {
1059 name: "biblio".into(),
1060 iri: "http://api.dasch.swiss/ontology/0801/biblio/v2".into(),
1061 },
1062 DataModelSummary {
1063 name: "leibniz".into(),
1064 iri: "http://api.dasch.swiss/ontology/0801/leibniz/v2".into(),
1065 },
1066 DataModelSummary {
1067 name: "newton".into(),
1068 iri: "http://api.dasch.swiss/ontology/0801/newton/v2".into(),
1069 },
1070 ],
1071 }
1072 }
1073
1074 #[test]
1075 fn project_describe_json_full() {
1076 let out = SharedBuf::new();
1077 let mut renderer = JsonRenderer::with_writer(out.clone());
1078 let meta = make_meta("anonymous", "https://api.test.dasch.swiss");
1079 renderer
1080 .project_describe(&make_beol_detail(), &meta)
1081 .unwrap();
1082
1083 let parsed: serde_json::Value = serde_json::from_str(out.string().trim()).unwrap();
1084
1085 assert_eq!(parsed["_meta"]["auth"], "anonymous");
1087 assert_eq!(parsed["_meta"]["exit_code"], 0);
1088
1089 let data = &parsed["data"];
1091 assert!(data.is_object());
1092 assert_eq!(
1093 data["iri"],
1094 "http://rdfh.ch/projects/yTerZGyxjZVqFMNNKXCDPF"
1095 );
1096 assert_eq!(data["shortcode"], "0801");
1097 assert_eq!(data["shortname"], "beol");
1098 assert_eq!(data["longname"], "Bernoulli-Euler Online");
1099 assert_eq!(data["status"], "active");
1100
1101 let desc = data["description"].as_array().unwrap();
1103 assert_eq!(desc.len(), 1);
1104 assert_eq!(desc[0]["value"], "<b>BEOL</b> — early modern mathematics.");
1105 assert_eq!(desc[0]["language"], "en");
1106
1107 let kws = data["keywords"].as_array().unwrap();
1109 assert_eq!(kws.len(), 3);
1110 assert_eq!(kws[0], "Bernoulli");
1111
1112 let dms = data["data_models"].as_array().unwrap();
1115 assert_eq!(dms.len(), 4, "all four data-models must be rendered");
1116 assert_eq!(dms[0]["name"], "beol");
1117 assert_eq!(
1118 dms[0]["iri"],
1119 "http://api.dasch.swiss/ontology/0801/beol/v2"
1120 );
1121 assert_eq!(dms[1]["name"], "biblio");
1122 assert_eq!(dms[2]["name"], "leibniz");
1123 assert_eq!(dms[3]["name"], "newton");
1124 }
1125
1126 #[test]
1127 fn project_describe_json_no_longname() {
1128 let out = SharedBuf::new();
1129 let mut renderer = JsonRenderer::with_writer(out.clone());
1130 let mut detail = make_beol_detail();
1131 detail.longname = None;
1132 let meta = make_meta("anonymous", "https://api.test.dasch.swiss");
1133 renderer.project_describe(&detail, &meta).unwrap();
1134
1135 let parsed: serde_json::Value = serde_json::from_str(out.string().trim()).unwrap();
1136 assert!(parsed["data"]["longname"].is_null());
1138 }
1139
1140 #[test]
1141 fn project_describe_json_empty_fields() {
1142 let out = SharedBuf::new();
1143 let mut renderer = JsonRenderer::with_writer(out.clone());
1144 let detail = ProjectDetail {
1145 iri: "http://rdfh.ch/projects/0000".into(),
1146 shortcode: "0000".into(),
1147 shortname: "minimal".into(),
1148 longname: None,
1149 status: ProjectStatus::Inactive,
1150 description: vec![],
1151 keywords: vec![],
1152 data_models: vec![],
1153 };
1154 let meta = make_meta("anonymous", "https://api.test.dasch.swiss");
1155 renderer.project_describe(&detail, &meta).unwrap();
1156
1157 let parsed: serde_json::Value = serde_json::from_str(out.string().trim()).unwrap();
1158 let data = &parsed["data"];
1159 assert_eq!(data["status"], "inactive");
1160 assert!(data["description"].as_array().unwrap().is_empty());
1161 assert!(data["keywords"].as_array().unwrap().is_empty());
1162 assert!(data["data_models"].as_array().unwrap().is_empty());
1163 }
1164
1165 fn make_data_model_fixture() -> Vec<DataModel> {
1166 vec![
1167 DataModel {
1168 name: "beol".into(),
1169 iri: "http://api.dasch.swiss/ontology/0801/beol/v2".into(),
1170 label: Some("The BEOL data-model".into()),
1171 last_modified: Some("2024-05-27T13:43:26.233048Z".into()),
1172 is_builtin: false,
1173 },
1174 DataModel {
1175 name: "biblio".into(),
1176 iri: "http://api.dasch.swiss/ontology/0801/biblio/v2".into(),
1177 label: None,
1178 last_modified: None,
1179 is_builtin: false,
1180 },
1181 DataModel {
1182 name: "knora-api".into(),
1183 iri: "http://api.knora.org/ontology/knora-api/v2".into(),
1184 label: None,
1185 last_modified: None,
1186 is_builtin: true,
1187 },
1188 ]
1189 }
1190
1191 #[test]
1192 fn data_models_json_output() {
1193 let out = SharedBuf::new();
1194 let mut renderer = JsonRenderer::with_writer(out.clone());
1195 let view = DataModelListView {
1196 items: make_data_model_fixture(),
1197 total: 3,
1198 filter: None,
1199 };
1200 let meta = make_meta("anonymous", "https://api.test.dasch.swiss");
1201 renderer.data_models(&view, &meta).unwrap();
1202
1203 let parsed: serde_json::Value = serde_json::from_str(out.string().trim()).unwrap();
1204
1205 assert_eq!(parsed["_meta"]["auth"], "anonymous");
1207 assert_eq!(parsed["_meta"]["server"], "https://api.test.dasch.swiss");
1208 assert_eq!(parsed["_meta"]["exit_code"], 0);
1209
1210 let data = parsed["data"].as_array().unwrap();
1212 assert_eq!(data.len(), 3);
1213
1214 assert_eq!(data[0]["name"], "beol");
1216 assert_eq!(
1217 data[0]["iri"],
1218 "http://api.dasch.swiss/ontology/0801/beol/v2"
1219 );
1220 assert_eq!(data[0]["label"], "The BEOL data-model");
1221 assert_eq!(data[0]["last_modified"], "2024-05-27T13:43:26.233048Z");
1222 assert_eq!(data[0]["is_builtin"], false);
1223
1224 assert_eq!(data[1]["name"], "biblio");
1226 assert!(data[1]["label"].is_null());
1227 assert!(data[1]["last_modified"].is_null());
1228 assert_eq!(data[1]["is_builtin"], false);
1229
1230 assert_eq!(data[2]["name"], "knora-api");
1232 assert!(data[2]["label"].is_null());
1233 assert!(data[2]["last_modified"].is_null());
1234 assert_eq!(data[2]["is_builtin"], true);
1235 }
1236
1237 #[test]
1238 fn data_models_json_empty_data_array() {
1239 let out = SharedBuf::new();
1240 let mut renderer = JsonRenderer::with_writer(out.clone());
1241 let view = DataModelListView {
1242 items: vec![],
1243 total: 0,
1244 filter: None,
1245 };
1246 let meta = make_meta("anonymous", "https://api.test.dasch.swiss");
1247 renderer.data_models(&view, &meta).unwrap();
1248
1249 let parsed: serde_json::Value = serde_json::from_str(out.string().trim()).unwrap();
1250 assert!(parsed["data"].as_array().unwrap().is_empty());
1251 }
1252
1253 fn make_beol_dm_detail() -> DataModelDetail {
1256 DataModelDetail {
1257 name: "beol".into(),
1258 iri: "http://api.dasch.swiss/ontology/0801/beol/v2".into(),
1259 label: Some("The BEOL data-model".into()),
1260 last_modified: Some("2024-05-27T13:43:26.233048Z".into()),
1261 resource_types: vec![
1262 ResourceTypeSummary {
1263 name: "Archive".into(),
1264 iri: "http://api.dasch.swiss/ontology/0801/beol/v2#Archive".into(),
1265 label: Some("Archive".into()),
1266 },
1267 ResourceTypeSummary {
1268 name: "basicLetter".into(),
1269 iri: "http://api.dasch.swiss/ontology/0801/beol/v2#basicLetter".into(),
1270 label: None,
1271 },
1272 ResourceTypeSummary {
1273 name: "letter".into(),
1274 iri: "http://api.dasch.swiss/ontology/0801/beol/v2#letter".into(),
1275 label: Some("Letter".into()),
1276 },
1277 ],
1278 }
1279 }
1280
1281 #[test]
1282 fn data_model_describe_json_full() {
1283 let out = SharedBuf::new();
1284 let mut renderer = JsonRenderer::with_writer(out.clone());
1285 let meta = make_meta("anonymous", "https://api.test.dasch.swiss");
1286 renderer
1287 .data_model_describe(&make_beol_dm_detail(), &meta)
1288 .unwrap();
1289
1290 let parsed: serde_json::Value = serde_json::from_str(out.string().trim()).unwrap();
1291
1292 assert_eq!(parsed["_meta"]["auth"], "anonymous");
1294 assert_eq!(parsed["_meta"]["server"], "https://api.test.dasch.swiss");
1295 assert_eq!(parsed["_meta"]["exit_code"], 0);
1296
1297 let data = &parsed["data"];
1299 assert!(data.is_object());
1300 assert_eq!(data["name"], "beol");
1301 assert_eq!(data["iri"], "http://api.dasch.swiss/ontology/0801/beol/v2");
1302 assert_eq!(data["label"], "The BEOL data-model");
1303 assert_eq!(data["last_modified"], "2024-05-27T13:43:26.233048Z");
1305
1306 let rts = data["resource_types"].as_array().unwrap();
1308 assert_eq!(rts.len(), 3);
1309 assert_eq!(rts[0]["name"], "Archive");
1310 assert_eq!(
1311 rts[0]["iri"],
1312 "http://api.dasch.swiss/ontology/0801/beol/v2#Archive"
1313 );
1314 assert_eq!(rts[0]["label"], "Archive");
1315 assert_eq!(rts[1]["name"], "basicLetter");
1317 assert!(rts[1]["label"].is_null());
1318 assert_eq!(rts[2]["name"], "letter");
1319 assert_eq!(rts[2]["label"], "Letter");
1320 }
1321
1322 #[test]
1323 fn data_model_describe_json_no_label_no_last_modified() {
1324 let out = SharedBuf::new();
1325 let mut renderer = JsonRenderer::with_writer(out.clone());
1326 let detail = DataModelDetail {
1327 name: "minimal".into(),
1328 iri: "http://api.dasch.swiss/ontology/0000/minimal/v2".into(),
1329 label: None,
1330 last_modified: None,
1331 resource_types: vec![],
1332 };
1333 let meta = make_meta("anonymous", "https://api.test.dasch.swiss");
1334 renderer.data_model_describe(&detail, &meta).unwrap();
1335
1336 let parsed: serde_json::Value = serde_json::from_str(out.string().trim()).unwrap();
1337 let data = &parsed["data"];
1338 assert!(data["label"].is_null());
1340 assert!(data["last_modified"].is_null());
1342 assert!(data["resource_types"].as_array().unwrap().is_empty());
1344 }
1345
1346 fn make_rt_fixture() -> Vec<ResourceType> {
1349 vec![
1350 ResourceType {
1351 name: "Archive".into(),
1352 iri: "http://api.dasch.swiss/ontology/0801/beol/v2#Archive".into(),
1353 label: Some("Archive".into()),
1354 is_builtin: false,
1355 count: None,
1356 },
1357 ResourceType {
1358 name: "letter".into(),
1359 iri: "http://api.dasch.swiss/ontology/0801/beol/v2#letter".into(),
1360 label: None,
1361 is_builtin: false,
1362 count: None,
1363 },
1364 ]
1365 }
1366
1367 #[test]
1368 fn resource_types_json_output() {
1369 let out = SharedBuf::new();
1370 let mut renderer = JsonRenderer::with_writer(out.clone());
1371 let view = ResourceTypeListView {
1372 items: make_rt_fixture(),
1373 total: 2,
1374 filter: None,
1375 data_model: "beol".into(),
1376 };
1377 let meta = make_meta("anonymous", "https://api.test.dasch.swiss");
1378 renderer.resource_types(&view, &meta).unwrap();
1379
1380 let parsed: serde_json::Value = serde_json::from_str(out.string().trim()).unwrap();
1381
1382 assert_eq!(parsed["_meta"]["auth"], "anonymous");
1384 assert_eq!(parsed["_meta"]["server"], "https://api.test.dasch.swiss");
1385 assert_eq!(parsed["_meta"]["exit_code"], 0);
1386
1387 let data = parsed["data"].as_array().unwrap();
1389 assert_eq!(data.len(), 2);
1390
1391 assert_eq!(data[0]["name"], "Archive");
1393 assert_eq!(
1394 data[0]["iri"],
1395 "http://api.dasch.swiss/ontology/0801/beol/v2#Archive"
1396 );
1397 assert_eq!(data[0]["label"], "Archive");
1398 assert_eq!(data[0]["is_builtin"], false);
1399
1400 assert_eq!(data[1]["name"], "letter");
1402 assert!(data[1]["label"].is_null());
1403 assert_eq!(data[1]["is_builtin"], false);
1404 }
1405
1406 #[test]
1407 fn resource_types_json_with_builtins() {
1408 let out = SharedBuf::new();
1409 let mut renderer = JsonRenderer::with_writer(out.clone());
1410 let view = ResourceTypeListView {
1411 items: vec![ResourceType {
1412 name: "Region".into(),
1413 iri: "http://api.knora.org/ontology/knora-api/v2#Region".into(),
1414 label: Some("Region".into()),
1415 is_builtin: true,
1416 count: None,
1417 }],
1418 total: 1,
1419 filter: None,
1420 data_model: "beol".into(),
1421 };
1422 let meta = make_meta("anonymous", "https://api.test.dasch.swiss");
1423 renderer.resource_types(&view, &meta).unwrap();
1424
1425 let parsed: serde_json::Value = serde_json::from_str(out.string().trim()).unwrap();
1426 let data = parsed["data"].as_array().unwrap();
1427 assert_eq!(data.len(), 1);
1428 assert_eq!(data[0]["name"], "Region");
1429 assert_eq!(data[0]["label"], "Region");
1430 assert_eq!(data[0]["is_builtin"], true);
1432 }
1433
1434 #[test]
1435 fn resource_types_json_with_filter() {
1436 let out = SharedBuf::new();
1438 let mut renderer = JsonRenderer::with_writer(out.clone());
1439 let view = ResourceTypeListView {
1440 items: vec![make_rt_fixture().remove(0)], total: 2,
1442 filter: Some("arch".to_string()),
1443 data_model: "beol".into(),
1444 };
1445 let meta = make_meta("anonymous", "https://api.test.dasch.swiss");
1446 renderer.resource_types(&view, &meta).unwrap();
1447
1448 let parsed: serde_json::Value = serde_json::from_str(out.string().trim()).unwrap();
1449 let data = parsed["data"].as_array().unwrap();
1450 assert_eq!(data.len(), 1);
1451 assert_eq!(data[0]["name"], "Archive");
1452 }
1453
1454 #[test]
1455 fn resource_types_json_empty_data_array() {
1456 let out = SharedBuf::new();
1457 let mut renderer = JsonRenderer::with_writer(out.clone());
1458 let view = ResourceTypeListView {
1459 items: vec![],
1460 total: 0,
1461 filter: None,
1462 data_model: "beol".into(),
1463 };
1464 let meta = make_meta("anonymous", "https://api.test.dasch.swiss");
1465 renderer.resource_types(&view, &meta).unwrap();
1466
1467 let parsed: serde_json::Value = serde_json::from_str(out.string().trim()).unwrap();
1468 assert!(parsed["data"].as_array().unwrap().is_empty());
1469 }
1470
1471 #[test]
1472 fn resource_types_json_with_count_and_caveat() {
1473 let out = SharedBuf::new();
1476 let mut renderer = JsonRenderer::with_writer(out.clone());
1477 let mut items = make_rt_fixture();
1478 items[0].count = Some(5);
1479 let view = ResourceTypeListView {
1480 items,
1481 total: 2,
1482 filter: None,
1483 data_model: "beol".into(),
1484 };
1485 let meta = crate::render::MetaContext {
1486 server_label: "https://api.test.dasch.swiss".into(),
1487 auth_state: "anonymous".into(),
1488 filter_warning: None,
1489 count_caveat: Some("counts are not permission-filtered".into()),
1490 };
1491 renderer.resource_types(&view, &meta).unwrap();
1492
1493 let parsed: serde_json::Value = serde_json::from_str(out.string().trim()).unwrap();
1494 assert_eq!(parsed["data"][0]["count"], 5);
1495 assert!(
1497 !parsed["data"][1].as_object().unwrap().contains_key("count"),
1498 "count key must be absent when None; got: {}",
1499 parsed["data"][1]
1500 );
1501 assert_eq!(
1502 parsed["_meta"]["note"], "counts are not permission-filtered",
1503 "note must carry count_caveat"
1504 );
1505 }
1506
1507 #[test]
1508 fn resource_types_json_no_count_no_note() {
1509 let out = SharedBuf::new();
1512 let mut renderer = JsonRenderer::with_writer(out.clone());
1513 let view = ResourceTypeListView {
1514 items: make_rt_fixture(),
1515 total: 2,
1516 filter: None,
1517 data_model: "beol".into(),
1518 };
1519 let meta = make_meta("anonymous", "https://api.test.dasch.swiss");
1520 renderer.resource_types(&view, &meta).unwrap();
1521
1522 let parsed: serde_json::Value = serde_json::from_str(out.string().trim()).unwrap();
1523 assert!(!parsed["_meta"].as_object().unwrap().contains_key("note"));
1524 assert!(!parsed["data"][0].as_object().unwrap().contains_key("count"));
1525 }
1526
1527 fn make_minimal_rt_detail() -> crate::model::ResourceTypeDetail {
1530 crate::model::ResourceTypeDetail {
1531 name: "manuscript".into(),
1532 iri: "http://api.dasch.swiss/ontology/0801/beol/v2#manuscript".into(),
1533 label: Some("Manuscript".into()),
1534 data_model: "beol".into(),
1535 representation: None,
1536 super_types: vec![],
1537 fields: vec![],
1538 count: None,
1539 }
1540 }
1541
1542 #[test]
1543 fn resource_type_describe_json_with_count_and_caveat() {
1544 let out = SharedBuf::new();
1545 let mut renderer = JsonRenderer::with_writer(out.clone());
1546 let mut detail = make_minimal_rt_detail();
1547 detail.count = Some(99);
1548 let meta = crate::render::MetaContext {
1549 server_label: "https://api.dasch.swiss".into(),
1550 auth_state: "anonymous".into(),
1551 filter_warning: None,
1552 count_caveat: Some("counts exclude deleted resources".into()),
1553 };
1554 renderer.resource_type_describe(&detail, &meta).unwrap();
1555
1556 let parsed: serde_json::Value = serde_json::from_str(out.string().trim()).unwrap();
1557 assert_eq!(parsed["data"]["count"], 99);
1558 assert_eq!(
1559 parsed["_meta"]["note"], "counts exclude deleted resources",
1560 "note must carry count_caveat"
1561 );
1562 }
1563
1564 #[test]
1565 fn resource_type_describe_json_no_count_no_note() {
1566 let out = SharedBuf::new();
1569 let mut renderer = JsonRenderer::with_writer(out.clone());
1570 let meta = make_meta("anonymous", "https://api.dasch.swiss");
1571 renderer
1572 .resource_type_describe(&make_minimal_rt_detail(), &meta)
1573 .unwrap();
1574
1575 let parsed: serde_json::Value = serde_json::from_str(out.string().trim()).unwrap();
1576 assert!(!parsed["data"].as_object().unwrap().contains_key("count"));
1577 assert!(!parsed["_meta"].as_object().unwrap().contains_key("note"));
1578 }
1579
1580 use crate::model::resource_type::ValueType;
1583 use crate::model::{
1584 DatePoint, DateValue, FieldValues, FileValue, ResourceAccess, ResourceDetail,
1585 ResourceVisibility, Value, ValueContent,
1586 };
1587
1588 fn make_resource_detail_no_values() -> ResourceDetail {
1589 ResourceDetail {
1590 label: "Test Resource".into(),
1591 iri: "http://rdfh.ch/0803/abc123".into(),
1592 resource_type: "Page".into(),
1593 ark_url: None,
1594 creation_date: None,
1595 last_modified: None,
1596 attached_project: None,
1597 owner: None,
1598 visibility: Some(ResourceVisibility::Public),
1599 your_access: Some(ResourceAccess::View),
1600 values: None,
1601 }
1602 }
1603
1604 #[test]
1605 fn resource_describe_json_no_values_key_absent() {
1606 let out = SharedBuf::new();
1608 let mut renderer = JsonRenderer::with_writer(out.clone());
1609 let meta = make_meta("anonymous", "https://api.test.dasch.swiss");
1610 renderer
1611 .resource_describe(&make_resource_detail_no_values(), &meta)
1612 .unwrap();
1613
1614 let parsed: serde_json::Value = serde_json::from_str(out.string().trim()).unwrap();
1615 let data = &parsed["data"];
1616 assert!(
1617 data["values"].is_null() && !data.as_object().unwrap().contains_key("values"),
1618 "values key must be absent when values is None; got data: {data}"
1619 );
1620 }
1621
1622 #[test]
1623 fn resource_describe_json_some_values_array_present() {
1624 let out = SharedBuf::new();
1626 let mut renderer = JsonRenderer::with_writer(out.clone());
1627 let mut detail = make_resource_detail_no_values();
1628 detail.values = Some(vec![FieldValues {
1629 name: "hasTitle".into(),
1630 label: Some("Title".into()),
1631 values: vec![ValueContent::Text("Hello".into()).into()],
1632 }]);
1633 let meta = make_meta("anonymous", "https://api.test.dasch.swiss");
1634 renderer.resource_describe(&detail, &meta).unwrap();
1635
1636 let parsed: serde_json::Value = serde_json::from_str(out.string().trim()).unwrap();
1637 let data = &parsed["data"];
1638 assert!(
1639 data.as_object().unwrap().contains_key("values"),
1640 "values key must be present when values is Some; got data: {data}"
1641 );
1642 let values = data["values"].as_array().unwrap();
1643 assert_eq!(values.len(), 1);
1644 assert_eq!(values[0]["field"], "hasTitle");
1645 assert_eq!(values[0]["field_label"], "Title");
1646 let val_objs = values[0]["values"].as_array().unwrap();
1647 assert_eq!(val_objs.len(), 1);
1648 assert_eq!(val_objs[0]["value_type"], "text");
1649 assert_eq!(val_objs[0]["text"], "Hello");
1650 }
1651
1652 #[test]
1653 fn resource_describe_json_some_empty_values_array() {
1654 let out = SharedBuf::new();
1656 let mut renderer = JsonRenderer::with_writer(out.clone());
1657 let mut detail = make_resource_detail_no_values();
1658 detail.values = Some(vec![]);
1659 let meta = make_meta("anonymous", "https://api.test.dasch.swiss");
1660 renderer.resource_describe(&detail, &meta).unwrap();
1661
1662 let parsed: serde_json::Value = serde_json::from_str(out.string().trim()).unwrap();
1663 let data = &parsed["data"];
1664 assert!(
1665 data.as_object().unwrap().contains_key("values"),
1666 "values key must be present even for empty Some(vec![]);"
1667 );
1668 assert!(data["values"].as_array().unwrap().is_empty());
1669 }
1670
1671 #[test]
1672 fn resource_describe_json_field_label_null_when_none() {
1673 let out = SharedBuf::new();
1675 let mut renderer = JsonRenderer::with_writer(out.clone());
1676 let mut detail = make_resource_detail_no_values();
1677 detail.values = Some(vec![FieldValues {
1678 name: "seqnum".into(),
1679 label: None,
1680 values: vec![ValueContent::Integer(42).into()],
1681 }]);
1682 let meta = make_meta("anonymous", "https://api.test.dasch.swiss");
1683 renderer.resource_describe(&detail, &meta).unwrap();
1684
1685 let parsed: serde_json::Value = serde_json::from_str(out.string().trim()).unwrap();
1686 let fg = &parsed["data"]["values"][0];
1687 assert!(
1688 fg["field_label"].is_null(),
1689 "field_label must be null when label is None"
1690 );
1691 let val = &fg["values"][0];
1692 assert_eq!(val["value_type"], "integer");
1693 assert_eq!(val["value"], 42);
1694 }
1695
1696 #[test]
1697 fn resource_describe_json_link_value() {
1698 let out = SharedBuf::new();
1699 let mut renderer = JsonRenderer::with_writer(out.clone());
1700 let mut detail = make_resource_detail_no_values();
1701 detail.values = Some(vec![FieldValues {
1702 name: "isPartOf".into(),
1703 label: None,
1704 values: vec![
1705 ValueContent::Link {
1706 target_iri: "http://rdfh.ch/0803/book1".into(),
1707 target_label: Some("My Book".into()),
1708 }
1709 .into(),
1710 ValueContent::Link {
1711 target_iri: "http://rdfh.ch/0803/book2".into(),
1712 target_label: None,
1713 }
1714 .into(),
1715 ],
1716 }]);
1717 let meta = make_meta("anonymous", "https://api.test.dasch.swiss");
1718 renderer.resource_describe(&detail, &meta).unwrap();
1719
1720 let parsed: serde_json::Value = serde_json::from_str(out.string().trim()).unwrap();
1721 let vals = &parsed["data"]["values"][0]["values"];
1722 assert_eq!(vals[0]["value_type"], "link");
1724 assert_eq!(vals[0]["target_iri"], "http://rdfh.ch/0803/book1");
1725 assert_eq!(vals[0]["target_label"], "My Book");
1726 assert_eq!(vals[1]["value_type"], "link");
1728 assert_eq!(vals[1]["target_iri"], "http://rdfh.ch/0803/book2");
1729 assert!(vals[1]["target_label"].is_null());
1730 }
1731
1732 #[test]
1733 fn resource_describe_json_still_image_value() {
1734 let out = SharedBuf::new();
1735 let mut renderer = JsonRenderer::with_writer(out.clone());
1736 let mut detail = make_resource_detail_no_values();
1737 detail.values = Some(vec![FieldValues {
1738 name: "hasStillImageFileValue".into(),
1739 label: None,
1740 values: vec![
1741 ValueContent::File(FileValue {
1742 value_type: ValueType::StillImage,
1743 filename: "image.jp2".into(),
1744 url: "https://iiif.example.com/image.jp2/full/max/0/default.jpg".into(),
1745 width: Some(1200),
1746 height: Some(800),
1747 })
1748 .into(),
1749 ],
1750 }]);
1751 let meta = make_meta("anonymous", "https://api.test.dasch.swiss");
1752 renderer.resource_describe(&detail, &meta).unwrap();
1753
1754 let parsed: serde_json::Value = serde_json::from_str(out.string().trim()).unwrap();
1755 let val = &parsed["data"]["values"][0]["values"][0];
1756 assert_eq!(val["value_type"], "still-image");
1757 assert_eq!(val["filename"], "image.jp2");
1758 assert_eq!(
1759 val["url"],
1760 "https://iiif.example.com/image.jp2/full/max/0/default.jpg"
1761 );
1762 assert_eq!(val["width"], 1200);
1763 assert_eq!(val["height"], 800);
1764 }
1765
1766 #[test]
1767 fn resource_describe_json_date_value() {
1768 let out = SharedBuf::new();
1769 let mut renderer = JsonRenderer::with_writer(out.clone());
1770 let mut detail = make_resource_detail_no_values();
1771 let pt = DatePoint {
1772 year: Some(1489),
1773 month: None,
1774 day: None,
1775 era: Some("CE".into()),
1776 };
1777 detail.values = Some(vec![FieldValues {
1778 name: "hasDate".into(),
1779 label: None,
1780 values: vec![
1781 ValueContent::Date(DateValue {
1782 calendar: "GREGORIAN".into(),
1783 start: pt.clone(),
1784 end: pt.clone(),
1785 })
1786 .into(),
1787 ],
1788 }]);
1789 let meta = make_meta("anonymous", "https://api.test.dasch.swiss");
1790 renderer.resource_describe(&detail, &meta).unwrap();
1791
1792 let parsed: serde_json::Value = serde_json::from_str(out.string().trim()).unwrap();
1793 let val = &parsed["data"]["values"][0]["values"][0];
1794 assert_eq!(val["value_type"], "date");
1795 assert_eq!(val["calendar"], "GREGORIAN");
1796 assert_eq!(val["start"]["year"], 1489);
1797 assert_eq!(val["start"]["era"], "CE");
1798 assert!(val["start"]["month"].is_null());
1799 assert!(val["start"]["day"].is_null());
1800 }
1801
1802 #[test]
1803 fn resource_describe_json_comment_present_when_set() {
1804 let out = SharedBuf::new();
1806 let mut renderer = JsonRenderer::with_writer(out.clone());
1807 let mut detail = make_resource_detail_no_values();
1808 detail.values = Some(vec![FieldValues {
1809 name: "hasTranscription".into(),
1810 label: None,
1811 values: vec![Value {
1812 content: ValueContent::Text("some transcription".into()),
1813 comment: Some("reading uncertain".into()),
1814 }],
1815 }]);
1816 let meta = make_meta("anonymous", "https://api.test.dasch.swiss");
1817 renderer.resource_describe(&detail, &meta).unwrap();
1818
1819 let parsed: serde_json::Value = serde_json::from_str(out.string().trim()).unwrap();
1820 let val = &parsed["data"]["values"][0]["values"][0];
1821 assert_eq!(val["value_type"], "text");
1822 assert_eq!(val["text"], "some transcription");
1823 assert_eq!(val["comment"], "reading uncertain");
1824 }
1825
1826 #[test]
1827 fn resource_describe_json_comment_absent_when_none() {
1828 let out = SharedBuf::new();
1830 let mut renderer = JsonRenderer::with_writer(out.clone());
1831 let mut detail = make_resource_detail_no_values();
1832 detail.values = Some(vec![FieldValues {
1833 name: "hasTranscription".into(),
1834 label: None,
1835 values: vec![ValueContent::Text("plain transcription".into()).into()],
1836 }]);
1837 let meta = make_meta("anonymous", "https://api.test.dasch.swiss");
1838 renderer.resource_describe(&detail, &meta).unwrap();
1839
1840 let parsed: serde_json::Value = serde_json::from_str(out.string().trim()).unwrap();
1841 let val = &parsed["data"]["values"][0]["values"][0];
1842 assert!(
1843 !val.as_object().unwrap().contains_key("comment"),
1844 "comment key must be absent when comment is None; got: {val}"
1845 );
1846 }
1847}