use crate::dependency::Dependencies;
use crate::diagnostic::{PathSegment, SchemaDiagnostic, Severity, child_path};
use crate::ordered::{MAX_SAFE_INTEGER, OrderedValue};
use crate::package::PackageMetadata;
use crate::read::DefinitionRead;
use serde::Serialize;
#[derive(Debug, Clone, Serialize)]
pub struct ManifestRead {
#[serde(flatten)]
pub package: DefinitionRead<PackageMetadata>,
#[serde(flatten)]
pub dependencies: Dependencies,
}
pub fn read(content: &str) -> ManifestRead {
let value: toml::Value = match toml::from_str(content) {
Ok(value) => value,
Err(error) => {
return ManifestRead::unread(vec![SchemaDiagnostic::new(
"toml.invalid",
Severity::Error,
Vec::new(),
error.to_string(),
)]);
}
};
let document = content
.parse::<toml_edit::DocumentMut>()
.expect("content that parses as TOML parses as a document");
let mut reading = read_package(&document, &value);
let dependencies = if reading.fields.is_some() {
Dependencies::read(&document, &mut reading.diagnostics)
} else {
Dependencies::default()
};
ManifestRead {
package: reading,
dependencies,
}
}
impl ManifestRead {
pub fn unread(diagnostics: Vec<SchemaDiagnostic>) -> Self {
Self {
package: DefinitionRead::unread(diagnostics),
dependencies: Dependencies::default(),
}
}
}
fn read_package(
document: &toml_edit::DocumentMut,
value: &toml::Value,
) -> DefinitionRead<PackageMetadata> {
use crate::diagnostic::key_path;
let Some(package) = value.get("package").and_then(toml::Value::as_table) else {
return DefinitionRead::unread(vec![SchemaDiagnostic::new(
"manifest.package-required",
Severity::Error,
key_path("package"),
"manifest requires a [package] table",
)]);
};
let known = [
"name",
"version",
"description",
"schema",
"requires-mars",
"requires-meridian",
"authors",
"homepage",
"license",
"uses-providers",
];
if let Some(toml::Value::Integer(schema @ 2..)) = package.get("schema") {
return DefinitionRead::unread(vec![SchemaDiagnostic::new(
"schema.newer",
Severity::Error,
child_path("package", "schema"),
format!(
"package declares schema {schema}; this Mars (v{}) reads schema 1 — upgrade Mars",
crate::VERSION
),
)]);
}
let mut diagnostics = Vec::new();
for key in package.keys() {
if key == "visibility" {
diagnostics.push(SchemaDiagnostic::new(
"field.removed",
Severity::Error,
child_path("package", "visibility"),
"`package.visibility` was removed; remove it",
));
} else if key == "bootstrap" {
diagnostics.push(SchemaDiagnostic::new(
"field.unknown",
Severity::Warning,
child_path("package", "bootstrap"),
"`package.bootstrap` is not read; Mars finds bootstrap docs in \
`bootstrap/<name>/BOOTSTRAP.md` — move the doc there and remove the field",
));
} else if !known.contains(&key.as_str()) {
diagnostics.push(SchemaDiagnostic::new(
"field.unknown",
Severity::Warning,
child_path("package", key),
format!("unknown package field `{key}` is preserved"),
));
}
}
if let Some(toml::Value::Array(entries)) = package.get("uses-providers") {
for (index, entry) in entries.iter().enumerate() {
if let toml::Value::String(name) = entry
&& !crate::provider::is_provider_name(name)
{
let mut path = child_path("package", "uses-providers");
path.push(PathSegment::Index(index));
diagnostics.push(SchemaDiagnostic::new(
"field.value",
Severity::Error,
path,
format!(
"`package.uses-providers` entry `{name}` is not a provider name; a provider name matches [a-z0-9][a-z0-9-]*"
),
));
}
}
}
if !matches!(package.get("schema"), Some(toml::Value::Integer(1))) {
diagnostics.push(SchemaDiagnostic::new(
"field.type",
Severity::Error,
child_path("package", "schema"),
"`package.schema` must be the integer 1",
));
}
if !matches!(package.get("name"), Some(toml::Value::String(value)) if !value.trim().is_empty())
{
diagnostics.push(SchemaDiagnostic::new(
"manifest.name-required",
Severity::Error,
child_path("package", "name"),
"package name must be a non-empty string",
));
}
match package.get("version") {
None => diagnostics.push(SchemaDiagnostic::new(
"manifest.version-missing",
Severity::Warning,
child_path("package", "version"),
"package version is missing",
)),
Some(toml::Value::String(_)) => {}
Some(_) => diagnostics.push(SchemaDiagnostic::new(
"field.type",
Severity::Error,
child_path("package", "version"),
"`package.version` must be a string",
)),
}
let fields = match toml::Value::Table(package.clone()).try_into::<PackageMetadata>() {
Ok(fields) => Some(fields),
Err(error) => {
if !diagnostics.iter().any(SchemaDiagnostic::is_error) {
diagnostics.push(SchemaDiagnostic::new(
"manifest.invalid-field",
Severity::Error,
key_path("package"),
format!("invalid package field: {error}"),
));
}
None
}
};
DefinitionRead {
frontmatter: package_pairs(document, &mut diagnostics),
fields,
body: None,
diagnostics,
}
}
fn package_pairs(
document: &toml_edit::DocumentMut,
diagnostics: &mut Vec<crate::diagnostic::SchemaDiagnostic>,
) -> Vec<(String, OrderedValue)> {
use toml_edit::Item;
let Some(package) = document.get("package").and_then(Item::as_table_like) else {
return Vec::new();
};
let mut pairs = Vec::new();
for (key, item) in package.iter() {
match item_json(item).and_then(|value| reject_key(key).map(|()| value)) {
Ok(value) => pairs.push((key.to_owned(), value)),
Err(message) => diagnostics.push(SchemaDiagnostic::new(
"toml.non-json-value",
Severity::Error,
child_path("package", key),
message,
)),
}
}
pairs
}
fn reject_key(key: &str) -> Result<(), String> {
crate::ordered::reject_unholdable_key(key)
}
fn item_json(item: &toml_edit::Item) -> Result<OrderedValue, String> {
use toml_edit::{Item, Value};
Ok(match item {
Item::None => OrderedValue::Null,
Item::Value(Value::String(v)) => OrderedValue::String(v.value().clone()),
Item::Value(Value::Integer(v)) => {
let n = *v.value();
if n.unsigned_abs() > MAX_SAFE_INTEGER {
return Err(format!(
"integer {n} is beyond ±(2^53 − 1), which JavaScript cannot hold exactly"
));
}
OrderedValue::Number(n.into())
}
Item::Value(Value::Float(v)) => serde_json::Number::from_f64(*v.value())
.map(OrderedValue::Number)
.ok_or("NaN and infinity have no JSON representation")?,
Item::Value(Value::Boolean(v)) => OrderedValue::Bool(*v.value()),
Item::Value(Value::Datetime(v)) => OrderedValue::String(v.value().to_string()),
Item::Value(Value::Array(values)) => OrderedValue::Array(
values
.iter()
.map(|value| item_json(&Item::Value(value.clone())))
.collect::<Result<_, _>>()?,
),
Item::ArrayOfTables(tables) => OrderedValue::Array(
tables
.iter()
.map(|table| item_json(&Item::Table(table.clone())))
.collect::<Result<_, _>>()?,
),
Item::Value(Value::InlineTable(_)) | Item::Table(_) => {
let table = item.as_table_like().expect("a table item is table-like");
let mut out = indexmap::IndexMap::new();
for (key, item) in table.iter() {
reject_key(key)?;
out.insert(key.to_owned(), item_json(item)?);
}
OrderedValue::Object(out)
}
})
}