Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
85 changes: 85 additions & 0 deletions crates/vt/src/session/json/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
//! Conventions shared by the JSON documents `vp run` prints: task IDs, paths, the `version`
//! field, and the JSON Schema that `$schema` points to.

mod schema;

use std::borrow::Cow;

use cow_utils::CowUtils as _;
#[cfg(test)]
pub use schema::materialize;
pub use schema::schema_url;
use serde::{Serialize, Serializer};
use vt_path::AbsolutePath;
use vt_str::Str;

/// A document's format version, `N`. Each document has its own, so a breaking change to one
/// bumps only its `N`.
#[derive(Debug, Clone, Copy, Default)]
pub struct Version<const N: u8>;

impl<const N: u8> Serialize for Version<N> {
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
serializer.serialize_u8(N)
}
}

#[cfg(test)]
impl<const N: u8> schemars::JsonSchema for Version<N> {
fn inline_schema() -> bool {
true
}

fn schema_name() -> Cow<'static, str> {
Cow::Borrowed("Version")
}

fn json_schema(_generator: &mut schemars::SchemaGenerator) -> schemars::Schema {
schemars::json_schema!({ "type": "integer", "const": N })
}
}

/// The ID of the task named `name` in the package at `package` (`"."` for the root).
#[must_use]
pub fn task_id(package: &str, name: &str) -> Str {
let name = encode_id_part(name);
if package == "." {
Str::from(name.as_ref())
} else {
vt_str::format!("{}#{name}", encode_id_part(package))
}
}

fn encode_id_part(part: &str) -> Cow<'_, str> {
match part.cow_replace('%', "%25") {
Cow::Borrowed(part) => part.cow_replace('#', "%23"),
Cow::Owned(part) => Cow::Owned(part.cow_replace('#', "%23").into_owned()),
}
}

/// `path` relative to the workspace root, with `/` separators and `"."` for the root
/// itself, or absolute if it's outside the workspace.
pub fn path_key(path: &AbsolutePath, workspace_root: &AbsolutePath) -> Str {
match path.strip_prefix(workspace_root) {
Ok(Some(relative)) if relative.as_str().is_empty() => Str::from("."),
Ok(Some(relative)) => Str::from(relative.as_str()),
Ok(None) | Err(_) => Str::from(path.as_path().to_string_lossy().as_ref()),
}
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn task_ids() {
assert_eq!(task_id(".", "ci"), "ci");
assert_eq!(task_id("packages/ui", "build"), "packages/ui#build");
// Encoding `#` keeps task `c` in the package at `a#b` apart from task `b#c` in `a`.
assert_eq!(task_id("a#b", "c"), "a%23b#c");
assert_eq!(task_id("a", "b#c"), "a#b%23c");
// Encoding `%` keeps root tasks `a#b` and `a%23b` apart.
assert_eq!(task_id(".", "a#b"), "a%23b");
assert_eq!(task_id(".", "a%23b"), "a%2523b");
}
}
80 changes: 80 additions & 0 deletions crates/vt/src/session/json/schema.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
//! The JSON Schema that `$schema` in a document points to.

use std::io;

use vt_materialized_artifact::Artifact;
use vt_path::{AbsolutePath, AbsolutePathBuf};
use vt_str::Str;

/// The `$schema` URL of a document whose JSON Schema is `artifact`. The schema is copied into
/// the user's own folder in the system temp dir, at a path named by a hash of its content,
/// unless an earlier run already did.
pub fn schema_url(artifact: Artifact) -> io::Result<Str> {
Ok(file_url(&materialize(artifact)?))
}

/// Copies the JSON Schema `artifact` into the user's own folder in the system temp dir,
/// unless an earlier run already did, and returns its path.
pub fn materialize(artifact: Artifact) -> io::Result<AbsolutePathBuf> {
let path = artifact.materialize().suffix(".json").at_user_temp_dir("vp_run")?;
AbsolutePathBuf::new(path)
.ok_or_else(|| io::Error::other("the system temp directory isn't an absolute path"))
}

/// `file://`, then the path with `/` separators, not percent-encoded, which both
/// VS Code and `JetBrains` IDEs read as a `$schema` location.
#[cfg(not(windows))]
fn file_url(path: &AbsolutePath) -> Str {
vt_str::format!("file://{}", path.as_path().to_string_lossy())
}

/// `file:///C:/…` for a path on a drive, and `file://host/share/…` for a UNC path.
#[cfg(windows)]
fn file_url(path: &AbsolutePath) -> Str {
use cow_utils::CowUtils as _;

let path = path.as_path().to_string_lossy();
let path = path.cow_replace('\\', "/");
if let Some(unc) = path.strip_prefix("//?/UNC/") {
return vt_str::format!("file://{unc}");
}
let path = path.strip_prefix("//?/").unwrap_or(&path);
if let Some(unc) = path.strip_prefix("//") {
return vt_str::format!("file://{unc}");
}
vt_str::format!("file:///{path}")
}

#[cfg(test)]
mod tests {
use super::*;

#[cfg(not(windows))]
#[test]
fn file_urls() {
let path = AbsolutePath::new("/var/my tmp/vp_run-501/task-graph_0123.json").unwrap();
assert_eq!(file_url(path), "file:///var/my tmp/vp_run-501/task-graph_0123.json");
}

#[cfg(windows)]
#[test]
fn file_urls() {
let url = |path: &str| file_url(AbsolutePath::new(path).unwrap());
assert_eq!(
url(r"C:\Users\me\App Data\Temp\vp_run\task-graph_0123.json"),
"file:///C:/Users/me/App Data/Temp/vp_run/task-graph_0123.json"
);
assert_eq!(
url(r"\\?\C:\Temp\task-graph_0123.json"),
"file:///C:/Temp/task-graph_0123.json"
);
assert_eq!(
url(r"\\host\share\task-graph_0123.json"),
"file://host/share/task-graph_0123.json"
);
assert_eq!(
url(r"\\?\UNC\host\share\task-graph_0123.json"),
"file://host/share/task-graph_0123.json"
);
}
}
1 change: 1 addition & 0 deletions crates/vt/src/session/mod.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
mod cache;
mod event;
mod execute;
mod json;
pub(crate) mod reporter;
mod task_graph_json;

Expand Down
64 changes: 5 additions & 59 deletions crates/vt/src/session/task_graph_json/document.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,14 @@
//! The doc comments here are the descriptions in `schemas/task-graph.json`, the JSON
//! Schema that `$schema` points to, which a test generates from these types.

use std::{borrow::Cow, collections::BTreeMap, sync::Arc};
use std::{collections::BTreeMap, sync::Arc};

use cow_utils::CowUtils as _;
use serde::{Serialize, Serializer};
use serde::Serialize;
use vt_path::AbsolutePath;
use vt_str::Str;

use crate::session::json::Version;

/// Every package and task in the workspace, and how the tasks depend on each other, as
/// printed by `vp run --json`.
#[derive(Debug, Serialize)]
Expand All @@ -23,7 +24,7 @@ pub struct TaskGraph {

/// Format version. It changes only for a breaking change: a rename, a removal, or a
/// changed meaning. New fields and values don't change it; ignore fields you don't know.
pub version: Version,
pub version: Version<1>,

/// Absolute path of the workspace root. Other paths are relative to it, with `/` on
/// every platform.
Expand All @@ -40,31 +41,6 @@ pub struct TaskGraph {
pub tasks: BTreeMap<Str, Task>,
}

/// The format version, `1`.
#[derive(Debug, Clone, Copy, Default)]
pub struct Version;

impl Serialize for Version {
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
serializer.serialize_u8(1)
}
}

#[cfg(test)]
impl schemars::JsonSchema for Version {
fn inline_schema() -> bool {
true
}

fn schema_name() -> Cow<'static, str> {
Cow::Borrowed("Version")
}

fn json_schema(_generator: &mut schemars::SchemaGenerator) -> schemars::Schema {
schemars::json_schema!({ "type": "integer", "const": 1 })
}
}

/// A package in the workspace.
#[derive(Debug, Default, Serialize)]
#[cfg_attr(test, derive(schemars::JsonSchema))]
Expand Down Expand Up @@ -140,40 +116,10 @@ impl Command {
}
}

/// The ID of the task named `name` in the package at `package` (`"."` for the root).
#[must_use]
pub fn task_id(package: &str, name: &str) -> Str {
let name = encode_id_part(name);
if package == "." {
Str::from(name.as_ref())
} else {
vt_str::format!("{}#{name}", encode_id_part(package))
}
}

fn encode_id_part(part: &str) -> Cow<'_, str> {
match part.cow_replace('%', "%25") {
Cow::Borrowed(part) => part.cow_replace('#', "%23"),
Cow::Owned(part) => Cow::Owned(part.cow_replace('#', "%23").into_owned()),
}
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn task_ids() {
assert_eq!(task_id(".", "ci"), "ci");
assert_eq!(task_id("packages/ui", "build"), "packages/ui#build");
// Encoding `#` keeps task `c` in the package at `a#b` apart from task `b#c` in `a`.
assert_eq!(task_id("a#b", "c"), "a%23b#c");
assert_eq!(task_id("a", "b#c"), "a#b%23c");
// Encoding `%` keeps root tasks `a#b` and `a%23b` apart.
assert_eq!(task_id(".", "a#b"), "a%23b");
assert_eq!(task_id(".", "a%23b"), "a%2523b");
}

#[test]
fn commands() {
let json = |commands: &[&str]| {
Expand Down
14 changes: 3 additions & 11 deletions crates/vt/src/session/task_graph_json/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,16 @@ mod schema;
use std::{collections::BTreeMap, sync::Arc};

pub use document::TaskGraph;
use document::{Command, Package, Task, Version, task_id};
use document::{Command, Package, Task};
use petgraph::visit::{EdgeRef as _, IntoNodeReferences as _};
pub use schema::schema_url;
use vt_graph::{IndexedTaskGraph, TaskGraphLoadError};
use vt_path::AbsolutePath;
use vt_str::Str;
use vt_workspace::{DependencyType, PackageNodeIndex};

use crate::session::json::{Version, path_key, task_id};

/// Builds the document from the task graph, loading every package's config, as listing
/// the tasks does.
pub async fn build(
Expand Down Expand Up @@ -85,13 +87,3 @@ pub async fn build(
tasks,
})
}

/// `path` relative to the workspace root, with `/` separators and `"."` for the root
/// itself, or absolute if it's outside the workspace.
fn path_key(path: &AbsolutePath, workspace_root: &AbsolutePath) -> Str {
match path.strip_prefix(workspace_root) {
Ok(Some(relative)) if relative.as_str().is_empty() => Str::from("."),
Ok(Some(relative)) => Str::from(relative.as_str()),
Ok(None) | Err(_) => Str::from(path.as_path().to_string_lossy().as_ref()),
}
}
Loading
Loading