Guia 22 de 53
Validação, schemas e unidades
Valores validados, capturas tipadas, schemas e unidades dimensionais.
Nesta página
Valores validados, capturas tipadas, schemas e unidades dimensionais.
Newtypes escalares validados¶
Um newtype com where incorpora o predicado à construção. Seu construtor new retorna Result; trate a falha ou propague-a. Operações derivadas mantêm a validação. Um newtype sem where preserva o construtor direto.
Pratique na By Example · Programa fonte
// Validation is part of construction: .new returns Result<Score, str>.
newtype Score(int) deriving Eq, Ord, Clone, Default, Add where |v| 0 <= v <= 100
newtype Positive(int) where |v| v > 0
newtype UserId(int)
let score = Score.new(40).unwrap()
let delta = Score.new(2).unwrap()
print(score.unwrap()) // expected: 40
print((score + delta).unwrap()) // expected: 42
print(score.clone() == score) // expected: true
print(Score.default().unwrap()) // expected: 0
print(Score.new(101).is_err()) // expected: true
print(Positive.new(0).is_err()) // expected: true
// Ordinary newtypes still construct directly.
print(UserId.new(7).unwrap()) // expected: 7Sufixos de unidades declarados¶
Associe @unit a um newtype escalar para dar um sufixo tipado aos literais. O sufixo preserva o tipo nominal e a precisão do escalar. Um sufixo sozinho não define conversão: declare dimensões e escalas para converter automaticamente.
Pratique na By Example · Programa fonte
// Declared suffixes retain exact scalar spelling and nominal ownership.
@unit("kg") newtype Mass(decimal) where |value| value >= 0d
@unit("g") newtype Grams(decimal)
@unit("crate") newtype Crates(int)
let parcel = 2.5kg
let total = parcel + 0.5kg
print(total.unwrap()) // expected: 3.0
print((parcel * 2d).unwrap()) // expected: 5.0
print(parcel / 0.5kg) // expected: 5
print(parcel > 1kg) // expected: true
let grams = 500g
let converted = Mass.new(grams.unwrap() / 1000d).unwrap()
print(converted.unwrap()) // expected: 0.50
let stock = 8crate
print((stock / 2).unwrap()) // expected: 4
print(stock / 2crate) // expected: 4Capturas tipadas em strings¶
Um literal pat compara a string inteira e converte as capturas antes de expor os bindings. Conversão escalar ou construção validada que falha significa não corresponder. O primeiro delimitador é usado sem backtracking; capturas adjacentes sem delimitador são rejeitadas.
Pratique na By Example · Programa fonte
// Complete-string matching; conversions and guards can fall through.
fn order(input: str) -> str {
match input {
pat"order/{id: int}/{status: str}" if id > 0 => "{id}:{status}",
_ => "invalid",
}
}
print(order("order/42/paid")) // expected: 42:paid
print(order("order/oops/paid")) // expected: invalid
print(order("prefixorder/7/paid")) // expected: invalid
print(order("order/-1/paid")) // expected: invalid
let enabled = match "flag/true" {
pat"flag/{value: bool}" => value,
_ => false,
}
print(enabled) // expected: true
let braces = match r"{done}/7" {
pat"{{done}}/{value: int}" => value,
_ => -1,
}
print(braces) // expected: 7
newtype Positive(int) where |value| value > 0
let checked = match "id/23" {
pat"id/{value: Positive}" => value.unwrap(),
_ => -1,
}
print(checked) // expected: 23
let rejected = match "id/-2" {
pat"id/{value: Positive}" => value.unwrap(),
_ => -1,
}
print(rejected) // expected: -1Literais de string nominais validados¶
Um literal como Email"[email protected]" só é aceito se o predicado do newtype de string validado aceitar seu conteúdo estático na compilação. Strings dinâmicas continuam usando o construtor falível. Templates prefixados por funções existentes preservam sua semântica.
Pratique na By Example · Programa fonte
// expect: [email protected]
// expect: true
// expect: empty
// expect: [email protected]
// expect: callable
const required = "@"
fn valid_email(value: str) -> bool = value.contains(required)
newtype Email(str) where |value| valid_email(value)
newtype Empty(str) where |value| value == ""
let email = Email"[email protected]"
print(email.unwrap())
let input = "invalid"
print(Email.new(input).is_err())
let empty = Empty""
print(if empty.unwrap() == "" { "empty" } else { "wrong" })
mod address { pub newtype Address(str) where |value| value.contains("@") }
use address::{Address as Mail}
print(Mail"[email protected]".unwrap())
fn callable() -> str { let Email = |parts: [str]| parts[0]; Email"callable" }
print(callable())Padrões de captura como valores¶
Guarde um literal pat como valor reutilizável. Seu método parse retorna Result com um record de capturas de tipos exatos. Entrada incompatível e conversões que falham retornam erros estruturados. Construir o padrão retém as funções de conversão sem executar seus predicados de validação.
Pratique na By Example · Programa fonte
// Exact public capture record; construction performs no parse or validation.
var input_calls = 0
fn input() -> str { input_calls += 1; "users/9223372036854775807/false" }
let route = pat"users/{id: int}/{ready: bool}"
let captured = route.parse(input()).unwrap()
print(captured.id)
print(captured.ready)
print(input_calls)
print(route.parse("prefix/users/1/true").is_err())
print(route.parse("users/9223372036854775808/true").is_err())
print(route.parse("users/1/true/trailing").is_err())
match route.parse("users/nope/true") {
.Err(error) => { print(error.kind); print(error.input) },
.Ok(_) => print("unexpected"),
}
let id = "outer"
let empty = pat""
print(empty.parse("").is_ok())
print(empty.parse("x").is_err())
let escaped = pat"{{literal}}/{id: str}"
print(escaped.parse(r"{literal}/é中😀").unwrap().id)
print(id)
newtype Positive(int) where |n| n > 0
let positive = pat"n/{value: Positive}"
print(positive.parse("n/3").unwrap().value.unwrap())
print(positive.parse("n/-1").is_err())
var cleanup = ""
struct ScopeResource {}
impl core::cleanup::Cleanup for ScopeResource { fn close(self) { cleanup += "closed" } }
using resource = ScopeResource {} {
let parsed = route.parse("bad")
defer { cleanup += "defer/" }
print(parsed.is_err())
}
print(cleanup)
fn parse_with_cleanup() -> Result<int, { kind: str, input: str, pattern: str }> {
using resource = ScopeResource {} {
defer { cleanup += "defer/" }
let args = route.parse("bad")?;
.Ok(args.id)
}
}
cleanup = ""
print(parse_with_cleanup().is_err())
print(cleanup)
var construction_calls = 0
fn make_route() { construction_calls += 1; pat"users/{id: int}" }
print(make_route().parse("users/8").unwrap().id)
print(construction_calls)Invariantes entre campos de schemas¶
Adicione predicados where após um schema para validar o valor completo. Conversão de campos e defaults acontecem primeiro. Invariantes retornam bool e executam na ordem; falhas viram DecodeError estruturado no caminho do objeto. Restrições de campos mantêm seu próprio escopo.
Pratique na By Example · Programa fonte
// Item 13: field checks -> defaults -> assembled-object invariants, once each.
var trace = ""
var cleaned = 0
fn last_default() -> int { trace += "D"; 10 }
fn nonnegative(value: int) -> bool { trace += "F"; value >= 0 }
schema Booking {
start: int where nonnegative,
end: int = last_default(),
}
where |booking| {
defer { cleaned += 1 }
trace += "I"
booking.start < booking.end
}
where |booking| { trace += "J"; booking.end <= 20 }
fn show(result: Result<Booking, DecodeError>) {
match result {
.Ok(booking) => print("ok:{booking.start}:{booking.end}"),
.Err(error) => {
print("{error.path}:{error.kind}:{error.expected}:{error.actual}:{error.message}")
print(error.causes.len())
},
}
}
fn main() {
let valid = Booking.parse(#{start: 2})
assert(match valid { .Ok(value) => value.start == 2 && value.end == 10, .Err(_) => false })
assert(trace == "FDIJ")
show(valid)
print(trace)
trace = ""
let invariant_failure = Booking.parse(#{start: 10, end: 1})
assert(match invariant_failure {
.Err(error) => error.path == "$" && error.expected == "Booking" && error.actual == "Booking"
&& error.message == "Booking: invariant 1 failed" && error.causes.len() == 0,
.Ok(_) => false,
})
assert(trace == "FI")
show(invariant_failure)
print(trace)
trace = ""
let field_failure = Booking.parse(#{start: -1})
assert(match field_failure {
.Err(error) => error.path == "$.start" && error.message == "start: constraint failed",
.Ok(_) => false,
})
assert(trace == "F")
show(field_failure)
print(trace)
trace = ""
let wrong_type = Booking.parse(#{start: 2, end: "bad"})
assert(match wrong_type {
.Err(error) => error.path == "$.end" && error.expected == "int" && error.actual == "string",
.Ok(_) => false,
})
assert(trace == "F")
show(wrong_type)
print(trace)
print(cleaned)
assert(cleaned == 2)
}Dimensões e escalas de unidades¶
Declare unidades base, dimensões derivadas e escalas exatas com @unit. Multiplicação e divisão derivam dimensões; as converte apenas dimensões compatíveis e verifica representabilidade. Sufixos de duração mantêm o comportamento em milissegundos fora de um contexto dimensional.
Pratique na By Example · Programa fonte
// Exact dimensional scales; standalone durations retain milliseconds.
@unit("m", base: true) newtype Meters(decimal) where |v| v >= 0d
@unit("km", of: Meters, scale: 1000d) newtype Kilometers(decimal)
@unit("s", base: true, duration: true) newtype Seconds(decimal)
@unit("h", of: Seconds, scale: 3600d, duration: true) newtype Hours(decimal)
@unit("kph", dimension: Kilometers / Hours) newtype Kph(decimal)
@unit("kmh", dimension: Kilometers * Hours) newtype Kmh(decimal)
fn main() {
let distance = 2km as Meters
assert(distance.unwrap() == 2000d)
print(distance.unwrap())
let speed = 80km / 2h
let displayed = speed as Kph
assert(displayed.unwrap() == 40d)
print(displayed.unwrap())
let covered = (speed * 2h) as Kilometers
assert(covered.unwrap() == 80d)
print(covered.unwrap())
let reverse = (2h * 80km) as Kmh
assert(reverse.unwrap() == 160d)
print(reverse.unwrap())
assert(((2km / 500m) as decimal) == 4d)
print((2km / 500m) as decimal)
let wait = 2h
assert(wait == 7_200_000)
print(wait)
let hours: Hours = 2h
assert(hours.unwrap() == 2d)
print(hours.unwrap())
let nano: Seconds = 1ns
assert(nano.unwrap() == 0.000000001d)
print(nano.unwrap())
assert(1ns == 0)
print(1ns)
let clock = 90_061_000
print("{clock as h}:{clock as min}:{clock as s}:{clock as ms}")
}
main()Continue aprendendo¶
DOCS / OPINIÃO
Esta página deixou alguma pergunta?
Conte onde a explicação perdeu você. A documentação também faz parte da experiência da linguagem.