blob: 394861971227f585dd459bb1d51354c69dd7ba9e [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,
serde::de,
serde::{Deserialize, Serialize},
std::{borrow::Cow, default::Default, fmt, str::FromStr},
thiserror::Error,
url,
};
/// 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, )*
}
}
}
};
}
pub const MAX_NAME_LENGTH: usize = 100;
pub const MAX_DYNAMIC_NAME_LENGTH: usize = 1024;
/// A name that can refer to a component, collection, or other entity in the
/// Component Manifest.
#[derive(Serialize, Clone, Debug, Hash, PartialEq, Eq, PartialOrd, Ord)]
pub struct Name(String);
/// The error representing a failure to parse a type from string.
#[derive(Debug, Error, PartialEq, Eq)]
pub enum ParseError {
/// The string did not match a valid value.
#[error("invalid value")]
InvalidValue,
/// The string was too long or too short.
#[error("invalid length")]
InvalidLength,
/// A name was expected and the string was a path.
#[error("not a name")]
NotAName,
// A path was expected and the string was a name.
#[error("not a path")]
NotAPath,
}
impl Name {
/// Creates a `Name` from a `String`, returning an `Err` if the string
/// fails validation. The string must be non-empty, no more than 100
/// characters in length, and consist of one or more of the
/// following characters: `a-z`, `0-9`, `_`, `.`, `-`.
pub fn new(name: impl Into<String>) -> Result<Self, ParseError> {
let name = name.into();
Self::validate(Cow::Owned(name.clone()), MAX_NAME_LENGTH)?;
Ok(Self(name))
}
pub fn validate(name: Cow<'_, str>, max_name_len: usize) -> Result<(), ParseError> {
if name.is_empty() || name.len() > max_name_len {
return Err(ParseError::InvalidLength);
}
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(())
}
pub fn as_str(&self) -> &str {
self.0.as_str()
}
}
impl PartialEq<&str> for Name {
fn eq(&self, o: &&str) -> bool {
self.0 == *o
}
}
impl PartialEq<String> for Name {
fn eq(&self, o: &String) -> bool {
self.0 == *o
}
}
impl fmt::Display for Name {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
<String as fmt::Display>::fmt(&self.0, f)
}
}
impl FromStr for Name {
type Err = ParseError;
fn from_str(name: &str) -> Result<Self, Self::Err> {
Self::validate(Cow::Borrowed(name), MAX_NAME_LENGTH)?;
Ok(Self(name.to_owned()))
}
}
impl From<Name> for String {
fn from(name: Name) -> String {
name.0
}
}
impl<'de> de::Deserialize<'de> for Name {
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 = Name;
fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(
"a non-empty string no more than 100 characters in length, \
consisting of [A-Za-z0-9_.-] and starting with [A-Za-z0-9_]",
)
}
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::InvalidLength => E::invalid_length(
s.len(),
&"a non-empty name no more than 100 characters in length",
),
e => {
panic!("unexpected parse error: {:?}", e);
}
})
}
}
deserializer.deserialize_string(Visitor)
}
}
/// A filesystem path.
#[derive(Serialize, Clone, Debug, PartialEq, Eq, Hash)]
pub struct Path(String);
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 1024
/// characters in length, start with a leading `/`, and contain no empty
/// path segments.
pub fn new(path: impl Into<String>) -> Result<Self, ParseError> {
let path = path.into();
Self::validate(&path)?;
Ok(Path(path))
}
/// Validates `path` but does not construct a new `Path` object.
pub fn validate(path: &str) -> Result<(), ParseError> {
if path.is_empty() || path.len() > 1024 {
return Err(ParseError::InvalidLength);
}
if !path.starts_with('/') {
return Err(ParseError::InvalidValue);
}
if !path[1..].split('/').all(|part| !part.is_empty()) {
return Err(ParseError::InvalidValue);
}
Ok(())
}
pub fn as_str(&self) -> &str {
self.0.as_str()
}
}
impl FromStr for Path {
type Err = ParseError;
fn from_str(path: &str) -> Result<Self, Self::Err> {
Self::validate(path)?;
Ok(Path(path.to_string()))
}
}
impl From<Path> for String {
fn from(path: Path) -> String {
path.0
}
}
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 1024 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 => E::invalid_value(
de::Unexpected::Str(s),
&"a path with leading `/` and non-empty segments",
),
ParseError::InvalidLength => E::invalid_length(
s.len(),
&"a non-empty path no more than 1024 characters in length",
),
e => {
panic!("unexpected parse error: {:?}", e);
}
})
}
}
deserializer.deserialize_string(Visitor)
}
}
/// A relative filesystem path.
#[derive(Serialize, Clone, Debug, PartialEq, Eq)]
pub struct RelativePath(String);
impl RelativePath {
/// Creates a `RelativePath` from a `String`, returning an `Err` if the string fails
/// validation. The string must be non-empty, no more than 1024 characters in length, not start
/// with a `/`, and contain no empty path segments.
pub fn new(path: String) -> Result<Self, ParseError> {
Self::from_str_impl(Cow::Owned(path))
}
fn from_str_impl(path: Cow<'_, str>) -> Result<Self, ParseError> {
if path.is_empty() || path.len() > 1024 {
return Err(ParseError::InvalidLength);
}
if !path.split('/').all(|part| !part.is_empty()) {
return Err(ParseError::InvalidValue);
}
return Ok(Self(path.into_owned()));
}
pub fn as_str(&self) -> &str {
self.0.as_str()
}
}
impl FromStr for RelativePath {
type Err = ParseError;
fn from_str(path: &str) -> Result<Self, Self::Err> {
Self::from_str_impl(Cow::Borrowed(path))
}
}
impl From<RelativePath> for String {
fn from(path: RelativePath) -> String {
path.0
}
}
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 1024 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 => E::invalid_value(
de::Unexpected::Str(s),
&"a path with no leading `/` and non-empty segments",
),
ParseError::InvalidLength => E::invalid_length(
s.len(),
&"a non-empty path no more than 1024 characters in length",
),
e => {
panic!("unexpected parse error: {:?}", e);
}
})
}
}
deserializer.deserialize_string(Visitor)
}
}
/// A component URL. The URL is validated, but represented as a string to avoid
/// normalization and retain the original representation.
#[derive(Serialize, Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
pub struct Url(String);
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 Into<String>) -> Result<Self, ParseError> {
let url = url.into();
Self::from_str_impl(Cow::Owned(url))
}
fn from_str_impl(url_str: Cow<'_, str>) -> Result<Self, ParseError> {
if url_str.is_empty() || url_str.len() > 4096 {
return Err(ParseError::InvalidLength);
}
let parsed_url = url::Url::parse(&url_str);
// We are considering relative URLs to be valid URLs, but only if it starts
// with the resource.
if parsed_url == Err(url::ParseError::RelativeUrlWithoutBase) {
if url_str.chars().nth(0) == Some('#') {
// Use the unparsed URL string so that the original format is preserved.
return Ok(Self(url_str.into_owned()));
}
}
let parsed_url = parsed_url.map_err(|_| ParseError::InvalidValue)?;
if parsed_url.cannot_be_a_base() {
return Err(ParseError::InvalidValue);
}
// Use the unparsed URL string so that the original format is preserved.
Ok(Self(url_str.into_owned()))
}
pub fn as_str(&self) -> &str {
self.0.as_str()
}
}
impl FromStr for Url {
type Err = ParseError;
fn from_str(url: &str) -> Result<Self, Self::Err> {
Self::from_str_impl(Cow::Borrowed(url))
}
}
impl From<Url> for String {
fn from(url: Url) -> String {
url.0
}
}
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::InvalidValue => {
E::invalid_value(de::Unexpected::Str(s), &"a valid URL")
}
ParseError::InvalidLength => 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)
}
}
/// A URL scheme.
#[derive(Clone, Debug, Eq, Hash, PartialEq, Serialize)]
pub struct UrlScheme(String);
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: String) -> Result<Self, ParseError> {
Self::validate(&url_scheme)?;
Ok(UrlScheme(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() || url_scheme.len() > MAX_NAME_LENGTH {
return Err(ParseError::InvalidLength);
}
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.as_str()
}
}
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::validate(s)?;
Ok(UrlScheme(s.to_string()))
}
}
impl From<UrlScheme> for String {
fn from(u: UrlScheme) -> String {
u.0
}
}
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::InvalidLength => 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 {
Persistent,
Transient,
/// An instance is started on creation and exists until it stops.
SingleRun,
}
symmetrical_enums!(Durability, fdecl::Durability, Persistent, 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,
WeakForMigration,
}
symmetrical_enums!(DependencyType, fdecl::DependencyType, Strong, Weak, WeakForMigration);
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)]
#[serde(rename_all = "snake_case")]
pub enum Availability {
Required,
Optional,
SameAsTarget,
}
symmetrical_enums!(Availability, fdecl::Availability, Required, Optional, SameAsTarget);
impl Default for Availability {
fn default() -> Self {
Self::Required
}
}
#[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::*,
serde_json::{self, json},
std::iter::repeat,
};
macro_rules! expect_ok {
($type_:ty, $($input:tt)+) => {
assert!(serde_json::from_str::<$type_>(&json!($($input)*).to_string()).is_ok());
};
}
macro_rules! expect_err {
($type_:ty, $($input:tt)+) => {
assert!(serde_json::from_str::<$type_>(&json!($($input)*).to_string()).is_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(100).collect::<String>());
}
#[test]
fn test_invalid_name() {
expect_err!(Name, "");
expect_err!(Name, "-");
expect_err!(Name, ".");
expect_err!(Name, "@&%^");
expect_err!(Name, repeat("x").take(101).collect::<String>());
}
#[test]
fn test_valid_path() {
expect_ok!(Path, "/foo");
expect_ok!(Path, "/foo/bar");
expect_ok!(Path, &format!("/{}", repeat("x").take(1023).collect::<String>()));
}
#[test]
fn test_invalid_path() {
expect_err!(Path, "");
expect_err!(Path, "/");
expect_err!(Path, "foo");
expect_err!(Path, "foo/");
expect_err!(Path, "/foo/");
expect_err!(Path, "/foo//bar");
expect_err!(Path, &format!("/{}", repeat("x").take(1024).collect::<String>()));
}
#[test]
fn test_valid_relative_path() {
expect_ok!(RelativePath, "foo");
expect_ok!(RelativePath, "foo/bar");
expect_ok!(RelativePath, &format!("{}", repeat("x").take(1024).collect::<String>()));
}
#[test]
fn test_invalid_relative_path() {
expect_err!(RelativePath, "");
expect_err!(RelativePath, "/");
expect_err!(RelativePath, "/foo");
expect_err!(RelativePath, "foo/");
expect_err!(RelativePath, "/foo/");
expect_err!(RelativePath, "foo//bar");
expect_err!(RelativePath, &format!("{}", repeat("x").take(1025).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, "");
expect_err!(Url, "foo");
expect_err!(Url, &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(100).collect::<String>()));
}
#[test]
fn test_invalid_url_scheme() {
expect_err!(UrlScheme, "");
expect_err!(UrlScheme, "0fuch.sia-pkg+0");
expect_err!(UrlScheme, "fuchsia_pkg");
expect_err!(UrlScheme, "FUCHSIA-PKG");
expect_err!(UrlScheme, &format!("{}", repeat("f").take(101).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 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());
}
}