[M1] add manifest::parse
This commit is contained in:
@@ -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
285
src/manifest/parser.cpp
Normal 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
|
||||
Reference in New Issue
Block a user