118 lines
4.1 KiB
C++
118 lines
4.1 KiB
C++
|
|
// 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<std::size_t> previous(b.size() + 1);
|
||
|
|
std::vector<std::size_t> 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<const std::string_view> 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<std::string_view> Crafter::Debug::Suggest(std::string_view word, std::span<const std::string_view> candidates) {
|
||
|
|
const std::size_t allowed = word.size() <= 4 ? 1 : 2;
|
||
|
|
std::optional<std::string_view> 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<const std::string_view> 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<const std::string_view> 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));
|
||
|
|
}
|