Skip to content

← Collections, Text and First Iterators step 18 of 21

Hard Framework

Cow in API design: impl Into<Cow<str>>

Use Cow to let a message hold either borrowed or owned text. Then render placeholders while returning borrowed text when the template needs no parsing.

Keep these public functions:

pub fn render(template: &str, vars: Vec<(String, String)>) -> Cow<'_, str>
pub fn build(inputs: Vec<String>) -> Vec<(String, bool)>

Define Message<'a> with a text: Cow<'a, str> field and a constructor Message::new(text: impl Into<Cow<'a, str>>) -> Self.

Render placeholders

If template contains no opening brace, return Cow::Borrowed(template) without allocating an output buffer. Otherwise return Cow::Owned, even when the final text is unchanged or empty.

Parse from left to right using these rules:

  1. Copy text before the next {.
  2. Find the first } after that opening brace. Everything between them is the key. Keys are case-sensitive and may be empty or contain whitespace, Unicode or another opening brace. Do not trim them.
  3. Replace a known key with its value. If vars contains duplicate keys, use the first matching pair. Preserve unknown placeholders, including braces.
  4. Continue after the closing brace. Do not parse replacement values again.
  5. If an opening brace has no closing brace, preserve the rest unchanged.

There is no nesting or escape syntax. A closing brace outside a placeholder is ordinary text. For example, {{x}} treats {x as the key and leaves the final } as ordinary text. A backslash does not escape a brace.

With vars equal to [("name", "Ada")], "Hi {name}!" becomes owned "Hi Ada!", "{missing}" stays owned "{missing}", and "Hi!" is borrowed. An empty template is borrowed. The harness checks both the text and variant.

Build messages

Process inputs in order. For an empty string, construct a message from the literal "(empty)". For any nonempty string s, construct one from the owned String produced by format!("<{s}>"). Whitespace-only strings are nonempty.

For each message, record whether its text is Cow::Borrowed, then convert the text into a String and return (text, was_borrowed). Empty inputs therefore produce ("(empty)", true), and "x" produces ("<x>", false). An empty input list produces no pairs.

The boolean describes the intermediate Cow. The returned pair always contains an owned String: converting the borrowed literal with into_owned copies it. This does not make the entire build path allocation-free.

What the lifetime means

Cow<'a, str> is either a borrowed &'a str or an owned String. A struct field using Cow needs a lifetime argument, so the starter's Cow<str> field fails with E0106. Use Message<'a> and impl<'a> Message<'a> to name the lifetime.

The lifetime limits any text the message borrows. It does not mean every message borrows, nor does it force every caller to spell out a named lifetime. Functions can often use Message<'_>, and an owned or static-string message can be represented as Message<'static>.

Cow can be stored in collections or sent between threads when its lifetimes and trait bounds permit. If an API always needs independent ownership, String may be simpler. Cow is useful when preserving a caller's borrow is valuable.

Accept either ownership form

The starter's String-only constructor rejects the literal in build with E0308. Accepting impl Into<Cow<'a, str>> lets the constructor call .into(): a string slice becomes Borrowed, a String becomes Owned, and an existing Cow can be passed through. Moving a String into Cow transfers its buffer rather than cloning the contents.

This complete example demonstrates the same constructor pattern:

use std::borrow::Cow;

struct Label<'a> { text: Cow<'a, str> }

impl<'a> Label<'a> {
    fn new(text: impl Into<Cow<'a, str>>) -> Self {
        Self { text: text.into() }
    }
}

fn main() {
    let source = String::from("draft");
    let borrowed = Label::new(source.as_str());
    assert!(matches!(borrowed.text, Cow::Borrowed(_)));
    assert_eq!(borrowed.text, "draft");

    let owned = Label::new(String::from("saved"));
    assert!(matches!(owned.text, Cow::Owned(_)));
    assert_eq!(owned.text.into_owned(), "saved");
}

AsRef<str> offers a shared string view. It is useful when the operation only needs to read text, but it does not promise that the function will never store its argument. Into<Cow> supplies the ownership conversion this constructor needs; AsRef alone does not let it extract an owned String buffer.

Avoid unnecessary copies

into_owned() consumes a Cow, moving out its String or copying borrowed text into one. to_owned() on a Cow clones the Cow itself: a borrowed Cow stays borrowed, while an owned Cow's contents are cloned. Choose based on whether you need another Cow or an independent String.

For render, the no-opening-brace check is the ownership rule of this exercise. It is not the only possible design. Another API could scan for actual changes and allocate only when needed; that would have a different variant contract.

str::find returns byte indices. Slicing around matched ASCII braces is safe for UTF-8 because those indices and the positions immediately after the braces are character boundaries. Avoid treating arbitrary byte indices as character positions.

Submissions must compile, pass tests and pass Clippy. The tests check output text and ownership flags, not exact allocation counts or every API signature.