1use anyhow::Result;
19use serde::Serialize;
20
21use crate::cli::drive::format::{write_items_jsonl, JsonlSerialize};
22use crate::drive::docs::api::{DocsApi, SuggestionsViewMode};
23use crate::drive::docs::structure::{flatten, flatten_content, DocElement};
24use crate::drive::docs::types::ResolvedSegment;
25
26#[derive(Debug, Clone)]
28pub struct ReadOptions {
29 pub document_id: String,
31 pub tab: Option<String>,
37 pub suggestions: SuggestionsViewMode,
39}
40
41#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
43pub struct SegmentContent {
44 pub segment_id: String,
46 pub elements: Vec<DocElement>,
48}
49
50#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
52pub struct TabContent {
53 #[serde(skip_serializing_if = "Option::is_none")]
55 pub tab_id: Option<String>,
56 #[serde(skip_serializing_if = "Option::is_none")]
58 pub title: Option<String>,
59 pub nesting_level: i64,
61 pub elements: Vec<DocElement>,
63 #[serde(default, skip_serializing_if = "Vec::is_empty")]
65 pub headers: Vec<SegmentContent>,
66 #[serde(default, skip_serializing_if = "Vec::is_empty")]
68 pub footers: Vec<SegmentContent>,
69 #[serde(default, skip_serializing_if = "Vec::is_empty")]
71 pub footnotes: Vec<SegmentContent>,
72}
73
74impl TabContent {
75 #[must_use]
82 pub fn segments_by_kind(&self) -> [(SegmentKind, &Vec<SegmentContent>); 3] {
83 [
84 (SegmentKind::Header, &self.headers),
85 (SegmentKind::Footer, &self.footers),
86 (SegmentKind::Footnote, &self.footnotes),
87 ]
88 }
89}
90
91#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
93pub struct ReadOutcome {
94 pub document_id: String,
96 #[serde(skip_serializing_if = "Option::is_none")]
98 pub title: Option<String>,
99 #[serde(skip_serializing_if = "Option::is_none")]
107 pub revision_id: Option<String>,
108 pub tabs: Vec<TabContent>,
110}
111
112#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
114#[serde(rename_all = "lowercase")]
115pub enum SegmentKind {
116 Header,
118 Footer,
120 Footnote,
122}
123
124impl SegmentKind {
125 #[must_use]
128 pub const fn as_str(self) -> &'static str {
129 match self {
130 Self::Header => "header",
131 Self::Footer => "footer",
132 Self::Footnote => "footnote",
133 }
134 }
135}
136
137#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
139pub struct SegmentRef<'a> {
140 pub kind: SegmentKind,
142 pub segment_id: &'a str,
144}
145
146#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
158pub struct FlatElement<'a> {
159 pub document_id: &'a str,
161 #[serde(skip_serializing_if = "Option::is_none")]
163 pub revision_id: Option<&'a str>,
164 #[serde(skip_serializing_if = "Option::is_none")]
166 pub tab_id: Option<&'a str>,
167 #[serde(skip_serializing_if = "Option::is_none")]
170 pub segment: Option<SegmentRef<'a>>,
171 #[serde(flatten)]
173 pub element: &'a DocElement,
174}
175
176impl JsonlSerialize for ReadOutcome {
177 fn write_jsonl(&self, out: &mut dyn std::io::Write) -> Result<()> {
178 write_items_jsonl(self.flat_elements().iter(), out)
179 }
180}
181
182impl ReadOutcome {
183 #[must_use]
188 pub fn flat_elements(&self) -> Vec<FlatElement<'_>> {
189 let mut out = Vec::new();
190 for tab in &self.tabs {
191 for element in &tab.elements {
192 out.push(FlatElement {
193 document_id: &self.document_id,
194 revision_id: self.revision_id.as_deref(),
195 tab_id: tab.tab_id.as_deref(),
196 segment: None,
197 element,
198 });
199 }
200 for (kind, segments) in tab.segments_by_kind() {
201 for segment in segments {
202 for element in &segment.elements {
203 out.push(FlatElement {
204 document_id: &self.document_id,
205 revision_id: self.revision_id.as_deref(),
206 tab_id: tab.tab_id.as_deref(),
207 segment: Some(SegmentRef {
208 kind,
209 segment_id: &segment.segment_id,
210 }),
211 element,
212 });
213 }
214 }
215 }
216 }
217 out
218 }
219}
220
221fn segments_to_content(segments: &[ResolvedSegment<'_>]) -> Vec<SegmentContent> {
224 segments
225 .iter()
226 .map(|segment| SegmentContent {
227 segment_id: segment.segment_id.to_string(),
228 elements: flatten_content(segment.content),
229 })
230 .collect()
231}
232
233pub async fn read(api: &DocsApi<'_>, opts: &ReadOptions) -> Result<ReadOutcome> {
235 let document = api
236 .get_document(&opts.document_id, opts.suggestions)
237 .await?;
238
239 let resolved = document.resolved_tabs();
240 let mut tabs: Vec<TabContent> = resolved
241 .iter()
242 .map(|tab| TabContent {
243 tab_id: tab.tab_id.map(ToString::to_string),
244 title: tab.title.map(ToString::to_string),
245 nesting_level: tab.nesting_level,
246 elements: tab.body.map(flatten).unwrap_or_default(),
247 headers: segments_to_content(&tab.headers()),
248 footers: segments_to_content(&tab.footers()),
249 footnotes: segments_to_content(&tab.footnotes()),
250 })
251 .collect();
252
253 if let Some(wanted) = &opts.tab {
254 let known: Vec<&str> = tabs.iter().filter_map(|t| t.tab_id.as_deref()).collect();
258 anyhow::ensure!(
259 known.contains(&wanted.as_str()),
260 "document '{}' has no tab '{wanted}'; it has {}",
261 opts.document_id,
262 if known.is_empty() {
263 "no tabs (it predates tabs, or was returned in the legacy single-body form)"
264 .to_string()
265 } else {
266 format!("tabs: {}", known.join(", "))
267 }
268 );
269 tabs.retain(|t| t.tab_id.as_deref() == Some(wanted.as_str()));
270 }
271
272 Ok(ReadOutcome {
273 document_id: opts.document_id.clone(),
274 title: document.title.clone(),
275 revision_id: document.revision_id.clone(),
276 tabs,
277 })
278}
279
280#[cfg(test)]
281#[allow(clippy::unwrap_used, clippy::expect_used)]
282mod tests {
283 use super::*;
284 use crate::drive::auth::{DriveCredentials, DriveGrantedScopes};
285 use crate::drive::client::DriveClient;
286 use crate::drive::docs::client::{DocsClient, DOCS_API_URL};
287 use crate::drive::docs::structure::ElementKind;
288 use crate::test_support::env::MapEnv;
289 use crate::utils::secret::Secret;
290 use wiremock::matchers::{method, path, query_param};
291 use wiremock::{Mock, MockServer, ResponseTemplate};
292
293 fn test_credentials() -> DriveCredentials {
294 DriveCredentials {
295 client_id: "client-1".to_string(),
296 client_secret: Secret::new("secret-1"),
297 refresh_token: Secret::new("refresh-1"),
298 scope: DriveGrantedScopes::READONLY,
299 }
300 }
301
302 async fn docs_client(server: &MockServer) -> DocsClient {
310 Mock::given(method("POST"))
311 .and(path("/token"))
312 .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
313 "access_token": "test-token",
314 "expires_in": 3600,
315 })))
316 .mount(server)
317 .await;
318
319 let mut drive = DriveClient::new(&server.uri(), &test_credentials()).unwrap();
320 crate::drive::client::test_support::replace_session(
321 &mut drive,
322 &test_credentials(),
323 &format!("{}/token", server.uri()),
324 );
325 let env = MapEnv::new().with(DOCS_API_URL, &server.uri());
326 DocsClient::from_drive_client_with(&env, &drive).unwrap()
327 }
328
329 fn opts(tab: Option<&str>) -> ReadOptions {
330 ReadOptions {
331 document_id: "d1".to_string(),
332 tab: tab.map(str::to_string),
333 suggestions: SuggestionsViewMode::default(),
334 }
335 }
336
337 fn paragraph(start: i64, end: i64, text: &str) -> serde_json::Value {
338 serde_json::json!({
339 "startIndex": start, "endIndex": end,
340 "paragraph": {
341 "elements": [{"textRun": {"content": text}}],
342 "paragraphStyle": {"namedStyleType": "NORMAL_TEXT"},
343 },
344 })
345 }
346
347 fn mount_document(body: serde_json::Value) -> Mock {
348 Mock::given(method("GET"))
349 .and(path("/v1/documents/d1"))
350 .respond_with(ResponseTemplate::new(200).set_body_json(body))
351 }
352
353 #[tokio::test]
354 async fn read_calls_documents_get_exactly_once() {
355 let server = MockServer::start().await;
356 let client = docs_client(&server).await;
357 mount_document(serde_json::json!({
358 "documentId": "d1", "title": "Doc", "revisionId": "rev-1",
359 "body": {"content": [paragraph(1, 10, "hello\n")]},
360 }))
361 .expect(1)
362 .mount(&server)
363 .await;
364
365 let outcome = read(&DocsApi::new(&client), &opts(None)).await.unwrap();
366 assert_eq!(outcome.tabs.len(), 1);
367 assert_eq!(outcome.tabs[0].elements[0].text, "hello");
368 }
369
370 #[tokio::test]
373 async fn read_carries_the_revision_id_through() {
374 let server = MockServer::start().await;
375 let client = docs_client(&server).await;
376 mount_document(serde_json::json!({
377 "documentId": "d1", "revisionId": "rev-abc",
378 "body": {"content": []},
379 }))
380 .mount(&server)
381 .await;
382
383 let outcome = read(&DocsApi::new(&client), &opts(None)).await.unwrap();
384 assert_eq!(outcome.revision_id.as_deref(), Some("rev-abc"));
385 }
386
387 #[tokio::test]
389 async fn read_without_edit_access_has_no_revision_id() {
390 let server = MockServer::start().await;
391 let client = docs_client(&server).await;
392 mount_document(serde_json::json!({"documentId": "d1", "body": {"content": []}}))
393 .mount(&server)
394 .await;
395
396 let outcome = read(&DocsApi::new(&client), &opts(None)).await.unwrap();
397 assert_eq!(outcome.revision_id, None);
398 }
399
400 #[tokio::test]
401 async fn read_of_a_tabbed_document_returns_every_tab_in_order() {
402 let server = MockServer::start().await;
403 let client = docs_client(&server).await;
404 mount_document(serde_json::json!({
405 "documentId": "d1", "revisionId": "rev-1",
406 "tabs": [
407 {
408 "tabProperties": {"tabId": "t.0", "title": "One", "nestingLevel": 0},
409 "documentTab": {"body": {"content": [paragraph(1, 5, "a\n")]}},
410 },
411 {
412 "tabProperties": {"tabId": "t.1", "title": "Two", "nestingLevel": 0},
413 "documentTab": {"body": {"content": [paragraph(1, 5, "b\n")]}},
414 },
415 ],
416 }))
417 .mount(&server)
418 .await;
419
420 let outcome = read(&DocsApi::new(&client), &opts(None)).await.unwrap();
421 let ids: Vec<_> = outcome
422 .tabs
423 .iter()
424 .map(|t| t.tab_id.as_deref().unwrap())
425 .collect();
426 assert_eq!(ids, vec!["t.0", "t.1"]);
427 assert_eq!(outcome.tabs[1].elements[0].text, "b");
428 }
429
430 #[tokio::test]
431 async fn read_of_a_legacy_body_document_returns_one_anonymous_tab() {
432 let server = MockServer::start().await;
433 let client = docs_client(&server).await;
434 mount_document(serde_json::json!({
435 "documentId": "d1",
436 "body": {"content": [{"endIndex": 1, "sectionBreak": {}}]},
437 }))
438 .mount(&server)
439 .await;
440
441 let outcome = read(&DocsApi::new(&client), &opts(None)).await.unwrap();
442 assert_eq!(outcome.tabs.len(), 1);
443 assert_eq!(outcome.tabs[0].tab_id, None);
444 assert_eq!(outcome.tabs[0].elements[0].kind, ElementKind::SectionBreak);
445 }
446
447 #[tokio::test]
448 async fn read_filters_to_a_single_tab() {
449 let server = MockServer::start().await;
450 let client = docs_client(&server).await;
451 mount_document(serde_json::json!({
452 "documentId": "d1",
453 "tabs": [
454 {
455 "tabProperties": {"tabId": "t.0", "title": "One"},
456 "documentTab": {"body": {"content": [paragraph(1, 5, "a\n")]}},
457 },
458 {
459 "tabProperties": {"tabId": "t.1", "title": "Two"},
460 "documentTab": {"body": {"content": [paragraph(1, 5, "b\n")]}},
461 },
462 ],
463 }))
464 .mount(&server)
465 .await;
466
467 let outcome = read(&DocsApi::new(&client), &opts(Some("t.1")))
468 .await
469 .unwrap();
470 assert_eq!(outcome.tabs.len(), 1);
471 assert_eq!(outcome.tabs[0].elements[0].text, "b");
472 }
473
474 #[tokio::test]
476 async fn read_with_an_unknown_tab_id_errors_and_names_the_real_ids() {
477 let server = MockServer::start().await;
478 let client = docs_client(&server).await;
479 mount_document(serde_json::json!({
480 "documentId": "d1",
481 "tabs": [
482 {"tabProperties": {"tabId": "t.0"}, "documentTab": {"body": {"content": []}}},
483 {"tabProperties": {"tabId": "t.1"}, "documentTab": {"body": {"content": []}}},
484 ],
485 }))
486 .mount(&server)
487 .await;
488
489 let err = read(&DocsApi::new(&client), &opts(Some("nope")))
490 .await
491 .unwrap_err()
492 .to_string();
493 assert!(err.contains("no tab 'nope'"), "{err}");
494 assert!(err.contains("t.0"), "{err}");
495 assert!(err.contains("t.1"), "{err}");
496 }
497
498 #[tokio::test]
501 async fn read_with_a_tab_filter_on_a_legacy_document_explains_itself() {
502 let server = MockServer::start().await;
503 let client = docs_client(&server).await;
504 mount_document(serde_json::json!({"documentId": "d1", "body": {"content": []}}))
505 .mount(&server)
506 .await;
507
508 let err = read(&DocsApi::new(&client), &opts(Some("t.0")))
509 .await
510 .unwrap_err()
511 .to_string();
512 assert!(err.contains("no tabs"), "{err}");
513 }
514
515 #[tokio::test]
516 async fn read_surfaces_an_api_error_with_its_status_and_message() {
517 let server = MockServer::start().await;
518 let client = docs_client(&server).await;
519 Mock::given(method("GET"))
520 .and(path("/v1/documents/d1"))
521 .respond_with(ResponseTemplate::new(404).set_body_json(serde_json::json!({
522 "error": {"code": 404, "message": "Requested entity was not found.",
523 "status": "NOT_FOUND"},
524 })))
525 .mount(&server)
526 .await;
527
528 let err = read(&DocsApi::new(&client), &opts(None))
529 .await
530 .unwrap_err()
531 .to_string();
532 assert!(err.contains("Docs API request failed"), "{err}");
533 assert!(err.contains("404"), "{err}");
534 assert!(err.contains("Requested entity was not found."), "{err}");
535 }
536
537 #[tokio::test]
538 async fn read_honours_the_suggestions_view_mode() {
539 let server = MockServer::start().await;
540 let client = docs_client(&server).await;
541 Mock::given(method("GET"))
542 .and(path("/v1/documents/d1"))
543 .and(query_param(
544 "suggestionsViewMode",
545 "PREVIEW_SUGGESTIONS_ACCEPTED",
546 ))
547 .and(query_param("includeTabsContent", "true"))
548 .respond_with(
549 ResponseTemplate::new(200).set_body_json(
550 serde_json::json!({"documentId": "d1", "body": {"content": []}}),
551 ),
552 )
553 .expect(1)
554 .mount(&server)
555 .await;
556
557 let options = ReadOptions {
558 suggestions: SuggestionsViewMode::PreviewAccepted,
559 ..opts(None)
560 };
561 read(&DocsApi::new(&client), &options).await.unwrap();
562 }
563
564 #[tokio::test]
565 async fn read_outcome_serialises_tabs_as_an_ordered_list() {
566 let server = MockServer::start().await;
567 let client = docs_client(&server).await;
568 mount_document(serde_json::json!({
569 "documentId": "d1", "title": "T", "revisionId": "rev-1",
570 "tabs": [
571 {"tabProperties": {"tabId": "t.0"}, "documentTab": {"body": {"content": []}}},
572 {"tabProperties": {"tabId": "t.1"}, "documentTab": {"body": {"content": []}}},
573 ],
574 }))
575 .mount(&server)
576 .await;
577
578 let outcome = read(&DocsApi::new(&client), &opts(None)).await.unwrap();
579 let json = serde_json::to_value(&outcome).unwrap();
580 assert!(json["tabs"].is_array());
583 assert_eq!(json["tabs"][0]["tab_id"], "t.0");
584 assert_eq!(json["revision_id"], "rev-1");
585 }
586
587 #[tokio::test]
588 async fn read_outcome_omits_absent_optional_fields() {
589 let server = MockServer::start().await;
590 let client = docs_client(&server).await;
591 mount_document(serde_json::json!({"documentId": "d1", "body": {"content": []}}))
592 .mount(&server)
593 .await;
594
595 let outcome = read(&DocsApi::new(&client), &opts(None)).await.unwrap();
596 let json = serde_json::to_value(&outcome).unwrap();
597 assert!(json.get("title").is_none());
598 assert!(json.get("revision_id").is_none());
599 assert!(json["tabs"][0].get("tab_id").is_none());
600 }
601
602 async fn two_tab_outcome(server: &MockServer) -> ReadOutcome {
605 let client = docs_client(server).await;
606 mount_document(serde_json::json!({
607 "documentId": "d1",
608 "revisionId": "rev-1",
609 "tabs": [
610 {
611 "tabProperties": {"tabId": "t.0"},
612 "documentTab": {"body": {"content": [
613 paragraph(1, 5, "a\n"), paragraph(5, 9, "b\n")]}},
614 },
615 {
616 "tabProperties": {"tabId": "t.1"},
617 "documentTab": {"body": {"content": [paragraph(1, 5, "c\n")]}},
618 },
619 ],
620 }))
621 .mount(server)
622 .await;
623
624 read(&DocsApi::new(&client), &opts(None)).await.unwrap()
625 }
626
627 fn render_jsonl(outcome: &ReadOutcome) -> String {
635 let mut buf = Vec::new();
636 outcome.write_jsonl(&mut buf).unwrap();
637 String::from_utf8(buf).unwrap()
638 }
639
640 #[tokio::test]
642 async fn jsonl_emits_one_line_per_element_across_every_tab() {
643 let server = MockServer::start().await;
644 let outcome = two_tab_outcome(&server).await;
645
646 let rendered = render_jsonl(&outcome);
647 let lines: Vec<&str> = rendered.lines().collect();
648 assert_eq!(lines.len(), 3);
649
650 let texts: Vec<String> = lines
651 .iter()
652 .map(|line| {
653 serde_json::from_str::<serde_json::Value>(line).unwrap()["text"].to_string()
654 })
655 .collect();
656 assert_eq!(texts, vec!["\"a\"", "\"b\"", "\"c\""]);
657 }
658
659 #[tokio::test]
662 async fn jsonl_denormalises_document_and_tab_identity_onto_every_line() {
663 let server = MockServer::start().await;
664 let outcome = two_tab_outcome(&server).await;
665 let rendered = render_jsonl(&outcome);
666
667 let lines: Vec<serde_json::Value> = rendered
668 .lines()
669 .map(|line| serde_json::from_str(line).unwrap())
670 .collect();
671
672 for line in &lines {
673 assert_eq!(line["document_id"], "d1");
674 assert_eq!(line["revision_id"], "rev-1");
675 }
676 assert_eq!(lines[0]["tab_id"], "t.0");
677 assert_eq!(lines[1]["tab_id"], "t.0");
678 assert_eq!(lines[2]["tab_id"], "t.1");
679 }
680
681 #[tokio::test]
685 async fn jsonl_flattens_element_fields_to_the_top_level() {
686 let server = MockServer::start().await;
687 let outcome = two_tab_outcome(&server).await;
688 let rendered = render_jsonl(&outcome);
689
690 let first: serde_json::Value =
691 serde_json::from_str(rendered.lines().next().unwrap()).unwrap();
692 assert_eq!(first["text"], "a");
693 assert_eq!(first["kind"], "paragraph");
694 assert_eq!(first["start_index"], 1);
695 assert_eq!(first["end_index"], 5);
696 assert!(first.get("element").is_none());
697 }
698
699 #[tokio::test]
702 async fn jsonl_omits_revision_and_tab_when_absent() {
703 let server = MockServer::start().await;
704 let client = docs_client(&server).await;
705 mount_document(serde_json::json!({
706 "documentId": "d1",
707 "body": {"content": [paragraph(1, 5, "a\n")]},
708 }))
709 .mount(&server)
710 .await;
711
712 let outcome = read(&DocsApi::new(&client), &opts(None)).await.unwrap();
713 let rendered = render_jsonl(&outcome);
714 let line: serde_json::Value =
715 serde_json::from_str(rendered.lines().next().unwrap()).unwrap();
716
717 assert_eq!(line["document_id"], "d1");
718 assert!(line.get("revision_id").is_none());
719 assert!(line.get("tab_id").is_none());
720 }
721
722 #[tokio::test]
725 async fn jsonl_emits_nothing_for_a_document_with_no_elements() {
726 let server = MockServer::start().await;
727 let client = docs_client(&server).await;
728 mount_document(serde_json::json!({"documentId": "d1", "body": {"content": []}}))
729 .mount(&server)
730 .await;
731
732 let outcome = read(&DocsApi::new(&client), &opts(None)).await.unwrap();
733 assert_eq!(render_jsonl(&outcome), "");
734 }
735
736 #[tokio::test]
739 async fn read_populates_headers_footers_and_footnotes() {
740 let server = MockServer::start().await;
741 let client = docs_client(&server).await;
742 mount_document(serde_json::json!({
743 "documentId": "d1",
744 "body": {"content": [paragraph(1, 5, "body\n")]},
745 "headers": {"h1": {"content": [paragraph(0, 8, "Header\n")]}},
746 "footers": {"f1": {"content": [paragraph(0, 8, "Footer\n")]}},
747 "footnotes": {"n1": {"content": [paragraph(0, 5, "Note\n")]}},
748 }))
749 .mount(&server)
750 .await;
751
752 let outcome = read(&DocsApi::new(&client), &opts(None)).await.unwrap();
753 let tab = &outcome.tabs[0];
754 assert_eq!(tab.headers.len(), 1);
755 assert_eq!(tab.headers[0].segment_id, "h1");
756 assert_eq!(tab.headers[0].elements[0].text, "Header");
757 assert_eq!(tab.footers[0].elements[0].text, "Footer");
758 assert_eq!(tab.footnotes[0].elements[0].text, "Note");
759 }
760
761 #[tokio::test]
764 async fn a_document_with_no_segments_serialises_without_the_new_fields() {
765 let server = MockServer::start().await;
766 let client = docs_client(&server).await;
767 mount_document(serde_json::json!({
768 "documentId": "d1",
769 "body": {"content": [paragraph(1, 5, "a\n")]},
770 }))
771 .mount(&server)
772 .await;
773
774 let outcome = read(&DocsApi::new(&client), &opts(None)).await.unwrap();
775 let json = serde_json::to_value(&outcome).unwrap();
776 let tab = &json["tabs"][0];
777 assert!(tab.get("headers").is_none());
778 assert!(tab.get("footers").is_none());
779 assert!(tab.get("footnotes").is_none());
780 }
781
782 #[tokio::test]
785 async fn jsonl_carries_segment_identity_for_segment_elements() {
786 let server = MockServer::start().await;
787 let client = docs_client(&server).await;
788 mount_document(serde_json::json!({
789 "documentId": "d1",
790 "body": {"content": [paragraph(1, 5, "body\n")]},
791 "footnotes": {"n1": {"content": [paragraph(0, 5, "Note\n")]}},
792 }))
793 .mount(&server)
794 .await;
795
796 let outcome = read(&DocsApi::new(&client), &opts(None)).await.unwrap();
797 let rendered = render_jsonl(&outcome);
798 let lines: Vec<serde_json::Value> = rendered
799 .lines()
800 .map(|line| serde_json::from_str(line).unwrap())
801 .collect();
802
803 assert_eq!(lines.len(), 2);
804 assert!(lines[0].get("segment").is_none());
805 assert_eq!(lines[1]["segment"]["kind"], "footnote");
806 assert_eq!(lines[1]["segment"]["segment_id"], "n1");
807 assert_eq!(lines[1]["text"], "Note");
808 }
809}