blob: c4c01c6275a57bc7c55efb8619b3d97aa2edfbf2 [file] [log] [blame]
// Copyright 2020 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.
//! A crate containing common Component Manager types used in Component Manifests
//! (`.cml` files and binary `.cm` files). These types come with `serde` serialization
//! and deserialization implementations that perform the required validation.
use {
fidl_fuchsia_component_decl as fdecl, fidl_fuchsia_io as fio,
flyweights::FlyStr,
lazy_static::lazy_static,
serde::{de, ser, Deserialize, Serialize},
std::{
borrow::Borrow,
cmp,
ffi::CString,
fmt::{self, Display},
iter,
path::PathBuf,
str::FromStr,
},
thiserror::Error,
};
lazy_static! {
/// A default base URL from which to parse relative component URL
/// components.
static ref DEFAULT_BASE_URL: url::Url = url::Url::parse("relative:///").unwrap();
}
/// Generate `impl From` for two trivial enums with identical values, allowing
/// converting to/from each other.
/// This is useful if you have a FIDL-generated enum and a hand-rolled
/// one that contain the same values.
/// # Arguments
///
/// * `$a`, `$b` - The enums to generate `impl From` for. Order doesn't matter because
/// implementation will be generated for both. Enums should be trivial.
/// * `id` - Exhaustive list of all enum values.
/// # Examples
///
/// ```
/// mod a {
/// #[derive(Debug, PartialEq, Eq)]
/// pub enum Streetlight {
/// Green,
/// Yellow,
/// Red,
/// }
/// }
///
/// mod b {
/// #[derive(Debug, PartialEq, Eq)]
/// pub enum Streetlight {
/// Green,
/// Yellow,
/// Red,
/// }
/// }
///
/// symmetrical_enums!(a::Streetlight, b::Streetlight, Green, Yellow, Red);
///
/// assert_eq!(a::Streetlight::Green, b::Streetlight::Green.into());
/// assert_eq!(b::Streetlight::Green, a::Streetlight::Green.into());
/// ```
#[macro_export]
macro_rules! symmetrical_enums {
($a:ty , $b:ty, $($id: ident),*) => {
impl From<$a> for $b {
fn from(input: $a) -> Self {
match input {
$( <$a>::$id => <$b>::$id, )*
}
}
}
impl From<$b> for $a {
fn from(input: $b) -> Self {
match input {
$( <$b>::$id => <$a>::$id, )*
}
}
}
};
}
/// The error representing a failure to parse a type from string.
#[derive(Serialize, Clone, Deserialize, Debug, Error, PartialEq, Eq)]
pub enum ParseError {
/// The string did not match a valid value.
#[error("invalid value")]
InvalidValue,
/// The string did not match a valid absolute or relative component URL
#[error("invalid URL: {details}")]
InvalidComponentUrl { details: String },
/// The string was empty.
#[error("empty")]
Empty,
/// The string was too long.
#[error("too long")]
TooLong,
/// A required leading slash was missing.
#[error("no leading slash")]
NoLeadingSlash,
/// The path segment is invalid.
#[error("invalid path segment")]
InvalidSegment,
}
pub const MAX_NAME_LENGTH: usize = name::MAX_NAME_LENGTH;
pub const MAX_LONG_NAME_LENGTH: usize = 1024;
pub const MAX_PATH_LENGTH: usize = fio::MAX_PATH_LENGTH as usize;
pub const MAX_URL_LENGTH: usize = 4096;
/// This asks for the maximum possible rights that the parent connection will allow; this will
/// include the writable and executable rights if the parent connection has them, but won't fail if
/// it doesn't.
pub const OPEN_FLAGS_MAX_POSSIBLE_RIGHTS: fio::OpenFlags = fio::OpenFlags::RIGHT_READABLE
.union(fio::OpenFlags::POSIX_WRITABLE)
.union(fio::OpenFlags::POSIX_EXECUTABLE);
/// A name that can refer to a component, collection, or other entity in the
/// Component Manifest. Its length is bounded to `MAX_NAME_LENGTH`.
pub type Name = BoundedName<MAX_NAME_LENGTH>;
/// A `Name` with a higher string capacity of `MAX_LONG_NAME_LENGTH`.
pub type LongName = BoundedName<MAX_LONG_NAME_LENGTH>;
/// A `BoundedName` is a `Name` that can have a max length of `N` bytes.
#[derive(Serialize, Clone, Debug, Hash, PartialEq, Eq, PartialOrd, Ord)]
pub struct BoundedName<const N: usize>(FlyStr);
impl<const N: usize> BoundedName<N> {
/// Creates a `BoundedName` from a `String`, returning an `Err` if the string
/// fails validation. The string must be non-empty, no more than `N`
/// characters in length, and consist of one or more of the
/// following characters: `A-Z`, `a-z`, `0-9`, `_`, `.`, `-`. It may not start
/// with `.` or `-`.
pub fn new(name: impl AsRef<str> + Into<String>) -> Result<Self, ParseError> {
{
let name = name.as_ref();
if name.is_empty() {
return Err(ParseError::Empty);
}
if name.len() > N {
return Err(ParseError::TooLong);
}
let mut char_iter = name.chars();
let first_char = char_iter.next().unwrap();
if !first_char.is_ascii_alphanumeric() && first_char != '_' {
return Err(ParseError::InvalidValue);
}
let valid_fn = |c: char| c.is_ascii_alphanumeric() || c == '_' || c == '-' || c == '.';
if !char_iter.all(valid_fn) {
return Err(ParseError::InvalidValue);
}
}
Ok(Self(FlyStr::new(name)))
}
pub fn as_str(&self) -> &str {
&self.0
}
pub fn is_empty(&self) -> bool {
self.0.is_empty()
}
pub fn len(&self) -> usize {
self.0.len()
}
}
impl<const N: usize> AsRef<str> for BoundedName<N> {
fn as_ref(&self) -> &str {
self.as_str()
}
}
impl<const N: usize> Borrow<FlyStr> for BoundedName<N> {
fn borrow(&self) -> &FlyStr {
&self.0
}
}
impl<'a, const N: usize> From<BoundedName<N>> for FlyStr {
fn from(o: BoundedName<N>) -> Self {
o.0
}
}
impl<'a, const N: usize> From<&'a BoundedName<N>> for &'a FlyStr {
fn from(o: &'a BoundedName<N>) -> Self {
&o.0
}
}
impl<const N: usize> PartialEq<&str> for BoundedName<N> {
fn eq(&self, o: &&str) -> bool {
&*self.0 == *o
}
}
impl<const N: usize> PartialEq<String> for BoundedName<N> {
fn eq(&self, o: &String) -> bool {
&*self.0 == *o
}
}
impl<const N: usize> fmt::Display for BoundedName<N> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
<FlyStr as fmt::Display>::fmt(&self.0, f)
}
}
impl<const N: usize> FromStr for BoundedName<N> {
type Err = ParseError;
fn from_str(name: &str) -> Result<Self, Self::Err> {
Self::new(name)
}
}
impl<const N: usize> From<BoundedName<N>> for String {
fn from(name: BoundedName<N>) -> String {
name.0.into()
}
}
impl From<Name> for LongName {
fn from(name: Name) -> Self {
Self(name.0)
}
}
impl<'de, const N: usize> de::Deserialize<'de> for BoundedName<N> {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: de::Deserializer<'de>,
{
struct Visitor<const N: usize>;
impl<'de, const N: usize> de::Visitor<'de> for Visitor<N> {
type Value = BoundedName<{ N }>;
fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&format!(
"a non-empty string no more than {} characters in length, \
consisting of [A-Za-z0-9_.-] and starting with [A-Za-z0-9_]",
N
))
}
fn visit_str<E>(self, s: &str) -> Result<Self::Value, E>
where
E: de::Error,
{
s.parse().map_err(|err| match err {
ParseError::InvalidValue => E::invalid_value(
de::Unexpected::Str(s),
&"a name that consists of [A-Za-z0-9_.-] and starts with [A-Za-z0-9_]",
),
ParseError::TooLong | ParseError::Empty => E::invalid_length(
s.len(),
&format!("a non-empty name no more than {} characters in length", N)
.as_str(),
),
e => {
panic!("unexpected parse error: {:?}", e);
}
})
}
}
deserializer.deserialize_string(Visitor)
}
}
impl IterablePath for Name {
fn iter_segments(&self) -> Box<dyn DoubleEndedIterator<Item = &Name> + Send + '_> {
Box::new(iter::once(self))
}
}
/// [NamespacePath] is the same as [Path] but accepts `"/"` (which is also a valid namespace
/// path).
///
/// Note that while `"/"` is accepted, `"."` (which is synonymous in fuchsia.io) is rejected.
#[derive(Eq, Ord, PartialOrd, PartialEq, Hash, Clone)]
pub struct NamespacePath(RelativePath);
impl NamespacePath {
/// Like [Path::new] but `path` may be `/`.
pub fn new(path: impl AsRef<str>) -> Result<Self, ParseError> {
let path = path.as_ref();
if path.is_empty() {
return Err(ParseError::Empty);
}
if path == "." {
return Err(ParseError::InvalidValue);
}
if !path.starts_with('/') {
return Err(ParseError::NoLeadingSlash);
}
if path.len() > MAX_PATH_LENGTH {
return Err(ParseError::TooLong);
}
if path == "/" {
Ok(Self(RelativePath::dot()))
} else {
let path: RelativePath = path[1..].parse()?;
if path.is_dot() {
// "/." is not a valid NamespacePath
return Err(ParseError::InvalidSegment);
}
Ok(Self(path))
}
}
/// Returns the [NamespacePath] for `"/"`.
pub fn root() -> Self {
Self(RelativePath::dot())
}
/// Splits the path according to `"/"`.
pub fn split(&self) -> Vec<Name> {
self.0.split()
}
pub fn to_path_buf(&self) -> PathBuf {
PathBuf::from(self.to_string())
}
/// Returns a path that represents the parent directory of this one, or None if this is a
/// root dir.
pub fn parent(&self) -> Option<Self> {
self.0.parent().map(|p| Self(p))
}
/// Returns whether `prefix` is a prefix of `self` in terms of path segments.
///
/// For example:
/// ```
/// Path("/pkg/data").has_prefix("/pkg") == true
/// Path("/pkg_data").has_prefix("/pkg") == false
/// ```
pub fn has_prefix(&self, prefix: &Self) -> bool {
let my_segments = self.split();
let prefix_segments = prefix.split();
prefix_segments.into_iter().zip(my_segments.into_iter()).all(|(a, b)| a == b)
}
/// The last path segment, or None.
pub fn basename(&self) -> Option<&Name> {
self.0.basename()
}
}
impl IterablePath for NamespacePath {
fn iter_segments(&self) -> Box<dyn DoubleEndedIterator<Item = &Name> + Send + '_> {
self.0.iter_segments()
}
}
impl serde::ser::Serialize for NamespacePath {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::ser::Serializer,
{
self.to_string().serialize(serializer)
}
}
impl TryFrom<CString> for NamespacePath {
type Error = ParseError;
fn try_from(path: CString) -> Result<Self, ParseError> {
Self::new(path.into_string().map_err(|_| ParseError::InvalidValue)?)
}
}
impl From<NamespacePath> for CString {
fn from(path: NamespacePath) -> Self {
// SAFETY: in `Path::new` we already verified that there are no
// embedded NULs.
unsafe { CString::from_vec_unchecked(path.to_string().as_bytes().to_owned()) }
}
}
impl From<NamespacePath> for String {
fn from(path: NamespacePath) -> Self {
path.to_string()
}
}
impl FromStr for NamespacePath {
type Err = ParseError;
fn from_str(path: &str) -> Result<Self, Self::Err> {
Self::new(path)
}
}
impl fmt::Debug for NamespacePath {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self)
}
}
impl fmt::Display for NamespacePath {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if !self.0.is_dot() {
write!(f, "/{}", self.0)
} else {
write!(f, "/")
}
}
}
/// A path type used throughout Component Framework, along with its variants [NamespacePath] and
/// [RelativePath]. Examples of use:
///
/// - [NamespacePath]: Namespace paths
/// - [Path]: Outgoing paths and namespace paths that can't be "/"
/// - [RelativePath]: Dictionary paths
///
/// [Path] obeys the following constraints:
///
/// - Is a [fuchsia.io.Path](https://fuchsia.dev/reference/fidl/fuchsia.io#Directory.Open).
/// - Begins with `/`.
/// - Is not `.`.
/// - Contains at least one path segment (just `/` is disallowed).
/// - Each path segment is a [Name]. (This is strictly more constrained than a fuchsia.io
/// path segment.)
#[derive(Clone, PartialEq, Eq, Hash, Ord, PartialOrd)]
pub struct Path(RelativePath);
impl fmt::Debug for Path {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self)
}
}
impl fmt::Display for Path {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "/{}", self.0)
}
}
impl ser::Serialize for Path {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::ser::Serializer,
{
self.to_string().serialize(serializer)
}
}
impl Path {
/// Creates a [`Path`] from a [`String`], returning an `Err` if the string fails validation.
/// The string must be non-empty, no more than [`MAX_PATH_LENGTH`] bytes in length, start with
/// a leading `/`, not be exactly `/` or `.`, and each segment must be a valid [`Name`]. As a
/// result, [`Path`]s are always valid [`NamespacePath`]s.
pub fn new(path: impl AsRef<str>) -> Result<Self, ParseError> {
let path = path.as_ref();
if path.is_empty() {
return Err(ParseError::Empty);
}
if path == "/" || path == "." {
return Err(ParseError::InvalidValue);
}
if !path.starts_with('/') {
return Err(ParseError::NoLeadingSlash);
}
if path.len() > MAX_PATH_LENGTH {
return Err(ParseError::TooLong);
}
let path: RelativePath = path[1..].parse()?;
if path.is_dot() {
// "/." is not a valid Path
return Err(ParseError::InvalidSegment);
}
Ok(Self(path))
}
/// Splits the path according to "/".
pub fn split(&self) -> Vec<Name> {
self.0.split()
}
pub fn to_path_buf(&self) -> PathBuf {
PathBuf::from(self.to_string())
}
/// Returns a path that represents the parent directory of this one. Returns [NamespacePath]
/// instead of [Path] because the parent could be the root dir.
pub fn parent(&self) -> NamespacePath {
let p = self.0.parent().expect("can't be root");
NamespacePath(p)
}
pub fn basename(&self) -> &Name {
self.0.basename().expect("can't be root")
}
pub fn extend(&mut self, other: RelativePath) {
self.0.extend(other);
}
}
impl IterablePath for Path {
fn iter_segments(&self) -> Box<dyn DoubleEndedIterator<Item = &Name> + Send + '_> {
Box::new(self.0.iter_segments())
}
}
impl From<Path> for NamespacePath {
fn from(value: Path) -> Self {
Self(value.0)
}
}
impl FromStr for Path {
type Err = ParseError;
fn from_str(path: &str) -> Result<Self, Self::Err> {
Self::new(path)
}
}
impl TryFrom<CString> for Path {
type Error = ParseError;
fn try_from(path: CString) -> Result<Self, ParseError> {
Self::new(path.into_string().map_err(|_| ParseError::InvalidValue)?)
}
}
impl From<Path> for CString {
fn from(path: Path) -> Self {
// SAFETY: in `Path::new` we already verified that there are no
// embedded NULs.
unsafe { CString::from_vec_unchecked(path.to_string().as_bytes().to_owned()) }
}
}
impl From<Path> for String {
fn from(path: Path) -> String {
path.to_string()
}
}
impl<'de> de::Deserialize<'de> for Path {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: de::Deserializer<'de>,
{
struct Visitor;
impl<'de> de::Visitor<'de> for Visitor {
type Value = Path;
fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(
"a non-empty path no more than fuchsia.io/MAX_PATH_LENGTH characters \
in length, with a leading `/`, and containing no \
empty path segments",
)
}
fn visit_str<E>(self, s: &str) -> Result<Self::Value, E>
where
E: de::Error,
{
s.parse().map_err(|err| {
match err {
ParseError::InvalidValue | ParseError::InvalidSegment | ParseError::NoLeadingSlash => E::invalid_value(
de::Unexpected::Str(s),
&"a path with leading `/` and non-empty segments, where each segment is no \
more than fuchsia.io/MAX_NAME_LENGTH bytes in length, cannot be . or .., \
and cannot contain embedded NULs",
),
ParseError::TooLong | ParseError::Empty => E::invalid_length(
s.len(),
&"a non-empty path no more than fuchsia.io/MAX_PATH_LENGTH bytes \
in length",
),
e => {
panic!("unexpected parse error: {:?}", e);
}
}
})
}
}
deserializer.deserialize_string(Visitor)
}
}
/// Same as [Path] except the path does not begin with `/`.
#[derive(Eq, Ord, PartialOrd, PartialEq, Hash, Clone, Default)]
pub struct RelativePath {
segments: Vec<Name>,
}
impl RelativePath {
/// Like [Path::new] but `path` must not begin with `/` and may be `.`.
pub fn new(path: impl AsRef<str>) -> Result<Self, ParseError> {
let path: &str = path.as_ref();
if path == "." {
return Ok(Self::dot());
}
if path.is_empty() {
return Err(ParseError::Empty);
}
if path.len() > MAX_PATH_LENGTH {
return Err(ParseError::TooLong);
}
let segments = path
.split('/')
.map(|s| {
Name::new(s).map_err(|e| match e {
ParseError::Empty => ParseError::InvalidValue,
_ => ParseError::InvalidSegment,
})
})
.collect::<Result<Vec<_>, _>>()?;
Ok(Self { segments })
}
pub fn dot() -> Self {
Self { segments: vec![] }
}
pub fn is_dot(&self) -> bool {
self.segments.is_empty()
}
pub fn parent(&self) -> Option<Self> {
if self.segments.is_empty() {
None
} else {
let segments: Vec<_> =
self.segments[0..self.segments.len() - 1].into_iter().map(Clone::clone).collect();
Some(Self { segments })
}
}
pub fn split(&self) -> Vec<Name> {
self.segments.clone()
}
pub fn basename(&self) -> Option<&Name> {
self.segments.last()
}
pub fn to_path_buf(&self) -> PathBuf {
if self.is_dot() {
PathBuf::new()
} else {
PathBuf::from(self.to_string())
}
}
pub fn extend(&mut self, other: Self) {
self.segments.extend(other.segments);
}
pub fn push(&mut self, segment: Name) {
self.segments.push(segment);
}
}
impl IterablePath for RelativePath {
fn iter_segments(&self) -> Box<dyn DoubleEndedIterator<Item = &Name> + Send + '_> {
Box::new(self.segments.iter())
}
}
impl FromStr for RelativePath {
type Err = ParseError;
fn from_str(path: &str) -> Result<Self, Self::Err> {
Self::new(path)
}
}
impl From<RelativePath> for String {
fn from(path: RelativePath) -> String {
path.to_string()
}
}
impl From<Vec<Name>> for RelativePath {
fn from(segments: Vec<Name>) -> Self {
Self { segments }
}
}
impl fmt::Debug for RelativePath {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self)
}
}
impl fmt::Display for RelativePath {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if self.is_dot() {
write!(f, ".")
} else {
write!(f, "{}", self.segments.iter().map(|s| s.as_str()).collect::<Vec<_>>().join("/"))
}
}
}
impl ser::Serialize for RelativePath {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::ser::Serializer,
{
self.to_string().serialize(serializer)
}
}
impl<'de> de::Deserialize<'de> for RelativePath {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: de::Deserializer<'de>,
{
struct Visitor;
impl<'de> de::Visitor<'de> for Visitor {
type Value = RelativePath;
fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(
"a non-empty path no more than fuchsia.io/MAX_PATH_LENGTH characters \
in length, not starting with `/`, and containing no empty path segments",
)
}
fn visit_str<E>(self, s: &str) -> Result<Self::Value, E>
where
E: de::Error,
{
s.parse().map_err(|err| match err {
ParseError::InvalidValue
| ParseError::InvalidSegment
| ParseError::NoLeadingSlash => E::invalid_value(
de::Unexpected::Str(s),
&"a path with no leading `/` and non-empty segments",
),
ParseError::TooLong | ParseError::Empty => E::invalid_length(
s.len(),
&"a non-empty path no more than fuchsia.io/MAX_PATH_LENGTH characters \
in length",
),
e => {
panic!("unexpected parse error: {:?}", e);
}
})
}
}
deserializer.deserialize_string(Visitor)
}
}
/// Path that separates the dirname and basename as different variables
/// (referencing type). Convenient for / path representations that split the
/// dirname and basename, like Fuchsia component decl.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct BorrowedSeparatedPath<'a> {
pub dirname: &'a RelativePath,
pub basename: &'a Name,
}
impl BorrowedSeparatedPath<'_> {
/// Converts this [BorrowedSeparatedPath] to the owned type.
pub fn to_owned(&self) -> SeparatedPath {
SeparatedPath { dirname: self.dirname.clone(), basename: self.basename.clone() }
}
}
impl fmt::Display for BorrowedSeparatedPath<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if !self.dirname.is_dot() {
write!(f, "{}/{}", self.dirname, self.basename)
} else {
write!(f, "{}", self.basename)
}
}
}
impl IterablePath for BorrowedSeparatedPath<'_> {
fn iter_segments(&self) -> Box<dyn DoubleEndedIterator<Item = &Name> + Send + '_> {
Box::new(self.dirname.iter_segments().chain(iter::once(self.basename)))
}
}
/// Path that separates the dirname and basename as different variables (owned
/// type). Convenient for path representations that split the dirname and
/// basename, like Fuchsia component decl.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SeparatedPath {
pub dirname: RelativePath,
pub basename: Name,
}
impl SeparatedPath {
/// Obtains a reference to this [SeparatedPath] as the borrowed type.
pub fn as_ref(&self) -> BorrowedSeparatedPath<'_> {
BorrowedSeparatedPath { dirname: &self.dirname, basename: &self.basename }
}
}
impl IterablePath for SeparatedPath {
fn iter_segments(&self) -> Box<dyn DoubleEndedIterator<Item = &Name> + Send + '_> {
Box::new(self.dirname.iter_segments().chain(iter::once(&self.basename)))
}
}
impl fmt::Display for SeparatedPath {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if !self.dirname.is_dot() {
write!(f, "{}/{}", self.dirname, self.basename)
} else {
write!(f, "{}", self.basename)
}
}
}
/// Trait implemented by path types that provides an API to iterate over path segments.
pub trait IterablePath: Clone + Send + Sync {
/// Returns a double-sided iterator over the segments in this path.
fn iter_segments(&self) -> Box<dyn DoubleEndedIterator<Item = &Name> + Send + '_>;
}
/// A component URL. The URL is validated, but represented as a string to avoid
/// normalization and retain the original representation.
#[derive(Clone, Debug, Hash, PartialEq, Eq, PartialOrd, Ord)]
pub struct Url(FlyStr);
impl Url {
/// Creates a `Url` from a `String`, returning an `Err` if the string fails
/// validation. The string must be non-empty, no more than 4096 characters
/// in length, and be a valid URL. See the [`url`](../../url/index.html) crate.
pub fn new(url: impl AsRef<str> + Into<String>) -> Result<Self, ParseError> {
Self::validate(url.as_ref())?;
Ok(Self(FlyStr::new(url)))
}
/// Verifies the given string is a valid absolute or relative component URL.
pub fn validate(url_str: &str) -> Result<(), ParseError> {
if url_str.is_empty() {
return Err(ParseError::Empty);
}
if url_str.len() > MAX_URL_LENGTH {
return Err(ParseError::TooLong);
}
match url::Url::parse(url_str).map(|url| (url, false)).or_else(|err| {
if err == url::ParseError::RelativeUrlWithoutBase {
DEFAULT_BASE_URL.join(url_str).map(|url| (url, true))
} else {
Err(err)
}
}) {
Ok((url, is_relative)) => {
let mut path = url.path();
if path.starts_with('/') {
path = &path[1..];
}
if is_relative && url.fragment().is_none() {
// TODO(https://fxbug.dev/42070831): Fragments should be optional
// for relative path URLs.
//
// Historically, a component URL string without a scheme
// was considered invalid, unless it was only a fragment.
// Subpackages allow a relative path URL, and by current
// definition they require a fragment. By declaring a
// relative path without a fragment "invalid", we can avoid
// breaking tests that expect a path-only string to be
// invalid. Sadly this appears to be a behavior of the
// public API.
return Err(ParseError::InvalidComponentUrl {
details: "Relative URL has no resource fragment.".to_string(),
});
}
if url.host_str().unwrap_or("").is_empty()
&& path.is_empty()
&& url.fragment().is_none()
{
return Err(ParseError::InvalidComponentUrl {
details: "URL is missing either `host`, `path`, and/or `resource`."
.to_string(),
});
}
}
Err(err) => {
return Err(ParseError::InvalidComponentUrl {
details: format!("Malformed URL: {err:?}."),
});
}
}
// Use the unparsed URL string so that the original format is preserved.
Ok(())
}
pub fn is_relative(&self) -> bool {
matches!(url::Url::parse(&self.0), Err(url::ParseError::RelativeUrlWithoutBase))
}
pub fn scheme(&self) -> Option<String> {
url::Url::parse(&self.0).ok().map(|u| u.scheme().into())
}
pub fn as_str(&self) -> &str {
&*self.0
}
}
impl FromStr for Url {
type Err = ParseError;
fn from_str(url: &str) -> Result<Self, Self::Err> {
Self::new(url)
}
}
impl From<Url> for String {
fn from(url: Url) -> String {
url.0.into()
}
}
impl fmt::Display for Url {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
fmt::Display::fmt(&self.0, f)
}
}
impl ser::Serialize for Url {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: ser::Serializer,
{
self.to_string().serialize(serializer)
}
}
impl<'de> de::Deserialize<'de> for Url {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: de::Deserializer<'de>,
{
struct Visitor;
impl<'de> de::Visitor<'de> for Visitor {
type Value = Url;
fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("a non-empty URL no more than 4096 characters in length")
}
fn visit_str<E>(self, s: &str) -> Result<Self::Value, E>
where
E: de::Error,
{
s.parse().map_err(|err| match err {
ParseError::InvalidComponentUrl { details: _ } => {
E::invalid_value(de::Unexpected::Str(s), &"a valid URL")
}
ParseError::TooLong | ParseError::Empty => E::invalid_length(
s.len(),
&"a non-empty URL no more than 4096 characters in length",
),
e => {
panic!("unexpected parse error: {:?}", e);
}
})
}
}
deserializer.deserialize_string(Visitor)
}
}
impl PartialEq<&str> for Url {
fn eq(&self, o: &&str) -> bool {
&*self.0 == *o
}
}
impl PartialEq<String> for Url {
fn eq(&self, o: &String) -> bool {
&*self.0 == *o
}
}
/// A URL scheme.
#[derive(Serialize, Clone, Debug, Eq, Hash, PartialEq)]
pub struct UrlScheme(FlyStr);
impl UrlScheme {
/// Creates a `UrlScheme` from a `String`, returning an `Err` if the string fails
/// validation. The string must be non-empty and no more than 100 characters
/// in length. It must start with a lowercase ASCII letter (a-z),
/// and contain only lowercase ASCII letters, digits, `+`, `-`, and `.`.
pub fn new(url_scheme: impl AsRef<str> + Into<String>) -> Result<Self, ParseError> {
Self::validate(url_scheme.as_ref())?;
Ok(UrlScheme(FlyStr::new(url_scheme)))
}
/// Validates `url_scheme` but does not construct a new `UrlScheme` object.
/// See [`UrlScheme::new`] for validation details.
pub fn validate(url_scheme: &str) -> Result<(), ParseError> {
if url_scheme.is_empty() {
return Err(ParseError::Empty);
}
if url_scheme.len() > MAX_NAME_LENGTH {
return Err(ParseError::TooLong);
}
let mut iter = url_scheme.chars();
let first_char = iter.next().unwrap();
if !first_char.is_ascii_lowercase() {
return Err(ParseError::InvalidValue);
}
if let Some(_) = iter.find(|&c| {
!c.is_ascii_lowercase() && !c.is_ascii_digit() && c != '.' && c != '+' && c != '-'
}) {
return Err(ParseError::InvalidValue);
}
Ok(())
}
pub fn as_str(&self) -> &str {
&*self.0
}
}
impl fmt::Display for UrlScheme {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
fmt::Display::fmt(&self.0, f)
}
}
impl FromStr for UrlScheme {
type Err = ParseError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Self::new(s)
}
}
impl From<UrlScheme> for String {
fn from(u: UrlScheme) -> String {
u.0.into()
}
}
impl<'de> de::Deserialize<'de> for UrlScheme {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: de::Deserializer<'de>,
{
struct Visitor;
impl<'de> de::Visitor<'de> for Visitor {
type Value = UrlScheme;
fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("a non-empty URL scheme no more than 100 characters in length")
}
fn visit_str<E>(self, s: &str) -> Result<Self::Value, E>
where
E: de::Error,
{
s.parse().map_err(|err| match err {
ParseError::InvalidValue => {
E::invalid_value(de::Unexpected::Str(s), &"a valid URL scheme")
}
ParseError::TooLong | ParseError::Empty => E::invalid_length(
s.len(),
&"a non-empty URL scheme no more than 100 characters in length",
),
e => {
panic!("unexpected parse error: {:?}", e);
}
})
}
}
deserializer.deserialize_string(Visitor)
}
}
/// The duration of child components in a collection. See [`Durability`].
///
/// [`Durability`]: ../../fidl_fuchsia_sys2/enum.Durability.html
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
#[serde(rename_all = "snake_case")]
pub enum Durability {
Transient,
/// An instance is started on creation and exists until it stops.
SingleRun,
}
symmetrical_enums!(Durability, fdecl::Durability, Transient, SingleRun);
/// A component instance's startup mode. See [`StartupMode`].
///
/// [`StartupMode`]: ../../fidl_fuchsia_sys2/enum.StartupMode.html
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
#[serde(rename_all = "snake_case")]
pub enum StartupMode {
Lazy,
Eager,
}
impl StartupMode {
pub fn is_lazy(&self) -> bool {
matches!(self, StartupMode::Lazy)
}
}
symmetrical_enums!(StartupMode, fdecl::StartupMode, Lazy, Eager);
impl Default for StartupMode {
fn default() -> Self {
Self::Lazy
}
}
/// A component instance's recovery policy. See [`OnTerminate`].
///
/// [`OnTerminate`]: ../../fidl_fuchsia_sys2/enum.OnTerminate.html
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
#[serde(rename_all = "snake_case")]
pub enum OnTerminate {
None,
Reboot,
}
symmetrical_enums!(OnTerminate, fdecl::OnTerminate, None, Reboot);
impl Default for OnTerminate {
fn default() -> Self {
Self::None
}
}
/// The kinds of offers that can target components in a given collection. See
/// [`AllowedOffers`].
///
/// [`AllowedOffers`]: ../../fidl_fuchsia_sys2/enum.AllowedOffers.html
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum AllowedOffers {
StaticOnly,
StaticAndDynamic,
}
symmetrical_enums!(AllowedOffers, fdecl::AllowedOffers, StaticOnly, StaticAndDynamic);
impl Default for AllowedOffers {
fn default() -> Self {
Self::StaticOnly
}
}
/// Offered dependency type. See [`DependencyType`].
///
/// [`DependencyType`]: ../../fidl_fuchsia_sys2/enum.DependencyType.html
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum DependencyType {
Strong,
Weak,
}
symmetrical_enums!(DependencyType, fdecl::DependencyType, Strong, Weak);
impl Default for DependencyType {
fn default() -> Self {
Self::Strong
}
}
/// Capability availability. See [`Availability`].
///
/// [`Availability`]: ../../fidl_fuchsia_sys2/enum.Availability.html
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, Hash, Copy)]
#[serde(rename_all = "snake_case")]
pub enum Availability {
Required,
Optional,
SameAsTarget,
Transitional,
}
symmetrical_enums!(
Availability,
fdecl::Availability,
Required,
Optional,
SameAsTarget,
Transitional
);
impl Display for Availability {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Availability::Required => write!(f, "Required"),
Availability::Optional => write!(f, "Optional"),
Availability::SameAsTarget => write!(f, "SameAsTarget"),
Availability::Transitional => write!(f, "Transitional"),
}
}
}
// TODO(cgonyeo): remove this once we've soft migrated to the availability field being required.
impl Default for Availability {
fn default() -> Self {
Self::Required
}
}
impl PartialOrd for Availability {
fn partial_cmp(&self, other: &Self) -> Option<cmp::Ordering> {
match (*self, *other) {
(Availability::Transitional, Availability::Optional)
| (Availability::Transitional, Availability::Required)
| (Availability::Optional, Availability::Required) => Some(cmp::Ordering::Less),
(Availability::Optional, Availability::Transitional)
| (Availability::Required, Availability::Transitional)
| (Availability::Required, Availability::Optional) => Some(cmp::Ordering::Greater),
(Availability::Required, Availability::Required)
| (Availability::Optional, Availability::Optional)
| (Availability::Transitional, Availability::Transitional)
| (Availability::SameAsTarget, Availability::SameAsTarget) => {
Some(cmp::Ordering::Equal)
}
(Availability::SameAsTarget, _) | (_, Availability::SameAsTarget) => None,
}
}
}
/// Specifies when the framework will open the protocol from the provider
/// component's outgoing directory when someone requests the capability. See
/// [`DeliveryType`].
///
/// [`DeliveryType`]: ../../fidl_fuchsia_component_decl/enum.DeliveryType.html
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, Hash, Copy)]
#[serde(rename_all = "snake_case")]
pub enum DeliveryType {
Immediate,
OnReadable,
}
impl TryFrom<fdecl::DeliveryType> for DeliveryType {
type Error = fdecl::DeliveryType;
fn try_from(value: fdecl::DeliveryType) -> Result<Self, Self::Error> {
match value {
fdecl::DeliveryType::Immediate => Ok(DeliveryType::Immediate),
fdecl::DeliveryType::OnReadable => Ok(DeliveryType::OnReadable),
fdecl::DeliveryTypeUnknown!() => Err(value),
}
}
}
impl From<DeliveryType> for fdecl::DeliveryType {
fn from(value: DeliveryType) -> Self {
match value {
DeliveryType::Immediate => fdecl::DeliveryType::Immediate,
DeliveryType::OnReadable => fdecl::DeliveryType::OnReadable,
}
}
}
impl Display for DeliveryType {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
DeliveryType::Immediate => write!(f, "Immediate"),
DeliveryType::OnReadable => write!(f, "OnReadable"),
}
}
}
impl Default for DeliveryType {
fn default() -> Self {
Self::Immediate
}
}
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum StorageId {
StaticInstanceId,
StaticInstanceIdOrMoniker,
}
symmetrical_enums!(StorageId, fdecl::StorageId, StaticInstanceId, StaticInstanceIdOrMoniker);
#[cfg(test)]
mod tests {
use {super::*, assert_matches::assert_matches, serde_json::json, std::iter::repeat};
macro_rules! expect_ok {
($type_:ty, $($input:tt)+) => {
assert_matches!(
serde_json::from_str::<$type_>(&json!($($input)*).to_string()),
Ok(_)
);
};
}
macro_rules! expect_ok_no_serialize {
($type_:ty, $($input:tt)+) => {
assert_matches!(
($($input)*).parse::<$type_>(),
Ok(_)
);
};
}
macro_rules! expect_err_no_serialize {
($type_:ty, $err:pat, $($input:tt)+) => {
assert_matches!(
($($input)*).parse::<$type_>(),
Err($err)
);
};
}
macro_rules! expect_err {
($type_:ty, $err:pat, $($input:tt)+) => {
assert_matches!(
($($input)*).parse::<$type_>(),
Err($err)
);
assert_matches!(
serde_json::from_str::<$type_>(&json!($($input)*).to_string()),
Err(_)
);
};
}
#[test]
fn test_valid_name() {
expect_ok!(Name, "foo");
expect_ok!(Name, "Foo");
expect_ok!(Name, "O123._-");
expect_ok!(Name, "_O123._-");
expect_ok!(Name, repeat("x").take(255).collect::<String>());
}
#[test]
fn test_invalid_name() {
expect_err!(Name, ParseError::Empty, "");
expect_err!(Name, ParseError::InvalidValue, "-");
expect_err!(Name, ParseError::InvalidValue, ".");
expect_err!(Name, ParseError::InvalidValue, "@&%^");
expect_err!(Name, ParseError::TooLong, repeat("x").take(256).collect::<String>());
}
#[test]
fn test_valid_path() {
expect_ok!(Path, "/foo");
expect_ok!(Path, "/foo/bar");
expect_ok!(Path, format!("/{}", repeat("x").take(100).collect::<String>()).as_str());
// 2047 * 2 characters per repeat = 4094
expect_ok!(Path, repeat("/x").take(2047).collect::<String>().as_str());
}
#[test]
fn test_invalid_path() {
expect_err!(Path, ParseError::Empty, "");
expect_err!(Path, ParseError::InvalidValue, "/");
expect_err!(Path, ParseError::InvalidValue, ".");
expect_err!(Path, ParseError::NoLeadingSlash, "foo");
expect_err!(Path, ParseError::NoLeadingSlash, "foo/");
expect_err!(Path, ParseError::InvalidValue, "/foo/");
expect_err!(Path, ParseError::InvalidValue, "/foo//bar");
expect_err!(Path, ParseError::InvalidSegment, "/fo\0b/bar");
expect_err!(Path, ParseError::InvalidSegment, "/.");
expect_err!(Path, ParseError::InvalidSegment, "/foo/.");
expect_err!(
Path,
ParseError::InvalidSegment,
format!("/{}", repeat("x").take(256).collect::<String>()).as_str()
);
// 2048 * 2 characters per repeat = 4096
expect_err!(
Path,
ParseError::TooLong,
repeat("/x").take(2048).collect::<String>().as_str()
);
}
#[test]
fn test_valid_namespace_path() {
expect_ok_no_serialize!(NamespacePath, "/");
expect_ok_no_serialize!(NamespacePath, "/foo");
expect_ok_no_serialize!(NamespacePath, "/foo/bar");
expect_ok_no_serialize!(
NamespacePath,
format!("/{}", repeat("x").take(100).collect::<String>()).as_str()
);
// 2047 * 2 characters per repeat = 4094
expect_ok_no_serialize!(
NamespacePath,
repeat("/x").take(2047).collect::<String>().as_str()
);
}
#[test]
fn test_invalid_namespace_path() {
expect_err_no_serialize!(NamespacePath, ParseError::Empty, "");
expect_err_no_serialize!(NamespacePath, ParseError::InvalidValue, ".");
expect_err_no_serialize!(NamespacePath, ParseError::NoLeadingSlash, "foo");
expect_err_no_serialize!(NamespacePath, ParseError::NoLeadingSlash, "foo/");
expect_err_no_serialize!(NamespacePath, ParseError::InvalidValue, "/foo/");
expect_err_no_serialize!(NamespacePath, ParseError::InvalidValue, "/foo//bar");
expect_err_no_serialize!(NamespacePath, ParseError::InvalidSegment, "/fo\0b/bar");
expect_err_no_serialize!(NamespacePath, ParseError::InvalidSegment, "/.");
expect_err_no_serialize!(NamespacePath, ParseError::InvalidSegment, "/foo/.");
expect_err_no_serialize!(
NamespacePath,
ParseError::InvalidSegment,
format!("/{}", repeat("x").take(256).collect::<String>()).as_str()
);
// 2048 * 2 characters per repeat = 4096
expect_err_no_serialize!(
Path,
ParseError::TooLong,
repeat("/x").take(2048).collect::<String>().as_str()
);
}
#[test]
fn test_path_parent_basename() {
let path = Path::new("/foo").unwrap();
assert_eq!(
(path.parent(), path.basename()),
("/".parse().unwrap(), &"foo".parse().unwrap())
);
let path = Path::new("/foo/bar").unwrap();
assert_eq!(
(path.parent(), path.basename()),
("/foo".parse().unwrap(), &"bar".parse().unwrap())
);
let path = Path::new("/foo/bar/baz").unwrap();
assert_eq!(
(path.parent(), path.basename()),
("/foo/bar".parse().unwrap(), &"baz".parse().unwrap())
);
}
#[test]
fn test_separated_path() {
fn test_path(path: SeparatedPath, in_expected_segments: Vec<&str>) {
let expected_segments: Vec<_> =
in_expected_segments.iter().map(|s| Name::new(*s).unwrap()).collect();
let expected_segments: Vec<_> = expected_segments.iter().collect();
let segments: Vec<_> = path.iter_segments().collect();
assert_eq!(segments, expected_segments);
let borrowed_path = path.as_ref();
let segments: Vec<_> = borrowed_path.iter_segments().collect();
assert_eq!(segments, expected_segments);
let owned_path = borrowed_path.to_owned();
assert_eq!(path, owned_path);
let expected_fmt = in_expected_segments.join("/");
assert_eq!(format!("{path}"), expected_fmt);
assert_eq!(format!("{owned_path}"), expected_fmt);
}
test_path(
SeparatedPath { dirname: ".".parse().unwrap(), basename: "foo".parse().unwrap() },
vec!["foo"],
);
test_path(
SeparatedPath { dirname: "bar".parse().unwrap(), basename: "foo".parse().unwrap() },
vec!["bar", "foo"],
);
test_path(
SeparatedPath { dirname: "bar/baz".parse().unwrap(), basename: "foo".parse().unwrap() },
vec!["bar", "baz", "foo"],
);
}
#[test]
fn test_valid_relative_path() {
expect_ok!(RelativePath, ".");
expect_ok!(RelativePath, "foo");
expect_ok!(RelativePath, "foo/bar");
expect_ok!(RelativePath, &format!("x{}", repeat("/x").take(2047).collect::<String>()));
}
#[test]
fn test_invalid_relative_path() {
expect_err!(RelativePath, ParseError::Empty, "");
expect_err!(RelativePath, ParseError::InvalidValue, "/");
expect_err!(RelativePath, ParseError::InvalidValue, "/foo");
expect_err!(RelativePath, ParseError::InvalidValue, "foo/");
expect_err!(RelativePath, ParseError::InvalidValue, "/foo/");
expect_err!(RelativePath, ParseError::InvalidValue, "foo//bar");
expect_err!(RelativePath, ParseError::InvalidSegment, "..");
expect_err!(RelativePath, ParseError::InvalidSegment, "foo/..");
expect_err!(
RelativePath,
ParseError::TooLong,
&format!("x{}", repeat("/x").take(2048).collect::<String>())
);
}
#[test]
fn test_valid_url() {
expect_ok!(Url, "a://foo");
expect_ok!(Url, "#relative-url");
expect_ok!(Url, &format!("a://{}", repeat("x").take(4092).collect::<String>()));
}
#[test]
fn test_invalid_url() {
expect_err!(Url, ParseError::Empty, "");
expect_err!(Url, ParseError::InvalidComponentUrl { .. }, "foo");
expect_err!(
Url,
ParseError::TooLong,
&format!("a://{}", repeat("x").take(4093).collect::<String>())
);
}
#[test]
fn test_valid_url_scheme() {
expect_ok!(UrlScheme, "fuch.sia-pkg+0");
expect_ok!(UrlScheme, &format!("{}", repeat("f").take(255).collect::<String>()));
}
#[test]
fn test_invalid_url_scheme() {
expect_err!(UrlScheme, ParseError::Empty, "");
expect_err!(UrlScheme, ParseError::InvalidValue, "0fuch.sia-pkg+0");
expect_err!(UrlScheme, ParseError::InvalidValue, "fuchsia_pkg");
expect_err!(UrlScheme, ParseError::InvalidValue, "FUCHSIA-PKG");
expect_err!(
UrlScheme,
ParseError::TooLong,
&format!("{}", repeat("f").take(256).collect::<String>())
);
}
#[test]
fn test_name_error_message() {
let input = r#"
"foo$"
"#;
let err = serde_json::from_str::<Name>(input).expect_err("must fail");
assert_eq!(
err.to_string(),
"invalid value: string \"foo$\", expected a name \
that consists of [A-Za-z0-9_.-] and starts with [A-Za-z0-9_] \
at line 2 column 18"
);
assert_eq!(err.line(), 2);
assert_eq!(err.column(), 18);
}
#[test]
fn test_path_error_message() {
let input = r#"
"foo";
"#;
let err = serde_json::from_str::<Path>(input).expect_err("must fail");
assert_eq!(
err.to_string(),
"invalid value: string \"foo\", expected a path with leading `/` and non-empty \
segments, where each segment is no \
more than fuchsia.io/MAX_NAME_LENGTH bytes in length, cannot be . or .., \
and cannot contain embedded NULs at line 2 column 17"
);
assert_eq!(err.line(), 2);
assert_eq!(err.column(), 17);
}
#[test]
fn test_url_error_message() {
let input = r#"
"foo";
"#;
let err = serde_json::from_str::<Url>(input).expect_err("must fail");
assert_eq!(
err.to_string(),
"invalid value: string \"foo\", expected a valid URL at line 2 \
column 17"
);
assert_eq!(err.line(), 2);
assert_eq!(err.column(), 17);
}
#[test]
fn test_url_scheme_error_message() {
let input = r#"
"9fuchsia_pkg"
"#;
let err = serde_json::from_str::<UrlScheme>(input).expect_err("must fail");
assert_eq!(
err.to_string(),
"invalid value: string \"9fuchsia_pkg\", expected a valid URL scheme at line 2 column 26"
);
assert_eq!(err.line(), 2);
assert_eq!(err.column(), 26);
}
#[test]
fn test_symmetrical_enums() {
mod a {
#[derive(Debug, PartialEq, Eq)]
pub enum Streetlight {
Green,
Yellow,
Red,
}
}
mod b {
#[derive(Debug, PartialEq, Eq)]
pub enum Streetlight {
Green,
Yellow,
Red,
}
}
symmetrical_enums!(a::Streetlight, b::Streetlight, Green, Yellow, Red);
assert_eq!(a::Streetlight::Green, b::Streetlight::Green.into());
assert_eq!(a::Streetlight::Yellow, b::Streetlight::Yellow.into());
assert_eq!(a::Streetlight::Red, b::Streetlight::Red.into());
assert_eq!(b::Streetlight::Green, a::Streetlight::Green.into());
assert_eq!(b::Streetlight::Yellow, a::Streetlight::Yellow.into());
assert_eq!(b::Streetlight::Red, a::Streetlight::Red.into());
}
}