Crafter.Review/implementations/Crafter.Review-PgWire.cpp
Jorijn van der Graaf 741e3a656d
feat(pipeline): the pure pipeline, checked against its goldens
The modules that turn a frozen range into passes and passes into a
verdict: :Schema, :Tools (the DENY, git and base lists, a leaf, so the
config and the pool both read them), :Plan, :Artifact, :Inventory,
:Dispositions, :State, :Normalize, :Stream, :Prompt, :Config, and the URL
half of :PgWire. Units, jobs, results, the config and the claude stream
stay JSON from end to end, so every key keeps its place; :Text gives JSON
values truthiness, get, ==, membership and Str(), and throws Dyn::Error
under a name for each kind of unexpected data.

Exactness where the bytes are seen:
- the stream classifiers are hand-written scanners with case-insensitive
  folds (the Kelvin sign is a k), Unicode \b and \s, and `.` stopping at
  \n; the SQL call detection takes `<exe> sql` with any path before it;
- config problems carry the JSON scanner's and the UTF-8 decoder's own
  texts, positions included;
- a float total is Neumaier's compensated sum, and the build passes
  -ffp-contract=off: -march=native would fuse a*b+c;
- `crafter-review inventory` and `config` print what the goldens record,
  byte for byte (the fixture and 14 configs, broken ones included); a
  message names this binary as the command to run.

tests/Goldens: the recorded answers over the fixtures next to them (the
commits fixture, a rich versioned series with CRLF, a folded subject,
Markdown headings in and out of fences, the code-key rewrites); the suite
compares 2304 results as DumpFile writes them, so a key out of order fails
too. All agree.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 06:48:55 +01:00

60 lines
2.7 KiB
C++

// SPDX-License-Identifier: AGPL-3.0-only
// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
module Crafter.Review:PgWire_impl;
import :PgWire;
import :Json;
import :Text;
import std;
namespace Crafter::Review::PgWire {
Url ParseUrl(std::string_view text) {
const Dyn::SplitUrl split = Dyn::UrlSplit(text);
Url url;
url.scheme = split.scheme;
url.host = Dyn::Lower(split.Hostname().value_or(""));
const std::optional<std::int32_t> port = split.Port();
url.port = port && *port != 0 ? *port : 5432;
const std::optional<std::string> user = split.Username();
url.user = Dyn::Unquote(user && !user->empty() ? *user : "postgres");
url.password = Dyn::Unquote(split.Password().value_or(""));
const std::string path = split.path.empty() ? "/" : split.path;
const std::string database = Dyn::LStrip(path, "/");
url.database = Dyn::Unquote(database.empty() ? "postgres" : database);
return url;
}
std::optional<std::string> CheckUrl(const Json& url) {
if (!url.is_string() || url.get_ref<const std::string&>().empty()) return "no database URL";
Url parsed;
try {
parsed = ParseUrl(url.get_ref<const std::string&>());
} catch (const std::invalid_argument& error) {
return std::format("not a URL ({})", error.what());
}
if (parsed.scheme != "postgres" && parsed.scheme != "postgresql") return "not a postgres:// or postgresql:// URL";
if (!std::ranges::contains(Loopback, parsed.host)) {
const std::vector<std::string> names(Loopback.begin(), Loopback.end());
return std::format("host {} is not this machine ({}): the database under test is a local stack, never a hosted one",
parsed.host.empty() ? "(none)" : parsed.host, Dyn::Join(names, ", "));
}
return std::nullopt;
}
std::string Redact(std::string_view url) {
try {
const Dyn::SplitUrl split = Dyn::UrlSplit(url);
if (!split.Password()) return std::string(url);
const std::string host = split.netloc.substr(split.netloc.rfind('@') + 1);
const std::string netloc = std::format("{}:***@{}", split.Username().value_or(""), host);
return Dyn::UrlUnsplit(split.scheme, netloc, split.path, split.query, split.fragment);
} catch (const std::invalid_argument&) {
return "(unparsable URL)";
}
}
std::string Where(std::string_view url) {
const Url parsed = ParseUrl(url);
return std::format("{} on {}:{} as {}", parsed.database, parsed.host, parsed.port, parsed.user);
}
}