3. Tratamento de falhas e design de APIs
Liste as falhas de cada operação antes de escolher a API. Preserve erros acionáveis, acrescente contexto determinístico em uma camada e converta para status de processo apenas na entrada. Exercite pressão de alocação, entrada truncada, chamadas repetidas, escritas parciais, timeouts e limpeza após erro.
O benchmark deve medir a operação enfrentada pelo usuário: inclua preparação apenas se ele pagar esse custo, consuma os resultados, aqueça runtimes comparáveis e reporte distribuições. Registre versões, alvo, otimização, entrada e comando para criar uma linha de base futura.
Mantenha módulos coesos e poucos exports. Separe formatos persistentes dos modelos em memória, dificulte estados inválidos e prefira uma conversão explícita a políticas implícitas espalhadas.
Exemplo testado
Mapeie falhas de domínio tipadas para mensagens CLI e códigos de saída estáveis em um só lugar.
Trecho em foco
curriculum/main-evidence/projects/ledger/src/main.fl:20–55
command := values[1].view(); error result = none; if command == "create" and values.len() == 3 { result = commands::create(values[2].view()); } else if command == "append" and values.len() == 4 { result = commands::append(values[2].view(), values[3].view()); } else if command == "verify" and values.len() == 3 { result = commands::verify(values[2].view()); } else if command == "list" and values.len() == 3 { result = commands::list(values[2].view()); } else if command == "help" or command == "--help" or command == "-h" { print_usage(); return 0; } else { result = model::LedgerError.Usage; }
if result == none { return 0; } if result == model::LedgerError.Usage { print_usage(); return 2; } if result == model::LedgerError.InvalidFormat or result == model::LedgerError.PayloadTooLarge { fmt::eprintln("ledger: invalid ledger format"); return 4; } if result == model::LedgerError.IntegrityFailure { fmt::eprintln("ledger: integrity verification failed"); return 5; }
fmt::eprintln("ledger: operation failed"); return 3;Este trecho depende dos outros arquivos do pacote. O código completo compilado está disponível abaixo.
Código completo do pacote (8 arquivos)
longbow.toml
[package]name = "ledger"version = "0.1.0"kind = "program"
[dependencies]longbow.lock
{ "schema": 1, "root": "root", "packages": [ { "id": "root", "identity": "local/ledger", "source": { "kind": "workspace", "path": "." }, "version": "0.1.0", "dependencies": { "std": "toolchain-std" } }, { "id": "toolchain-std", "identity": "flecha/std", "source": { "kind": "toolchain" }, "dependencies": {} } ]}src/codec.fl
module codec;
@imports :: root::model :: lib::std::bytes::buffer :: lib::std::bytes::sequence :: lib::std::containers::collections :: lib::std::cryptography::sha2 :: lib::std::encoding::binary :: lib::std::text::strings;
export const stringv FILE_HEADER = "FLECHA-LEDGER\n1\n";const stringv HASH_DOMAIN = "FLECHA-LEDGER-BLOCK-V1\n";const usize RECORD_HEADER_SIZE = 48;const usize DIGEST_SIZE = 32;const usize RECORD_OVERHEAD = RECORD_HEADER_SIZE + DIGEST_SIZE;
export (buffer::Buffer, [32]u8, error) encode_block( u64 index, []const u8 previousHash, stringv payload) { usize payloadLength = payload.len().cast<usize>; usize capacity = RECORD_OVERHEAD + payloadLength; if capacity < payloadLength or previousHash.len() != DIGEST_SIZE { empty, allocationError := buffer::Buffer::withCapacity(0); if allocationError != none { return (empty, none, allocationError); } return (empty, none, model::LedgerError.PayloadTooLarge); }
output, allocationError := buffer::Buffer::withCapacity(capacity); if allocationError != none { return (output, none, allocationError); }
[48]u8 header = none; headerWriter := binary::Writer::init(header[:]); error writeError = headerWriter.writeBig<u64>(index); if writeError == none { writeError = headerWriter.writeBig<u64>(payloadLength.cast<u64>); } if writeError == none { writeError = headerWriter.write(previousHash); } if writeError != none { return (output, none, writeError); }
digest, digestError := hash_block(header[:], payload); if digestError != none { return (output, none, digestError); } [32]u8 digestBytes = model::copy_digest(digest.bytes());
writeError = output.write(header[:]); if writeError == none { writeError = output.write(payload.cast<[]const u8>); } if writeError == none { writeError = output.write(digestBytes[:]); } return (output, digestBytes, writeError);}
(sha2::Sha256Digest, error) hash_block( []const u8 header, stringv payload) { hasher := sha2::Sha256::init(); error digestError = hasher.update(HASH_DOMAIN.cast<[]const u8>); if digestError == none { digestError = hasher.update(header); } if digestError == none { digestError = hasher.update(payload.cast<[]const u8>); } if digestError != none { return (none, digestError); } return hasher.finalize();}
export (collections::Array<model::Block>, error) parse(stringv contents) { blocks := collections::Array<model::Block>::init(); if contents.len() < FILE_HEADER.len() or contents.substr(0, FILE_HEADER.len()) != FILE_HEADER { return (blocks, model::LedgerError.InvalidFormat); }
source := contents.cast<[]const u8>; reader := binary::Reader::init(source[FILE_HEADER.len().cast<usize>:]); u64 expectedIndex = 0; [32]u8 expectedPrevious = none;
while !reader.empty() { if reader.remaining() < RECORD_OVERHEAD { return (blocks, model::LedgerError.InvalidFormat); }
index, indexError := reader.readBig<u64>(); payloadLength, lengthError := reader.readBig<u64>(); if indexError != none or lengthError != none { return (blocks, model::LedgerError.InvalidFormat); } [32]u8 previousValue = none; { previous, previousError := reader.read(DIGEST_SIZE); if previousError != none { return (blocks, model::LedgerError.InvalidFormat); } previousValue = model::copy_digest(previous); } if payloadLength > (reader.remaining() - DIGEST_SIZE).cast<u64> { return (blocks, model::LedgerError.InvalidFormat); }
string payload = none; { payloadBytes, payloadError := reader.read( payloadLength.cast<usize> ); if payloadError != none { return (blocks, model::LedgerError.InvalidFormat); } error allocationError; payload, allocationError = strings::string_from_bytes(payloadBytes); if allocationError != none { return (blocks, allocationError); } } [32]u8 digestValue = none; { storedDigest, storedError := reader.read(DIGEST_SIZE); if storedError != none { return (blocks, model::LedgerError.InvalidFormat); } digestValue = model::copy_digest(storedDigest); } if index != expectedIndex or !sequence::constant_time_equal( previousValue[:], expectedPrevious[:] ) { return (blocks, model::LedgerError.IntegrityFailure); }
[48]u8 canonicalHeader = none; canonicalWriter := binary::Writer::init(canonicalHeader[:]); error canonicalError = canonicalWriter.writeBig<u64>(index); if canonicalError == none { canonicalError = canonicalWriter.writeBig<u64>(payloadLength); } if canonicalError == none { canonicalError = canonicalWriter.write(previousValue[:]); } if canonicalError != none { return (blocks, canonicalError); }
calculated, digestError := hash_block( canonicalHeader[:], payload.view() ); if digestError != none { return (blocks, digestError); } if !sequence::constant_time_equal(calculated.bytes(), digestValue[:]) { return (blocks, model::LedgerError.IntegrityFailure); }
pushError := blocks.push(model::Block::init( index, previousValue, payload, digestValue )); if pushError != none { return (blocks, pushError); }
expectedPrevious = digestValue; expectedIndex++; if expectedIndex == 0 and !reader.empty() { return (blocks, model::LedgerError.InvalidFormat); } }
return (blocks, none);}src/codec_test.fl
module codec_test;
@imports :: root::codec :: root::model :: lib::std::bytes::buffer :: lib::std::bytes::sequence :: lib::std::encoding::hex :: lib::std::tests::asserts :: lib::std::tests::testing :: lib::std::text::strings;
const stringv ALPHA_DIGEST = "aba5ed27f8ee302525deccc60adfb950fd3d71699082bb3b5c0bcad6a36ec139";const stringv BETA_DIGEST = "d4298ba679357d6f92c5ea1da21f8895a3e6d514b35e51b3e68081744d9f6b4a";const stringv ALPHA_RECORD = "000000000000000000000000000000050000000000000000000000000000000000000000000000000000000000000000616c706861aba5ed27f8ee302525deccc60adfb950fd3d71699082bb3b5c0bcad6a36ec139";
test canonical_block_matches_published_golden_vector(&testing::UnitTest t) { [32]u8 genesis = none; record, digest, encodeError := codec::encode_block( 0, genesis[:], "alpha" ); asserts::is_true(t, encodeError == none, "golden block encodes"); if encodeError != none { return; }
recordHex, recordHexError := hex::encode_bytes(record.bytes()); digestHex, digestHexError := hex::encode_bytes(digest[:]); asserts::is_true( t, recordHexError == none and recordHex.view() == ALPHA_RECORD, "canonical record bytes match the golden vector" ); asserts::is_true( t, digestHexError == none and digestHex.view() == ALPHA_DIGEST, "domain-separated block hash matches the golden vector" );}
test chain_round_trips_and_links_every_block(&testing::UnitTest t) { chain, chainError := make_chain("alpha", "beta"); asserts::is_true(t, chainError == none, "chain construction succeeds"); if chainError != none { return; }
blocks, parseError := codec::parse(chain.view()); asserts::is_true(t, parseError == none, "canonical chain parses"); asserts::is_true(t, blocks.len() == 2, "both blocks reload"); if parseError != none or blocks.len() != 2 { return; }
values := blocks.view(); firstDigest, firstHexError := hex::encode_bytes(values[0].hash()); secondDigest, secondHexError := hex::encode_bytes(values[1].hash()); asserts::is_true( t, values[0].index() == 0 and values[0].payload() == "alpha", "the genesis block preserves its fields" ); asserts::is_true( t, values[1].index() == 1 and values[1].payload() == "beta", "the second block preserves its fields" ); asserts::is_true( t, firstHexError == none and firstDigest.view() == ALPHA_DIGEST, "genesis digest is stable" ); asserts::is_true( t, secondHexError == none and secondDigest.view() == BETA_DIGEST, "head digest is stable" ); asserts::is_true( t, sequence::equal(values[1].previousHash(), values[0].hash()), "the second block links to the genesis digest" );}
test identical_chains_produce_identical_bytes(&testing::UnitTest t) { first, firstError := make_chain("alpha", "beta"); second, secondError := make_chain("alpha", "beta"); asserts::is_true( t, firstError == none and secondError == none and first.view() == second.view(), "the canonical chain is reproducible" );}
test malformed_and_truncated_storage_is_rejected(&testing::UnitTest t) { malformed, malformedError := codec::parse("not-a-ledger"); asserts::is_true( t, malformedError == model::LedgerError.InvalidFormat and malformed.empty(), "a malformed header is rejected" );
chain, chainError := make_chain("alpha", "beta"); if chainError != none { t->fail(); return; } truncated, truncatedError := codec::parse( chain.view().substr(0, chain.len() - 1) ); asserts::is_true( t, truncatedError == model::LedgerError.InvalidFormat and truncated.len() == 1, "a truncated final digest is rejected after the valid prefix" );}
test tampered_payload_and_digest_are_rejected(&testing::UnitTest t) { chain, chainError := make_chain("alpha", "beta"); if chainError != none { t->fail(); return; }
tamperedPayload, payloadError := changed_byte(chain.view(), 64, 'A'); if payloadError != none { t->fail(); return; } _, tamperError := codec::parse(tamperedPayload.view()); asserts::is_true( t, tamperError == model::LedgerError.IntegrityFailure, "payload tampering is detected" );
tamperedDigest, digestError := changed_byte(chain.view(), 84, 0); if digestError != none { t->fail(); return; } _, storedDigestError := codec::parse(tamperedDigest.view()); asserts::is_true( t, storedDigestError == model::LedgerError.IntegrityFailure, "stored digest tampering is detected" );}
test reordered_blocks_are_rejected(&testing::UnitTest t) { chain, chainError := make_chain("alpha", "bravo"); if chainError != none { t->fail(); return; }
source := chain.view().cast<[]const u8>; reordered, allocationError := buffer::Buffer::withCapacity(source.len()); if allocationError != none or reordered.write(codec::FILE_HEADER.cast<[]const u8>) != none or reordered.write(source[101:186]) != none or reordered.write(source[16:101]) != none { t->fail(); return; } reorderedText, textError := strings::string_from_bytes(reordered.bytes()); if textError != none { t->fail(); return; }
_, parseError := codec::parse(reorderedText.view()); asserts::is_true( t, parseError == model::LedgerError.IntegrityFailure, "record reordering breaks the index and hash chain" );}
(string, error) make_chain(stringv firstPayload, stringv secondPayload) { output, allocationError := buffer::Buffer::withCapacity(256); if allocationError != none { return (none, allocationError); } error writeError = output.write(codec::FILE_HEADER.cast<[]const u8>); if writeError != none { return (none, writeError); }
[32]u8 genesis = none; first, firstDigest, firstError := codec::encode_block( 0, genesis[:], firstPayload ); if firstError != none { return (none, firstError); } second, _, secondError := codec::encode_block( 1, firstDigest[:], secondPayload ); if secondError != none { return (none, secondError); } writeError = output.write(first.bytes()); if writeError == none { writeError = output.write(second.bytes()); } if writeError != none { return (none, writeError); } return strings::string_from_bytes(output.bytes());}
(string, error) changed_byte(stringv source, usize offset, u8 value) { bytes, allocationError := new<own []u8>(source.len().cast<usize>); if allocationError != none { return (none, allocationError); } loop usize index = 0; index < bytes.len(); index++ { bytes[index] = source[index.cast<u32>]; } bytes[offset] = value; return strings::string_from_bytes(bytes[:]);}src/commands.fl
module commands;
@imports :: root::storage :: lib::std::encoding::hex :: lib::std::fmt;
export error create(stringv path) { result := storage::create(path); if result == none { fmt::println("created: 0 blocks"); } return result;}
export error append(stringv path, stringv payload) { index, digest, appendError := storage::append(path, payload); if appendError != none { return appendError; } fmt::print("appended: "); fmt::print(index); fmt::print(" "); fmt::println(digest.view()); return none;}
export error verify(stringv path) { blocks, loadError := storage::load(path); if loadError != none { return loadError; }
fmt::print("verified: "); fmt::print(blocks.len()); fmt::print(" blocks"); if !blocks.empty() { values := blocks.view(); head, hexError := hex::encode_bytes(values[values.len() - 1].hash()); if hexError != none { return hexError; } fmt::print(" "); fmt::print(head.view()); } fmt::println(""); return none;}
export error list(stringv path) { blocks, loadError := storage::load(path); if loadError != none { return loadError; }
values := blocks.view(); loop usize index = 0; index < values.len(); index++ { digest, digestError := hex::encode_bytes(values[index].hash()); if digestError != none { return digestError; } payload, payloadError := hex::encode(values[index].payload()); if payloadError != none { return payloadError; } fmt::print(values[index].index()); fmt::print(" "); fmt::print(digest.view()); fmt::print(" "); fmt::println(payload.view()); } return none;}src/main.fl
module main;
@imports :: root::commands :: root::model :: lib::std::fmt :: lib::std::os::arguments;
i32 main() { values, captureError := arguments::capture(); if captureError != none { fmt::eprintln("ledger: cannot capture command-line arguments"); return 3; } if values.len() < 2 { print_usage(); return 2; }
command := values[1].view(); error result = none; if command == "create" and values.len() == 3 { result = commands::create(values[2].view()); } else if command == "append" and values.len() == 4 { result = commands::append(values[2].view(), values[3].view()); } else if command == "verify" and values.len() == 3 { result = commands::verify(values[2].view()); } else if command == "list" and values.len() == 3 { result = commands::list(values[2].view()); } else if command == "help" or command == "--help" or command == "-h" { print_usage(); return 0; } else { result = model::LedgerError.Usage; }
if result == none { return 0; } if result == model::LedgerError.Usage { print_usage(); return 2; } if result == model::LedgerError.InvalidFormat or result == model::LedgerError.PayloadTooLarge { fmt::eprintln("ledger: invalid ledger format"); return 4; } if result == model::LedgerError.IntegrityFailure { fmt::eprintln("ledger: integrity verification failed"); return 5; }
fmt::eprintln("ledger: operation failed"); return 3;}
void print_usage() { fmt::println("ledger - deterministic append-only SHA-256 chain"); fmt::println(""); fmt::println("usage:"); fmt::println(" ledger create <file>"); fmt::println(" ledger append <file> <payload>"); fmt::println(" ledger verify <file>"); fmt::println(" ledger list <file>");}src/model.fl
module model;
export error LedgerError { Usage, InvalidFormat, IntegrityFailure, PayloadTooLarge}
export structure Block { private u64 blockIndex; private [32]u8 previousDigest; private string payloadValue; private [32]u8 blockDigest;};
Block Block::init( u64 blockIndex, [32]u8 previousDigest, string payloadValue, [32]u8 blockDigest) override { return build Block { self.blockIndex = blockIndex; self.previousDigest = previousDigest; self.payloadValue = payloadValue; self.blockDigest = blockDigest; };}
u64 Block::index(&const self) { return self->blockIndex;}
[]const u8 Block::previousHash(&const self) <-self { return self->previousDigest[:];}
stringv Block::payload(&const self) <-self { return self->payloadValue.view();}
[]const u8 Block::hash(&const self) <-self { return self->blockDigest[:];}
export [32]u8 copy_digest([]const u8 source) { [32]u8 result = none; loop usize index = 0; index < result.len(); index++ { result[index] = source[index]; } return result;}src/storage.fl
module storage;
@imports :: root::codec :: root::model :: lib::std::containers::collections :: lib::std::encoding::hex :: lib::std::io::file :: lib::std::io::filesystem;
export error create(stringv path) { options := build file::OpenOptions { self.write = true; self.createNew = true; }; output, openError := file::File::open(path, options); if openError != none { return openError; }
error result = output.writeAll(codec::FILE_HEADER.cast<[]const u8>); if result == none { result = output.flush(); } closeError := output.close(); if result == none { result = closeError; } if result != none { error _ = filesystem::remove_file(path); } return result;}
export (collections::Array<model::Block>, error) load(stringv path) { contents, readError := filesystem::read_file(path); if readError != none { return (collections::Array<model::Block>::init(), readError); } return codec::parse(contents.view());}
export (u64, string, error) append(stringv path, stringv payload) { blocks, loadError := load(path); if loadError != none { return (none, none, loadError); }
u64 index = blocks.len().cast<u64>; [32]u8 previous = none; if !blocks.empty() { values := blocks.view(); previous = model::copy_digest(values[values.len() - 1].hash()); }
record, digest, encodeError := codec::encode_block( index, previous[:], payload ); if encodeError != none { return (none, none, encodeError); }
output, openError := file::File::open(path, file::OpenOptions::appendOnly()); if openError != none { return (none, none, openError); } error result = output.writeAll(record.bytes()); if result == none { result = output.flush(); } closeError := output.close(); if result == none { result = closeError; } if result != none { return (none, none, result); }
digestText, hexError := hex::encode_bytes(digest[:]); return (index, digestText, hexError);}A fronteira da CLI traduz erros tipados do ledger uma vez. Os módulos de domínio não dependem do texto do terminal, enquanto os chamadores recebem mensagens e códigos de saída estáveis para automação.
Comandos
longbow check curriculum/main-evidence/projects/ledgerlongbow run curriculum/main-evidence/projects/ledger -- helpSaída esperada
ledger - deterministic append-only SHA-256 chain
usage: ledger create <file> ledger append <file> <payload> ledger verify <file> ledger list <file>status de saída: 0Referência relacionada: /examples/ledger/src/main/.