#![cfg_attr(docsrs, feature(doc_auto_cfg))]
#![deny(missing_docs)]
use camino::Utf8PathBuf;
#[cfg(feature = "builder")]
use derive_builder::Builder;
use std::collections::BTreeMap;
use std::env;
use std::ffi::OsString;
use std::fmt;
use std::hash::Hash;
use std::path::PathBuf;
use std::process::{Command, Stdio};
use std::str::from_utf8;
pub use camino;
pub use semver;
use semver::Version;
pub use dependency::Dependency;
#[cfg(feature = "builder")]
pub use dependency::DependencyBuilder;
pub use errors::{Error, Result};
use serde::{Deserialize, Serialize};
mod dependency;
mod errors;
#[derive(Clone, Serialize, Deserialize, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
#[serde(transparent)]
pub struct PackageId {
pub repr: String,
}
impl fmt::Display for PackageId {
fn fmt(
&self,
formatter: &mut fmt::Formatter<'_>,
) -> fmt::Result {
fmt::Display::fmt(&self.repr, formatter)
}
}
const fn is_null(value: &serde_json::Value) -> bool {
matches!(value, serde_json::Value::Null)
}
#[derive(Clone, Serialize, Deserialize, Debug, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "builder", derive(Builder))]
#[non_exhaustive]
#[cfg_attr(feature = "builder", builder(pattern = "owned", setter(into)))]
pub struct Metadata {
pub package_manager: PackageManager,
pub packages: Vec<Package>,
pub resolve: Option<Resolve>,
pub target_directory: Utf8PathBuf,
pub version: usize,
pub root_package_directory: Utf8PathBuf,
}
#[derive(Clone, Copy, Serialize, Deserialize, Debug, PartialEq, Eq, Hash)]
pub enum PackageManager {
Npm,
Cargo,
}
impl Metadata {
#[must_use]
pub fn root_package(&self) -> Option<&Package> {
if let Some(resolve) = &self.resolve {
let root = resolve.root.as_ref()?;
self.packages.iter().find(|pkg| &pkg.id == root)
} else {
let root_manifest_path = self.root_package_directory.join("wesl.toml");
self.packages
.iter()
.find(|pkg| pkg.manifest_path == root_manifest_path)
}
}
}
impl<'item> std::ops::Index<&'item PackageId> for Metadata {
type Output = Package;
fn index(
&self,
index: &'item PackageId,
) -> &Self::Output {
self.packages
.iter()
.find(|package| package.id == *index)
.unwrap_or_else(|| panic!("no package with this id: {index:?}"))
}
}
#[derive(Clone, Serialize, Deserialize, Debug, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "builder", derive(Builder))]
#[non_exhaustive]
#[cfg_attr(feature = "builder", builder(pattern = "owned", setter(into)))]
pub struct Resolve {
pub nodes: Vec<Node>,
pub root: Option<PackageId>,
}
impl<'item> std::ops::Index<&'item PackageId> for Resolve {
type Output = Node;
fn index(
&self,
index: &'item PackageId,
) -> &Self::Output {
self.nodes
.iter()
.find(|package| package.id == *index)
.unwrap_or_else(|| panic!("no Node with this id: {index:?}"))
}
}
#[derive(Clone, Serialize, Deserialize, Debug, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "builder", derive(Builder))]
#[non_exhaustive]
#[cfg_attr(feature = "builder", builder(pattern = "owned", setter(into)))]
pub struct Node {
pub id: PackageId,
#[serde(default)]
pub renamed_dependencies: Vec<NodeDependency>,
pub dependencies: Vec<PackageId>,
}
#[derive(Clone, Serialize, Deserialize, Debug, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "builder", derive(Builder))]
#[non_exhaustive]
#[cfg_attr(feature = "builder", builder(pattern = "owned", setter(into)))]
pub struct NodeDependency {
pub name: String,
pub pkg: PackageId,
}
#[derive(Clone, Serialize, Deserialize, Debug, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "builder", derive(Builder))]
#[non_exhaustive]
#[cfg_attr(feature = "builder", builder(pattern = "owned", setter(into)))]
pub struct Package {
pub name: String,
pub version: Version,
#[serde(default)]
#[cfg_attr(feature = "builder", builder(default))]
pub authors: Vec<String>,
pub id: PackageId,
pub source: Option<Source>,
#[cfg_attr(feature = "builder", builder(default))]
pub description: Option<String>,
#[cfg_attr(feature = "builder", builder(default))]
pub dependencies: Vec<Dependency>,
#[cfg_attr(feature = "builder", builder(default))]
pub license: Option<String>,
#[cfg_attr(feature = "builder", builder(default))]
pub license_file: Option<Utf8PathBuf>,
pub manifest_path: Utf8PathBuf,
#[serde(default)]
#[cfg_attr(feature = "builder", builder(default))]
pub categories: Vec<String>,
#[serde(default)]
#[cfg_attr(feature = "builder", builder(default))]
pub keywords: Vec<String>,
#[cfg_attr(feature = "builder", builder(default))]
pub readme: Option<Utf8PathBuf>,
#[cfg_attr(feature = "builder", builder(default))]
pub repository: Option<String>,
#[cfg_attr(feature = "builder", builder(default))]
pub homepage: Option<String>,
#[cfg_attr(feature = "builder", builder(default))]
pub documentation: Option<String>,
#[serde(default)]
#[cfg_attr(feature = "builder", builder(default))]
pub edition: Edition,
#[serde(default, skip_serializing_if = "is_null")]
#[cfg_attr(feature = "builder", builder(default))]
pub metadata: serde_json::Value,
}
#[cfg(feature = "builder")]
impl PackageBuilder {
pub fn new<
Namish: Into<String>,
Versionish: Into<Version>,
PackageIdish: Into<PackageId>,
Pathish: Into<Utf8PathBuf>,
>(
name: Namish,
version: Versionish,
id: PackageIdish,
path: Pathish,
) -> Self {
Self::default()
.name(name)
.version(version)
.id(id)
.manifest_path(path)
}
}
impl Package {
#[must_use]
pub fn license_file(&self) -> Option<Utf8PathBuf> {
self.license_file.as_ref().map(|file| {
self.manifest_path
.parent()
.unwrap_or(&self.manifest_path)
.join(file)
})
}
#[must_use]
pub fn readme(&self) -> Option<Utf8PathBuf> {
self.readme.as_ref().map(|file| {
self.manifest_path
.parent()
.unwrap_or(&self.manifest_path)
.join(file)
})
}
}
#[derive(Clone, Serialize, Deserialize, Debug, PartialEq, Eq, Hash)]
#[serde(transparent)]
pub struct Source {
pub representation: String,
}
impl Source {
#[must_use]
pub fn is_crates_io(&self) -> bool {
self.representation == "registry+https://github.com/rust-lang/crates.io-index"
}
#[must_use]
pub fn is_npmjs_org(&self) -> bool {
self.representation == "registry+https://registry.npmjs.org/"
}
}
impl fmt::Display for Source {
fn fmt(
&self,
formatter: &mut fmt::Formatter<'_>,
) -> fmt::Result {
fmt::Display::fmt(&self.representation, formatter)
}
}
#[derive(Clone, Serialize, Deserialize, Debug, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "builder", derive(Builder))]
#[cfg_attr(feature = "builder", builder(pattern = "owned", setter(into)))]
#[non_exhaustive]
pub struct Target {
pub name: String,
#[serde(default)]
#[cfg_attr(feature = "builder", builder(default))]
#[serde(rename = "required-features")]
pub required_features: Vec<String>,
pub src_path: Utf8PathBuf,
#[serde(default)]
#[cfg_attr(feature = "builder", builder(default))]
pub edition: Edition,
#[serde(default = "default_true")]
#[cfg_attr(feature = "builder", builder(default = "true"))]
pub doctest: bool,
#[serde(default = "default_true")]
#[cfg_attr(feature = "builder", builder(default = "true"))]
pub test: bool,
#[serde(default = "default_true")]
#[cfg_attr(feature = "builder", builder(default = "true"))]
pub doc: bool,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Hash, PartialOrd, Ord)]
#[non_exhaustive]
#[derive(Default)]
pub enum Edition {
#[serde(rename = "WGSL")]
#[default]
Wgsl,
#[serde(rename = "WESL")]
WeslUnstable2025,
}
impl Edition {
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::Wgsl => "WGSL",
Self::WeslUnstable2025 => "WESL",
}
}
}
impl fmt::Display for Edition {
fn fmt(
&self,
formatter: &mut fmt::Formatter<'_>,
) -> fmt::Result {
formatter.write_str(self.as_str())
}
}
const fn default_true() -> bool {
true
}
#[derive(Debug, Clone, Default)]
pub struct MetadataCommand {
wesl_path: Option<PathBuf>,
manifest_path: Option<PathBuf>,
current_dir: Option<PathBuf>,
no_dependencies: bool,
other_options: Vec<String>,
env: BTreeMap<OsString, Option<OsString>>,
verbose: bool,
}
impl MetadataCommand {
#[must_use]
pub fn new() -> Self {
Self::default()
}
pub fn wesl_path<Pathish: Into<PathBuf>>(
&mut self,
path: Pathish,
) -> &mut Self {
self.wesl_path = Some(path.into());
self
}
pub fn manifest_path<Pathish: Into<PathBuf>>(
&mut self,
path: Pathish,
) -> &mut Self {
self.manifest_path = Some(path.into());
self
}
pub fn current_dir<Pathish: Into<PathBuf>>(
&mut self,
path: Pathish,
) -> &mut Self {
self.current_dir = Some(path.into());
self
}
pub const fn no_dependencies(&mut self) -> &mut Self {
self.no_dependencies = true;
self
}
pub fn other_options<Options: Into<Vec<String>>>(
&mut self,
options: Options,
) -> &mut Self {
self.other_options = options.into();
self
}
pub fn env<K: Into<OsString>, V: Into<OsString>>(
&mut self,
key: K,
val: V,
) -> &mut Self {
self.env.insert(key.into(), Some(val.into()));
self
}
pub fn env_remove<K: Into<OsString>>(
&mut self,
key: K,
) -> &mut Self {
self.env.insert(key.into(), None);
self
}
pub const fn verbose(
&mut self,
verbose: bool,
) -> &mut Self {
self.verbose = verbose;
self
}
#[must_use]
pub fn wesl_command(&self) -> Command {
let wesl = self
.wesl_path
.clone()
.or_else(|| env::var("WESL").map(PathBuf::from).ok())
.unwrap_or_else(|| PathBuf::from("wesl"));
let mut cmd = Command::new(wesl);
cmd.arg("metadata");
if self.no_dependencies {
cmd.arg("--no-dependencies");
}
if let Some(path) = self.current_dir.as_ref() {
cmd.current_dir(path);
}
if let Some(manifest_path) = &self.manifest_path {
cmd.arg("--manifest-path").arg(manifest_path.as_os_str());
}
cmd.args(&self.other_options);
for (key, val) in &self.env {
match val {
Some(val) => cmd.env(key, val),
None => cmd.env_remove(key),
};
}
cmd
}
pub fn parse<T: AsRef<str>>(data: T) -> Result<Metadata> {
let meta = serde_json::from_str(data.as_ref())?;
Ok(meta)
}
pub fn exec(&self) -> Result<Metadata> {
let mut command = self.wesl_command();
if self.verbose {
command.stderr(Stdio::inherit());
}
let output = command.output()?;
if !output.status.success() {
return Err(Error::WeslMetadata {
stderr: String::from_utf8(output.stderr)?,
});
}
let stdout = from_utf8(&output.stdout)?
.lines()
.find(|line| line.starts_with('{'))
.ok_or(Error::NoJson)?;
Self::parse(stdout)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn todo() {}
}