[M1] add manifest::parse

This commit is contained in:
2026-05-08 00:23:46 +00:00
parent fba725e192
commit 95a8890623
7 changed files with 18463 additions and 0 deletions

View File

@@ -1,3 +1,37 @@
export module cargoxx.manifest;
import std;
import cargoxx.util;
export namespace cargoxx::manifest {
struct Dependency {
std::string name;
std::string version_spec;
std::vector<std::string> components;
};
struct BuildSettings {
bool warnings_as_errors = false;
std::vector<std::string> sanitizers;
};
enum class Edition { Cpp20, Cpp23, Cpp26 };
struct Package {
std::string name;
std::string version;
Edition edition = Edition::Cpp23;
std::vector<std::string> authors;
std::optional<std::string> license;
};
struct Manifest {
Package package;
std::vector<Dependency> dependencies;
BuildSettings build;
};
auto parse(const std::filesystem::path& path) -> util::Result<Manifest>;
} // namespace cargoxx::manifest

285
src/manifest/parser.cpp Normal file
View File

@@ -0,0 +1,285 @@
module;
#include <toml.hpp>
module cargoxx.manifest;
import std;
import cargoxx.util;
namespace cargoxx::manifest {
namespace {
using util::Error;
using util::ErrorCode;
auto err(ErrorCode code, std::string msg, std::filesystem::path path,
std::optional<std::pair<int, int>> line_col = std::nullopt) -> Error {
return Error{code, std::move(msg), "", std::move(path), line_col};
}
auto is_valid_name(std::string_view s) -> bool {
if (s.empty()) {
return false;
}
if (std::isdigit(static_cast<unsigned char>(s[0]))) {
return false;
}
for (char c : s) {
bool ok = std::isalnum(static_cast<unsigned char>(c)) || c == '_' || c == '-';
if (!ok) {
return false;
}
}
return true;
}
auto parse_edition(std::string_view s) -> std::optional<Edition> {
if (s == "cpp20") {
return Edition::Cpp20;
}
if (s == "cpp23") {
return Edition::Cpp23;
}
if (s == "cpp26") {
return Edition::Cpp26;
}
return std::nullopt;
}
auto source_pos(const toml::node& n) -> std::pair<int, int> {
auto src = n.source();
return {static_cast<int>(src.begin.line), static_cast<int>(src.begin.column)};
}
auto extract_string_array(const toml::array& arr, std::string_view field,
const std::filesystem::path& path)
-> util::Result<std::vector<std::string>> {
std::vector<std::string> out;
out.reserve(arr.size());
for (const auto& el : arr) {
if (auto s = el.value<std::string>()) {
out.push_back(*s);
} else {
return std::unexpected(err(
ErrorCode::ManifestInvalidField,
std::format("'{}' must be an array of strings", field),
path, source_pos(el)));
}
}
return out;
}
constexpr std::array PACKAGE_KNOWN_KEYS = {
"name", "version", "edition", "authors", "license", "description", "repository",
};
constexpr std::array BUILD_KNOWN_KEYS = {
"warnings_as_errors", "sanitizers",
};
constexpr std::array TOPLEVEL_KNOWN_KEYS = {
"package", "dependencies", "build",
// reserved (accepted, ignored for v0.1)
"dev-dependencies", "features", "workspace",
};
auto parse_package(const toml::table& tbl, const std::filesystem::path& path)
-> util::Result<Package> {
Package pkg;
for (const auto& [key, value] : tbl) {
std::string k{key.str()};
if (std::ranges::find(PACKAGE_KNOWN_KEYS, k) == PACKAGE_KNOWN_KEYS.end()) {
return std::unexpected(err(ErrorCode::ManifestUnknownField,
std::format("unknown key '[package].{}'", k), path,
source_pos(value)));
}
}
if (auto name = tbl["name"].value<std::string>()) {
if (!is_valid_name(*name)) {
return std::unexpected(err(ErrorCode::LayoutInvalidName,
std::format("invalid package name '{}'", *name), path,
source_pos(*tbl.get("name"))));
}
pkg.name = *name;
} else {
return std::unexpected(
err(ErrorCode::ManifestInvalidField, "[package].name is required", path));
}
if (auto version = tbl["version"].value<std::string>()) {
pkg.version = *version;
} else {
return std::unexpected(
err(ErrorCode::ManifestInvalidField, "[package].version is required", path));
}
if (auto edition = tbl["edition"].value<std::string>()) {
auto parsed = parse_edition(*edition);
if (!parsed) {
return std::unexpected(err(
ErrorCode::ManifestInvalidField,
std::format("invalid edition '{}': expected cpp20, cpp23, or cpp26", *edition),
path, source_pos(*tbl.get("edition"))));
}
pkg.edition = *parsed;
}
if (const auto* authors = tbl["authors"].as_array()) {
auto r = extract_string_array(*authors, "[package].authors", path);
if (!r) {
return std::unexpected(r.error());
}
pkg.authors = std::move(*r);
}
if (auto license = tbl["license"].value<std::string>()) {
pkg.license = *license;
}
return pkg;
}
auto parse_dependency(std::string name, const toml::node& value,
const std::filesystem::path& path) -> util::Result<Dependency> {
Dependency dep;
dep.name = std::move(name);
if (auto v = value.value<std::string>()) {
dep.version_spec = *v;
return dep;
}
if (const auto* tbl = value.as_table()) {
if (auto v = (*tbl)["version"].value<std::string>()) {
dep.version_spec = *v;
} else {
return std::unexpected(err(
ErrorCode::ManifestInvalidField,
std::format("dependency '{}' table must have a 'version' string", dep.name),
path, source_pos(value)));
}
if (const auto* comps = (*tbl)["components"].as_array()) {
auto r = extract_string_array(
*comps, std::format("dependencies.{}.components", dep.name), path);
if (!r) {
return std::unexpected(r.error());
}
dep.components = std::move(*r);
}
return dep;
}
return std::unexpected(err(
ErrorCode::ManifestInvalidField,
std::format("dependency '{}' must be a version string or a table", dep.name),
path, source_pos(value)));
}
auto parse_dependencies(const toml::table& tbl, const std::filesystem::path& path)
-> util::Result<std::vector<Dependency>> {
std::vector<Dependency> out;
out.reserve(tbl.size());
for (const auto& [key, value] : tbl) {
auto d = parse_dependency(std::string{key.str()}, value, path);
if (!d) {
return std::unexpected(d.error());
}
out.push_back(std::move(*d));
}
return out;
}
auto parse_build(const toml::table& tbl, const std::filesystem::path& path)
-> util::Result<BuildSettings> {
BuildSettings b;
for (const auto& [key, value] : tbl) {
std::string k{key.str()};
if (std::ranges::find(BUILD_KNOWN_KEYS, k) == BUILD_KNOWN_KEYS.end()) {
return std::unexpected(err(ErrorCode::ManifestUnknownField,
std::format("unknown key '[build].{}'", k), path,
source_pos(value)));
}
}
if (auto v = tbl["warnings_as_errors"].value<bool>()) {
b.warnings_as_errors = *v;
}
if (const auto* sans = tbl["sanitizers"].as_array()) {
auto r = extract_string_array(*sans, "[build].sanitizers", path);
if (!r) {
return std::unexpected(r.error());
}
b.sanitizers = std::move(*r);
}
return b;
}
} // namespace
auto parse(const std::filesystem::path& path) -> util::Result<Manifest> {
std::error_code ec;
if (!std::filesystem::exists(path, ec) || ec) {
return std::unexpected(err(ErrorCode::ManifestNotFound,
std::format("manifest not found: {}", path.string()), path));
}
toml::table root;
try {
root = toml::parse_file(path.string());
} catch (const toml::parse_error& e) {
auto src = e.source();
return std::unexpected(err(ErrorCode::ManifestParseError,
std::format("toml parse error: {}", e.description()), path,
std::pair{static_cast<int>(src.begin.line),
static_cast<int>(src.begin.column)}));
}
for (const auto& [key, value] : root) {
std::string k{key.str()};
if (std::ranges::find(TOPLEVEL_KNOWN_KEYS, k) == TOPLEVEL_KNOWN_KEYS.end()) {
return std::unexpected(err(ErrorCode::ManifestUnknownField,
std::format("unknown top-level key '{}'", k), path,
source_pos(value)));
}
}
Manifest m;
const auto* pkg_tbl = root["package"].as_table();
if (!pkg_tbl) {
return std::unexpected(
err(ErrorCode::ManifestInvalidField, "[package] table is required", path));
}
auto pkg = parse_package(*pkg_tbl, path);
if (!pkg) {
return std::unexpected(pkg.error());
}
m.package = std::move(*pkg);
if (const auto* deps_tbl = root["dependencies"].as_table()) {
auto deps = parse_dependencies(*deps_tbl, path);
if (!deps) {
return std::unexpected(deps.error());
}
m.dependencies = std::move(*deps);
}
if (const auto* build_tbl = root["build"].as_table()) {
auto build = parse_build(*build_tbl, path);
if (!build) {
return std::unexpected(build.error());
}
m.build = std::move(*build);
}
return m;
}
} // namespace cargoxx::manifest