gemini_adk_fluent_rs/compose/
prompt.rs

1//! P — Prompt composition.
2//!
3//! Compose prompt sections additively with `+`.
4
5/// A section of a prompt.
6#[derive(Clone)]
7pub struct PromptSection {
8    /// The semantic category of this section.
9    pub kind: PromptSectionKind,
10    /// The text content of this section.
11    pub content: String,
12    /// Optional name for this section (used for name-based filtering/reordering).
13    pub name: Option<String>,
14    /// Optional adapter function for adaptive prompts.
15    pub adapter: Option<std::sync::Arc<dyn Fn(&str) -> String + Send + Sync>>,
16}
17
18impl std::fmt::Debug for PromptSection {
19    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
20        f.debug_struct("PromptSection")
21            .field("kind", &self.kind)
22            .field("content", &self.content)
23            .field("name", &self.name)
24            .field(
25                "adapter",
26                &self.adapter.as_ref().map(|_| "Fn(&str) -> String"),
27            )
28            .finish()
29    }
30}
31
32/// The semantic category of a prompt section.
33#[derive(Clone, Debug, PartialEq)]
34pub enum PromptSectionKind {
35    /// Agent role definition (e.g., "You are ...").
36    Role,
37    /// Task description (e.g., "Your task: ...").
38    Task,
39    /// Behavioral constraint (e.g., "Constraint: ...").
40    Constraint,
41    /// Output format specification.
42    Format,
43    /// Input/output example.
44    Example,
45    /// Free-form text.
46    Text,
47    /// Background context.
48    Context,
49    /// Personality or persona description.
50    Persona,
51    /// Bulleted guideline list.
52    Guidelines,
53    /// Step-by-step scaffolded prompt.
54    Scaffolded,
55    /// Versioned prompt section.
56    Versioned,
57    /// Marker indicating the prompt should be compressed.
58    Compressed,
59    /// Adaptive prompt that adjusts based on context.
60    Adaptive,
61}
62
63impl PromptSection {
64    /// Render this section as a formatted string.
65    pub fn render(&self) -> String {
66        match &self.kind {
67            PromptSectionKind::Role => format!("You are {}.", self.content),
68            PromptSectionKind::Task => format!("Your task: {}", self.content),
69            PromptSectionKind::Constraint => format!("Constraint: {}", self.content),
70            PromptSectionKind::Format => format!("Output format: {}", self.content),
71            PromptSectionKind::Example => self.content.clone(),
72            PromptSectionKind::Text => self.content.clone(),
73            PromptSectionKind::Context => format!("Context: {}", self.content),
74            PromptSectionKind::Persona => format!("Persona: {}", self.content),
75            PromptSectionKind::Guidelines => self.content.clone(),
76            PromptSectionKind::Scaffolded => self.content.clone(),
77            PromptSectionKind::Versioned => self.content.clone(),
78            PromptSectionKind::Compressed => format!("[compressed] {}", self.content),
79            PromptSectionKind::Adaptive => self.content.clone(),
80        }
81    }
82
83    /// Render an adaptive section with a context string.
84    ///
85    /// If this section has an adapter function, invokes it with the given context.
86    /// Otherwise, falls back to the normal `render()`.
87    pub fn render_with_context(&self, ctx: &str) -> String {
88        if let Some(adapter) = &self.adapter {
89            adapter(ctx)
90        } else {
91            self.render()
92        }
93    }
94
95    /// Attach an adapter function to this section (builder pattern).
96    pub fn with_adapter<F>(mut self, f: F) -> Self
97    where
98        F: Fn(&str) -> String + Send + Sync + 'static,
99    {
100        self.adapter = Some(std::sync::Arc::new(f));
101        self
102    }
103}
104
105/// Compose two prompt sections with `+`.
106impl std::ops::Add for PromptSection {
107    type Output = PromptComposite;
108
109    fn add(self, rhs: PromptSection) -> Self::Output {
110        PromptComposite {
111            sections: vec![self, rhs],
112        }
113    }
114}
115
116/// A composed prompt built from multiple sections.
117#[derive(Clone, Debug)]
118pub struct PromptComposite {
119    /// The ordered list of prompt sections.
120    pub sections: Vec<PromptSection>,
121}
122
123impl PromptComposite {
124    /// Render the full prompt by joining all sections.
125    pub fn render(&self) -> String {
126        self.sections
127            .iter()
128            .map(|s| s.render())
129            .collect::<Vec<_>>()
130            .join("\n\n")
131    }
132}
133
134impl PromptComposite {
135    /// Keep only sections of specified kinds.
136    pub fn only(self, kinds: &[PromptSectionKind]) -> Self {
137        Self {
138            sections: self
139                .sections
140                .into_iter()
141                .filter(|s| kinds.contains(&s.kind))
142                .collect(),
143        }
144    }
145
146    /// Remove sections of specified kinds.
147    pub fn without(self, kinds: &[PromptSectionKind]) -> Self {
148        Self {
149            sections: self
150                .sections
151                .into_iter()
152                .filter(|s| !kinds.contains(&s.kind))
153                .collect(),
154        }
155    }
156
157    /// Reorder sections by kind priority.
158    pub fn reorder(mut self, order: &[PromptSectionKind]) -> Self {
159        self.sections.sort_by_key(|s| {
160            order
161                .iter()
162                .position(|k| k == &s.kind)
163                .unwrap_or(usize::MAX)
164        });
165        self
166    }
167
168    /// Reorder sections by name. Sections matching the given names come first
169    /// (in the specified order); unmatched sections are appended at the end
170    /// in their original order.
171    pub fn reorder_by_name(self, order: &[&str]) -> Self {
172        let order: Vec<&str> = order.to_vec();
173        let mut ordered = Vec::with_capacity(self.sections.len());
174        let mut remaining = self.sections;
175
176        for name in &order {
177            let mut i = 0;
178            while i < remaining.len() {
179                if remaining[i].name.as_deref() == Some(name) {
180                    ordered.push(remaining.remove(i));
181                } else {
182                    i += 1;
183                }
184            }
185        }
186        ordered.extend(remaining);
187        Self { sections: ordered }
188    }
189
190    /// Keep only sections whose names match the given list.
191    pub fn only_by_name(self, names: &[&str]) -> Self {
192        Self {
193            sections: self
194                .sections
195                .into_iter()
196                .filter(|s| {
197                    s.name
198                        .as_deref()
199                        .map(|n| names.contains(&n))
200                        .unwrap_or(false)
201                })
202                .collect(),
203        }
204    }
205
206    /// Remove sections whose names match the given list.
207    pub fn without_by_name(self, names: &[&str]) -> Self {
208        Self {
209            sections: self
210                .sections
211                .into_iter()
212                .filter(|s| {
213                    s.name
214                        .as_deref()
215                        .map(|n| !names.contains(&n))
216                        .unwrap_or(true)
217                })
218                .collect(),
219        }
220    }
221
222    /// Apply a `PromptTransform` to this composite.
223    pub fn apply(self, transform: PromptTransform) -> Self {
224        match transform {
225            PromptTransform::Reorder(order) => {
226                let refs: Vec<&str> = order.iter().map(|s| s.as_str()).collect();
227                self.reorder_by_name(&refs)
228            }
229            PromptTransform::Only(names) => {
230                let refs: Vec<&str> = names.iter().map(|s| s.as_str()).collect();
231                self.only_by_name(&refs)
232            }
233            PromptTransform::Without(names) => {
234                let refs: Vec<&str> = names.iter().map(|s| s.as_str()).collect();
235                self.without_by_name(&refs)
236            }
237        }
238    }
239}
240
241/// A declarative transform that can be applied to a `PromptComposite`.
242///
243/// Created by `P::reorder()`, `P::only()`, and `P::without()`.
244#[derive(Clone, Debug)]
245pub enum PromptTransform {
246    /// Reorder sections by name.
247    Reorder(Vec<String>),
248    /// Keep only sections with these names.
249    Only(Vec<String>),
250    /// Remove sections with these names.
251    Without(Vec<String>),
252}
253
254impl From<PromptComposite> for String {
255    fn from(p: PromptComposite) -> String {
256        p.render()
257    }
258}
259
260impl From<PromptSection> for String {
261    fn from(s: PromptSection) -> String {
262        s.render()
263    }
264}
265
266impl std::ops::Add<PromptSection> for PromptComposite {
267    type Output = PromptComposite;
268
269    fn add(mut self, rhs: PromptSection) -> Self::Output {
270        self.sections.push(rhs);
271        self
272    }
273}
274
275/// The `P` namespace — static factory methods for prompt sections.
276pub struct P;
277
278impl P {
279    /// Define the agent's role.
280    pub fn role(role: &str) -> PromptSection {
281        PromptSection {
282            kind: PromptSectionKind::Role,
283            content: role.to_string(),
284            name: Some("role".to_string()),
285            adapter: None,
286        }
287    }
288
289    /// Define the agent's task.
290    pub fn task(task: &str) -> PromptSection {
291        PromptSection {
292            kind: PromptSectionKind::Task,
293            content: task.to_string(),
294            name: Some("task".to_string()),
295            adapter: None,
296        }
297    }
298
299    /// Add a constraint.
300    pub fn constraint(c: &str) -> PromptSection {
301        PromptSection {
302            kind: PromptSectionKind::Constraint,
303            content: c.to_string(),
304            name: Some("constraint".to_string()),
305            adapter: None,
306        }
307    }
308
309    /// Specify output format.
310    pub fn format(f: &str) -> PromptSection {
311        PromptSection {
312            kind: PromptSectionKind::Format,
313            content: f.to_string(),
314            name: Some("format".to_string()),
315            adapter: None,
316        }
317    }
318
319    /// Add an input/output example.
320    pub fn example(input: &str, output: &str) -> PromptSection {
321        PromptSection {
322            kind: PromptSectionKind::Example,
323            content: format!("Example:\nInput: {input}\nOutput: {output}"),
324            name: Some("example".to_string()),
325            adapter: None,
326        }
327    }
328
329    /// Add free-form text.
330    pub fn text(t: &str) -> PromptSection {
331        PromptSection {
332            kind: PromptSectionKind::Text,
333            content: t.to_string(),
334            name: None,
335            adapter: None,
336        }
337    }
338
339    /// Add background context.
340    pub fn context(ctx: &str) -> PromptSection {
341        PromptSection {
342            kind: PromptSectionKind::Context,
343            content: ctx.to_string(),
344            name: Some("context".to_string()),
345            adapter: None,
346        }
347    }
348
349    /// Define a personality/persona.
350    pub fn persona(desc: &str) -> PromptSection {
351        PromptSection {
352            kind: PromptSectionKind::Persona,
353            content: desc.to_string(),
354            name: Some("persona".to_string()),
355            adapter: None,
356        }
357    }
358
359    /// Add multiple guidelines as a bulleted list.
360    pub fn guidelines(items: &[&str]) -> PromptSection {
361        let content = items
362            .iter()
363            .map(|item| format!("- {item}"))
364            .collect::<Vec<_>>()
365            .join("\n");
366        PromptSection {
367            kind: PromptSectionKind::Guidelines,
368            content: format!("Guidelines:\n{content}"),
369            name: Some("guidelines".to_string()),
370            adapter: None,
371        }
372    }
373
374    /// Add a named section (flexible section kind).
375    pub fn section(name: &str, text: &str) -> PromptSection {
376        PromptSection {
377            kind: PromptSectionKind::Text,
378            content: format!("## {}\n{}", name, text),
379            name: Some(name.to_string()),
380            adapter: None,
381        }
382    }
383
384    /// Template with `{key}` placeholders — rendered with state values at runtime.
385    pub fn template(tpl: &str) -> PromptSection {
386        PromptSection {
387            kind: PromptSectionKind::Text,
388            content: tpl.to_string(),
389            name: Some("template".to_string()),
390            adapter: None,
391        }
392    }
393
394    /// Reorder sections in a composite by name.
395    ///
396    /// Sections whose names match the given order come first (in order);
397    /// unmatched sections are appended at the end in their original order.
398    ///
399    /// ```ignore
400    /// let prompt = (P::role("analyst") + P::task("analyze") + P::format("JSON"))
401    ///     .reorder_by_name(&["format", "role", "task"]);
402    /// ```
403    pub fn reorder(order: &[&str]) -> PromptTransform {
404        let order: Vec<String> = order.iter().map(|s| s.to_string()).collect();
405        PromptTransform::Reorder(order)
406    }
407
408    /// Keep only sections whose names match the given list.
409    ///
410    /// ```ignore
411    /// let prompt = (P::role("analyst") + P::task("analyze") + P::format("JSON"))
412    ///     .only_by_name(&["role", "task"]);
413    /// ```
414    pub fn only(names: &[&str]) -> PromptTransform {
415        let names: Vec<String> = names.iter().map(|s| s.to_string()).collect();
416        PromptTransform::Only(names)
417    }
418
419    /// Remove sections whose names match the given list.
420    ///
421    /// ```ignore
422    /// let prompt = (P::role("analyst") + P::task("analyze") + P::format("JSON"))
423    ///     .without_by_name(&["format"]);
424    /// ```
425    pub fn without(names: &[&str]) -> PromptTransform {
426        let names: Vec<String> = names.iter().map(|s| s.to_string()).collect();
427        PromptTransform::Without(names)
428    }
429
430    /// Mark prompt for compression. This is a placeholder/marker indicating
431    /// the prompt content should be compressed before sending to the model.
432    pub fn compress() -> PromptSection {
433        PromptSection {
434            kind: PromptSectionKind::Compressed,
435            content: String::new(),
436            name: Some("compress".to_string()),
437            adapter: None,
438        }
439    }
440
441    /// Create an adaptive prompt that adjusts based on a context function.
442    ///
443    /// The function receives context (e.g., token budget, turn count) and returns
444    /// the adapted prompt text.
445    ///
446    /// ```ignore
447    /// let prompt = P::adapt(|ctx| {
448    ///     if ctx.contains("detailed") {
449    ///         "Provide a thorough analysis with citations.".to_string()
450    ///     } else {
451    ///         "Be concise.".to_string()
452    ///     }
453    /// });
454    /// ```
455    pub fn adapt<F>(f: F) -> PromptSection
456    where
457        F: Fn(&str) -> String + Send + Sync + 'static,
458    {
459        PromptSection {
460            kind: PromptSectionKind::Adaptive,
461            content: String::new(),
462            name: Some("adapt".to_string()),
463            adapter: None,
464        }
465        .with_adapter(f)
466    }
467
468    /// Create a step-by-step scaffolded prompt from ordered steps.
469    ///
470    /// ```ignore
471    /// let prompt = P::scaffolded(&["Identify the problem", "Gather data", "Analyze", "Conclude"]);
472    /// ```
473    pub fn scaffolded(steps: &[&str]) -> PromptSection {
474        let content = steps
475            .iter()
476            .enumerate()
477            .map(|(i, step)| format!("Step {}: {step}", i + 1))
478            .collect::<Vec<_>>()
479            .join("\n");
480        PromptSection {
481            kind: PromptSectionKind::Scaffolded,
482            content: format!("Follow these steps:\n{content}"),
483            name: Some("scaffolded".to_string()),
484            adapter: None,
485        }
486    }
487
488    /// Create a versioned prompt section with a version tag.
489    ///
490    /// ```ignore
491    /// let prompt = P::versioned("v2.1", "Analyze the data using the new methodology");
492    /// ```
493    pub fn versioned(version: &str, text: &str) -> PromptSection {
494        PromptSection {
495            kind: PromptSectionKind::Versioned,
496            content: format!("[{version}] {text}"),
497            name: Some(format!("versioned:{version}")),
498            adapter: None,
499        }
500    }
501
502    // ── Instruction modifier factories ──────────────────────────────────────
503    // Bridge P-module composition to the InstructionModifier system.
504
505    /// Create a state-append modifier that renders selected state keys into the instruction.
506    ///
507    /// ```ignore
508    /// let modifiers = P::with_state(&["emotional_state", "willingness_to_pay"]);
509    /// ```
510    pub fn with_state(keys: &[&str]) -> gemini_adk_rs::live::InstructionModifier {
511        gemini_adk_rs::live::InstructionModifier::StateAppend(
512            keys.iter().map(|k| k.to_string()).collect(),
513        )
514    }
515
516    /// Create a conditional modifier that appends text when the predicate is true.
517    ///
518    /// ```ignore
519    /// let risk_mod = P::when(risk_is_elevated, "IMPORTANT: Show extra empathy.");
520    /// ```
521    pub fn when(
522        predicate: impl Fn(&gemini_adk_rs::State) -> bool + Send + Sync + 'static,
523        text: impl Into<String>,
524    ) -> gemini_adk_rs::live::InstructionModifier {
525        gemini_adk_rs::live::InstructionModifier::Conditional {
526            predicate: std::sync::Arc::new(predicate),
527            text: text.into(),
528        }
529    }
530
531    /// Create a custom-append modifier from a formatting function.
532    ///
533    /// ```ignore
534    /// let ctx = P::context_fn(|s| format!("Customer: {}", s.get::<String>("name").unwrap_or_default()));
535    /// ```
536    pub fn context_fn(
537        f: impl Fn(&gemini_adk_rs::State) -> String + Send + Sync + 'static,
538    ) -> gemini_adk_rs::live::InstructionModifier {
539        gemini_adk_rs::live::InstructionModifier::CustomAppend(std::sync::Arc::new(f))
540    }
541}
542
543#[cfg(test)]
544mod tests {
545    use super::*;
546
547    #[test]
548    fn role_renders() {
549        let s = P::role("analyst");
550        assert_eq!(s.render(), "You are analyst.");
551    }
552
553    #[test]
554    fn task_renders() {
555        let s = P::task("analyze data");
556        assert_eq!(s.render(), "Your task: analyze data");
557    }
558
559    #[test]
560    fn constraint_renders() {
561        let s = P::constraint("be concise");
562        assert_eq!(s.render(), "Constraint: be concise");
563    }
564
565    #[test]
566    fn format_renders() {
567        let s = P::format("JSON");
568        assert_eq!(s.render(), "Output format: JSON");
569    }
570
571    #[test]
572    fn example_renders() {
573        let s = P::example("hello", "world");
574        assert!(s.render().contains("Input: hello"));
575        assert!(s.render().contains("Output: world"));
576    }
577
578    #[test]
579    fn compose_with_add() {
580        let prompt = P::role("analyst") + P::task("analyze data") + P::format("JSON");
581        assert_eq!(prompt.sections.len(), 3);
582    }
583
584    #[test]
585    fn composite_renders_all() {
586        let prompt = P::role("analyst") + P::task("analyze data");
587        let rendered = prompt.render();
588        assert!(rendered.contains("You are analyst."));
589        assert!(rendered.contains("Your task: analyze data"));
590    }
591
592    #[test]
593    fn context_renders() {
594        let s = P::context("user is a developer");
595        assert_eq!(s.render(), "Context: user is a developer");
596        assert_eq!(s.kind, PromptSectionKind::Context);
597    }
598
599    #[test]
600    fn persona_renders() {
601        let s = P::persona("friendly and concise");
602        assert_eq!(s.render(), "Persona: friendly and concise");
603        assert_eq!(s.kind, PromptSectionKind::Persona);
604    }
605
606    #[test]
607    fn guidelines_renders() {
608        let s = P::guidelines(&["be concise", "use examples", "cite sources"]);
609        assert!(s.render().contains("Guidelines:"));
610        assert!(s.render().contains("- be concise"));
611        assert!(s.render().contains("- use examples"));
612        assert!(s.render().contains("- cite sources"));
613        assert_eq!(s.kind, PromptSectionKind::Guidelines);
614    }
615
616    #[test]
617    fn section_kinds() {
618        assert_eq!(P::role("x").kind, PromptSectionKind::Role);
619        assert_eq!(P::task("x").kind, PromptSectionKind::Task);
620        assert_eq!(P::text("x").kind, PromptSectionKind::Text);
621    }
622
623    #[test]
624    fn section_into_string() {
625        let s: String = P::role("analyst").into();
626        assert_eq!(s, "You are analyst.");
627    }
628
629    #[test]
630    fn composite_into_string() {
631        let s: String = (P::role("analyst") + P::task("analyze data")).into();
632        assert!(s.contains("You are analyst."));
633        assert!(s.contains("Your task: analyze data"));
634    }
635
636    #[test]
637    fn reorder_by_name() {
638        let prompt = P::role("analyst") + P::task("analyze") + P::format("JSON");
639        let reordered = prompt.reorder_by_name(&["format", "task", "role"]);
640        assert_eq!(reordered.sections[0].name.as_deref(), Some("format"));
641        assert_eq!(reordered.sections[1].name.as_deref(), Some("task"));
642        assert_eq!(reordered.sections[2].name.as_deref(), Some("role"));
643    }
644
645    #[test]
646    fn only_by_name() {
647        let prompt = P::role("analyst") + P::task("analyze") + P::format("JSON");
648        let filtered = prompt.only_by_name(&["role", "task"]);
649        assert_eq!(filtered.sections.len(), 2);
650        assert_eq!(filtered.sections[0].name.as_deref(), Some("role"));
651        assert_eq!(filtered.sections[1].name.as_deref(), Some("task"));
652    }
653
654    #[test]
655    fn without_by_name() {
656        let prompt = P::role("analyst") + P::task("analyze") + P::format("JSON");
657        let filtered = prompt.without_by_name(&["format"]);
658        assert_eq!(filtered.sections.len(), 2);
659        assert!(filtered
660            .sections
661            .iter()
662            .all(|s| s.name.as_deref() != Some("format")));
663    }
664
665    #[test]
666    fn reorder_transform_via_apply() {
667        let prompt = P::role("analyst") + P::task("analyze") + P::format("JSON");
668        let transform = P::reorder(&["format", "role"]);
669        let reordered = prompt.apply(transform);
670        assert_eq!(reordered.sections[0].name.as_deref(), Some("format"));
671        assert_eq!(reordered.sections[1].name.as_deref(), Some("role"));
672    }
673
674    #[test]
675    fn only_transform_via_apply() {
676        let prompt = P::role("analyst") + P::task("analyze") + P::format("JSON");
677        let transform = P::only(&["task"]);
678        let filtered = prompt.apply(transform);
679        assert_eq!(filtered.sections.len(), 1);
680        assert_eq!(filtered.sections[0].name.as_deref(), Some("task"));
681    }
682
683    #[test]
684    fn without_transform_via_apply() {
685        let prompt = P::role("analyst") + P::task("analyze") + P::format("JSON");
686        let transform = P::without(&["role", "format"]);
687        let filtered = prompt.apply(transform);
688        assert_eq!(filtered.sections.len(), 1);
689        assert_eq!(filtered.sections[0].name.as_deref(), Some("task"));
690    }
691
692    #[test]
693    fn compress_renders() {
694        let s = P::compress();
695        assert_eq!(s.kind, PromptSectionKind::Compressed);
696        assert_eq!(s.render(), "[compressed] ");
697    }
698
699    #[test]
700    fn adapt_renders_with_context() {
701        let s = P::adapt(|ctx| {
702            if ctx.contains("detailed") {
703                "Be thorough.".to_string()
704            } else {
705                "Be concise.".to_string()
706            }
707        });
708        assert_eq!(s.kind, PromptSectionKind::Adaptive);
709        assert_eq!(s.render_with_context("detailed"), "Be thorough.");
710        assert_eq!(s.render_with_context("brief"), "Be concise.");
711    }
712
713    #[test]
714    fn adapt_fallback_render() {
715        let s = P::adapt(|_| "adapted".to_string());
716        // render() without context returns the empty content
717        assert_eq!(s.render(), "");
718    }
719
720    #[test]
721    fn scaffolded_renders() {
722        let s = P::scaffolded(&["Identify", "Analyze", "Conclude"]);
723        assert_eq!(s.kind, PromptSectionKind::Scaffolded);
724        let rendered = s.render();
725        assert!(rendered.contains("Follow these steps:"));
726        assert!(rendered.contains("Step 1: Identify"));
727        assert!(rendered.contains("Step 2: Analyze"));
728        assert!(rendered.contains("Step 3: Conclude"));
729    }
730
731    #[test]
732    fn versioned_renders() {
733        let s = P::versioned("v2.1", "Use the new methodology");
734        assert_eq!(s.kind, PromptSectionKind::Versioned);
735        assert_eq!(s.render(), "[v2.1] Use the new methodology");
736        assert_eq!(s.name.as_deref(), Some("versioned:v2.1"));
737    }
738
739    #[test]
740    fn sections_have_names() {
741        assert_eq!(P::role("x").name.as_deref(), Some("role"));
742        assert_eq!(P::task("x").name.as_deref(), Some("task"));
743        assert_eq!(P::constraint("x").name.as_deref(), Some("constraint"));
744        assert_eq!(P::format("x").name.as_deref(), Some("format"));
745        assert_eq!(P::example("x", "y").name.as_deref(), Some("example"));
746        assert_eq!(P::text("x").name, None);
747        assert_eq!(P::context("x").name.as_deref(), Some("context"));
748        assert_eq!(P::persona("x").name.as_deref(), Some("persona"));
749        assert_eq!(P::guidelines(&["x"]).name.as_deref(), Some("guidelines"));
750        assert_eq!(P::section("foo", "bar").name.as_deref(), Some("foo"));
751        assert_eq!(P::scaffolded(&["x"]).name.as_deref(), Some("scaffolded"));
752        assert_eq!(P::compress().name.as_deref(), Some("compress"));
753    }
754}