[assembly] Add product-provided config-data to schema
This adds product-provided config_data to the product assembly
configuration schema.
It's only stubbed out in this CL.
Change-Id: I926ab96cb3e085f175835a08c01332f80c8c7b07
Reviewed-on: https://fuchsia-review.googlesource.com/c/fuchsia/+/686764
Reviewed-by: Aidan Wolter <awolter@google.com>
Reviewed-by: Yaar Schnitman <yaar@google.com>
Commit-Queue: Aaron Wood <aaronwood@google.com>
Fuchsia-Auto-Submit: Aaron Wood <aaronwood@google.com>
diff --git a/build/assembly/product_assembly_configuration.gni b/build/assembly/product_assembly_configuration.gni
index 7f9f399..2043a32 100644
--- a/build/assembly/product_assembly_configuration.gni
+++ b/build/assembly/product_assembly_configuration.gni
@@ -126,9 +126,20 @@
packages.cache = []
}
- packages.base += rebase_path(files.base_package_manifests, root_build_dir)
- packages.cache +=
- rebase_path(files.cache_package_manifests, root_build_dir)
+ foreach(manifest_path, files.base_package_manifests) {
+ packages.base += [
+ {
+ manifest = rebase_path(manifest_path, root_build_dir)
+ },
+ ]
+ }
+ foreach(manifest_path, files.cache_package_manifests) {
+ packages.cache += [
+ {
+ manifest = rebase_path(manifest_path, root_build_dir)
+ },
+ ]
+ }
}
}
diff --git a/build/assembly/scripts/compare_image_assembly_config_contents.py b/build/assembly/scripts/compare_image_assembly_config_contents.py
index 2962d1f..c246f32 100644
--- a/build/assembly/scripts/compare_image_assembly_config_contents.py
+++ b/build/assembly/scripts/compare_image_assembly_config_contents.py
@@ -205,16 +205,25 @@
errors = []
errors.extend(
compare_pkg_sets(
- legacy.base | set(product_packages.get('base', [])), generated.base,
- "base"))
+ legacy.base | set(
+ [
+ entry["manifest"]
+ for entry in product_packages.get('base', [])
+ ]), generated.base, "base"))
errors.extend(
compare_pkg_sets(
- legacy.cache | set(product_packages.get('cache', [])),
- generated.cache, "cache"))
+ legacy.cache | set(
+ [
+ entry["manifest"]
+ for entry in product_packages.get('cache', [])
+ ]), generated.cache, "cache"))
errors.extend(
compare_pkg_sets(
- legacy.system | set(product_packages.get('system', [])),
- generated.system, "system"))
+ legacy.system | set(
+ [
+ entry["manifest"]
+ for entry in product_packages.get('system', [])
+ ]), generated.system, "system"))
errors.extend(
compare_file_entry_sets(
diff --git a/build/assembly/scripts/generated_assembly_inputs.py b/build/assembly/scripts/generated_assembly_inputs.py
index 7940d5c..5fdeab2 100644
--- a/build/assembly/scripts/generated_assembly_inputs.py
+++ b/build/assembly/scripts/generated_assembly_inputs.py
@@ -10,6 +10,8 @@
from depfile import DepFile
+from typing import Dict, Optional
+
def main():
parser = argparse.ArgumentParser(
@@ -50,13 +52,18 @@
# Add a package and all the included blobs.
manifests_for_depfile = []
- def add_package(manifest):
+ def add_package(entry: Dict):
+ manifest = entry["manifest"]
manifests_for_depfile.append(manifest)
add_source(manifest)
with open(manifest, 'r') as f:
manifest = json.load(f)
for blob in manifest.get("blobs", []):
add_source(blob["source_path"])
+ config_data: Optional[Dict[str, str]] = entry.get("config_data")
+ if config_data:
+ for (_dest, source) in config_data.items():
+ add_source(source)
# Add the product config.
add_source(args.product_config.name)
diff --git a/src/developer/ffx/plugins/assembly/BUILD.gn b/src/developer/ffx/plugins/assembly/BUILD.gn
index 1b482c2..e0f414e 100644
--- a/src/developer/ffx/plugins/assembly/BUILD.gn
+++ b/src/developer/ffx/plugins/assembly/BUILD.gn
@@ -50,6 +50,7 @@
"//src/lib/assembly/images_manifest",
"//src/lib/assembly/minfs",
"//src/lib/assembly/package_list",
+ "//src/lib/assembly/package_utils",
"//src/lib/assembly/partitions_config",
"//src/lib/assembly/structured_config",
"//src/lib/assembly/test_keys",
diff --git a/src/developer/ffx/plugins/assembly/src/operations/product/assembly_builder.rs b/src/developer/ffx/plugins/assembly/src/operations/product/assembly_builder.rs
index e3198a1..5b3f5191 100644
--- a/src/developer/ffx/plugins/assembly/src/operations/product/assembly_builder.rs
+++ b/src/developer/ffx/plugins/assembly/src/operations/product/assembly_builder.rs
@@ -163,10 +163,10 @@
/// flagged as being the issue (and not the platform being the issue).
pub fn add_product_packages(&mut self, packages: &ProductPackagesConfig) -> Result<()> {
for p in &packages.base {
- self.base.add_package_from_path(p)?
+ self.base.add_package_from_path(p.manifest.as_std_path())?
}
for p in &packages.cache {
- self.cache.add_package_from_path(p)?
+ self.cache.add_package_from_path(p.manifest.as_std_path())?
}
Ok(())
}
@@ -469,22 +469,24 @@
#[cfg(test)]
mod tests {
use super::*;
+ use assembly_package_utils::PackageManifestPathBuf;
use camino::Utf8PathBuf;
use fuchsia_pkg::{PackageBuilder, PackageManifest};
use std::fs::File;
use tempfile::TempDir;
- fn write_empty_pkg(path: impl AsRef<Path>, name: &str) -> Utf8PathBuf {
+ fn write_empty_pkg(path: impl AsRef<Path>, name: &str) -> PackageManifestPathBuf {
let path = path.as_ref();
let mut builder = PackageBuilder::new(name);
let manifest_path = path.join(name);
builder.manifest_path(&manifest_path);
builder.build(path, path.join(format!("{}_meta.far", name))).unwrap();
- Utf8PathBuf::from_path_buf(manifest_path).unwrap()
+ Utf8PathBuf::from_path_buf(manifest_path).unwrap().into()
}
fn make_test_assembly_bundle(bundle_path: &Path) -> AssemblyInputBundle {
- let write_empty_bundle_pkg = |name: &str| write_empty_pkg(bundle_path, name).into();
+ let write_empty_bundle_pkg =
+ |name: &str| write_empty_pkg(bundle_path, name).clone().into_std_path_buf();
AssemblyInputBundle {
image_assembly: image_assembly_config::PartialImageAssemblyConfig {
base: vec![write_empty_bundle_pkg("base_package0")],
@@ -604,8 +606,14 @@
let outdir = TempDir::new().unwrap();
let packages = ProductPackagesConfig {
- base: vec![write_empty_pkg(&outdir, "base_a"), write_empty_pkg(&outdir, "base_b")],
- cache: vec![write_empty_pkg(&outdir, "cache_a"), write_empty_pkg(&outdir, "cache_b")],
+ base: vec![
+ write_empty_pkg(&outdir, "base_a").into(),
+ write_empty_pkg(&outdir, "base_b").into(),
+ ],
+ cache: vec![
+ write_empty_pkg(&outdir, "cache_a").into(),
+ write_empty_pkg(&outdir, "cache_b").into(),
+ ],
};
let minimum_bundle = AssemblyInputBundle {
image_assembly: image_assembly_config::PartialImageAssemblyConfig {
@@ -647,7 +655,7 @@
let outdir = TempDir::new().unwrap();
let packages = ProductPackagesConfig {
- base: vec![write_empty_pkg(&outdir, "base_a")],
+ base: vec![write_empty_pkg(&outdir, "base_a").into()],
..ProductPackagesConfig::default()
};
let minimum_bundle = AssemblyInputBundle {
diff --git a/src/lib/assembly/BUILD.gn b/src/lib/assembly/BUILD.gn
index a706b66..a102217 100644
--- a/src/lib/assembly/BUILD.gn
+++ b/src/lib/assembly/BUILD.gn
@@ -14,6 +14,7 @@
"images_config:host_tests",
"images_manifest:host_tests",
"minfs:host_tests",
+ "package_utils:host_tests",
"partitions_config:host_tests",
"structured_config:host_tests",
"test_util:host_tests",
diff --git a/src/lib/assembly/config/BUILD.gn b/src/lib/assembly/config/BUILD.gn
index a7cda17..5f7a5a0 100644
--- a/src/lib/assembly/config/BUILD.gn
+++ b/src/lib/assembly/config/BUILD.gn
@@ -14,6 +14,7 @@
"//sdk/fidl/fuchsia.logger:fuchsia.logger-rustc",
"//src/developer/ffx/config:lib",
"//src/lib/assembly/fvm",
+ "//src/lib/assembly/package_utils",
"//src/lib/assembly/util",
"//third_party/rust_crates:anyhow",
"//third_party/rust_crates:assert_matches",
diff --git a/src/lib/assembly/config/src/product_config.rs b/src/lib/assembly/config/src/product_config.rs
index 84f4ee1..89951b6 100644
--- a/src/lib/assembly/config/src/product_config.rs
+++ b/src/lib/assembly/config/src/product_config.rs
@@ -5,6 +5,7 @@
use crate as image_assembly_config;
use crate::FileEntry;
use anyhow::ensure;
+use assembly_package_utils::{PackageInternalPathBuf, PackageManifestPathBuf, SourcePathBuf};
use camino::Utf8PathBuf;
use fidl_fuchsia_logger::MAX_TAGS;
use serde::{Deserialize, Serialize};
@@ -68,15 +69,65 @@
}
/// Packages provided by the product, to add to the assembled images.
+///
+/// This also includes configuration for those packages:
+///
+/// ```json5
+/// packages: {
+/// base: [
+/// {
+/// manifest: "path/to/package_a/package_manifest.json",
+/// },
+/// {
+/// manifest: "path/to/package_b/package_manifest.json",
+/// config_data: {
+/// "foo.cfg": "path/to/some/source/file/foo.cfg",
+/// "bar/more/data.json": "path/to/some.json",
+/// },
+/// },
+/// ],
+/// cache: []
+/// }
+/// ```
+///
#[derive(Debug, Default, Deserialize, Serialize)]
pub struct ProductPackagesConfig {
- /// Paths to package manifests for packages to add to the 'base' package set.
+ /// Paths to package manifests, or more detailed json entries for packages
+ /// to add to the 'base' package set.
#[serde(default)]
- pub base: Vec<Utf8PathBuf>,
+ #[serde(skip_serializing_if = "Vec::is_empty")]
+ pub base: Vec<ProductPackageDetails>,
- /// Paths to package manifests for packages to add to the 'cache' package set.
+ /// Paths to package manifests, or more detailed json entries for packages
+ /// to add to the 'cache' package set.
#[serde(default)]
- pub cache: Vec<Utf8PathBuf>,
+ #[serde(skip_serializing_if = "Vec::is_empty")]
+ pub cache: Vec<ProductPackageDetails>,
+}
+
+/// Describes in more detail a package to add to the assembly.
+#[derive(Debug, PartialEq, Deserialize, Serialize)]
+pub struct ProductPackageDetails {
+ /// Path to the package manifest for this package.
+ pub manifest: PackageManifestPathBuf,
+
+ /// Map of config_data entries for this package, from the destination path
+ /// within the package, to the path where the source file is to be found.
+ #[serde(default)]
+ #[serde(skip_serializing_if = "BTreeMap::is_empty")]
+ pub config_data: BTreeMap<PackageInternalPathBuf, SourcePathBuf>,
+}
+
+impl From<PackageManifestPathBuf> for ProductPackageDetails {
+ fn from(manifest: PackageManifestPathBuf) -> Self {
+ Self { manifest, config_data: BTreeMap::default() }
+ }
+}
+
+impl From<&str> for ProductPackageDetails {
+ fn from(s: &str) -> Self {
+ ProductPackageDetails { manifest: s.into(), config_data: BTreeMap::default() }
+ }
}
const BASE_CONSOLE_ALLOWED_TAGS: &[&str] = &[
@@ -300,10 +351,10 @@
product: {
packages: {
base: [
- "path/to/base/package_manifest.json"
+ { manifest: "path/to/base/package_manifest.json" }
],
cache: [
- "path/to/cache/package_manifest.json"
+ { manifest: "path/to/cache/package_manifest.json" }
]
}
},
@@ -313,8 +364,118 @@
let mut cursor = std::io::Cursor::new(json5);
let config: ProductAssemblyConfig = util::from_reader(&mut cursor).unwrap();
assert_eq!(config.platform.build_type, BuildType::Eng);
- assert_eq!(config.product.packages.base, vec!["path/to/base/package_manifest.json"]);
- assert_eq!(config.product.packages.cache, vec!["path/to/cache/package_manifest.json"]);
+ assert_eq!(
+ config.product.packages.base,
+ vec![ProductPackageDetails {
+ manifest: "path/to/base/package_manifest.json".into(),
+ config_data: BTreeMap::default()
+ }]
+ );
+ assert_eq!(
+ config.product.packages.cache,
+ vec![ProductPackageDetails {
+ manifest: "path/to/cache/package_manifest.json".into(),
+ config_data: BTreeMap::default()
+ }]
+ );
+ }
+
+ #[test]
+ fn test_product_provided_config_data() {
+ let json5 = r#"
+ {
+ base: [
+ {
+ manifest: "path/to/base/package_manifest.json"
+ },
+ {
+ manifest: "some/other/manifest.json",
+ config_data: {
+ "dest/path/cfg.txt": "source/path/cfg.txt",
+ "other_data.json": "source_other_data.json",
+ }
+ }
+ ],
+ cache: [
+ {
+ manifest: "path/to/cache/package_manifest.json"
+ }
+ ]
+ }
+ "#;
+
+ let mut cursor = std::io::Cursor::new(json5);
+ let packages: ProductPackagesConfig = util::from_reader(&mut cursor).unwrap();
+ assert_eq!(
+ packages.base,
+ vec![
+ ProductPackageDetails::from("path/to/base/package_manifest.json"),
+ ProductPackageDetails {
+ manifest: "some/other/manifest.json".into(),
+ config_data: BTreeMap::from([
+ ("dest/path/cfg.txt".into(), "source/path/cfg.txt".into()),
+ ("other_data.json".into(), "source_other_data.json".into())
+ ])
+ }
+ ]
+ );
+ assert_eq!(packages.cache, vec!["path/to/cache/package_manifest.json".into()]);
+ }
+
+ #[test]
+ fn product_package_details_deserialization() {
+ let json5 = r#"
+ {
+ manifest: "some/other/manifest.json",
+ config_data: {
+ "dest/path/cfg.txt": "source/path/cfg.txt",
+ "other_data.json": "source_other_data.json",
+ }
+ }
+ "#;
+ let expected = ProductPackageDetails {
+ manifest: "some/other/manifest.json".into(),
+ config_data: BTreeMap::from([
+ ("dest/path/cfg.txt".into(), "source/path/cfg.txt".into()),
+ ("other_data.json".into(), "source_other_data.json".into()),
+ ]),
+ };
+ let mut cursor = std::io::Cursor::new(json5);
+ let details: ProductPackageDetails = util::from_reader(&mut cursor).unwrap();
+ assert_eq!(details, expected);
+ }
+
+ #[test]
+ fn product_package_details_serialization() {
+ let entries = vec![
+ ProductPackageDetails {
+ manifest: "path/to/manifest.json".into(),
+ config_data: BTreeMap::default(),
+ },
+ ProductPackageDetails {
+ manifest: "another/path/to/a/manifest.json".into(),
+ config_data: BTreeMap::from([
+ ("dest/path/A".into(), "source/path/A".into()),
+ ("dest/path/B".into(), "source/path/B".into()),
+ ]),
+ },
+ ];
+ let serialized = serde_json::to_value(&entries).unwrap();
+ let expected = serde_json::json!(
+ [
+ {
+ "manifest": "path/to/manifest.json"
+ },
+ {
+ "manifest": "another/path/to/a/manifest.json",
+ "config_data": {
+ "dest/path/A": "source/path/A",
+ "dest/path/B": "source/path/B"
+ }
+ }
+ ]
+ );
+ assert_eq!(serialized, expected);
}
#[test]
diff --git a/src/lib/assembly/package_utils/BUILD.gn b/src/lib/assembly/package_utils/BUILD.gn
new file mode 100644
index 0000000..db9267e
--- /dev/null
+++ b/src/lib/assembly/package_utils/BUILD.gn
@@ -0,0 +1,30 @@
+# Copyright 2022 The Fuchsia Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+import("//build/rust/rustc_library.gni")
+
+rustc_library("package_utils") {
+ edition = "2021"
+ name = "assembly_package_utils"
+ version = "0.1.0"
+ with_unit_tests = true
+ deps = [
+ "//src/lib/assembly/util",
+ "//third_party/rust_crates:anyhow",
+ "//third_party/rust_crates:serde",
+ ]
+ test_deps = [
+ "//third_party/rust_crates:serde_json",
+ "//third_party/rust_crates:serde_json5",
+ ]
+ sources = [
+ "src/lib.rs",
+ "src/package_utils.rs",
+ ]
+}
+
+group("host_tests") {
+ testonly = true
+ deps = [ ":package_utils_test" ]
+}
diff --git a/src/lib/assembly/package_utils/src/lib.rs b/src/lib/assembly/package_utils/src/lib.rs
new file mode 100644
index 0000000..cb291c7
--- /dev/null
+++ b/src/lib/assembly/package_utils/src/lib.rs
@@ -0,0 +1,9 @@
+// Copyright 2022 The Fuchsia Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+/// 'package_utils' is a crate of utility types and fns for working with
+/// fuchsia-packages.
+pub use package_utils::{PackageInternalPathBuf, PackageManifestPathBuf, SourcePathBuf};
+
+mod package_utils;
diff --git a/src/lib/assembly/package_utils/src/package_utils.rs b/src/lib/assembly/package_utils/src/package_utils.rs
new file mode 100644
index 0000000..ce905bd9
--- /dev/null
+++ b/src/lib/assembly/package_utils/src/package_utils.rs
@@ -0,0 +1,43 @@
+// Copyright 2022 The Fuchsia Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+use assembly_util::{impl_path_type_marker, PathTypeMarker, TypedPathBuf};
+use serde::{Deserialize, Serialize};
+
+/// PackageIdentity is an opaque type that allows for the string that's used as
+/// a package's identity to be evolved over time, compared with other instances,
+/// and used as a key in maps / sets.
+#[derive(Debug, Clone, PartialEq, PartialOrd, Serialize, Deserialize)]
+struct PackageIdentity(String);
+
+impl std::str::FromStr for PackageIdentity {
+ type Err = String;
+
+ fn from_str(s: &str) -> Result<Self, Self::Err> {
+ Ok(Self(s.to_owned()))
+ }
+}
+
+/// The marker trait for paths within a package
+pub struct InternalPathMarker {}
+impl_path_type_marker!(InternalPathMarker);
+
+/// The semantic type for paths within a package
+pub type PackageInternalPathBuf = TypedPathBuf<InternalPathMarker>;
+
+/// The marker trait for the source path when that's ambiguous (like in a list
+/// of source to destination paths)
+pub struct SourcePathMarker {}
+impl_path_type_marker!(SourcePathMarker);
+
+/// The semantic type for paths that are the path to the source of a file to use
+/// in some context. Such as the source file for a blob in a package.
+pub type SourcePathBuf = TypedPathBuf<SourcePathMarker>;
+
+/// The marker trait for paths to a PackageManifest
+pub struct PackageManifestPathMarker {}
+impl_path_type_marker!(PackageManifestPathMarker);
+
+/// The semantic type for paths that are the path to a package manifest.
+pub type PackageManifestPathBuf = TypedPathBuf<PackageManifestPathMarker>;
diff --git a/src/lib/assembly/util/src/lib.rs b/src/lib/assembly/util/src/lib.rs
index 8bbe51c..7f58baf7 100644
--- a/src/lib/assembly/util/src/lib.rs
+++ b/src/lib/assembly/util/src/lib.rs
@@ -8,13 +8,14 @@
mod insert_unique;
mod path_to_string;
+
mod paths;
pub use insert_unique::{DuplicateKeyError, InsertAllUniqueExt, InsertUniqueExt, MapEntry};
pub use path_to_string::PathToStringExt;
pub use paths::{
normalize_path, path_relative_from, path_relative_from_file, resolve_path,
- resolve_path_from_file,
+ resolve_path_from_file, PathTypeMarker, TypedPathBuf,
};
use anyhow::{Context as _, Result};
diff --git a/src/lib/assembly/util/src/paths.rs b/src/lib/assembly/util/src/paths.rs
index 4936eb5..8270175 100644
--- a/src/lib/assembly/util/src/paths.rs
+++ b/src/lib/assembly/util/src/paths.rs
@@ -3,8 +3,189 @@
// found in the LICENSE file.
use anyhow::{anyhow, Context, Result};
+use camino::Utf8PathBuf;
use pathdiff::diff_paths;
-use std::path::{Component, Path, PathBuf};
+use serde::{Deserialize, Serialize};
+use std::{
+ hash::Hash,
+ marker::PhantomData,
+ path::{Component, Path, PathBuf},
+};
+
+/// A base trait for TypePath's marker traits.
+pub trait PathTypeMarker {
+ /// A reference to an object that implements Display, and gives the
+ /// displayable semantic type for this path. This is used by the Debug
+ /// implementation of `TypedPathBuf` to display the semantic type for the
+ /// path:
+ ///
+ /// ```
+ /// struct MarkerStructType;
+ /// impl_path_type_marker!(MarkerStructType);
+ ///
+ /// let typed_path = TypedPathBuf<MarkerStructType>::from("some/path");
+ /// println!("{:?}", typed_path);
+ /// ```
+ /// will print:
+ ///
+ /// ```text
+ /// TypedPathBuf<MarkerStructType>("some/path")
+ /// ```
+ fn path_type_display() -> &'static dyn std::fmt::Display;
+}
+
+/// Implement the `PathTypeMarker` trait for a given marker-type struct. This
+/// mainly simplifies the creation of a display-string for the type.
+#[macro_export]
+macro_rules! impl_path_type_marker {
+ // This macro takes an argument of the marker struct's type name, and then
+ // provides an implementation of 'PathTypeMarker' for it.
+ ($struct_name:ident) => {
+ impl PathTypeMarker for $struct_name {
+ fn path_type_display() -> &'static dyn std::fmt::Display {
+ &stringify!($struct_name)
+ }
+ }
+ };
+}
+
+/// A path, in valid utf-8, which carries a marker for what kind of path it is.
+#[derive(Clone, Serialize, Deserialize)]
+#[repr(transparent)]
+#[serde(transparent)]
+pub struct TypedPathBuf<P: PathTypeMarker> {
+ #[serde(flatten)]
+ inner: Utf8PathBuf,
+
+ #[serde(skip)]
+ _marker: PhantomData<P>,
+}
+
+/// This derefs into the typed version of utf8 path, not utf8 path itself, so
+/// that it is easier to use in typed contexts, and makes the switchover to
+/// a non-typed context more explicit.
+///
+/// This also causes any path manipulations (join, etc.) to be done without the
+/// semantic type, so that the caller has to be explicit that it's still the
+/// semantic type (using 'into()', for instance).
+impl<P: PathTypeMarker> std::ops::Deref for TypedPathBuf<P> {
+ type Target = Utf8PathBuf;
+
+ fn deref(&self) -> &Self::Target {
+ &self.inner
+ }
+}
+
+impl<P: PathTypeMarker> TypedPathBuf<P> {
+ /// Convert this TypedPathBuf into a standard (OsStr-based) `PathBuf`. This
+ /// both strips it of semantic type and that it's known to be Utf-8.
+ pub fn into_std_path_buf(self) -> PathBuf {
+ self.inner.into_std_path_buf()
+ }
+}
+
+/// The Debug implementation displays like a type-struct that carries the marker
+/// type for the path:
+///
+/// ```text
+/// TypedPathBuf<MarkerStructType>("some/path")
+/// ```
+impl<P: PathTypeMarker> std::fmt::Debug for TypedPathBuf<P> {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ f.debug_tuple(&format!("TypedPathBuf<{}>", P::path_type_display()))
+ .field(&self.inner.to_string())
+ .finish()
+ }
+}
+
+/// The Display implementation defers to the wrapped path.
+impl<P: PathTypeMarker> std::fmt::Display for TypedPathBuf<P> {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ self.inner.fmt(f)
+ }
+}
+
+/// Implement From<> for path-like sources. Note that these also will infer the
+/// semantic type, which while useful in some contexts, can cause issues in
+/// places where multiple different type markers are used:
+///
+/// ```
+/// fn some_func(source: TypedPathBuf<Source>, TypedPathBuf<Destination>);
+///
+/// // This infers the types of the paths:
+/// some_func("source_path".into(), "destination_path".into());
+///
+/// // allowing this error:
+/// some_func("destination_path".into(), "source_path",into());
+///
+/// // In these cases, it's best to strongly type one or both of them:
+/// some_func(TypedPathBuf<Source>::from("source_path"), "destination_path".into());
+///
+/// // or (better)
+/// some_func(TypedPathBuf<Source>::from("source_path"),
+/// TypedPathBuf<Destination>::from("destination_path"));
+/// ```
+// inner module used to group impls and to add above documentation.
+mod from_impls {
+ use super::*;
+
+ impl<P: PathTypeMarker> From<Utf8PathBuf> for TypedPathBuf<P> {
+ fn from(path: Utf8PathBuf) -> Self {
+ Self { inner: path, _marker: PhantomData }
+ }
+ }
+
+ impl<P: PathTypeMarker> From<String> for TypedPathBuf<P> {
+ fn from(s: String) -> TypedPathBuf<P> {
+ TypedPathBuf::from(Utf8PathBuf::from(s))
+ }
+ }
+
+ impl<P: PathTypeMarker> From<&str> for TypedPathBuf<P> {
+ fn from(s: &str) -> TypedPathBuf<P> {
+ TypedPathBuf::from(Utf8PathBuf::from(s))
+ }
+ }
+
+ impl<P: PathTypeMarker> std::str::FromStr for TypedPathBuf<P> {
+ type Err = String;
+
+ fn from_str(s: &str) -> Result<Self, Self::Err> {
+ Ok(Self::from(s))
+ }
+ }
+}
+
+// These comparison implementations are required because #[derive(...)] will not
+// derive these if `P` doesn't implement them, but `P` has no reason to
+// implement them, so these implementations just pass through to the Utf8PathBuf
+// implementations.
+
+impl<P: PathTypeMarker> PartialOrd for TypedPathBuf<P> {
+ fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
+ self.inner.partial_cmp(&other.inner)
+ }
+}
+
+impl<P: PathTypeMarker> Ord for TypedPathBuf<P> {
+ fn cmp(&self, other: &Self) -> std::cmp::Ordering {
+ self.inner.cmp(&other.inner)
+ }
+}
+
+impl<P: PathTypeMarker> PartialEq for TypedPathBuf<P> {
+ fn eq(&self, other: &Self) -> bool {
+ self.inner == other.inner
+ }
+}
+
+impl<P: PathTypeMarker> Eq for TypedPathBuf<P> {}
+
+impl<P: PathTypeMarker> Hash for TypedPathBuf<P> {
+ fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
+ self.inner.hash(state);
+ }
+}
/// Helper to make one path relative to a directory.
///
@@ -165,8 +346,72 @@
#[cfg(test)]
mod tests {
use super::*;
+ use std::str::FromStr;
use std::{iter::FromIterator, path::PathBuf};
+ struct TestPathType {}
+ impl_path_type_marker!(TestPathType);
+
+ #[test]
+ fn make_typed_path_from_string() {
+ let original: String = "/this/is/a/string".to_string();
+ let typed = TypedPathBuf::<TestPathType>::from_str(&original).unwrap();
+ assert_eq!(typed.to_string(), original);
+ }
+
+ #[test]
+ fn make_typed_path_from_str() {
+ let original: &str = "/this/is/a/string";
+ let typed = TypedPathBuf::<TestPathType>::from_str(&original).unwrap();
+ assert_eq!(typed.to_string(), original);
+ }
+
+ #[test]
+ fn path_type_deserialization() {
+ #[derive(Debug, Deserialize)]
+ struct Sample {
+ pub path: TypedPathBuf<TestPathType>,
+ }
+ let parsed: Sample = serde_json::from_str("{ \"path\": \"this/is/a/path\"}").unwrap();
+ assert_eq!(parsed.path, TypedPathBuf::<TestPathType>::from("this/is/a/path"));
+ }
+
+ #[test]
+ fn path_type_serialization() {
+ #[derive(Debug, Serialize)]
+ struct Sample {
+ pub path: TypedPathBuf<TestPathType>,
+ }
+ let sample = Sample { path: "this/is/a/path".into() };
+ let expected = serde_json::json!({ "path": "this/is/a/path"});
+ assert_eq!(serde_json::to_value(sample).unwrap(), expected);
+ }
+
+ #[test]
+ fn typed_path_debug_impl() {
+ let typed = TypedPathBuf::<TestPathType>::from("some/path");
+ assert_eq!(format!("{:?}", typed), "TypedPathBuf<TestPathType>(\"some/path\")");
+ }
+
+ #[test]
+ fn typed_path_display_impl() {
+ let typed = TypedPathBuf::<TestPathType>::from("some/path");
+ assert_eq!(format!("{}", typed), "some/path");
+ }
+
+ #[test]
+ fn typed_path_buf_into_path_buf() {
+ let typed = TypedPathBuf::<TestPathType>::from("some/path");
+ assert_eq!(typed.into_std_path_buf(), PathBuf::from("some/path"));
+ }
+
+ #[test]
+ fn typed_path_derefs_into_utf8_path() {
+ let typed = TypedPathBuf::<TestPathType>::from("some/path");
+ let utf8_path = Utf8PathBuf::from("some/path");
+ assert_eq!(*typed, utf8_path);
+ }
+
#[test]
fn resolve_path_from_file_simple() {
let result = resolve_path_from_file("an/internal/path", "path/to/manifest.txt").unwrap();