diff --git a/examples/app_blueprint/blueprint.ron b/examples/app_blueprint/blueprint.ron index b26620058..5e1ca16dc 100644 --- a/examples/app_blueprint/blueprint.ron +++ b/examples/app_blueprint/blueprint.ron @@ -20,6 +20,10 @@ ), ], component_lifecycles: { + ( + registered_at: "app_blueprint", + import_path: "crate :: http_client", + ): Singleton, ( registered_at: "app_blueprint", import_path: "crate :: extract_path", @@ -28,10 +32,6 @@ registered_at: "app_blueprint", import_path: "crate :: logger", ): Transient, - ( - registered_at: "app_blueprint", - import_path: "crate :: http_client", - ): Singleton, }, router: { "/home": ( @@ -54,25 +54,25 @@ constructor_locations: { ( registered_at: "app_blueprint", - import_path: "crate :: extract_path", + import_path: "crate :: http_client", ): ( - line: 39, + line: 38, column: 10, file: "examples/app_blueprint/src/lib.rs", ), ( registered_at: "app_blueprint", - import_path: "crate :: http_client", + import_path: "crate :: logger", ): ( - line: 38, + line: 40, column: 10, file: "examples/app_blueprint/src/lib.rs", ), ( registered_at: "app_blueprint", - import_path: "crate :: logger", + import_path: "crate :: extract_path", ): ( - line: 40, + line: 39, column: 10, file: "examples/app_blueprint/src/lib.rs", ), diff --git a/libs/pavex/Cargo.toml b/libs/pavex/Cargo.toml index 5bab957fe..14d3a5c3d 100644 --- a/libs/pavex/Cargo.toml +++ b/libs/pavex/Cargo.toml @@ -24,3 +24,5 @@ itertools = "0.10.3" cargo-manifest = "0.3" toml = "0.5" pathdiff = "0.2.1" +elsa = "1.4.0" +tracing = "0.1" diff --git a/libs/pavex/src/rustdoc/compute.rs b/libs/pavex/src/rustdoc/compute.rs index 30eccd531..8f2d16a54 100644 --- a/libs/pavex/src/rustdoc/compute.rs +++ b/libs/pavex/src/rustdoc/compute.rs @@ -1,8 +1,10 @@ use std::path::{Path, PathBuf}; use anyhow::Context; +use guppy::Version; use crate::rustdoc::package_id_spec::PackageIdSpecification; +use crate::rustdoc::utils::normalize_crate_name; use crate::rustdoc::TOOLCHAIN_CRATES; #[derive(Debug, thiserror::Error)] @@ -13,6 +15,17 @@ pub struct CannotGetCrateData { pub source: anyhow::Error, } +fn format_optional_version(v: &Option) -> Option> { + v.as_ref().map(|v| { + use std::fmt::Write; + let mut s = format!("v{}.{}.{}", v.major, v.minor, v.patch); + if !v.pre.is_empty() { + write!(&mut s, "-{}", v.pre).unwrap(); + } + tracing::field::display(s) + }) +} + /// Return the JSON documentation for a crate. /// The crate is singled out, within the current workspace, using a [`PackageIdSpecification`]. /// @@ -21,7 +34,15 @@ pub struct CannotGetCrateData { /// /// `root_folder` is `cargo`'s target directory for the current workspace: that is where we are /// going to look for the JSON files generated by `rustdoc`. -pub(super) fn get_crate_data( +#[tracing::instrument( +skip_all, +fields( +crate.name = package_id_spec.name, +crate.version = format_optional_version(& package_id_spec.version), +crate.source = package_id_spec.source +) +)] +pub(super) fn compute_crate_docs( root_folder: &Path, package_id_spec: &PackageIdSpecification, ) -> Result { @@ -34,9 +55,9 @@ pub(super) fn get_crate_data( // documentation on the fly. We assume that their JSON docs have been pre-computed and are // available for us to look at. if TOOLCHAIN_CRATES.contains(&package_id_spec.name.as_str()) { - get_toolchain_crate_data(package_id_spec) + get_toolchain_crate_docs(package_id_spec) } else { - _get_crate_data(root_folder, package_id_spec) + _compute_crate_docs(root_folder, package_id_spec) } .map_err(|e| CannotGetCrateData { package_spec: package_id_spec.to_string(), @@ -44,7 +65,7 @@ pub(super) fn get_crate_data( }) } -fn get_toolchain_crate_data( +fn get_toolchain_crate_docs( package_id_spec: &PackageIdSpecification, ) -> Result { let root_folder = get_json_docs_root_folder_via_rustup()?; @@ -113,7 +134,7 @@ fn get_nightly_toolchain_root_folder_via_rustup() -> Result Result { @@ -142,9 +163,10 @@ fn _get_crate_data( ); } - let json_path = target_directory - .join("doc") - .join(format!("{}.json", &package_id_spec.name)); + let json_path = target_directory.join("doc").join(format!( + "{}.json", + normalize_crate_name(&package_id_spec.name) + )); let json = fs_err::read_to_string(json_path).with_context(|| { format!( diff --git a/libs/pavex/src/rustdoc/mod.rs b/libs/pavex/src/rustdoc/mod.rs index 69c1c23cc..ed5a90ec0 100644 --- a/libs/pavex/src/rustdoc/mod.rs +++ b/libs/pavex/src/rustdoc/mod.rs @@ -9,6 +9,7 @@ pub use queries::{Crate, CrateCollection, GlobalTypeId, UnknownTypePath}; mod compute; mod package_id_spec; mod queries; +mod utils; pub const STD_PACKAGE_ID: &str = "std"; pub const TOOLCHAIN_CRATES: [&str; 3] = ["std", "core", "alloc"]; diff --git a/libs/pavex/src/rustdoc/queries.rs b/libs/pavex/src/rustdoc/queries.rs index 11aa58238..b50e70acb 100644 --- a/libs/pavex/src/rustdoc/queries.rs +++ b/libs/pavex/src/rustdoc/queries.rs @@ -1,16 +1,18 @@ use std::collections::{BTreeSet, HashMap}; +use std::fmt; use std::fmt::{Display, Formatter}; use anyhow::{anyhow, Context}; +use elsa::FrozenMap; use guppy::graph::PackageGraph; use guppy::{PackageId, Version}; +use indexmap::IndexSet; use rustdoc_types::{ExternalCrate, Item, ItemEnum, ItemKind, Visibility}; use crate::language::{ImportPath, ResolvedPath}; use crate::rustdoc::package_id_spec::PackageIdSpecification; -use crate::rustdoc::{compute::get_crate_data, CannotGetCrateData, TOOLCHAIN_CRATES}; +use crate::rustdoc::{compute::compute_crate_docs, utils, CannotGetCrateData, TOOLCHAIN_CRATES}; -#[derive(Debug, Clone)] /// The main entrypoint for accessing the documentation of the crates /// in a specific `PackageGraph`. /// @@ -18,29 +20,35 @@ use crate::rustdoc::{compute::get_crate_data, CannotGetCrateData, TOOLCHAIN_CRAT /// - Computing and caching the JSON documentation for crates in the graph; /// - Execute queries that span the documentation of multiple crates (e.g. following crate /// re-exports or star re-exports). -pub struct CrateCollection(HashMap, PackageGraph); +pub struct CrateCollection(FrozenMap>, PackageGraph); + +impl fmt::Debug for CrateCollection { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + write!(f, "{:?}", self.1) + } +} impl CrateCollection { /// Initialise the collection for a `PackageGraph`. pub fn new(package_graph: PackageGraph) -> Self { - Self(Default::default(), package_graph) + Self(FrozenMap::new(), package_graph) } /// Compute the documentation for the crate associated with a specific [`PackageId`]. /// /// It will be retrieved from [`CrateCollection`]'s internal cache if it was computed before. pub fn get_or_compute_crate_by_package_id( - &mut self, + &self, package_id: &PackageId, ) -> Result<&Crate, CannotGetCrateData> { let package_spec = PackageIdSpecification::from_package_id(package_id, &self.1); if self.0.get(&package_spec).is_none() { - let krate = get_crate_data( + let krate = compute_crate_docs( self.1.workspace().target_directory().as_std_path(), &package_spec, )?; let krate = Crate::new(self, krate, package_id.to_owned()); - self.0.insert(package_spec.clone(), krate); + self.0.insert(package_spec.clone(), Box::new(krate)); } Ok(self.get_crate_by_package_id_spec(&package_spec)) } @@ -59,7 +67,7 @@ impl CrateCollection { /// /// It panics if no documentation is found for the specified [`PackageIdSpecification`]. pub fn get_crate_by_package_id_spec(&self, package_spec: &PackageIdSpecification) -> &Crate { - &self.0.get(package_spec).unwrap_or_else(|| { + self.0.get(package_spec).unwrap_or_else(|| { panic!( "No JSON docs were found for the following package ID specification: {:?}", package_spec @@ -78,7 +86,7 @@ impl CrateCollection { /// Retrieve information about a type given its path and the id of the package where /// it was defined. pub fn get_type_by_resolved_path( - &mut self, + &self, path: &ResolvedPath, package_id: &PackageId, ) -> Result, CannotGetCrateData> { @@ -100,13 +108,11 @@ impl CrateCollection { if path.len() < 3 { // It has to be at least three segments - crate name, type name, method name. // If it's shorter than three, it's just an unknown path. - return Ok(Err(UnknownTypePath { - type_path: path.to_owned(), - })); + return Ok(Err(UnknownTypePath { type_path: path })); } let (method_name, type_path_segments) = path.split_last().unwrap(); - if let Ok(type_id) = krate.get_type_id_by_path(&type_path_segments) { + if let Ok(type_id) = krate.get_type_id_by_path(type_path_segments) { let t = self.get_type_by_global_type_id(type_id); let impl_block_ids = match &t.inner { ItemEnum::Struct(s) => &s.impls, @@ -144,7 +150,10 @@ impl CrateCollection { /// Retrieve the canonical path for a struct, enum or function given its [`GlobalTypeId`]. /// /// It panics if no item is found for the specified [`GlobalTypeId`]. - pub fn get_canonical_path_by_global_type_id(&self, type_id: &GlobalTypeId) -> &[String] { + pub fn get_canonical_path_by_global_type_id( + &self, + type_id: &GlobalTypeId, + ) -> Result<&[String], anyhow::Error> { let krate = self.get_crate_by_package_id(&type_id.package_id); krate.get_canonical_path(type_id) } @@ -152,30 +161,22 @@ impl CrateCollection { /// Retrieve the canonical path and the [`GlobalTypeId`] for a struct, enum or function given /// its **local** id. pub fn get_canonical_path_by_local_type_id( - &mut self, + &self, used_by_package_id: &PackageId, item_id: &rustdoc_types::Id, ) -> Result<(GlobalTypeId, &[String]), anyhow::Error> { let (definition_package_id, path) = { - let used_by_krate = { - self.get_or_compute_crate_by_package_id(used_by_package_id)?; - self.get_crate_by_package_id(used_by_package_id) - }; + let used_by_krate = self.get_or_compute_crate_by_package_id(used_by_package_id)?; let local_type_summary = used_by_krate.get_type_summary_by_local_type_id(item_id)?; ( used_by_krate.compute_package_id_for_crate_id(local_type_summary.crate_id, self), local_type_summary.path.clone(), ) }; - let definition_krate = { - self.get_or_compute_crate_by_package_id(&definition_package_id)?; - self.get_crate_by_package_id(&definition_package_id) - }; + let definition_krate = self.get_or_compute_crate_by_package_id(&definition_package_id)?; let type_id = definition_krate.get_type_id_by_path(&path)?; - Ok(( - type_id.clone(), - self.get_canonical_path_by_global_type_id(&type_id), - )) + let canonical_path = self.get_canonical_path_by_global_type_id(type_id)?; + Ok((type_id.clone(), canonical_path)) } } @@ -188,7 +189,13 @@ impl CrateCollection { #[derive(Debug, Clone)] pub struct Crate { core: CrateCore, - path_index: HashMap, GlobalTypeId>, + /// An index to lookup the global id of a type given a local importable path + /// that points at it. + /// + /// The index does NOT contain macros, since macros and types live in two + /// different namespaces and can contain items with the same name. + /// E.g. `core::clone::Clone` is both a trait and a derive macro. + types_path_index: HashMap, GlobalTypeId>, public_local_path_index: HashMap>>, } @@ -215,9 +222,17 @@ impl CrateCore { crate_id: u32, collection: &CrateCollection, ) -> PackageId { + #[derive(Debug, Hash, Eq, PartialEq)] + struct PackageLinkMetadata<'a> { + id: &'a PackageId, + name: &'a str, + version: &'a Version, + } + if crate_id == 0 { return self.package_id.clone(); } + let package_graph = &collection.1; let (external_crate, external_crate_version) = self.get_external_crate_name(crate_id) @@ -229,53 +244,94 @@ impl CrateCore { ) }).unwrap(); if TOOLCHAIN_CRATES.contains(&external_crate.name.as_str()) { - PackageId::new(external_crate.name.clone()) - } else { - let transitive_dependencies = package_graph - .query_forward([&self.package_id]) - .with_context(|| { - format!( - "`{}` does not appear in the package graph for the current workspace", - &self.package_id.repr() - ) - }) - .unwrap() - .resolve(); - let mut iterator = - transitive_dependencies.links(guppy::graph::DependencyDirection::Forward); - iterator - .find(|link| { - link.to().name() == external_crate.name - && external_crate_version - .as_ref() - .map(|v| link.to().version() == v) - .unwrap_or(true) - }) + return PackageId::new(external_crate.name.clone()); + } + + let transitive_dependencies = package_graph + .query_forward([&self.package_id]) + .with_context(|| { + format!( + "`{}` does not appear in the package graph for the current workspace", + &self.package_id.repr() + ) + }) + .unwrap() + .resolve(); + let expected_link_name = utils::normalize_crate_name(&external_crate.name); + let package_candidates: IndexSet<_> = transitive_dependencies + .links(guppy::graph::DependencyDirection::Forward) + .filter(|link| utils::normalize_crate_name(link.to().name()) == expected_link_name) + .map(|link| { + let l = link.to(); + PackageLinkMetadata { + id: l.id(), + name: l.name(), + version: l.version(), + } + }) + .collect(); + + if package_candidates.is_empty() { + Err(anyhow!( + "I could not find any crate named `{}` among the dependencies of {}", + expected_link_name, + self.package_id + )) + .unwrap() + } + if package_candidates.len() == 1 { + return package_candidates.first().unwrap().id.to_owned(); + } + + // We have multiple packages with the same name. + // We try to use the version to identify the one we are looking for. + // If we don't have a version, we panic: better than picking one randomly and failing + // later with a confusing message. + if let Some(expected_link_version) = external_crate_version.as_ref() { + package_candidates + .into_iter() + .find(|l| l.version == expected_link_version) .ok_or_else(|| { anyhow!( - "I could not find the package id for the crate {} among the dependencies of {}", - external_crate.name, self.package_id + "None of the dependencies of {} named `{}` matches the version we expect ({})", + self.package_id, + expected_link_name, + expected_link_version ) - }) - .unwrap() - .to() - .id() - .to_owned() + }).unwrap().id.to_owned() + } else { + Err( + anyhow!( + "There are multiple packages named `{}` among the dependencies of {}. \ + I was not able to extract the expected version for `{}` from the JSON documentation for {}, \ + therefore I do not have a way to disambiguate among the matches we found", + expected_link_name, + self.package_id.repr(), + expected_link_name, + self.package_id.repr() + ) + ).unwrap() } } } impl Crate { + #[tracing::instrument(skip_all, name = "index_crate_docs", fields(package.id = package_id.repr()))] fn new( - collection: &mut CrateCollection, + collection: &CrateCollection, krate: rustdoc_types::Crate, package_id: PackageId, ) -> Self { let crate_core = CrateCore { package_id, krate }; - let mut path_index: HashMap<_, _> = crate_core + let mut types_path_index: HashMap<_, _> = crate_core .krate .paths .iter() + // We only want types, no macros + .filter(|(_, summary)| match summary.kind { + ItemKind::Macro | ItemKind::ProcDerive => false, + _ => true, + }) .map(|(id, summary)| { ( summary.path.clone(), @@ -285,7 +341,7 @@ impl Crate { .collect(); let mut public_local_path_index = HashMap::new(); - index_local_items( + index_local_types( &crate_core, collection, vec![], @@ -293,18 +349,18 @@ impl Crate { &crate_core.krate.root, ); - path_index.reserve(public_local_path_index.len()); + types_path_index.reserve(public_local_path_index.len()); for (id, public_paths) in &public_local_path_index { for public_path in public_paths { - if path_index.get(public_path).is_none() { - path_index.insert(public_path.to_owned(), id.to_owned()); + if types_path_index.get(public_path).is_none() { + types_path_index.insert(public_path.to_owned(), id.to_owned()); } } } Self { core: crate_core, - path_index, + types_path_index, public_local_path_index, } } @@ -323,9 +379,11 @@ impl Crate { } pub fn get_type_id_by_path(&self, path: &[String]) -> Result<&GlobalTypeId, UnknownTypePath> { - self.path_index.get(path).ok_or_else(|| UnknownTypePath { - type_path: path.to_owned(), - }) + self.types_path_index + .get(path) + .ok_or_else(|| UnknownTypePath { + type_path: path.to_owned(), + }) } /// Return the crate_id, path and item kind for a **local** type id. @@ -361,22 +419,22 @@ impl Crate { /// Types can be exposed under multiple paths. /// This method returns a "canonical" importable path - i.e. the shortest importable path /// pointing at the type you specified. - fn get_canonical_path(&self, type_id: &GlobalTypeId) -> &[String] { + fn get_canonical_path(&self, type_id: &GlobalTypeId) -> Result<&[String], anyhow::Error> { if let Some(path) = self.public_local_path_index.get(type_id) { - return path.iter().next().unwrap(); + Ok(path.iter().next().unwrap()) + } else { + Err(anyhow::anyhow!( + "Failed to find a publicly importable path for the type id `{:?}` in the index I computed for `{:?}`. \ + This is likely to be a bug in pavex's handling of rustdoc's JSON output or in rustdoc itself.", + type_id, self.core.package_id.repr() + )) } - - panic!( - "Failed to find a publicly importable path for the type id `{:?}`. \ - This is likely to be a bug in our handling of rustdoc's JSON output.", - type_id - ) } } -fn index_local_items<'a>( +fn index_local_types<'a>( crate_core: &'a CrateCore, - collection: &mut CrateCollection, + collection: &'a CrateCollection, mut current_path: Vec<&'a str>, path_index: &mut HashMap>>, current_item_id: &rustdoc_types::Id, @@ -415,7 +473,7 @@ fn index_local_items<'a>( .expect("All 'module' items have a 'name' property"); current_path.push(current_path_segment); for item_id in &m.items { - index_local_items( + index_local_types( crate_core, collection, current_path.clone(), @@ -452,10 +510,8 @@ fn index_local_items<'a>( external_crate.get_type_id_by_path(&imported_summary.path) { let foreign_item_id = foreign_item_id.raw_id.clone(); - // TODO: super-wasteful - let external_core = external_crate.core.clone(); - index_local_items( - &external_core, + index_local_types( + &external_crate.core, collection, current_path, path_index, @@ -474,7 +530,7 @@ fn index_local_items<'a>( current_path.push(&i.name); } } - index_local_items( + index_local_types( crate_core, collection, current_path.clone(), @@ -485,11 +541,10 @@ fn index_local_items<'a>( } } } - ItemEnum::Struct(_) => { - let struct_name = current_item - .name - .as_deref() - .expect("All 'struct' items have a 'name' property"); + ItemEnum::Trait(_) | ItemEnum::Function(_) | ItemEnum::Enum(_) | ItemEnum::Struct(_) => { + let struct_name = current_item.name.as_deref().expect( + "All 'struct', 'function', 'enum' and 'trait' items have a 'name' property", + ); current_path.push(struct_name); let path = current_path.into_iter().map(|s| s.to_string()).collect(); path_index @@ -500,36 +555,6 @@ fn index_local_items<'a>( .or_default() .insert(path); } - ItemEnum::Enum(_) => { - let enum_name = current_item - .name - .as_deref() - .expect("All 'enum' items have a 'name' property"); - current_path.push(enum_name); - let path = current_path.into_iter().map(|s| s.to_string()).collect(); - path_index - .entry(GlobalTypeId::new( - current_item_id.to_owned(), - crate_core.package_id.to_owned(), - )) - .or_default() - .insert(path); - } - ItemEnum::Function(_) => { - let function_name = current_item - .name - .as_deref() - .expect("All 'function' items have a 'name' property"); - current_path.push(function_name); - let path = current_path.into_iter().map(|s| s.to_string()).collect(); - path_index - .entry(GlobalTypeId::new( - current_item_id.to_owned(), - crate_core.package_id.to_owned(), - )) - .or_default() - .insert(path); - } _ => {} } } @@ -569,8 +594,10 @@ impl Display for UnknownTypePath { trait RustdocCrateExt { /// Given a crate id, return the corresponding external crate object. - /// We also try to return the crate version, if we manage to parse it out of the crate HTML - /// root URL. + /// We try to guess the crate version by parsing it out of the root URL for the HTML documentation. + /// The extracted version is not guaranteed to be correct: crates can set an arbitrary root URL + /// via `#[doc(html_root_url)]` - e.g. pointing at an outdated version of their docs (see + /// https://github.com/tokio-rs/tracing/pull/2384 as an example). fn get_external_crate_name(&self, crate_id: u32) -> Option<(&ExternalCrate, Option)>; } diff --git a/libs/pavex/src/rustdoc/utils.rs b/libs/pavex/src/rustdoc/utils.rs new file mode 100644 index 000000000..5cb649e1b --- /dev/null +++ b/libs/pavex/src/rustdoc/utils.rs @@ -0,0 +1,4 @@ +// Ensure that crate names are in canonical form! Damn automated hyphen substitution! +pub fn normalize_crate_name(s: &str) -> String { + s.replace('-', "_") +} diff --git a/libs/pavex/src/web/app.rs b/libs/pavex/src/web/app.rs index 8a6f00e52..d7fe74e5f 100644 --- a/libs/pavex/src/web/app.rs +++ b/libs/pavex/src/web/app.rs @@ -28,6 +28,7 @@ use crate::web::diagnostic::{ use crate::web::generated_app::GeneratedApp; use crate::web::handler_call_graph::HandlerCallGraph; use crate::web::resolvers::{CallableResolutionError, CallableType}; +use crate::web::traits::assert_trait_is_implemented; use crate::web::{codegen, diagnostic, resolvers}; pub(crate) const GENERATED_APP_PACKAGE_ID: &str = "crate"; @@ -41,7 +42,19 @@ pub struct App { codegen_types: HashSet, } +#[tracing::instrument] +fn compute_package_graph() -> Result { + // `cargo metadata` seems to be the only reliable way of retrieving the path to + // the root manifest of the current workspace for a Rust project. + guppy::MetadataCommand::new() + .exec() + .map_err(|e| miette!(e))? + .build_graph() + .map_err(|e| miette!(e)) +} + impl App { + #[tracing::instrument(skip_all)] pub fn build(app_blueprint: AppBlueprint) -> Result { // We collect all the unique raw identifiers from the blueprint. let raw_identifiers_db: HashSet = { @@ -55,11 +68,7 @@ impl App { // `cargo metadata` seems to be the only reliable way of retrieving the path to // the root manifest of the current workspace for a Rust project. - let package_graph = guppy::MetadataCommand::new() - .exec() - .map_err(|e| miette!(e))? - .build_graph() - .map_err(|e| miette!(e))?; + let package_graph = compute_package_graph()?; let mut krate_collection = CrateCollection::new(package_graph.clone()); let resolved_paths2identifiers: HashMap> = { @@ -122,7 +131,7 @@ impl App { map }; - let constructor_paths = { + let constructor_paths: IndexSet = { let mut set = IndexSet::with_capacity(app_blueprint.constructors.len()); for constructor_identifiers in &app_blueprint.constructors { let constructor_path = identifiers2path[constructor_identifiers].clone(); @@ -149,11 +158,7 @@ impl App { }; let (constructor_callable_resolver, constructor_callables) = - match resolvers::resolve_constructors( - &constructor_paths, - &mut krate_collection, - &package_graph, - ) { + match resolvers::resolve_constructors(&constructor_paths, &mut krate_collection) { Ok((resolver, constructors)) => (resolver, constructors), Err(e) => { return Err(e.into_diagnostic( @@ -166,8 +171,8 @@ impl App { }; let mut constructors: IndexMap = IndexMap::new(); - for (output_type, callable) in constructor_callables.into_iter() { - let constructor = match callable.try_into() { + for (output_type, callable) in &constructor_callables { + let constructor = match callable.to_owned().try_into() { Ok(c) => c, Err(e) => { return match e { @@ -198,7 +203,7 @@ impl App { }; } }; - constructors.insert(output_type, constructor); + constructors.insert(output_type.to_owned(), constructor); } // For each non-reference type, register an inlineable constructor that transforms @@ -212,26 +217,23 @@ impl App { } } - let (handler_resolver, handlers) = match resolvers::resolve_handlers( - &handler_paths, - &mut krate_collection, - &package_graph, - ) { - Ok(h) => h, - Err(e) => { - return Err(e.into_diagnostic( - &resolved_paths2identifiers, - |identifiers| { - app_blueprint.handler_locations[identifiers] - .first() - .unwrap() - .clone() - }, - &package_graph, - CallableType::Handler, - )?); - } - }; + let (handler_resolver, handlers) = + match resolvers::resolve_handlers(&handler_paths, &mut krate_collection) { + Ok(h) => h, + Err(e) => { + return Err(e.into_diagnostic( + &resolved_paths2identifiers, + |identifiers| { + app_blueprint.handler_locations[identifiers] + .first() + .unwrap() + .clone() + }, + &package_graph, + CallableType::Handler, + )?); + } + }; let mut router = BTreeMap::new(); for (route, callable_identifiers) in app_blueprint.router { @@ -268,6 +270,38 @@ impl App { map }; + // All singletons must implement `Clone`, `Send` and `Sync`. + for singleton_type in component2lifecycle.iter().filter_map(|(t, l)| { + if l == &Lifecycle::Singleton { + Some(t) + } else { + None + } + }) { + for trait_path in [ + &["core", "marker", "Send"], + &["core", "marker", "Sync"], + &["core", "clone", "Clone"], + ] { + if let Err(e) = + assert_trait_is_implemented(&krate_collection, singleton_type, trait_path) + { + return Err(e + .into_diagnostic( + &constructor_callables, + &constructor_callable_resolver, + &resolved_paths2identifiers, + &app_blueprint.constructor_locations, + &package_graph, + Some("All singletons must implement the `Send`, `Sync` and `Clone` traits.\n \ + `pavex` runs on a multi-threaded HTTP server and singletons must be shared \ + across all worker threads.".into()), + )? + .into()); + } + } + } + let handler_call_graphs: IndexMap<_, _> = handler_dependency_graphs .iter() .map(|(path, dep_graph)| { @@ -511,7 +545,9 @@ fn process_framework_path( RawCallableIdentifiers::from_raw_parts(raw_path.into(), "pavex_builder".into()); let path = ResolvedPath::parse(&identifiers, package_graph).unwrap(); let type_id = path.find_type_id(krate_collection).unwrap(); - let base_path = krate_collection.get_canonical_path_by_global_type_id(&type_id); + let base_path = krate_collection + .get_canonical_path_by_global_type_id(&type_id) + .unwrap(); ResolvedType { package_id: type_id.package_id().to_owned(), base_type: base_path.to_vec(), diff --git a/libs/pavex/src/web/application_state_call_graph.rs b/libs/pavex/src/web/application_state_call_graph.rs index 9b779a8ec..6ff475ba4 100644 --- a/libs/pavex/src/web/application_state_call_graph.rs +++ b/libs/pavex/src/web/application_state_call_graph.rs @@ -30,6 +30,7 @@ pub(crate) struct ApplicationStateCallGraph { } impl ApplicationStateCallGraph { + #[tracing::instrument(name = "compute_application_state_call_graph", skip_all)] pub(crate) fn new( runtime_singleton_bindings: BiHashMap, lifecycles: HashMap, diff --git a/libs/pavex/src/web/constructors.rs b/libs/pavex/src/web/constructors.rs index 7e2473246..9c5da411b 100644 --- a/libs/pavex/src/web/constructors.rs +++ b/libs/pavex/src/web/constructors.rs @@ -25,7 +25,7 @@ impl TryFrom for Constructor { fn try_from(c: Callable) -> Result { if c.output.base_type == vec!["()"] { return Err(ConstructorValidationError::CannotReturnTheUnitType( - c.path.to_owned(), + c.path, )); } Ok(Constructor::Callable(c)) diff --git a/libs/pavex/src/web/dependency_graph.rs b/libs/pavex/src/web/dependency_graph.rs index d0c28bf24..a48441c11 100644 --- a/libs/pavex/src/web/dependency_graph.rs +++ b/libs/pavex/src/web/dependency_graph.rs @@ -35,6 +35,7 @@ pub(crate) struct CallableDependencyGraph { impl CallableDependencyGraph { /// Starting from a callable, build up its dependency graph: what types it needs to be fed as /// inputs and what types are needed, in turn, to construct those inputs. + #[tracing::instrument(name = "compute_callable_dependency_graph", skip_all, fields(callable))] pub fn new(callable: Callable, constructors: &IndexMap) -> Self { fn process_callable( callable: &Callable, diff --git a/libs/pavex/src/web/diagnostic.rs b/libs/pavex/src/web/diagnostic.rs index 969092cad..2791befc3 100644 --- a/libs/pavex/src/web/diagnostic.rs +++ b/libs/pavex/src/web/diagnostic.rs @@ -42,6 +42,14 @@ impl CompilerDiagnosticBuilder { } } + pub fn optional_help(self, help: Option) -> Self { + if let Some(help) = help { + self.help(help) + } else { + self + } + } + pub fn optional_related_error(self, related_error: Option) -> Self { if let Some(related) = related_error { self.related_error(related) @@ -301,7 +309,7 @@ pub fn read_source_file( if path.is_absolute() { fs_err::read_to_string(path) } else { - let path = workspace.root().as_std_path().join(&path); + let path = workspace.root().as_std_path().join(path); fs_err::read_to_string(&path) } } diff --git a/libs/pavex/src/web/generated_app.rs b/libs/pavex/src/web/generated_app.rs index 82f64ca9c..814fd007d 100644 --- a/libs/pavex/src/web/generated_app.rs +++ b/libs/pavex/src/web/generated_app.rs @@ -63,7 +63,7 @@ impl GeneratedApp { let root_manifest = fs_err::read_to_string(&root_manifest_path)?; let mut root_manifest: toml::Value = toml::from_str(&root_manifest)?; - let member_path = pathdiff::diff_paths(&generated_crate_directory, root_path) + let member_path = pathdiff::diff_paths(generated_crate_directory, root_path) .unwrap() .to_string_lossy() .to_string(); @@ -82,9 +82,7 @@ impl GeneratedApp { .unwrap(); if let Some(members) = workspace.get_mut("members") { if let Some(members) = members.as_array_mut() { - if !members - .iter().any(|m| m.as_str() == Some(&member_path)) - { + if !members.iter().any(|m| m.as_str() == Some(&member_path)) { members.push(member_path.into()); } } diff --git a/libs/pavex/src/web/handler_call_graph.rs b/libs/pavex/src/web/handler_call_graph.rs index 368df4538..ea8cabaec 100644 --- a/libs/pavex/src/web/handler_call_graph.rs +++ b/libs/pavex/src/web/handler_call_graph.rs @@ -45,6 +45,7 @@ pub(crate) struct HandlerCallGraph { } impl HandlerCallGraph { + #[tracing::instrument(name = "compute_handler_call_graph", skip_all)] pub(crate) fn new( dependency_graph: &'_ CallableDependencyGraph, lifecycles: HashMap, diff --git a/libs/pavex/src/web/mod.rs b/libs/pavex/src/web/mod.rs index 0d71ecc2e..d5db5915c 100644 --- a/libs/pavex/src/web/mod.rs +++ b/libs/pavex/src/web/mod.rs @@ -11,3 +11,4 @@ mod diagnostic; mod generated_app; mod handler_call_graph; mod resolvers; +mod traits; diff --git a/libs/pavex/src/web/resolvers.rs b/libs/pavex/src/web/resolvers.rs index 5d1aff958..6bf126bf9 100644 --- a/libs/pavex/src/web/resolvers.rs +++ b/libs/pavex/src/web/resolvers.rs @@ -31,7 +31,6 @@ use crate::web::diagnostic::{ pub(crate) fn resolve_constructors( constructor_paths: &IndexSet, krate_collection: &mut CrateCollection, - package_graph: &PackageGraph, ) -> Result< ( BiHashMap, @@ -42,8 +41,7 @@ pub(crate) fn resolve_constructors( let mut resolution_map = BiHashMap::with_capacity(constructor_paths.len()); let mut constructors = IndexMap::with_capacity(constructor_paths.len()); for constructor_identifiers in constructor_paths { - let constructor = - resolve_callable(krate_collection, constructor_identifiers, package_graph)?; + let constructor = resolve_callable(krate_collection, constructor_identifiers)?; constructors.insert(constructor.output.clone(), constructor.clone()); resolution_map.insert(constructor_identifiers.to_owned(), constructor); } @@ -55,12 +53,11 @@ pub(crate) fn resolve_constructors( pub(crate) fn resolve_handlers( handler_paths: &IndexSet, krate_collection: &mut CrateCollection, - package_graph: &PackageGraph, ) -> Result<(HashMap, IndexSet), CallableResolutionError> { let mut handlers = IndexSet::with_capacity(handler_paths.len()); let mut handler_resolver = HashMap::new(); for callable_path in handler_paths { - let handler = resolve_callable(krate_collection, callable_path, package_graph)?; + let handler = resolve_callable(krate_collection, callable_path)?; handlers.insert(handler.clone()); handler_resolver.insert(callable_path.to_owned(), handler); } @@ -72,7 +69,6 @@ fn process_type( // The package id where the type we are trying to process has been referenced (e.g. as an // input/output parameter). used_by_package_id: &PackageId, - package_graph: &PackageGraph, krate_collection: &mut CrateCollection, ) -> Result { match type_ { @@ -92,7 +88,6 @@ fn process_type( generics.push(process_type( generic_type, used_by_package_id, - package_graph, krate_collection, )?); } @@ -132,8 +127,7 @@ fn process_type( by value (`move` semantic) or via a shared reference (`&MyType`)", )); } - let mut resolved_type = - process_type(type_, used_by_package_id, package_graph, krate_collection)?; + let mut resolved_type = process_type(type_, used_by_package_id, krate_collection)?; resolved_type.is_shared_reference = true; Ok(resolved_type) } @@ -147,7 +141,6 @@ fn process_type( fn resolve_callable( krate_collection: &mut CrateCollection, callable_path: &ResolvedPath, - package_graph: &PackageGraph, ) -> Result { let type_ = callable_path.find_type(krate_collection)?; let used_by_package_id = &callable_path.package_id; @@ -191,12 +184,7 @@ fn resolve_callable( let mut parameter_paths = Vec::with_capacity(decl.inputs.len()); for (parameter_index, (_, parameter_type)) in decl.inputs.iter().enumerate() { - match process_type( - parameter_type, - used_by_package_id, - package_graph, - krate_collection, - ) { + match process_type(parameter_type, used_by_package_id, krate_collection) { Ok(p) => parameter_paths.push(p), Err(e) => { return Err(ParameterResolutionError { @@ -219,12 +207,7 @@ fn resolve_callable( is_shared_reference: false, }, Some(output_type) => { - match process_type( - output_type, - used_by_package_id, - package_graph, - krate_collection, - ) { + match process_type(output_type, used_by_package_id, krate_collection) { Ok(p) => p, Err(e) => { return Err(OutputTypeResolutionError { @@ -352,7 +335,7 @@ impl CallableResolutionError { ) .labeled("I do not know how handle this parameter".into()); let source_code = NamedSource::new( - &definition_span.filename.to_str().unwrap(), + definition_span.filename.to_str().unwrap(), source_contents, ); Some( @@ -447,7 +430,7 @@ impl CallableResolutionError { let label = source_span.labeled("The output type that I cannot handle".into()); let source_code = NamedSource::new( - &definition_span.filename.to_str().unwrap(), + definition_span.filename.to_str().unwrap(), source_contents, ); Some( diff --git a/libs/pavex/src/web/traits.rs b/libs/pavex/src/web/traits.rs new file mode 100644 index 000000000..92ff523ae --- /dev/null +++ b/libs/pavex/src/web/traits.rs @@ -0,0 +1,135 @@ +use std::collections::{HashMap, HashSet}; +use std::fmt::Formatter; + +use bimap::BiHashMap; +use guppy::graph::PackageGraph; +use indexmap::IndexMap; +use rustdoc_types::ItemEnum; + +use pavex_builder::{Location, RawCallableIdentifiers}; + +use crate::language::{Callable, ResolvedPath, ResolvedType}; +use crate::rustdoc::CrateCollection; +use crate::web::diagnostic::{CompilerDiagnosticBuilder, ParsedSourceFile, SourceSpanExt}; +use crate::web::{diagnostic, CompilerDiagnostic}; + +/// It returns an error if `type_` does not implement the specified trait. +/// +/// The trait path must be fully resolved: it should NOT point to a re-export +/// (e.g. `std::marker::Sync` won't work, you should use `core::marker::Sync`). +pub(crate) fn assert_trait_is_implemented( + krate_collection: &CrateCollection, + type_: &ResolvedType, + expected_trait_path: &[&'static str], +) -> Result<(), MissingTraitImplementationError> { + if !implements_trait(krate_collection, type_, expected_trait_path) { + Err(MissingTraitImplementationError { + type_: type_.to_owned(), + trait_path: expected_trait_path.to_vec(), + }) + } else { + Ok(()) + } +} + +/// It returns `true` if `type_` implements the specified trait. +/// +/// The trait path must be fully resolved: it should NOT point to a re-export +/// (e.g. `std::marker::Sync` won't work, you should use `core::marker::Sync`). +pub(crate) fn implements_trait( + krate_collection: &CrateCollection, + type_: &ResolvedType, + expected_trait_path: &[&'static str], +) -> bool { + let krate = krate_collection.get_crate_by_package_id(&type_.package_id); + let type_id = krate.get_type_id_by_path(&type_.base_type).unwrap(); + let type_item = krate_collection.get_type_by_global_type_id(type_id); + let impls = match &type_item.inner { + ItemEnum::Struct(s) => &s.impls, + ItemEnum::Enum(e) => &e.impls, + _ => unreachable!(), + }; + for impl_id in impls { + let trait_id = match &krate.get_type_by_local_type_id(impl_id).inner { + ItemEnum::Impl(impl_) => { + if impl_.negative { + continue; + } + impl_.trait_.as_ref().map(|p| &p.id) + } + _ => unreachable!(), + }; + if let Some(trait_id) = trait_id { + if let Ok((_, trait_path)) = + krate_collection.get_canonical_path_by_local_type_id(&type_.package_id, trait_id) + { + if trait_path == expected_trait_path { + return true; + } + } + } + } + false +} + +#[derive(Debug)] +pub(crate) struct MissingTraitImplementationError { + pub type_: ResolvedType, + pub trait_path: Vec<&'static str>, +} + +impl std::error::Error for MissingTraitImplementationError {} + +impl std::fmt::Display for MissingTraitImplementationError { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!( + f, + "`{:?}` does not implement the `{}` trait.", + &self.type_, + self.trait_path.join("::") + ) + } +} + +impl MissingTraitImplementationError { + pub(crate) fn into_diagnostic( + mut self, + constructor_callables: &IndexMap, + constructor_callable_resolver: &BiHashMap, + resolved_paths2identifiers: &HashMap>, + constructor_locations: &HashMap, + package_graph: &PackageGraph, + help: Option, + ) -> Result { + let constructor_callable: &Callable = match constructor_callables.get(&self.type_) { + Some(c) => c, + None => { + if self.type_.is_shared_reference { + self.type_.is_shared_reference = false; + &constructor_callables[&self.type_] + } else { + unreachable!() + } + } + }; + let constructor_path = constructor_callable_resolver + .get_by_right(constructor_callable) + .unwrap(); + let raw_identifier = resolved_paths2identifiers[constructor_path] + .iter() + .next() + .unwrap(); + let location = &constructor_locations[raw_identifier]; + let source = + ParsedSourceFile::new(location.file.as_str().into(), &package_graph.workspace()) + .map_err(miette::MietteError::IoError)?; + let label = + diagnostic::get_f_macro_invocation_span(&source.contents, &source.parsed, location) + .map(|s| s.labeled("The constructor was registered here".into())); + let diagnostic = CompilerDiagnosticBuilder::new(source, self) + .optional_label(label) + .optional_help(help) + .build(); + Ok(diagnostic) + } +} diff --git a/libs/pavex_builder/src/app.rs b/libs/pavex_builder/src/app.rs index 5e96bc237..c3a2eb651 100644 --- a/libs/pavex_builder/src/app.rs +++ b/libs/pavex_builder/src/app.rs @@ -56,7 +56,7 @@ impl AppBlueprint { let location = std::panic::Location::caller(); self.constructor_locations .entry(callable_identifiers.clone()) - .or_insert(location.into()); + .or_insert_with(|| location.into()); self.component_lifecycles .insert(callable_identifiers.clone(), lifecycle); self.constructors.insert(callable_identifiers); diff --git a/libs/pavex_cli/Cargo.toml b/libs/pavex_cli/Cargo.toml index d09741642..3cff425b9 100644 --- a/libs/pavex_cli/Cargo.toml +++ b/libs/pavex_cli/Cargo.toml @@ -13,6 +13,8 @@ pavex = { path = "../pavex" } pavex_builder = { path = "../pavex_builder" } miette = { version = "5.3.0", features = ["fancy"] } fs-err = "2.7.0" +tracing-subscriber = { version = "0.3", features = ["fmt", "env-filter"] } +tracing = "0.1" [dev-dependencies] pavex_test_runner = { path = "../pavex_test_runner" } diff --git a/libs/pavex_cli/src/main.rs b/libs/pavex_cli/src/main.rs index 4f45fe79a..8fe366870 100644 --- a/libs/pavex_cli/src/main.rs +++ b/libs/pavex_cli/src/main.rs @@ -1,6 +1,10 @@ use std::path::PathBuf; use clap::{Parser, Subcommand}; +use tracing_subscriber::fmt::format::FmtSpan; +use tracing_subscriber::layer::SubscriberExt; +use tracing_subscriber::util::SubscriberInitExt; +use tracing_subscriber::EnvFilter; use pavex::App; use pavex_builder::AppBlueprint; @@ -33,6 +37,22 @@ enum Commands { }, } +fn init_telemetry() { + let fmt_layer = tracing_subscriber::fmt::layer() + .with_file(false) + .with_target(false) + .with_span_events(FmtSpan::NEW | FmtSpan::EXIT) + .with_timer(tracing_subscriber::fmt::time::uptime()); + let filter_layer = EnvFilter::try_from_default_env() + .or_else(|_| EnvFilter::try_new("info")) + .unwrap(); + + tracing_subscriber::registry() + .with(filter_layer) + .with(fmt_layer) + .init(); +} + fn main() -> Result<(), Box> { let cli = Cli::parse(); miette::set_hook(Box::new(move |_| { @@ -45,6 +65,7 @@ fn main() -> Result<(), Box> { Box::new(config.build()) })) .unwrap(); + init_telemetry(); match cli.command { Commands::Generate { blueprint, diff --git a/libs/pavex_cli/tests/ui_tests/references_to_a_constructible_type_are_not_allowed/lib.rs b/libs/pavex_cli/tests/ui_tests/references_to_a_constructible_type_are_not_allowed/lib.rs index 6033f6597..0e07f4a41 100644 --- a/libs/pavex_cli/tests/ui_tests/references_to_a_constructible_type_are_not_allowed/lib.rs +++ b/libs/pavex_cli/tests/ui_tests/references_to_a_constructible_type_are_not_allowed/lib.rs @@ -1,5 +1,6 @@ use pavex_builder::{f, AppBlueprint, Lifecycle}; +#[derive(Clone)] pub struct Singleton; impl Singleton { diff --git a/libs/pavex_cli/tests/ui_tests/singletons_must_be_clonable/expectations/stderr.txt b/libs/pavex_cli/tests/ui_tests/singletons_must_be_clonable/expectations/stderr.txt new file mode 100644 index 000000000..e40a00480 --- /dev/null +++ b/libs/pavex_cli/tests/ui_tests/singletons_must_be_clonable/expectations/stderr.txt @@ -0,0 +1,13 @@ +Error: + × `app::NonCloneSingleton` does not implement the `core::clone::Clone` + │ trait. + ╭─[src/lib.rs:11:1] + 11 │ pub fn blueprint() -> AppBlueprint { + 12 │ AppBlueprint::new().constructor(f!(crate::NonCloneSingleton::new), Lifecycle::Singleton) + · ────────────────┬──────────────── + · ╰── The constructor was registered here + 13 │ } + ╰──── + help: All singletons must implement the `Send`, `Sync` and `Clone` traits. + `pavex` runs on a multi-threaded HTTP server and singletons must be + shared across all worker threads. \ No newline at end of file diff --git a/libs/pavex_cli/tests/ui_tests/singletons_must_be_clonable/lib.rs b/libs/pavex_cli/tests/ui_tests/singletons_must_be_clonable/lib.rs new file mode 100644 index 000000000..3b5e7d831 --- /dev/null +++ b/libs/pavex_cli/tests/ui_tests/singletons_must_be_clonable/lib.rs @@ -0,0 +1,13 @@ +use pavex_builder::{f, AppBlueprint, Lifecycle}; + +pub struct NonCloneSingleton; + +impl NonCloneSingleton { + pub fn new() -> NonCloneSingleton { + todo!() + } +} + +pub fn blueprint() -> AppBlueprint { + AppBlueprint::new().constructor(f!(crate::NonCloneSingleton::new), Lifecycle::Singleton) +} diff --git a/libs/pavex_cli/tests/ui_tests/singletons_must_be_clonable/test_config.toml b/libs/pavex_cli/tests/ui_tests/singletons_must_be_clonable/test_config.toml new file mode 100644 index 000000000..e65edde40 --- /dev/null +++ b/libs/pavex_cli/tests/ui_tests/singletons_must_be_clonable/test_config.toml @@ -0,0 +1,8 @@ +description = "Singletons must implement Clone" + +[expectations] +codegen = "fail" + +[dependencies] +http = "0.2" +hyper = { version = "0.14", features = ["server", "http1", "http2"] } diff --git a/libs/pavex_cli/tests/ui_tests/singletons_must_be_send/expectations/stderr.txt b/libs/pavex_cli/tests/ui_tests/singletons_must_be_send/expectations/stderr.txt new file mode 100644 index 000000000..322461a67 --- /dev/null +++ b/libs/pavex_cli/tests/ui_tests/singletons_must_be_send/expectations/stderr.txt @@ -0,0 +1,12 @@ +Error: + × `app::NonSendSingleton` does not implement the `core::marker::Send` trait. + ╭─[src/lib.rs:18:1] + 18 │ pub fn blueprint() -> AppBlueprint { + 19 │ AppBlueprint::new().constructor(f!(crate::NonSendSingleton::new), Lifecycle::Singleton) + · ────────────────┬─────────────── + · ╰── The constructor was registered here + 20 │ } + ╰──── + help: All singletons must implement the `Send`, `Sync` and `Clone` traits. + `pavex` runs on a multi-threaded HTTP server and singletons must be + shared across all worker threads. \ No newline at end of file diff --git a/libs/pavex_cli/tests/ui_tests/singletons_must_be_send/lib.rs b/libs/pavex_cli/tests/ui_tests/singletons_must_be_send/lib.rs new file mode 100644 index 000000000..d90a9c7e4 --- /dev/null +++ b/libs/pavex_cli/tests/ui_tests/singletons_must_be_send/lib.rs @@ -0,0 +1,20 @@ +use pavex_builder::{f, AppBlueprint, Lifecycle}; +use std::rc::Rc; + +pub struct NonSendSingleton(Rc<()>); + +impl Clone for NonSendSingleton { + fn clone(&self) -> NonSendSingleton { + Self(Rc::clone(&self.0)) + } +} + +impl NonSendSingleton { + pub fn new() -> NonSendSingleton { + todo!() + } +} + +pub fn blueprint() -> AppBlueprint { + AppBlueprint::new().constructor(f!(crate::NonSendSingleton::new), Lifecycle::Singleton) +} diff --git a/libs/pavex_cli/tests/ui_tests/singletons_must_be_send/test_config.toml b/libs/pavex_cli/tests/ui_tests/singletons_must_be_send/test_config.toml new file mode 100644 index 000000000..b96a6fd68 --- /dev/null +++ b/libs/pavex_cli/tests/ui_tests/singletons_must_be_send/test_config.toml @@ -0,0 +1,8 @@ +description = "Singletons must implement Send" + +[expectations] +codegen = "fail" + +[dependencies] +http = "0.2" +hyper = { version = "0.14", features = ["server", "http1", "http2"] } diff --git a/libs/pavex_cli/tests/ui_tests/singletons_must_be_sync/expectations/stderr.txt b/libs/pavex_cli/tests/ui_tests/singletons_must_be_sync/expectations/stderr.txt new file mode 100644 index 000000000..6834f02fc --- /dev/null +++ b/libs/pavex_cli/tests/ui_tests/singletons_must_be_sync/expectations/stderr.txt @@ -0,0 +1,12 @@ +Error: + × `app::NonSyncSingleton` does not implement the `core::marker::Sync` trait. + ╭─[src/lib.rs:18:1] + 18 │ pub fn blueprint() -> AppBlueprint { + 19 │ AppBlueprint::new().constructor(f!(crate::NonSyncSingleton::new), Lifecycle::Singleton) + · ────────────────┬─────────────── + · ╰── The constructor was registered here + 20 │ } + ╰──── + help: All singletons must implement the `Send`, `Sync` and `Clone` traits. + `pavex` runs on a multi-threaded HTTP server and singletons must be + shared across all worker threads. \ No newline at end of file diff --git a/libs/pavex_cli/tests/ui_tests/singletons_must_be_sync/lib.rs b/libs/pavex_cli/tests/ui_tests/singletons_must_be_sync/lib.rs new file mode 100644 index 000000000..ddc5518c8 --- /dev/null +++ b/libs/pavex_cli/tests/ui_tests/singletons_must_be_sync/lib.rs @@ -0,0 +1,20 @@ +use pavex_builder::{f, AppBlueprint, Lifecycle}; +use std::rc::Rc; + +pub struct NonSyncSingleton(std::sync::mpsc::Sender<()>); + +impl Clone for NonSyncSingleton { + fn clone(&self) -> NonSyncSingleton { + Self(self.0.clone()) + } +} + +impl NonSyncSingleton { + pub fn new() -> NonSyncSingleton { + todo!() + } +} + +pub fn blueprint() -> AppBlueprint { + AppBlueprint::new().constructor(f!(crate::NonSyncSingleton::new), Lifecycle::Singleton) +} diff --git a/libs/pavex_cli/tests/ui_tests/singletons_must_be_sync/test_config.toml b/libs/pavex_cli/tests/ui_tests/singletons_must_be_sync/test_config.toml new file mode 100644 index 000000000..b4ad88fda --- /dev/null +++ b/libs/pavex_cli/tests/ui_tests/singletons_must_be_sync/test_config.toml @@ -0,0 +1,8 @@ +description = "Singletons must implement Sync" + +[expectations] +codegen = "fail" + +[dependencies] +http = "0.2" +hyper = { version = "0.14", features = ["server", "http1", "http2"] }