Skip to main content

cdx_core/extensions/
legal.rs

1//! Legal extension for Codex documents.
2//!
3//! This extension provides specialized content types for legal documents
4//! including citations, tables of authorities, and court captions.
5//!
6//! # Features
7//!
8//! - **`TableOfAuthorities`**: Auto-generated citation index
9//! - **`Caption`**: Court caption block
10//! - **`SignatureBlock`**: Legal signature block format
11//! - **`LegalCitation`**: Legal citation marks (Bluebook, ALWD, etc.)
12//!
13//! # Example
14//!
15//! ```json
16//! {
17//!   "type": "legal:cite",
18//!   "citation": "Brown v. Board of Education",
19//!   "cite": "347 U.S. 483",
20//!   "year": 1954,
21//!   "category": "case"
22//! }
23//! ```
24
25use serde::{Deserialize, Serialize};
26
27// ============================================================================
28// Table of Authorities
29// ============================================================================
30
31/// A table of authorities listing all legal citations in the document.
32#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
33#[serde(rename_all = "camelCase")]
34pub struct TableOfAuthorities {
35    /// Optional unique identifier.
36    #[serde(default, skip_serializing_if = "Option::is_none")]
37    pub id: Option<String>,
38
39    /// Title for the table (defaults to "TABLE OF AUTHORITIES").
40    #[serde(default, skip_serializing_if = "Option::is_none")]
41    pub title: Option<String>,
42
43    /// Citation categories to include.
44    #[serde(default, skip_serializing_if = "Vec::is_empty")]
45    pub categories: Vec<CitationCategory>,
46
47    /// Whether to generate automatically from document citations.
48    #[serde(default = "default_true")]
49    pub auto_generate: bool,
50
51    /// Citation format to use.
52    #[serde(default)]
53    pub format: LegalCitationFormat,
54}
55
56fn default_true() -> bool {
57    true
58}
59
60impl TableOfAuthorities {
61    /// Create a new table of authorities.
62    #[must_use]
63    pub fn new() -> Self {
64        Self {
65            id: None,
66            title: None,
67            categories: Vec::new(),
68            auto_generate: true,
69            format: LegalCitationFormat::default(),
70        }
71    }
72
73    /// Set the title.
74    #[must_use]
75    pub fn with_title(mut self, title: impl Into<String>) -> Self {
76        self.title = Some(title.into());
77        self
78    }
79
80    /// Add a category.
81    #[must_use]
82    pub fn with_category(mut self, category: CitationCategory) -> Self {
83        self.categories.push(category);
84        self
85    }
86
87    /// Set the citation format.
88    #[must_use]
89    pub fn with_format(mut self, format: LegalCitationFormat) -> Self {
90        self.format = format;
91        self
92    }
93}
94
95impl Default for TableOfAuthorities {
96    fn default() -> Self {
97        Self::new()
98    }
99}
100
101/// A category in the table of authorities.
102#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
103#[serde(rename_all = "camelCase")]
104pub struct CitationCategory {
105    /// Category type.
106    pub category_type: LegalCitationType,
107
108    /// Custom heading (overrides default).
109    #[serde(default, skip_serializing_if = "Option::is_none")]
110    pub heading: Option<String>,
111
112    /// Entries in this category (populated during generation).
113    #[serde(default, skip_serializing_if = "Vec::is_empty")]
114    pub entries: Vec<TableOfAuthoritiesEntry>,
115}
116
117/// An entry in the table of authorities.
118#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
119#[serde(rename_all = "camelCase")]
120pub struct TableOfAuthoritiesEntry {
121    /// Full citation text.
122    pub citation: String,
123
124    /// Page numbers where cited.
125    pub pages: Vec<String>,
126
127    /// Whether this is a primary authority.
128    #[serde(default)]
129    pub primary: bool,
130}
131
132// ============================================================================
133// Court Caption
134// ============================================================================
135
136/// A court caption block for legal documents.
137#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
138#[serde(rename_all = "camelCase")]
139pub struct Caption {
140    /// Optional unique identifier.
141    #[serde(default, skip_serializing_if = "Option::is_none")]
142    pub id: Option<String>,
143
144    /// Court name.
145    pub court: String,
146
147    /// Case number/docket number.
148    #[serde(default, skip_serializing_if = "Option::is_none")]
149    pub case_number: Option<String>,
150
151    /// Docket identifier.
152    #[serde(default, skip_serializing_if = "Option::is_none")]
153    pub docket: Option<String>,
154
155    /// Plaintiffs/appellants.
156    pub plaintiffs: Vec<Party>,
157
158    /// Defendants/appellees.
159    pub defendants: Vec<Party>,
160
161    /// Case title override (if different from generated).
162    #[serde(default, skip_serializing_if = "Option::is_none")]
163    pub title: Option<String>,
164
165    /// Document type (Brief, Motion, etc.).
166    #[serde(default, skip_serializing_if = "Option::is_none")]
167    pub document_type: Option<String>,
168
169    /// Judge name.
170    #[serde(default, skip_serializing_if = "Option::is_none")]
171    pub judge: Option<String>,
172
173    /// Caption style.
174    #[serde(default)]
175    pub style: CaptionStyle,
176}
177
178impl Caption {
179    /// Create a new caption.
180    #[must_use]
181    pub fn new(court: impl Into<String>) -> Self {
182        Self {
183            id: None,
184            court: court.into(),
185            case_number: None,
186            docket: None,
187            plaintiffs: Vec::new(),
188            defendants: Vec::new(),
189            title: None,
190            document_type: None,
191            judge: None,
192            style: CaptionStyle::default(),
193        }
194    }
195
196    /// Set the case number.
197    #[must_use]
198    pub fn with_case_number(mut self, case_number: impl Into<String>) -> Self {
199        self.case_number = Some(case_number.into());
200        self
201    }
202
203    /// Add a plaintiff.
204    #[must_use]
205    pub fn with_plaintiff(mut self, party: Party) -> Self {
206        self.plaintiffs.push(party);
207        self
208    }
209
210    /// Add a defendant.
211    #[must_use]
212    pub fn with_defendant(mut self, party: Party) -> Self {
213        self.defendants.push(party);
214        self
215    }
216
217    /// Set the document type.
218    #[must_use]
219    pub fn with_document_type(mut self, doc_type: impl Into<String>) -> Self {
220        self.document_type = Some(doc_type.into());
221        self
222    }
223}
224
225/// A party in a legal case.
226#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
227#[serde(rename_all = "camelCase")]
228pub struct Party {
229    /// Party name.
230    pub name: String,
231
232    /// Party role (e.g., "Appellant", "Defendant-Appellee").
233    #[serde(default, skip_serializing_if = "Option::is_none")]
234    pub role: Option<String>,
235
236    /// Whether this is the primary party (for "et al." shortening).
237    #[serde(default)]
238    pub primary: bool,
239}
240
241impl Party {
242    /// Create a new party.
243    #[must_use]
244    pub fn new(name: impl Into<String>) -> Self {
245        Self {
246            name: name.into(),
247            role: None,
248            primary: false,
249        }
250    }
251
252    /// Set the role.
253    #[must_use]
254    pub fn with_role(mut self, role: impl Into<String>) -> Self {
255        self.role = Some(role.into());
256        self
257    }
258
259    /// Mark as primary party.
260    #[must_use]
261    pub fn primary(mut self) -> Self {
262        self.primary = true;
263        self
264    }
265}
266
267/// Caption style format.
268#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
269#[serde(rename_all = "lowercase")]
270pub enum CaptionStyle {
271    /// Standard federal style.
272    #[default]
273    Federal,
274    /// California style.
275    California,
276    /// New York style.
277    NewYork,
278    /// Texas style.
279    Texas,
280}
281
282// ============================================================================
283// Legal Signature Block
284// ============================================================================
285
286/// A legal signature block.
287#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
288#[serde(rename_all = "camelCase")]
289pub struct LegalSignatureBlock {
290    /// Optional unique identifier.
291    #[serde(default, skip_serializing_if = "Option::is_none")]
292    pub id: Option<String>,
293
294    /// Role of the signer (e.g., "Attorney for Plaintiff").
295    #[serde(default, skip_serializing_if = "Option::is_none")]
296    pub role: Option<String>,
297
298    /// Signer information.
299    pub signer: LegalSigner,
300
301    /// Date signed.
302    #[serde(default, skip_serializing_if = "Option::is_none")]
303    pub date: Option<String>,
304
305    /// Certificate of service included.
306    #[serde(default)]
307    pub certificate_of_service: bool,
308}
309
310impl LegalSignatureBlock {
311    /// Create a new legal signature block.
312    #[must_use]
313    pub fn new(signer: LegalSigner) -> Self {
314        Self {
315            id: None,
316            role: None,
317            signer,
318            date: None,
319            certificate_of_service: false,
320        }
321    }
322
323    /// Set the role.
324    #[must_use]
325    pub fn with_role(mut self, role: impl Into<String>) -> Self {
326        self.role = Some(role.into());
327        self
328    }
329
330    /// Set the date.
331    #[must_use]
332    pub fn with_date(mut self, date: impl Into<String>) -> Self {
333        self.date = Some(date.into());
334        self
335    }
336
337    /// Include certificate of service.
338    #[must_use]
339    pub fn with_certificate_of_service(mut self) -> Self {
340        self.certificate_of_service = true;
341        self
342    }
343}
344
345/// A legal signer with flattened personal and firm information.
346#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
347#[serde(rename_all = "camelCase")]
348pub struct LegalSigner {
349    /// Name of the signer.
350    pub name: String,
351
352    /// Title/position.
353    #[serde(default, skip_serializing_if = "Option::is_none")]
354    pub title: Option<String>,
355
356    /// Bar number.
357    #[serde(default, skip_serializing_if = "Option::is_none")]
358    pub bar_number: Option<String>,
359
360    /// Firm or organization name.
361    #[serde(default, skip_serializing_if = "Option::is_none")]
362    pub firm: Option<String>,
363
364    /// Address (single string, replaces `Vec<String>`).
365    #[serde(default, skip_serializing_if = "Option::is_none")]
366    pub address: Option<String>,
367
368    /// Telephone number.
369    #[serde(default, skip_serializing_if = "Option::is_none")]
370    pub telephone: Option<String>,
371}
372
373impl LegalSigner {
374    /// Create a new legal signer.
375    #[must_use]
376    pub fn new(name: impl Into<String>) -> Self {
377        Self {
378            name: name.into(),
379            title: None,
380            bar_number: None,
381            firm: None,
382            address: None,
383            telephone: None,
384        }
385    }
386
387    /// Set the title.
388    #[must_use]
389    pub fn with_title(mut self, title: impl Into<String>) -> Self {
390        self.title = Some(title.into());
391        self
392    }
393
394    /// Set the bar number.
395    #[must_use]
396    pub fn with_bar_number(mut self, bar_number: impl Into<String>) -> Self {
397        self.bar_number = Some(bar_number.into());
398        self
399    }
400
401    /// Set the firm name.
402    #[must_use]
403    pub fn with_firm(mut self, firm: impl Into<String>) -> Self {
404        self.firm = Some(firm.into());
405        self
406    }
407
408    /// Set the address.
409    #[must_use]
410    pub fn with_address(mut self, address: impl Into<String>) -> Self {
411        self.address = Some(address.into());
412        self
413    }
414
415    /// Set the telephone number.
416    #[must_use]
417    pub fn with_telephone(mut self, telephone: impl Into<String>) -> Self {
418        self.telephone = Some(telephone.into());
419        self
420    }
421}
422
423// ============================================================================
424// Legal Citations
425// ============================================================================
426
427/// A legal citation (for inline marks).
428#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
429#[serde(rename_all = "camelCase")]
430pub struct LegalCitation {
431    /// Full citation text.
432    pub citation: String,
433
434    /// Short form for subsequent references.
435    #[serde(default, skip_serializing_if = "Option::is_none")]
436    pub short_form: Option<String>,
437
438    /// Reporter citation (e.g., "347 U.S. 483").
439    #[serde(default, skip_serializing_if = "Option::is_none")]
440    pub cite: Option<String>,
441
442    /// Year of decision.
443    #[serde(default, skip_serializing_if = "Option::is_none")]
444    pub year: Option<u16>,
445
446    /// Citation category.
447    pub category: LegalCitationType,
448
449    /// Pinpoint reference (page, paragraph, section).
450    #[serde(default, skip_serializing_if = "Option::is_none")]
451    pub pinpoint: Option<Pinpoint>,
452
453    /// Parenthetical explanation.
454    #[serde(default, skip_serializing_if = "Option::is_none")]
455    pub parenthetical: Option<String>,
456
457    /// Signal (e.g., "See", "Cf.", "But see").
458    #[serde(default, skip_serializing_if = "Option::is_none")]
459    pub signal: Option<CitationSignal>,
460
461    /// Whether this is the first reference.
462    #[serde(default = "default_true")]
463    pub first_reference: bool,
464}
465
466impl LegalCitation {
467    /// Create a new case citation.
468    #[must_use]
469    pub fn case(citation: impl Into<String>, cite: impl Into<String>, year: u16) -> Self {
470        Self {
471            citation: citation.into(),
472            short_form: None,
473            cite: Some(cite.into()),
474            year: Some(year),
475            category: LegalCitationType::Case,
476            pinpoint: None,
477            parenthetical: None,
478            signal: None,
479            first_reference: true,
480        }
481    }
482
483    /// Create a new statute citation.
484    #[must_use]
485    pub fn statute(citation: impl Into<String>) -> Self {
486        Self {
487            citation: citation.into(),
488            short_form: None,
489            cite: None,
490            year: None,
491            category: LegalCitationType::Statute,
492            pinpoint: None,
493            parenthetical: None,
494            signal: None,
495            first_reference: true,
496        }
497    }
498
499    /// Set short form.
500    #[must_use]
501    pub fn with_short_form(mut self, short_form: impl Into<String>) -> Self {
502        self.short_form = Some(short_form.into());
503        self
504    }
505
506    /// Set pinpoint reference.
507    #[must_use]
508    pub fn at(mut self, pinpoint: Pinpoint) -> Self {
509        self.pinpoint = Some(pinpoint);
510        self
511    }
512
513    /// Set parenthetical.
514    #[must_use]
515    pub fn with_parenthetical(mut self, text: impl Into<String>) -> Self {
516        self.parenthetical = Some(text.into());
517        self
518    }
519
520    /// Set signal.
521    #[must_use]
522    pub fn with_signal(mut self, signal: CitationSignal) -> Self {
523        self.signal = Some(signal);
524        self
525    }
526
527    /// Mark as subsequent reference (not first).
528    #[must_use]
529    pub fn subsequent(mut self) -> Self {
530        self.first_reference = false;
531        self
532    }
533
534    /// Convert to an extension mark for use in inline text.
535    #[must_use]
536    pub fn to_extension_mark(&self) -> crate::content::ExtensionMark {
537        let attrs = serde_json::to_value(self).unwrap_or_default();
538        crate::content::ExtensionMark::new("legal", "cite").with_attributes(attrs)
539    }
540
541    /// Try to create a `LegalCitation` from an extension mark.
542    ///
543    /// Returns `None` if the mark is not a `legal:cite` type or if
544    /// the attributes cannot be deserialized.
545    #[must_use]
546    pub fn from_extension_mark(mark: &crate::content::ExtensionMark) -> Option<Self> {
547        if mark.is_type("legal", "cite") {
548            serde_json::from_value(mark.attributes.clone()).ok()
549        } else {
550            None
551        }
552    }
553}
554
555/// Type of legal citation.
556#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, strum::Display)]
557#[serde(rename_all = "lowercase")]
558pub enum LegalCitationType {
559    /// Court case.
560    #[strum(serialize = "Cases")]
561    Case,
562    /// Statute or legislation.
563    #[strum(serialize = "Statutes")]
564    Statute,
565    /// Regulation.
566    #[strum(serialize = "Regulations")]
567    Regulation,
568    /// Constitutional provision.
569    #[strum(serialize = "Constitutional Provisions")]
570    Constitution,
571    /// Secondary source (treatise, law review, etc.).
572    #[strum(serialize = "Secondary Sources")]
573    Secondary,
574    /// Book or treatise.
575    #[strum(serialize = "Books")]
576    Book,
577    /// Law review article.
578    #[strum(serialize = "Law Review Articles")]
579    LawReview,
580    /// Legislative history.
581    #[strum(serialize = "Legislative History")]
582    Legislative,
583    /// Other authority.
584    #[strum(serialize = "Other Authorities")]
585    Other,
586}
587
588/// Pinpoint reference within a citation.
589#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
590#[serde(rename_all = "camelCase")]
591pub struct Pinpoint {
592    /// Type of pinpoint.
593    pub pinpoint_type: PinpointType,
594
595    /// Value (page number, section, etc.).
596    pub value: String,
597}
598
599impl Pinpoint {
600    /// Page pinpoint.
601    #[must_use]
602    pub fn page(page: impl Into<String>) -> Self {
603        Self {
604            pinpoint_type: PinpointType::Page,
605            value: page.into(),
606        }
607    }
608
609    /// Section pinpoint.
610    #[must_use]
611    pub fn section(section: impl Into<String>) -> Self {
612        Self {
613            pinpoint_type: PinpointType::Section,
614            value: section.into(),
615        }
616    }
617
618    /// Paragraph pinpoint.
619    #[must_use]
620    pub fn paragraph(para: impl Into<String>) -> Self {
621        Self {
622            pinpoint_type: PinpointType::Paragraph,
623            value: para.into(),
624        }
625    }
626}
627
628/// Type of pinpoint reference.
629#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
630#[serde(rename_all = "lowercase")]
631pub enum PinpointType {
632    /// Page number.
633    Page,
634    /// Section.
635    Section,
636    /// Paragraph.
637    Paragraph,
638    /// Footnote.
639    Footnote,
640    /// Clause.
641    Clause,
642    /// Article.
643    Article,
644}
645
646/// Citation signal indicating how authority supports proposition.
647#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, strum::Display)]
648#[serde(rename_all = "lowercase")]
649pub enum CitationSignal {
650    /// No signal (direct support).
651    #[strum(serialize = "")]
652    None,
653    /// E.g.,
654    #[strum(serialize = "E.g.")]
655    Eg,
656    /// Accord
657    Accord,
658    /// See
659    See,
660    /// See also
661    #[strum(serialize = "See also")]
662    SeeAlso,
663    /// Cf.
664    #[strum(serialize = "Cf.")]
665    Cf,
666    /// Compare
667    Compare,
668    /// Contra
669    Contra,
670    /// But see
671    #[strum(serialize = "But see")]
672    ButSee,
673    /// But cf.
674    #[strum(serialize = "But cf.")]
675    ButCf,
676    /// See generally
677    #[strum(serialize = "See generally")]
678    SeeGenerally,
679}
680
681/// Legal citation format.
682#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize, strum::Display)]
683#[serde(rename_all = "lowercase")]
684pub enum LegalCitationFormat {
685    /// Bluebook format (US).
686    #[default]
687    Bluebook,
688    /// ALWD Citation Manual.
689    #[strum(serialize = "ALWD")]
690    Alwd,
691    /// `McGill` Guide (Canada).
692    McGill,
693    /// OSCOLA (UK).
694    #[strum(serialize = "OSCOLA")]
695    Oscola,
696}
697
698#[cfg(test)]
699mod tests {
700    use super::*;
701
702    #[test]
703    fn test_caption_new() {
704        let caption = Caption::new("United States District Court, Southern District of New York");
705        assert_eq!(
706            caption.court,
707            "United States District Court, Southern District of New York"
708        );
709        assert!(caption.plaintiffs.is_empty());
710        assert!(caption.defendants.is_empty());
711    }
712
713    #[test]
714    fn test_caption_builder() {
715        let caption = Caption::new("Supreme Court of the United States")
716            .with_case_number("No. 21-1234")
717            .with_plaintiff(Party::new("John Doe").primary())
718            .with_defendant(Party::new("Acme Corp"))
719            .with_document_type("Brief for Petitioner");
720
721        assert_eq!(caption.case_number, Some("No. 21-1234".to_string()));
722        assert_eq!(caption.plaintiffs.len(), 1);
723        assert!(caption.plaintiffs[0].primary);
724        assert_eq!(caption.defendants.len(), 1);
725    }
726
727    #[test]
728    fn test_legal_citation_case() {
729        let cite = LegalCitation::case("Brown v. Board of Education", "347 U.S. 483", 1954);
730
731        assert_eq!(cite.category, LegalCitationType::Case);
732        assert_eq!(cite.year, Some(1954));
733        assert_eq!(cite.cite, Some("347 U.S. 483".to_string()));
734    }
735
736    #[test]
737    fn test_legal_citation_builder() {
738        let cite = LegalCitation::case("Miranda v. Arizona", "384 U.S. 436", 1966)
739            .with_short_form("Miranda")
740            .at(Pinpoint::page("444"))
741            .with_parenthetical("establishing Miranda warnings");
742
743        assert_eq!(cite.short_form, Some("Miranda".to_string()));
744        assert!(cite.pinpoint.is_some());
745        assert!(cite.parenthetical.is_some());
746    }
747
748    #[test]
749    fn test_legal_signer() {
750        let signer = LegalSigner::new("Jane Doe")
751            .with_bar_number("123456")
752            .with_firm("Smith & Associates")
753            .with_address("123 Legal Way, Suite 100, New York, NY 10001")
754            .with_telephone("(555) 123-4567")
755            .with_title("Partner");
756
757        assert_eq!(signer.bar_number, Some("123456".to_string()));
758        assert_eq!(signer.firm, Some("Smith & Associates".to_string()));
759        assert_eq!(signer.telephone, Some("(555) 123-4567".to_string()));
760        assert_eq!(signer.title, Some("Partner".to_string()));
761    }
762
763    #[test]
764    fn test_legal_signer_serde_roundtrip() {
765        let signer = LegalSigner::new("John Smith")
766            .with_bar_number("789012")
767            .with_firm("Law Corp");
768        let json = serde_json::to_string(&signer).unwrap();
769        assert!(json.contains("\"barNumber\":\"789012\""));
770        assert!(json.contains("\"firm\":\"Law Corp\""));
771
772        let parsed: LegalSigner = serde_json::from_str(&json).unwrap();
773        assert_eq!(parsed.name, "John Smith");
774        assert_eq!(parsed.bar_number, Some("789012".to_string()));
775    }
776
777    #[test]
778    fn test_legal_signature_block_with_role() {
779        let block = LegalSignatureBlock::new(LegalSigner::new("Jane Doe"))
780            .with_role("Attorney for Plaintiff")
781            .with_date("2024-01-15")
782            .with_certificate_of_service();
783
784        assert_eq!(block.role, Some("Attorney for Plaintiff".to_string()));
785        assert_eq!(block.date, Some("2024-01-15".to_string()));
786        assert!(block.certificate_of_service);
787
788        let json = serde_json::to_string(&block).unwrap();
789        assert!(json.contains("\"role\":\"Attorney for Plaintiff\""));
790
791        let parsed: LegalSignatureBlock = serde_json::from_str(&json).unwrap();
792        assert_eq!(parsed.role, Some("Attorney for Plaintiff".to_string()));
793    }
794
795    #[test]
796    fn test_legal_signature_block_backward_compat() {
797        // JSON without role should deserialize fine
798        let json = r#"{
799            "signer": { "name": "Test Lawyer" },
800            "certificateOfService": false
801        }"#;
802        let block: LegalSignatureBlock = serde_json::from_str(json).unwrap();
803        assert!(block.role.is_none());
804        assert_eq!(block.signer.name, "Test Lawyer");
805    }
806
807    #[test]
808    fn test_table_of_authorities() {
809        let toa = TableOfAuthorities::new()
810            .with_title("TABLE OF AUTHORITIES")
811            .with_format(LegalCitationFormat::Bluebook);
812
813        assert!(toa.auto_generate);
814        assert_eq!(toa.format, LegalCitationFormat::Bluebook);
815    }
816
817    #[test]
818    fn test_citation_serialization() {
819        let cite = LegalCitation::statute("42 U.S.C. § 1983").at(Pinpoint::section("1983"));
820
821        let json = serde_json::to_string(&cite).unwrap();
822        assert!(json.contains("\"category\":\"statute\""));
823        assert!(json.contains("\"citation\":\"42 U.S.C. § 1983\""));
824    }
825
826    #[test]
827    fn test_citation_type_display() {
828        assert_eq!(LegalCitationType::Case.to_string(), "Cases");
829        assert_eq!(LegalCitationType::Statute.to_string(), "Statutes");
830        assert_eq!(
831            LegalCitationType::Constitution.to_string(),
832            "Constitutional Provisions"
833        );
834    }
835
836    #[test]
837    fn test_signal_display() {
838        assert_eq!(CitationSignal::See.to_string(), "See");
839        assert_eq!(CitationSignal::ButSee.to_string(), "But see");
840        assert_eq!(CitationSignal::None.to_string(), "");
841    }
842
843    #[test]
844    fn test_caption_docket_roundtrip() {
845        let caption = Caption::new("District Court").with_case_number("No. 24-1234");
846        // Manually set docket
847        let mut caption = caption;
848        caption.docket = Some("DKT-2024-5678".to_string());
849
850        let json = serde_json::to_string(&caption).unwrap();
851        assert!(json.contains("\"docket\":\"DKT-2024-5678\""));
852
853        let parsed: Caption = serde_json::from_str(&json).unwrap();
854        assert_eq!(parsed.docket, Some("DKT-2024-5678".to_string()));
855    }
856
857    #[test]
858    fn test_caption_without_docket_defaults_to_none() {
859        let json = r#"{
860            "court": "Supreme Court",
861            "plaintiffs": [],
862            "defendants": [],
863            "style": "federal"
864        }"#;
865        let caption: Caption = serde_json::from_str(json).unwrap();
866        assert!(caption.docket.is_none());
867    }
868
869    #[test]
870    fn test_legal_citation_to_extension_mark() {
871        let cite = LegalCitation::case("Brown v. Board of Education", "347 U.S. 483", 1954);
872        let mark = cite.to_extension_mark();
873
874        assert_eq!(mark.namespace, "legal");
875        assert_eq!(mark.mark_type, "cite");
876        assert_eq!(
877            mark.get_string_attribute("citation"),
878            Some("Brown v. Board of Education")
879        );
880        assert_eq!(mark.get_string_attribute("cite"), Some("347 U.S. 483"));
881    }
882
883    #[test]
884    fn test_legal_citation_mark_roundtrip() {
885        let original = LegalCitation::case("Miranda v. Arizona", "384 U.S. 436", 1966)
886            .with_signal(CitationSignal::See);
887        let mark = original.to_extension_mark();
888        let recovered = LegalCitation::from_extension_mark(&mark).unwrap();
889
890        assert_eq!(recovered.citation, "Miranda v. Arizona");
891        assert_eq!(recovered.cite, Some("384 U.S. 436".to_string()));
892        assert_eq!(recovered.year, Some(1966));
893        assert_eq!(recovered.signal, Some(CitationSignal::See));
894    }
895
896    #[test]
897    fn test_legal_citation_from_wrong_mark_returns_none() {
898        let mark = crate::content::ExtensionMark::new("academic", "equation-ref");
899        assert!(LegalCitation::from_extension_mark(&mark).is_none());
900    }
901}