// SPDX-License-Identifier: LGPL-3.0-only // SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts® module Crafter.Debug.Core:Codec_impl; import std; import :Value; import :Codec; using namespace Crafter::Debug; namespace { // "a string" or "an integer", for a kind as errors name it. std::string WithArticle(ValueKind kind) { const std::string_view name = KindName(kind); if (kind == ValueKind::Null) return std::string(name); const bool vowel = name.starts_with('a') || name.starts_with('i') || name.starts_with('o'); return std::format("{} {}", vowel ? "an" : "a", name); } std::size_t Distance(std::string_view a, std::string_view b) { std::vector previous(b.size() + 1); std::vector current(b.size() + 1); for (std::size_t j = 0; j <= b.size(); ++j) previous[j] = j; for (std::size_t i = 1; i <= a.size(); ++i) { current[0] = i; for (std::size_t j = 1; j <= b.size(); ++j) { const std::size_t substitution = previous[j - 1] + (a[i - 1] == b[j - 1] ? 0 : 1); current[j] = std::min({ previous[j] + 1, current[j - 1] + 1, substitution }); } std::swap(previous, current); } return previous[b.size()]; } std::string Join(std::span names) { std::string joined; for (const std::string_view name : names) { if (!joined.empty()) joined += ", "; joined += name; } return joined; } } void DecodeContext::Fail(std::string message) { issues.push_back({ Path(), std::move(message) }); } std::string DecodeContext::Report() const { std::string report; for (const DecodeIssue& issue : issues) { if (!report.empty()) report += "; "; report += issue.path.empty() ? issue.message : std::format("{}: {}", issue.path, issue.message); } return report; } DecodeContext::Scope::~Scope() { context.segments.pop_back(); } DecodeContext::Scope DecodeContext::Field(std::string_view name) { segments.emplace_back(name); return Scope(*this); } DecodeContext::Scope DecodeContext::Index(std::size_t index) { segments.push_back(std::format("[{}]", index)); return Scope(*this); } std::string DecodeContext::Path() const { std::string path; for (const std::string& segment : segments) { if (!path.empty() && !segment.starts_with('[')) path += '.'; path += segment; } return path; } std::optional Crafter::Debug::Suggest(std::string_view word, std::span candidates) { const std::size_t allowed = word.size() <= 4 ? 1 : 2; std::optional best; std::size_t bestDistance = allowed + 1; for (const std::string_view candidate : candidates) { const std::size_t distance = Distance(word, candidate); if (distance < bestDistance) { best = candidate; bestDistance = distance; } } return best; } std::string Crafter::Debug::Detail::Expected(std::string_view wanted, const Value& got) { return std::format("expected {}, got {}", wanted, WithArticle(got.Kind())); } Value Crafter::Debug::Detail::Type(std::string_view type) { Value description = Value::MakeObject(); description.Set("type", Value(type)); return description; } std::string Crafter::Debug::Detail::UnknownField(std::string_view key, std::span fields) { if (fields.empty()) return "unknown field; this takes no arguments"; if (const auto suggestion = Suggest(key, fields)) { return std::format("unknown field; did you mean '{}'? (fields: {})", *suggestion, Join(fields)); } return std::format("unknown field (fields: {})", Join(fields)); } std::string Crafter::Debug::Detail::UnknownName(std::string_view name, std::span names) { if (const auto suggestion = Suggest(name, names)) { return std::format("'{}' is not a name here; did you mean '{}'? (one of: {})", name, *suggestion, Join(names)); } return std::format("'{}' is not a name here (one of: {})", name, Join(names)); }