examples/template/
Un programma che inserisce valori in un modello di testo usando una mappa.
- 235 righe
La mappa associa a ogni nome il testo da inserire. Se manca una chiave, il programma restituisce un errore oppure usa un valore predefinito richiesto esplicitamente. Puoi anche inserire parentesi graffe letterali. Un test ha individuato una differenza involontaria nel modo in cui il programma trattava le graffe aperte e quelle chiuse.
Eseguilo
Hello, Ada.
Ada the engineer
no holes here
a {literal} brace
{name}
name,role,name
true
true
true
true
Hello, friend.
exit 0 · 11 righe
Il programma
# Text with holes in it, filled from a map — and the three ways filling one
# can go wrong.
#
# Corpus and regression material (CLAUDE.md §9). It closes no measured form
# gap; what it adds is the shape a person reaches for when they want a
# message, and the reason it is worth a program is that the naive version
# silently produces nonsense. A substitution that cannot find its key has
# three defensible answers — leave the hole, drop it, or refuse — and a
# library that picks one without saying so is the source of every
# "Hello, {name}" anybody has ever received in an email.
#
# THIS ONE REFUSES, and says why in the code: `spec:141`'s `T?` makes refusing
# cost one character at the call site, so the cheapest thing is also the
# loudest. `render_or` is there for the caller who genuinely wants a default,
# and it is a different function rather than a flag, so the choice is visible
# at the call site instead of buried in an argument.
#
# THE ESCAPE IS DOUBLING, as in the CSV program next door: `{{` is one `{`.
# That is the rule with the fewest cases — no backslash, no lookbehind — and
# it is the same rule the format strings of four other languages use.
function render(template: str, values: {str: str}) -> str?
out: str @= ""
key: str @= ""
inside: bool @= false
pending: bool @= false
closing: bool @= false
for ch in template.chars()
if pending
pending @ false
if ch == "{"
# `{{` is one literal brace.
out @ out + "{"
continue
inside @ true
key @ ""
# The character after a single `{` is the first of the key, so it
# falls through to the `inside` branch below rather than being
# dropped — which is the bug the first draft of this had.
if inside
if ch == "}"
inside @ false
if key.len() == 0
return fail(code: "empty_key", msg: "`{}` names nothing")
found = values[key]
if found.is_err()
return fail(code: "unknown_key", msg: "the template wants `" + key + "` and it was not given")
out @ out + found.must()
else
key @ key + ch
continue
if ch == "{"
pending @ true
continue
if ch == "}"
if closing
# `}}` is one literal brace, symmetrically with `{{`. The
# first draft handled only the opening half and refused
# `{{name}}`, which is what a writer types when they mean a
# literal `{name}` — a rule that escapes one delimiter and
# not the other is a rule with a hole in it.
closing @ false
out @ out + "}"
continue
closing @ true
continue
if closing
return fail(code: "stray_brace", msg: "a `}` with no `{` before it")
out @ out + ch
if closing
return fail(code: "stray_brace", msg: "a `}` with no `{` before it")
if inside || pending
return fail(code: "unterminated", msg: "a `{` that never closes")
return ok(out)
# The same, with a default for every key the map does not hold. A separate
# function and not a flag: the caller who wants this is making a different
# decision, and `spec` has no default arguments to hide it in.
function render_or(template: str, values: {str: str}, missing: str) -> str?
filled: {str: str} @= values
for key in needed(template)
if filled[key].is_err()
filled[key] @ missing
return render(template, values: filled)
# Every key a template asks for, in the order it asks. Its own function
# because a caller often wants to know before it has the values — which is
# what turns a runtime failure into a check.
#
# ORDER: none — the order is the TEMPLATE's, read left to right, not a map
# walk. Stated rather than left out, because a reader who sees a list of keys
# beside a map is right to ask.
function needed(template: str) -> [str]
out: [str] @= []
key: str @= ""
inside: bool @= false
pending: bool @= false
for ch in template.chars()
if pending
pending @ false
if ch == "{"
continue
inside @ true
key @ ""
if inside
if ch == "}"
inside @ false
if key.len() > 0
out @ out.push(key)
else
key @ key + ch
continue
if ch == "{"
pending @ true
return out
function sample_values() -> {str: str}
values: {str: str} @= {}
values["name"] @ "Ada"
values["role"] @ "engineer"
return values
function main()
values = sample_values()
print(render("Hello, {name}.", values: values).must())
print(render("{name} the {role}", values: values).must())
print(render("no holes here", values: values).must())
print(render("a {{literal}} brace", values: values).must())
print(render("{{name}}", values: values).must())
print(join(needed("{name} and {role} and {name}"), ","))
# The four refusals, each a different code for a different repair.
print(render("Hello, {nobody}.", values: values).is_err())
print(render("Hello, {name", values: values).is_err())
print(render("Hello, {}.", values: values).is_err())
print(render("Hello, name}.", values: values).is_err())
# And the caller who would rather have a placeholder than a failure.
print(render_or(template: "Hello, {nobody}.", values: values, missing: "friend").must())
test "a hole is filled and the text around it is untouched"
values = sample_values()
assert render("Hello, {name}.", values: values).must() == "Hello, Ada."
assert render("{name}", values: values).must() == "Ada"
assert render("{name}{role}", values: values).must() == "Adaengineer"
assert render("a{name}b", values: values).must() == "aAdab"
test "a template with no holes is itself"
values = sample_values()
assert render("plain", values: values).must() == "plain"
assert render("", values: values).must() == ""
test "a doubled brace is one brace, on both sides"
# Both halves, because the first draft escaped only the opening one and
# `{{name}}` — which is what a writer types for a literal `{name}` — came
# back as `error[stray_brace]`.
values = sample_values()
assert render("{{name}}", values: values).must() == "{name}"
assert render("{{}}", values: values).must() == "{}"
assert render("a {{ b", values: values).must() == "a { b"
test "the four refusals are four different codes"
values = sample_values()
# A key nobody gave.
assert render("{nobody}", values: values).is_err()
# A brace that never closes.
assert render("{name", values: values).is_err()
assert render("{", values: values).is_err()
# A hole with no name in it.
assert render("{}", values: values).is_err()
# A closing brace with nothing open.
assert render("}", values: values).is_err()
assert render("name}", values: values).is_err()
test "the keys a template needs are read in the template's own order"
assert needed("{name} and {role}") == ["name", "role"]
assert needed("{role} and {name}") == ["role", "name"]
assert needed("{name} twice {name}") == ["name", "name"]
assert needed("no holes") == []
assert needed("{{literal}}") == []
test "the keys can be checked before the values exist"
# The property that makes `needed` worth its own function: a caller can
# refuse a template before it has anything to fill it with.
values = sample_values()
wanted = needed("Hello, {name} the {nobody}")
unknown: [str] @= []
for key in wanted
if values[key].is_err()
unknown @ unknown.push(key)
assert unknown == ["nobody"]
assert render("Hello, {name} the {nobody}", values: values).is_err()
test "render_or fills what is missing and leaves what is not"
values = sample_values()
assert render_or(template: "{name} and {nobody}", values: values, missing: "?").must() == "Ada and ?"
assert render_or(template: "{name}", values: values, missing: "?").must() == "Ada"
assert render_or(template: "plain", values: values, missing: "?").must() == "plain"
# It fills a missing key and still refuses a broken template, which is
# the line between "a value I do not have" and "a template I cannot read".
assert render_or(template: "{name", values: values, missing: "?").is_err()
assert render_or(template: "{}", values: values, missing: "?").is_err()
test "an empty map is not an error until a hole asks for something"
empty: {str: str} = {}
assert render("plain", values: empty).must() == "plain"
assert render("{anything}", values: empty).is_err()
assert render_or(template: "{anything}", values: empty, missing: "x").must() == "x"
impronta sha256 4242465e775d · 2 file · index.json
Successivo: rpn. Tutti gli esempi. ← Precedente: ini
Eseguine uno: i quattro comandi. Impara il linguaggio, oppure leggi la specifica completa in un solo file.