Skip to content

← Ownership I: Moves, Copy, Clone, Drop step 18 of 20

Medium Primitives

Method receivers and the consuming builder

Build one configuration for each command list. Some methods return an updated configuration by value; a summary method borrows it so you can continue adding settings afterwards.

pub fn build_configs(specs: Vec<Vec<String>>) -> Vec<String>

Configuration and commands

Use Config { retries: u32, tags: Vec<String> } with these methods:

Method Behavior
new() -> Config Start with zero retries and no tags.
retries(mut self, n: u32) -> Self Replace the retry count and return the config.
tag(mut self, t: String) -> Self Append the tag and return the config.
to_summary(&self) -> String Return r<count> t<number-of-tags> without consuming the config.
build(self) -> String Return retries=<count>;tags=<comma-joined-tags>, consuming the config.

Process each spec independently and preserve spec order. Within a spec, process commands in order using these exact, case-sensitive rules:

  • summary records a summary of the current state. Repeated summaries each produce an entry.
  • A command starting with retries parses its entire suffix as u32. On failure, set retries to zero. This replaces the previous count.
  • A command starting with tag appends its entire suffix unchanged. Empty tags, repeated tags, spaces, commas and Unicode are allowed.
  • Ignore every other command. Do not trim commands or their suffixes.

Retry parsing accepts ASCII decimal digits with an optional leading + and leading zeros, in 0..=4294967295. Negative signs, whitespace, underscores, empty suffixes and overflow fail. Thus retries +003 sets three retries, retries 3 sets zero, and retries without a space is ignored.

For each spec, join its summaries with |, put them in brackets, append a space, then append the built config. Preserve tags in insertion order and join them with commas, without escaping their contents.

For example, ["retries 3", "tag a", "summary", "tag b"] produces "[r3 t1] retries=3;tags=a,b". An empty spec produces "[] retries=0;tags=". An empty outer list produces no output elements. Implement the required consuming methods without cloning the config or its owned fields. The harness checks output; it does not enforce every method signature or detect all possible cloning implementations.

Read the receiver as an ownership decision

Common receiver What the method receives
&self A shared borrow.
&mut self An exclusive borrow that permits mutation.
self The value by value.
mut self The value by value, in a mutable local binding.

These are common forms, not the complete receiver syntax: methods can also use receivers such as self: Box<Self>. A shared receiver does not guarantee an unchanged value, because interior mutability can permit changes. A returned reference can also keep a borrow active after a call.

For this non-Copy Config, passing self moves the value into the method. A Copy type can instead be copied into a by-value call. In mut self, mut changes the local binding; it does not turn the receiver into a borrow.

Retain the returned builder

The starter calls cfg.retries(...) and cfg.tag(...) as statements. Each call moves cfg in, returns an updated value, then discards that value. A later use of the old binding produces E0382. Make the binding mutable and reassign the returned config after each step.

The starter also gives to_summary a by-value receiver. Change it to &self so recording a summary leaves the config available for later commands. Clippy's wrong_self_convention can flag a consuming to_* method on a non-Copy type, but move errors may prevent that lint from being reached.

Here is a complete consuming chain using a smaller builder:

struct Label(String);

impl Label {
    fn new() -> Self { Self(String::new()) }
    fn append(mut self, text: &str) -> Self {
        self.0.push_str(text);
        self
    }
    fn build(self) -> String { self.0 }
}

fn main() {
    let text = Label::new().append("hello").append(" world").build();
    assert_eq!(text, "hello world");
}

Each step transfers ownership and returns it. This permits chaining without cloning. It does not itself guarantee all required settings were supplied; that would need validation or types representing different builder states. Here, the default configuration is already valid.

A mutable builder is another valid design

A method can instead take &mut self and return &mut Self. With an owned variable, finish the mutable borrow before calling a consuming build:

struct Label(String);

impl Label {
    fn new() -> Self { Self(String::new()) }
    fn append(&mut self, text: &str) -> &mut Self {
        self.0.push_str(text);
        self
    }
    fn render(&self) -> String { format!("[{}]", self.0) }
    fn build(self) -> String { self.0 }
}

fn main() {
    let mut label = Label::new();
    label.append("hello").append(" world");
    assert_eq!(label.build(), "hello world");

    // A temporary chain works when the final method borrows and returns owned data.
    let rendered = Label::new().append("temporary").render();
    assert_eq!(rendered, "[temporary]");
}

Calling a non-Copy builder's build(self) directly on the &mut Self returned by a setter would attempt to move through a borrow. The example avoids that by ending the statement before consuming the owned variable. Use the consuming signatures requested for this exercise.

Method prefixes are useful conventions, not cost guarantees. as_* often exposes a borrowed view, to_* often creates an owned representation, and into_* conventionally consumes its receiver. The implementation determines allocations and other work. Remove unnecessary mut bindings if the compiler reports unused_mut. Submissions must compile, pass tests and pass Clippy.