Skip to main content

aozora_syntax/borrowed/
non_empty.rs

1//! Non-emptiness invariant for [`Content<'src>`].
2//!
3//! AST variants like [`Ruby`](super::Ruby) and [`Bouten`](super::Bouten)
4//! semantically require non-empty content payloads (an empty ruby base
5//! is a parse bug, not a valid state).
6//!
7//! [`NonEmpty<Content<'src>>`] makes the invariant a build-time fact:
8//! the `NonEmpty::new` constructor returns `Option`, so empty content
9//! cannot enter the AST without a deliberate `unwrap` / `expect`. The
10//! allocator (`aozora_syntax::alloc`) does the `expect` exactly once
11//! per node variant — Phase 3's classifier guarantees the input is
12//! non-empty, and an empty payload at allocation time signals a
13//! pipeline-internal bug rather than valid input.
14//!
15//! Read access is via [`Deref`] so existing
16//! consumers of [`Content`] inherent methods (`as_plain`, `iter`)
17//! work unchanged on `NonEmpty<Content>`.
18
19use core::ops::Deref;
20
21use super::types::Content;
22
23/// Non-emptiness wrapper for an AST payload.
24///
25/// Only constructable through [`Self::new`] (returns `Option`) or
26/// [`Self::new_unchecked`] (caller-asserted, used only by the
27/// allocator after Phase 3 classification has guaranteed the
28/// non-emptiness). Auto-derefs to the inner payload for read access.
29#[derive(Debug, Clone, Copy, PartialEq, Eq)]
30pub struct NonEmpty<T>(T);
31
32impl<T> NonEmpty<T> {
33    /// Construct without checking. Caller must guarantee the value is
34    /// non-empty by some other means (typically: this is a pipeline
35    /// allocator that just classified the payload as non-empty).
36    ///
37    /// Marked `#[doc(hidden)]` to discourage casual use; the typed
38    /// constructor [`Self::new`] is the supported path for outside
39    /// callers.
40    #[doc(hidden)]
41    #[must_use]
42    pub const fn new_unchecked(value: T) -> Self {
43        Self(value)
44    }
45
46    /// Consume the wrapper and return the inner payload.
47    #[must_use]
48    pub const fn into_inner(self) -> T
49    where
50        T: Copy,
51    {
52        self.0
53    }
54
55    /// Borrow the inner payload directly. Equivalent to dereferencing
56    /// through [`Deref`].
57    #[must_use]
58    pub const fn as_inner(&self) -> &T {
59        &self.0
60    }
61}
62
63impl<T> Deref for NonEmpty<T> {
64    type Target = T;
65    fn deref(&self) -> &T {
66        &self.0
67    }
68}
69
70impl<'src> NonEmpty<Content<'src>> {
71    /// Construct, returning `None` if the content is empty.
72    ///
73    /// `Plain("")` and `Segments(&[])` are both rejected; everything
74    /// else (including a single segment that happens to carry empty
75    /// text) is accepted.
76    #[must_use]
77    pub fn new(content: Content<'src>) -> Option<Self> {
78        match content {
79            Content::Plain(s) if !s.is_empty() => Some(Self(content)),
80            Content::Segments(segs) if !segs.is_empty() => Some(Self(content)),
81            _ => None,
82        }
83    }
84
85    /// Underlying [`Content`]. Convenience accessor for callers that
86    /// want a `Copy`-style move rather than a deref-borrow.
87    #[must_use]
88    pub const fn get(self) -> Content<'src> {
89        self.0
90    }
91}
92
93/// Non-empty `&'src str` newtype.
94///
95/// Used by AST fields where an empty string would never represent a
96/// valid input — e.g. [`super::Sashie::file`] (illustration path),
97/// [`super::HeadingHint::target`] (forward-reference target),
98/// [`super::Annotation::raw`] (annotation body bytes), and
99/// [`super::Kaeriten::mark`] (kanbun mark text).
100///
101/// Constructable through [`Self::new`] (returns `Option`) or
102/// [`Self::new_unchecked`] (caller-asserted). Auto-derefs to `str`
103/// for read access — `s.len()` / `s.starts_with(...)` etc. work
104/// unchanged.
105#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
106pub struct NonEmptyStr<'src>(&'src str);
107
108impl<'src> NonEmptyStr<'src> {
109    /// Construct, returning `None` if `s` is empty.
110    #[must_use]
111    pub const fn new(s: &'src str) -> Option<Self> {
112        if s.is_empty() { None } else { Some(Self(s)) }
113    }
114
115    /// Construct without checking. Caller must guarantee the string
116    /// is non-empty. Marked `#[doc(hidden)]` to discourage casual use;
117    /// the typed constructor [`Self::new`] is the supported path.
118    #[doc(hidden)]
119    #[must_use]
120    pub const fn new_unchecked(s: &'src str) -> Self {
121        Self(s)
122    }
123
124    /// Underlying `&str`. Equivalent to dereferencing through
125    /// [`Deref`].
126    #[must_use]
127    pub const fn as_str(self) -> &'src str {
128        self.0
129    }
130}
131
132impl Deref for NonEmptyStr<'_> {
133    type Target = str;
134    fn deref(&self) -> &str {
135        self.0
136    }
137}
138
139#[cfg(test)]
140mod tests {
141    use super::super::types::Segment;
142    use super::*;
143
144    #[test]
145    fn new_rejects_plain_empty_string() {
146        assert!(NonEmpty::new(Content::Plain("")).is_none());
147    }
148
149    #[test]
150    fn new_rejects_empty_segments_slice() {
151        assert!(NonEmpty::new(Content::Segments(&[])).is_none());
152    }
153
154    #[test]
155    fn new_accepts_plain_non_empty() {
156        let ne = NonEmpty::new(Content::Plain("text")).expect("non-empty");
157        assert!(matches!(ne.get(), Content::Plain("text")));
158    }
159
160    #[test]
161    fn new_accepts_non_empty_segments() {
162        static SEGS: &[Segment<'static>] = &[Segment::Text("a")];
163        let ne = NonEmpty::new(Content::Segments(SEGS)).expect("non-empty");
164        assert!(matches!(ne.get(), Content::Segments(s) if s.len() == 1));
165    }
166
167    #[test]
168    fn deref_gives_inner_methods() {
169        let ne = NonEmpty::new(Content::Plain("abc")).expect("non-empty");
170        // Deref to Content lets us call inherent methods directly.
171        assert_eq!(ne.as_plain(), Some("abc"));
172    }
173
174    #[test]
175    fn empty_content_const_is_rejected() {
176        assert!(NonEmpty::new(Content::EMPTY).is_none());
177    }
178}