|
|
@@ -0,0 +1,1382 @@
|
|
|
+//! Types for parsing the protocol grammar.
|
|
|
+
|
|
|
+use proc_macro2::Span;
|
|
|
+use quote::format_ident;
|
|
|
+use std::{ops::Deref, rc::Rc};
|
|
|
+use syn::{
|
|
|
+ bracketed, parenthesized,
|
|
|
+ parse::{Parse, ParseStream},
|
|
|
+ punctuated::Punctuated,
|
|
|
+ spanned::Spanned,
|
|
|
+ token::Bracket,
|
|
|
+ Ident, LitInt, Token,
|
|
|
+};
|
|
|
+
|
|
|
+/// This type represents the top-level production for the protocol grammar.
|
|
|
+#[cfg_attr(test, derive(Debug, PartialEq))]
|
|
|
+pub(crate) struct Protocol {
|
|
|
+ pub(crate) name_def: NameDef,
|
|
|
+ name_def_semi: Token![;],
|
|
|
+ #[allow(dead_code)]
|
|
|
+ version_def: Option<(VersionDef, Token![;])>,
|
|
|
+ pub(crate) actor_defs: Punctuated<Rc<ActorDef>, Token![;]>,
|
|
|
+ pub(crate) transitions: Punctuated<Rc<Transition>, Token![;]>,
|
|
|
+}
|
|
|
+
|
|
|
+#[cfg(test)]
|
|
|
+impl Protocol {
|
|
|
+ pub(crate) fn new(
|
|
|
+ name_def: NameDef,
|
|
|
+ actor_def: impl IntoIterator<Item = ActorDef>,
|
|
|
+ transitions: impl IntoIterator<Item = Transition>,
|
|
|
+ ) -> Self {
|
|
|
+ let mut actor_def: Punctuated<Rc<ActorDef>, Token![;]> =
|
|
|
+ actor_def.into_iter().map(Rc::new).collect();
|
|
|
+ actor_def.push_punct(Token));
|
|
|
+ let mut transitions: Punctuated<Rc<Transition>, Token![;]> =
|
|
|
+ transitions.into_iter().map(Rc::new).collect();
|
|
|
+ transitions.push_punct(Token));
|
|
|
+ Self {
|
|
|
+ name_def,
|
|
|
+ name_def_semi: Token),
|
|
|
+ version_def: None,
|
|
|
+ actor_defs: actor_def,
|
|
|
+ transitions,
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ pub(crate) fn with_version(
|
|
|
+ name_def: NameDef,
|
|
|
+ version_def: VersionDef,
|
|
|
+ actor_def: impl IntoIterator<Item = ActorDef>,
|
|
|
+ transitions: impl IntoIterator<Item = Transition>,
|
|
|
+ ) -> Self {
|
|
|
+ let mut protocol = Self::new(name_def, actor_def, transitions);
|
|
|
+ protocol.version_def = Some((version_def, Token)));
|
|
|
+ protocol
|
|
|
+ }
|
|
|
+
|
|
|
+ /// Creates minimal [Protocol] value.
|
|
|
+ pub(crate) fn minimal() -> Protocol {
|
|
|
+ const STATE_NAME: &str = "Init";
|
|
|
+ Protocol::new(
|
|
|
+ NameDef::new("Test"),
|
|
|
+ [
|
|
|
+ ActorDef::new("server", [STATE_NAME]),
|
|
|
+ ActorDef::new("client", ["Client"]),
|
|
|
+ ],
|
|
|
+ [
|
|
|
+ Transition::new(
|
|
|
+ State::new("Client", []),
|
|
|
+ None,
|
|
|
+ [State::new("End", [])],
|
|
|
+ [Dest::new(
|
|
|
+ DestinationState::Service(State::new(STATE_NAME, [])),
|
|
|
+ Message::new("Msg", false, []),
|
|
|
+ )],
|
|
|
+ ),
|
|
|
+ Transition::new(
|
|
|
+ State::new(STATE_NAME, []),
|
|
|
+ Some(Message::new("Msg", false, [])),
|
|
|
+ [State::new("End", [])],
|
|
|
+ [],
|
|
|
+ ),
|
|
|
+ ],
|
|
|
+ )
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+impl Parse for Protocol {
|
|
|
+ /// protocol : name_def ';' ( version_def ';')? ( actor_def ';' )+ ( transition ';' )+ ;
|
|
|
+ fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
|
|
|
+ let name_def = input.parse()?;
|
|
|
+ let name_def_semi = input.parse()?;
|
|
|
+ let version_def = if let Some((ident, _)) = input.cursor().ident() {
|
|
|
+ if ident == VersionDef::VERSION_IDENT {
|
|
|
+ Some((input.parse::<VersionDef>()?, input.parse::<Token![;]>()?))
|
|
|
+ } else {
|
|
|
+ None
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ None
|
|
|
+ };
|
|
|
+ let actor_defs = Punctuated::parse_list(input, |input| !input.peek(Token![let]))?;
|
|
|
+ let transitions =
|
|
|
+ input.parse_terminated(|input| Ok(Rc::new(Transition::parse(input)?)), Token![;])?;
|
|
|
+ Ok(Protocol {
|
|
|
+ name_def,
|
|
|
+ name_def_semi,
|
|
|
+ version_def,
|
|
|
+ actor_defs,
|
|
|
+ transitions,
|
|
|
+ })
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+impl GetSpan for Protocol {
|
|
|
+ fn span(&self) -> Span {
|
|
|
+ self.name_def
|
|
|
+ .span()
|
|
|
+ .left_join(
|
|
|
+ self.version_def
|
|
|
+ .as_ref()
|
|
|
+ .map(|(left, right)| left.span().left_join(right.span())),
|
|
|
+ )
|
|
|
+ .left_join(self.name_def_semi.span())
|
|
|
+ .left_join(punctuated_span(&self.actor_defs))
|
|
|
+ .left_join(punctuated_span(&self.transitions))
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+#[cfg_attr(test, derive(Debug, PartialEq))]
|
|
|
+pub(crate) struct NameDef {
|
|
|
+ named_ident: Ident,
|
|
|
+ pub(crate) name: Ident,
|
|
|
+}
|
|
|
+
|
|
|
+impl NameDef {
|
|
|
+ const NAME_IDENT: &str = "named";
|
|
|
+ const NAME_IDENT_ERR: &str = "Invalid name declaration. Expected 'named'.";
|
|
|
+}
|
|
|
+
|
|
|
+#[cfg(test)]
|
|
|
+impl NameDef {
|
|
|
+ pub(crate) fn new(name: &str) -> Self {
|
|
|
+ Self {
|
|
|
+ named_ident: new_ident(Self::NAME_IDENT),
|
|
|
+ name: new_ident(name),
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+impl Parse for NameDef {
|
|
|
+ /// name_def : "named" Ident ;
|
|
|
+ fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
|
|
|
+ Ok(NameDef {
|
|
|
+ named_ident: check_ident(input.parse()?, Self::NAME_IDENT, Self::NAME_IDENT_ERR)?,
|
|
|
+ name: input.parse()?,
|
|
|
+ })
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+impl GetSpan for NameDef {
|
|
|
+ fn span(&self) -> Span {
|
|
|
+ self.named_ident.span().left_join(self.name.span())
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+#[cfg_attr(test, derive(Debug, PartialEq))]
|
|
|
+pub(crate) struct VersionDef {
|
|
|
+ version_ident: Ident,
|
|
|
+ literal: LitInt,
|
|
|
+ version: u64,
|
|
|
+}
|
|
|
+
|
|
|
+impl VersionDef {
|
|
|
+ const VERSION_IDENT: &str = "version";
|
|
|
+ const VERSION_IDENT_ERR: &str = "Invalid version specifier. Expected 'version'.";
|
|
|
+
|
|
|
+ #[cfg(test)]
|
|
|
+ pub(crate) fn new(version: u64) -> Self {
|
|
|
+ Self {
|
|
|
+ version_ident: new_ident("version"),
|
|
|
+ literal: LitInt::new(&version.to_string(), Span::call_site()),
|
|
|
+ version,
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ #[allow(dead_code)]
|
|
|
+ pub(crate) fn version(&self) -> u64 {
|
|
|
+ self.version
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+impl Parse for VersionDef {
|
|
|
+ /// version_def: "version" LitInt;
|
|
|
+ fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
|
|
|
+ let version_ident =
|
|
|
+ check_ident(input.parse()?, Self::VERSION_IDENT, Self::VERSION_IDENT_ERR)?;
|
|
|
+ let literal: LitInt = input.parse()?;
|
|
|
+ let version = literal.base10_parse()?;
|
|
|
+ Ok(Self {
|
|
|
+ version_ident,
|
|
|
+ literal,
|
|
|
+ version,
|
|
|
+ })
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+impl GetSpan for VersionDef {
|
|
|
+ fn span(&self) -> Span {
|
|
|
+ self.version_ident.span().left_join(self.literal.span())
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+#[cfg_attr(test, derive(Debug, PartialEq))]
|
|
|
+pub(crate) struct ActorDef {
|
|
|
+ let_token: Token![let],
|
|
|
+ pub(crate) actor: Rc<Ident>,
|
|
|
+ eq_token: Token![=],
|
|
|
+ pub(crate) states: IdentArray,
|
|
|
+}
|
|
|
+
|
|
|
+#[cfg(test)]
|
|
|
+impl ActorDef {
|
|
|
+ pub(crate) fn new<T, I>(actor: &str, state_names: T) -> Self
|
|
|
+ where
|
|
|
+ T: IntoIterator<IntoIter = I>,
|
|
|
+ I: ExactSizeIterator<Item = &'static str>,
|
|
|
+ {
|
|
|
+ Self {
|
|
|
+ let_token: Token),
|
|
|
+ actor: new_ident(actor).into(),
|
|
|
+ eq_token: Token),
|
|
|
+ states: IdentArray::new(state_names).unwrap(),
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+impl Parse for ActorDef {
|
|
|
+ /// actor_def : "let" Ident '=' ident_array ;
|
|
|
+ fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
|
|
|
+ Ok(Self {
|
|
|
+ let_token: input.parse()?,
|
|
|
+ actor: Rc::new(input.parse()?),
|
|
|
+ eq_token: input.parse()?,
|
|
|
+ states: input.parse()?,
|
|
|
+ })
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+impl GetSpan for ActorDef {
|
|
|
+ fn span(&self) -> Span {
|
|
|
+ self.let_token
|
|
|
+ .span()
|
|
|
+ .left_join(self.actor.span())
|
|
|
+ .left_join(self.eq_token.span())
|
|
|
+ .left_join(self.states.span())
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+#[cfg_attr(test, derive(Debug))]
|
|
|
+#[derive(Hash, PartialEq, Eq)]
|
|
|
+pub(crate) struct IdentArray {
|
|
|
+ bracket: Bracket,
|
|
|
+ idents: Punctuated<Rc<Ident>, Token![,]>,
|
|
|
+}
|
|
|
+
|
|
|
+impl IdentArray {
|
|
|
+ const EMPTY_ERR: &str = "at least one state is required";
|
|
|
+}
|
|
|
+
|
|
|
+#[cfg(test)]
|
|
|
+impl IdentArray {
|
|
|
+ pub(crate) fn new<T, I>(state_names: T) -> Option<Self>
|
|
|
+ where
|
|
|
+ T: IntoIterator<IntoIter = I>,
|
|
|
+ I: ExactSizeIterator<Item = &'static str>,
|
|
|
+ {
|
|
|
+ let state_names = state_names.into_iter();
|
|
|
+ if state_names.len() > 0 {
|
|
|
+ Some(Self {
|
|
|
+ bracket: Bracket::default(),
|
|
|
+ idents: state_names.map(new_ident).map(Rc::new).collect(),
|
|
|
+ })
|
|
|
+ } else {
|
|
|
+ None
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+impl GetSpan for IdentArray {
|
|
|
+ fn span(&self) -> Span {
|
|
|
+ let delim_span = &self.bracket.span;
|
|
|
+ delim_span
|
|
|
+ .open()
|
|
|
+ .left_join(self.idents.span())
|
|
|
+ .left_join(delim_span.close())
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+impl Parse for IdentArray {
|
|
|
+ /// ident_array : '[' Ident ( ',' Ident )* ','? ']' ;
|
|
|
+ fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
|
|
|
+ let content;
|
|
|
+ let bracket = bracketed!(content in input);
|
|
|
+ let idents =
|
|
|
+ content.parse_terminated(|input| Ok(Rc::new(Ident::parse(input)?)), Token![,])?;
|
|
|
+ if idents.is_empty() {
|
|
|
+ return Err(syn::Error::new(bracket.span.open(), Self::EMPTY_ERR));
|
|
|
+ }
|
|
|
+ Ok(Self { bracket, idents })
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+impl AsRef<Punctuated<Rc<Ident>, Token![,]>> for IdentArray {
|
|
|
+ fn as_ref(&self) -> &Punctuated<Rc<Ident>, Token![,]> {
|
|
|
+ &self.idents
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+#[cfg_attr(test, derive(Debug, PartialEq))]
|
|
|
+pub(crate) struct Transition {
|
|
|
+ pub(crate) in_state: State,
|
|
|
+ in_msg: Option<(Token![?], Rc<Message>)>,
|
|
|
+ arrow: Token![->],
|
|
|
+ pub(crate) out_states: StatesList,
|
|
|
+ #[allow(dead_code)]
|
|
|
+ redirect: Option<Token![>]>,
|
|
|
+ pub(crate) out_msgs: DestList,
|
|
|
+}
|
|
|
+
|
|
|
+impl Transition {
|
|
|
+ pub(crate) fn in_msg(&self) -> Option<&Rc<Message>> {
|
|
|
+ self.in_msg.as_ref().map(|(_, msg)| msg)
|
|
|
+ }
|
|
|
+
|
|
|
+ pub(crate) fn is_client(&self) -> bool {
|
|
|
+ self.in_msg.is_none()
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+#[cfg(test)]
|
|
|
+impl Transition {
|
|
|
+ pub(crate) fn new(
|
|
|
+ in_state: State,
|
|
|
+ in_msg: Option<Message>,
|
|
|
+ out_states: impl IntoIterator<Item = State>,
|
|
|
+ out_msgs: impl IntoIterator<Item = Dest>,
|
|
|
+ ) -> Self {
|
|
|
+ let out_msgs = DestList(out_msgs.into_iter().map(Rc::new).collect());
|
|
|
+ let redirect = if out_msgs.as_ref().is_empty() {
|
|
|
+ None
|
|
|
+ } else {
|
|
|
+ Some(Token))
|
|
|
+ };
|
|
|
+ Self {
|
|
|
+ in_state,
|
|
|
+ in_msg: in_msg.map(|msg| (Token), Rc::new(msg))),
|
|
|
+ arrow: Token),
|
|
|
+ out_states: StatesList(out_states.into_iter().map(Rc::new).collect()),
|
|
|
+ redirect,
|
|
|
+ out_msgs,
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+impl Parse for Transition {
|
|
|
+ /// transition : state ( '?' message )? "->" states_list ( '>' dest_list )? ;
|
|
|
+ fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
|
|
|
+ let in_state = State::parse(input)?;
|
|
|
+ let in_msg = if let Ok(question_mark) = input.parse::<Token![?]>() {
|
|
|
+ Some((question_mark, Rc::new(Message::parse(input)?)))
|
|
|
+ } else {
|
|
|
+ None
|
|
|
+ };
|
|
|
+ let arrow = input.parse::<Token![->]>()?;
|
|
|
+ let out_states = StatesList::parse(input)?;
|
|
|
+ let (redirect, out_msgs) = if let Ok(redirect) = input.parse::<Token![>]>() {
|
|
|
+ (Some(redirect), DestList::parse(input)?)
|
|
|
+ } else {
|
|
|
+ (None, DestList::empty())
|
|
|
+ };
|
|
|
+ Ok(Self {
|
|
|
+ in_state,
|
|
|
+ in_msg,
|
|
|
+ arrow,
|
|
|
+ out_states,
|
|
|
+ redirect,
|
|
|
+ out_msgs,
|
|
|
+ })
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+impl GetSpan for Transition {
|
|
|
+ fn span(&self) -> Span {
|
|
|
+ self.arrow.span()
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+struct IdentIter<'a> {
|
|
|
+ idents: Option<syn::punctuated::Iter<'a, Rc<Ident>>>,
|
|
|
+}
|
|
|
+
|
|
|
+impl<'a> IdentIter<'a> {
|
|
|
+ fn new(idents: Option<syn::punctuated::Iter<'a, Rc<Ident>>>) -> Self {
|
|
|
+ Self { idents }
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+impl<'a> Iterator for IdentIter<'a> {
|
|
|
+ type Item = &'a Rc<Ident>;
|
|
|
+ fn next(&mut self) -> Option<Self::Item> {
|
|
|
+ if let Some(idents) = self.idents.as_mut() {
|
|
|
+ idents.next()
|
|
|
+ } else {
|
|
|
+ None
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+#[cfg_attr(test, derive(Debug))]
|
|
|
+#[derive(Hash, PartialEq, Eq)]
|
|
|
+pub(crate) struct State {
|
|
|
+ pub(crate) state_trait: Rc<Ident>,
|
|
|
+ pub(crate) owned_states: Option<IdentArray>,
|
|
|
+}
|
|
|
+
|
|
|
+impl State {
|
|
|
+ pub(crate) fn owned_states(&self) -> impl Iterator<Item = &'_ Rc<Ident>> {
|
|
|
+ IdentIter::new(
|
|
|
+ self.owned_states
|
|
|
+ .as_ref()
|
|
|
+ .map(|idents| idents.as_ref().iter()),
|
|
|
+ )
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+#[cfg(test)]
|
|
|
+impl State {
|
|
|
+ pub(crate) fn new<T, I>(state_trait: &str, owned_states: T) -> Self
|
|
|
+ where
|
|
|
+ T: IntoIterator<IntoIter = I>,
|
|
|
+ I: ExactSizeIterator<Item = &'static str>,
|
|
|
+ {
|
|
|
+ Self {
|
|
|
+ state_trait: new_ident(state_trait).into(),
|
|
|
+ owned_states: IdentArray::new(owned_states),
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ fn new_empty_owned(state_trait: &str) -> Self {
|
|
|
+ Self::new(state_trait, std::iter::empty())
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+impl Parse for State {
|
|
|
+ /// state : Ident ident_array? ;
|
|
|
+ fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
|
|
|
+ let state_trait = Ident::parse(input)?.into();
|
|
|
+ let owned_states = if input.peek(Bracket) {
|
|
|
+ Some(IdentArray::parse(input)?)
|
|
|
+ } else {
|
|
|
+ None
|
|
|
+ };
|
|
|
+ Ok(Self {
|
|
|
+ state_trait,
|
|
|
+ owned_states,
|
|
|
+ })
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+impl GetSpan for State {
|
|
|
+ fn span(&self) -> Span {
|
|
|
+ self.state_trait.span().left_join(&self.owned_states)
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+#[cfg_attr(test, derive(Debug, PartialEq))]
|
|
|
+pub(crate) struct StatesList(Punctuated<Rc<State>, Token![,]>);
|
|
|
+
|
|
|
+impl StatesList {
|
|
|
+ const EMPTY_ERR: &str = "at lest one out state is required";
|
|
|
+}
|
|
|
+
|
|
|
+impl Parse for StatesList {
|
|
|
+ /// states_list : state ( ',' state )* ','? ;
|
|
|
+ fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
|
|
|
+ let states = Punctuated::parse_list(input, |input| {
|
|
|
+ input.peek(Token![>]) || input.peek(Token![;])
|
|
|
+ })?;
|
|
|
+ if states.is_empty() {
|
|
|
+ return Err(input.error(Self::EMPTY_ERR));
|
|
|
+ }
|
|
|
+ Ok(Self(states))
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+impl GetSpan for StatesList {
|
|
|
+ fn span(&self) -> Span {
|
|
|
+ punctuated_span(&self.0)
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+impl AsRef<Punctuated<Rc<State>, Token![,]>> for StatesList {
|
|
|
+ fn as_ref(&self) -> &Punctuated<Rc<State>, Token![,]> {
|
|
|
+ &self.0
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+#[cfg_attr(test, derive(Debug, PartialEq))]
|
|
|
+pub(crate) struct DestList(Punctuated<Rc<Dest>, Token![,]>);
|
|
|
+
|
|
|
+impl DestList {
|
|
|
+ const TRAILING_COMMA_ERR: &str = "No trailing comma is allowed in a destination list.";
|
|
|
+
|
|
|
+ fn empty() -> Self {
|
|
|
+ Self(Punctuated::new())
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+impl Parse for DestList {
|
|
|
+ /// dest_list : dest ( ',' dest )* ;
|
|
|
+ fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
|
|
|
+ let dests = Punctuated::parse_list(input, |input| input.peek(Token![;]))?;
|
|
|
+ if dests.trailing_punct() {
|
|
|
+ return Err(syn::Error::new(
|
|
|
+ punctuated_span(&dests),
|
|
|
+ Self::TRAILING_COMMA_ERR,
|
|
|
+ ));
|
|
|
+ }
|
|
|
+ Ok(DestList(dests))
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+impl MaybeGetSpan for DestList {
|
|
|
+ fn maybe_span(&self) -> Option<Span> {
|
|
|
+ if self.0.is_empty() {
|
|
|
+ None
|
|
|
+ } else {
|
|
|
+ Some(punctuated_span(&self.0))
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+impl AsRef<Punctuated<Rc<Dest>, Token![,]>> for DestList {
|
|
|
+ fn as_ref(&self) -> &Punctuated<Rc<Dest>, Token![,]> {
|
|
|
+ &self.0
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+#[cfg_attr(test, derive(Debug, PartialEq))]
|
|
|
+pub(crate) struct Dest {
|
|
|
+ pub(crate) state: DestinationState,
|
|
|
+ exclamation: Token![!],
|
|
|
+ pub(crate) msg: Rc<Message>,
|
|
|
+}
|
|
|
+
|
|
|
+#[cfg(test)]
|
|
|
+impl Dest {
|
|
|
+ pub(crate) fn new(state: DestinationState, msg: Message) -> Self {
|
|
|
+ Self {
|
|
|
+ state,
|
|
|
+ exclamation: Token),
|
|
|
+ msg: Rc::new(msg),
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+impl Parse for Dest {
|
|
|
+ /// dest : dest_state '!' message
|
|
|
+ fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
|
|
|
+ Ok(Self {
|
|
|
+ state: input.parse()?,
|
|
|
+ exclamation: input.parse()?,
|
|
|
+ msg: Rc::new(input.parse()?),
|
|
|
+ })
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+impl GetSpan for Dest {
|
|
|
+ fn span(&self) -> Span {
|
|
|
+ self.state
|
|
|
+ .span()
|
|
|
+ .left_join(self.exclamation.span())
|
|
|
+ .left_join(self.msg.span())
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+#[cfg_attr(test, derive(Debug, PartialEq))]
|
|
|
+pub(crate) enum DestinationState {
|
|
|
+ Service(State),
|
|
|
+ Individual(State),
|
|
|
+}
|
|
|
+
|
|
|
+impl DestinationState {
|
|
|
+ const NO_STATE_ERR: &str = "expected destination state";
|
|
|
+ const MULTI_STATE_ERR: &str = "only one destination state is allowed";
|
|
|
+}
|
|
|
+
|
|
|
+impl Parse for DestinationState {
|
|
|
+ /// dest_state : ( "service" '(' Ident ')' ) | state ;
|
|
|
+ fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
|
|
|
+ let tester = input.fork();
|
|
|
+ let ident = Ident::parse(&tester)?;
|
|
|
+ if ident == "service" {
|
|
|
+ Ident::parse(input)?;
|
|
|
+ let content;
|
|
|
+ let paren_token = parenthesized!(content in input);
|
|
|
+ let mut dest_states = content
|
|
|
+ .parse_terminated(Ident::parse, Token![,])?
|
|
|
+ .into_iter();
|
|
|
+ let dest_state = dest_states
|
|
|
+ .next()
|
|
|
+ .ok_or(syn::Error::new(paren_token.span.open(), Self::NO_STATE_ERR))?;
|
|
|
+ if let Some(extra_dest) = dest_states.next() {
|
|
|
+ return Err(syn::Error::new(extra_dest.span(), Self::MULTI_STATE_ERR));
|
|
|
+ }
|
|
|
+ Ok(DestinationState::Service(State {
|
|
|
+ state_trait: dest_state.into(),
|
|
|
+ owned_states: None,
|
|
|
+ }))
|
|
|
+ } else {
|
|
|
+ Ok(DestinationState::Individual(input.parse()?))
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+impl GetSpan for DestinationState {
|
|
|
+ fn span(&self) -> Span {
|
|
|
+ let state = match self {
|
|
|
+ Self::Service(state) => state,
|
|
|
+ Self::Individual(state) => state,
|
|
|
+ };
|
|
|
+ state.span()
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+#[cfg_attr(test, derive(Debug))]
|
|
|
+#[derive(Hash, PartialEq, Eq)]
|
|
|
+pub(crate) struct Message {
|
|
|
+ pub(crate) msg_type: Rc<Ident>,
|
|
|
+ reply_part: Option<MessageReplyPart>,
|
|
|
+ pub(crate) owned_states: Option<IdentArray>,
|
|
|
+ ident: Option<Ident>,
|
|
|
+}
|
|
|
+
|
|
|
+impl Message {
|
|
|
+ /// Returns the identifier to use when naming types and variants after this message.
|
|
|
+ pub(crate) fn variant(&self) -> &Ident {
|
|
|
+ if let Some(ident) = &self.ident {
|
|
|
+ ident
|
|
|
+ } else {
|
|
|
+ &self.msg_type
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /// Returns true if and only if this message is a reply.
|
|
|
+ pub(crate) fn is_reply(&self) -> bool {
|
|
|
+ self.reply_part.is_some()
|
|
|
+ }
|
|
|
+
|
|
|
+ pub(crate) fn owned_states(&self) -> impl Iterator<Item = &'_ Rc<Ident>> {
|
|
|
+ IdentIter::new(
|
|
|
+ self.owned_states
|
|
|
+ .as_ref()
|
|
|
+ .map(|states| states.as_ref().iter()),
|
|
|
+ )
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+#[cfg(test)]
|
|
|
+impl Message {
|
|
|
+ pub(crate) fn new<T, I>(msg_type: &str, is_reply: bool, owned_states: T) -> Self
|
|
|
+ where
|
|
|
+ T: IntoIterator<IntoIter = I>,
|
|
|
+ I: ExactSizeIterator<Item = &'static str>,
|
|
|
+ {
|
|
|
+ let (reply_part, ident_field) = if is_reply {
|
|
|
+ let reply_part = MessageReplyPart {
|
|
|
+ colons: Token),
|
|
|
+ reply: new_ident(MessageReplyPart::REPLY_IDENT),
|
|
|
+ };
|
|
|
+ let variant = format_ident!("{}{}", msg_type, MessageReplyPart::REPLY_IDENT);
|
|
|
+ (Some(reply_part), Some(variant))
|
|
|
+ } else {
|
|
|
+ (None, None)
|
|
|
+ };
|
|
|
+ Self {
|
|
|
+ msg_type: Rc::new(new_ident(msg_type)),
|
|
|
+ reply_part,
|
|
|
+ owned_states: IdentArray::new(owned_states),
|
|
|
+ ident: ident_field,
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+impl Parse for Message {
|
|
|
+ /// message : Ident ( "::" "Reply" )? ident_array? ;
|
|
|
+ fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
|
|
|
+ let msg_type = Rc::new(Ident::parse(input)?);
|
|
|
+ let (reply_part, ident) = if input.peek(Token![::]) {
|
|
|
+ let reply_part = input.parse()?;
|
|
|
+ (
|
|
|
+ Some(reply_part),
|
|
|
+ Some(format_ident!("{msg_type}{}", MessageReplyPart::REPLY_IDENT)),
|
|
|
+ )
|
|
|
+ } else {
|
|
|
+ (None, None)
|
|
|
+ };
|
|
|
+ let owned_states = if input.peek(Bracket) {
|
|
|
+ Some(IdentArray::parse(input)?)
|
|
|
+ } else {
|
|
|
+ None
|
|
|
+ };
|
|
|
+ Ok(Self {
|
|
|
+ msg_type,
|
|
|
+ reply_part,
|
|
|
+ owned_states,
|
|
|
+ ident,
|
|
|
+ })
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+impl GetSpan for Message {
|
|
|
+ fn span(&self) -> Span {
|
|
|
+ self.msg_type
|
|
|
+ .span()
|
|
|
+ .left_join(self.reply_part.as_ref())
|
|
|
+ .left_join(&self.owned_states)
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+#[cfg_attr(test, derive(Debug))]
|
|
|
+#[derive(Hash, PartialEq, Eq)]
|
|
|
+pub(crate) struct MessageReplyPart {
|
|
|
+ colons: Token![::],
|
|
|
+ reply: Ident,
|
|
|
+}
|
|
|
+
|
|
|
+impl MessageReplyPart {
|
|
|
+ const REPLY_ERR: &str = "expected 'Reply'";
|
|
|
+ pub(crate) const REPLY_IDENT: &str = "Reply";
|
|
|
+}
|
|
|
+
|
|
|
+impl Parse for MessageReplyPart {
|
|
|
+ fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
|
|
|
+ Ok(Self {
|
|
|
+ colons: input.parse()?,
|
|
|
+ reply: check_ident(input.parse()?, Self::REPLY_IDENT, Self::REPLY_ERR)?,
|
|
|
+ })
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+impl GetSpan for MessageReplyPart {
|
|
|
+ fn span(&self) -> Span {
|
|
|
+ self.colons.span().left_join(self.reply.span())
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+/// Verifies that an ident has the expected string contents and returns it if it does. An error is
|
|
|
+/// returned containing the given error message if it does not.
|
|
|
+fn check_ident(ident: Ident, expected: &str, err_msg: &str) -> syn::Result<Ident> {
|
|
|
+ if ident == expected {
|
|
|
+ Ok(ident)
|
|
|
+ } else {
|
|
|
+ Err(syn::Error::new(ident.span(), err_msg))
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+/// Trait for types which represent a region of source code. This is similar to the [Spanned] trait
|
|
|
+/// in the [syn] crate. A new trait was needed because [Spanned] is sealed.
|
|
|
+pub(crate) trait GetSpan {
|
|
|
+ /// Returns the [Span] covering the source code represented by this syntax value.
|
|
|
+ fn span(&self) -> Span;
|
|
|
+}
|
|
|
+
|
|
|
+impl GetSpan for Span {
|
|
|
+ fn span(&self) -> Span {
|
|
|
+ *self
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+impl<'a, T: GetSpan> GetSpan for &'a T {
|
|
|
+ fn span(&self) -> Span {
|
|
|
+ (*self).span()
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+impl<T: GetSpan> GetSpan for Rc<T> {
|
|
|
+ fn span(&self) -> Span {
|
|
|
+ self.deref().span()
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+/// Returns the span of a punctuated sequence of values which implement [GetSpan].
|
|
|
+fn punctuated_span<T: GetSpan, P>(arg: &Punctuated<T, P>) -> Span {
|
|
|
+ let mut iter = arg.into_iter();
|
|
|
+ let mut span = if let Some(state) = iter.next() {
|
|
|
+ state.span()
|
|
|
+ } else {
|
|
|
+ return Span::call_site();
|
|
|
+ };
|
|
|
+ for state in iter {
|
|
|
+ span = span.left_join(state.span());
|
|
|
+ }
|
|
|
+ span
|
|
|
+}
|
|
|
+
|
|
|
+pub(crate) trait MaybeGetSpan {
|
|
|
+ fn maybe_span(&self) -> Option<Span>;
|
|
|
+}
|
|
|
+
|
|
|
+impl<'a, T: MaybeGetSpan> MaybeGetSpan for &'a T {
|
|
|
+ fn maybe_span(&self) -> Option<Span> {
|
|
|
+ (*self).maybe_span()
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+impl<T: GetSpan> MaybeGetSpan for Option<T> {
|
|
|
+ fn maybe_span(&self) -> Option<Span> {
|
|
|
+ self.as_ref().map(|x| x.span())
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+impl MaybeGetSpan for Span {
|
|
|
+ fn maybe_span(&self) -> Option<Span> {
|
|
|
+ Some(*self)
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+trait LeftJoin<Rhs> {
|
|
|
+ /// Attempts to join two [GetSpan] values, but if the result of the join is `None`, then just
|
|
|
+ /// the left span is returned.
|
|
|
+ fn left_join(&self, other: Rhs) -> Span;
|
|
|
+}
|
|
|
+
|
|
|
+impl<R: MaybeGetSpan> LeftJoin<R> for Span {
|
|
|
+ fn left_join(&self, other: R) -> Span {
|
|
|
+ if let Some(span) = other.maybe_span() {
|
|
|
+ self.join(span).unwrap_or(*self)
|
|
|
+ } else {
|
|
|
+ *self
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+/// Returns a new span whose text content is the given string and whose span is `Span::call_site`.
|
|
|
+#[cfg(test)]
|
|
|
+pub(crate) fn new_ident(str: &str) -> Ident {
|
|
|
+ Ident::new(str, Span::call_site())
|
|
|
+}
|
|
|
+
|
|
|
+trait ParsePunctuatedList: Sized {
|
|
|
+ fn parse_list(
|
|
|
+ input: ParseStream,
|
|
|
+ should_break: impl Fn(ParseStream) -> bool,
|
|
|
+ ) -> syn::Result<Self>;
|
|
|
+}
|
|
|
+
|
|
|
+impl<T: Parse, U: Parse> ParsePunctuatedList for Punctuated<Rc<T>, U> {
|
|
|
+ fn parse_list(
|
|
|
+ input: ParseStream,
|
|
|
+ should_break: impl Fn(ParseStream) -> bool,
|
|
|
+ ) -> syn::Result<Self> {
|
|
|
+ let mut output = Punctuated::new();
|
|
|
+ while !input.cursor().eof() {
|
|
|
+ if should_break(input) {
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ output.push_value(Rc::new(input.parse()?));
|
|
|
+ if let Ok(punct) = input.parse() {
|
|
|
+ output.push_punct(punct);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ Ok(output)
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+#[cfg(test)]
|
|
|
+mod tests {
|
|
|
+ use super::*;
|
|
|
+ use syn::parse_str;
|
|
|
+
|
|
|
+ #[test]
|
|
|
+ fn protocol_parse_minimal() {
|
|
|
+ const EXPECTED_ACTOR: &str = "server";
|
|
|
+ const EXPECTED_NAME: &str = "Foo";
|
|
|
+ const EXPECTED_STATES: [&str; 3] = ["First", "Second", "Third"];
|
|
|
+ let input = format!(
|
|
|
+ "named {EXPECTED_NAME};
|
|
|
+let {EXPECTED_ACTOR} = [{}];
|
|
|
+{} -> {};
|
|
|
+{} -> {};",
|
|
|
+ EXPECTED_STATES.join(", "),
|
|
|
+ EXPECTED_STATES[0],
|
|
|
+ EXPECTED_STATES[1],
|
|
|
+ EXPECTED_STATES[1],
|
|
|
+ EXPECTED_STATES[2],
|
|
|
+ );
|
|
|
+ let expected = Protocol::new(
|
|
|
+ NameDef::new(EXPECTED_NAME),
|
|
|
+ [ActorDef::new(EXPECTED_ACTOR, EXPECTED_STATES)],
|
|
|
+ [
|
|
|
+ Transition::new(
|
|
|
+ State::new(EXPECTED_STATES[0], []),
|
|
|
+ None,
|
|
|
+ [State::new(EXPECTED_STATES[1], [])],
|
|
|
+ [],
|
|
|
+ ),
|
|
|
+ Transition::new(
|
|
|
+ State::new(EXPECTED_STATES[1], []),
|
|
|
+ None,
|
|
|
+ [State::new(EXPECTED_STATES[2], [])],
|
|
|
+ [],
|
|
|
+ ),
|
|
|
+ ],
|
|
|
+ );
|
|
|
+
|
|
|
+ let actual = parse_str::<Protocol>(&input).unwrap();
|
|
|
+
|
|
|
+ assert_eq!(expected, actual);
|
|
|
+ }
|
|
|
+
|
|
|
+ #[test]
|
|
|
+ fn protocol_parse_with_version() {
|
|
|
+ const EXPECTED_VERSION: u64 = 37;
|
|
|
+ let input = format!(
|
|
|
+ "named VersionTest;
|
|
|
+version {EXPECTED_VERSION};
|
|
|
+let actor = [Init];
|
|
|
+Init?Activate -> End;"
|
|
|
+ );
|
|
|
+ let expected = Protocol::with_version(
|
|
|
+ NameDef::new("VersionTest"),
|
|
|
+ VersionDef::new(EXPECTED_VERSION),
|
|
|
+ [ActorDef::new("actor", ["Init"])],
|
|
|
+ [Transition::new(
|
|
|
+ State::new("Init", []),
|
|
|
+ Some(Message::new("Activate", false, [])),
|
|
|
+ [State::new("End", [])],
|
|
|
+ [],
|
|
|
+ )],
|
|
|
+ );
|
|
|
+
|
|
|
+ let actual = parse_str::<Protocol>(&input).unwrap();
|
|
|
+
|
|
|
+ assert_eq!(expected, actual);
|
|
|
+ }
|
|
|
+
|
|
|
+ #[test]
|
|
|
+ fn protocol_parse_version_mismatch_is_err() {
|
|
|
+ const EXPECTED_VERSION: u64 = 37;
|
|
|
+ let input = format!(
|
|
|
+ "named VersionTest;
|
|
|
+version {EXPECTED_VERSION};
|
|
|
+let actor = [Init];
|
|
|
+Init?Activate -> End;"
|
|
|
+ );
|
|
|
+ let expected = Protocol::with_version(
|
|
|
+ NameDef::new("VersionTest"),
|
|
|
+ VersionDef::new(EXPECTED_VERSION + 1),
|
|
|
+ [ActorDef::new("actor", ["Init"])],
|
|
|
+ [Transition::new(
|
|
|
+ State::new("Init", []),
|
|
|
+ Some(Message::new("Activate", false, [])),
|
|
|
+ [State::new("End", [])],
|
|
|
+ [],
|
|
|
+ )],
|
|
|
+ );
|
|
|
+
|
|
|
+ let actual = parse_str::<Protocol>(&input).unwrap();
|
|
|
+
|
|
|
+ assert_ne!(expected, actual);
|
|
|
+ }
|
|
|
+
|
|
|
+ #[test]
|
|
|
+ fn name_def_parse() {
|
|
|
+ const EXPECTED_NAME: &str = "Foofercorg";
|
|
|
+ let input = format!("named {EXPECTED_NAME}");
|
|
|
+ let expected = NameDef::new(EXPECTED_NAME);
|
|
|
+
|
|
|
+ let actual = parse_str::<NameDef>(&input).unwrap();
|
|
|
+
|
|
|
+ assert_eq!(expected, actual);
|
|
|
+ }
|
|
|
+
|
|
|
+ #[test]
|
|
|
+ fn name_def_wrong_ident_err() {
|
|
|
+ let result = parse_str::<NameDef>("nmaed Shodan");
|
|
|
+
|
|
|
+ assert!(result.is_err());
|
|
|
+ let err_str = result.err().unwrap().to_string();
|
|
|
+ assert_eq!(NameDef::NAME_IDENT_ERR, err_str);
|
|
|
+ }
|
|
|
+
|
|
|
+ #[test]
|
|
|
+ fn version_def_parse() {
|
|
|
+ const EXPECTED_VERSION: u64 = 1;
|
|
|
+ let input = format!("version {EXPECTED_VERSION}");
|
|
|
+ let expected = VersionDef::new(EXPECTED_VERSION);
|
|
|
+
|
|
|
+ let actual = parse_str::<VersionDef>(&input).unwrap();
|
|
|
+
|
|
|
+ assert_eq!(expected, actual);
|
|
|
+ }
|
|
|
+
|
|
|
+ #[test]
|
|
|
+ fn version_def_parse_wrong_ident_err() {
|
|
|
+ let result = parse_str::<VersionDef>("versoin 3");
|
|
|
+
|
|
|
+ assert!(result.is_err());
|
|
|
+ let err_str = result.err().unwrap().to_string();
|
|
|
+ assert_eq!(VersionDef::VERSION_IDENT_ERR, err_str);
|
|
|
+ }
|
|
|
+
|
|
|
+ #[test]
|
|
|
+ fn actor_def_parse() {
|
|
|
+ const EXPECTED_ACTOR: &str = "server";
|
|
|
+ const EXPECTED_STATES: [&str; 2] = ["First", "Second"];
|
|
|
+ let input = format!("let {EXPECTED_ACTOR} = [{}]", EXPECTED_STATES.join(", "));
|
|
|
+ let expected = ActorDef::new(EXPECTED_ACTOR, EXPECTED_STATES);
|
|
|
+
|
|
|
+ let actual = parse_str::<ActorDef>(&input).unwrap();
|
|
|
+
|
|
|
+ assert_eq!(expected, actual);
|
|
|
+ }
|
|
|
+
|
|
|
+ #[test]
|
|
|
+ fn ident_array_new() {
|
|
|
+ const EXPECTED: [&str; 2] = ["Red", "Green"];
|
|
|
+
|
|
|
+ let actual = IdentArray::new(EXPECTED).unwrap();
|
|
|
+
|
|
|
+ assert_eq!(EXPECTED.len(), actual.idents.len());
|
|
|
+ assert_eq!(actual.idents[0].as_ref(), EXPECTED[0]);
|
|
|
+ assert_eq!(actual.idents[1].as_ref(), EXPECTED[1]);
|
|
|
+ }
|
|
|
+
|
|
|
+ #[test]
|
|
|
+ fn ident_array_not_equal() {
|
|
|
+ let expected = IdentArray::new(["Red", "Green"]);
|
|
|
+
|
|
|
+ let actual = IdentArray::new(["Blue", "Gold"]);
|
|
|
+
|
|
|
+ assert_ne!(expected, actual);
|
|
|
+ }
|
|
|
+
|
|
|
+ #[test]
|
|
|
+ fn ident_array_not_equal_different_lens() {
|
|
|
+ let expected = IdentArray::new(["Red", "Green"]);
|
|
|
+
|
|
|
+ let actual = IdentArray::new(["Red"]);
|
|
|
+
|
|
|
+ assert_ne!(expected, actual);
|
|
|
+ }
|
|
|
+
|
|
|
+ #[test]
|
|
|
+ fn ident_array_parse() {
|
|
|
+ const EXPECTED_STATES: [&str; 2] = ["Sad", "Glad"];
|
|
|
+ let input = format!("[{}, {}]", EXPECTED_STATES[0], EXPECTED_STATES[1]);
|
|
|
+ let expected = IdentArray::new(EXPECTED_STATES).unwrap();
|
|
|
+
|
|
|
+ let actual = parse_str::<IdentArray>(&input).unwrap();
|
|
|
+
|
|
|
+ assert_eq!(expected, actual);
|
|
|
+ }
|
|
|
+
|
|
|
+ #[test]
|
|
|
+ fn ident_array_parse_empty_err() {
|
|
|
+ let result = parse_str::<IdentArray>("[]");
|
|
|
+
|
|
|
+ assert!(result.is_err());
|
|
|
+ let err_str = result.err().unwrap().to_string();
|
|
|
+ assert_eq!(IdentArray::EMPTY_ERR, err_str);
|
|
|
+ }
|
|
|
+
|
|
|
+ #[test]
|
|
|
+ fn transition_parse_minimal() {
|
|
|
+ const EXPECTED_IN_STATE: &str = "Catcher";
|
|
|
+ const EXPECTED_OUT_STATE: &str = "End";
|
|
|
+ let input = format!("{EXPECTED_IN_STATE} -> {EXPECTED_OUT_STATE}");
|
|
|
+ let expected = Transition::new(
|
|
|
+ State::new(EXPECTED_IN_STATE, []),
|
|
|
+ None,
|
|
|
+ [EXPECTED_OUT_STATE].map(State::new_empty_owned),
|
|
|
+ [],
|
|
|
+ );
|
|
|
+
|
|
|
+ let actual = parse_str::<Transition>(&input).unwrap();
|
|
|
+
|
|
|
+ assert_eq!(expected, actual);
|
|
|
+ }
|
|
|
+
|
|
|
+ #[test]
|
|
|
+ fn transition_parse_with_in_msg() {
|
|
|
+ const EXPECTED_IN_STATE: &str = "Catcher";
|
|
|
+ const EXPECTED_IN_MSG: &str = "Msg";
|
|
|
+ const EXPECTED_OUT_STATE: &str = "End";
|
|
|
+ let input = format!("{EXPECTED_IN_STATE}?{EXPECTED_IN_MSG} -> {EXPECTED_OUT_STATE}");
|
|
|
+ let expected = Transition::new(
|
|
|
+ State::new(EXPECTED_IN_STATE, []),
|
|
|
+ Some(Message::new(EXPECTED_IN_MSG, false, [])),
|
|
|
+ [EXPECTED_OUT_STATE].map(State::new_empty_owned),
|
|
|
+ [],
|
|
|
+ );
|
|
|
+
|
|
|
+ let actual = parse_str::<Transition>(&input).unwrap();
|
|
|
+
|
|
|
+ assert_eq!(expected, actual);
|
|
|
+ }
|
|
|
+
|
|
|
+ #[test]
|
|
|
+ fn transition_parse_with_in_msg_reply() {
|
|
|
+ const EXPECTED_IN_STATE: &str = "Catcher";
|
|
|
+ const EXPECTED_IN_MSG: &str = "Msg";
|
|
|
+ const EXPECTED_OUT_STATE: &str = "End";
|
|
|
+ let input = format!("{EXPECTED_IN_STATE}?{EXPECTED_IN_MSG}::Reply -> {EXPECTED_OUT_STATE}");
|
|
|
+ let expected = Transition::new(
|
|
|
+ State::new(EXPECTED_IN_STATE, []),
|
|
|
+ Some(Message::new(EXPECTED_IN_MSG, true, [])),
|
|
|
+ [EXPECTED_OUT_STATE].map(State::new_empty_owned),
|
|
|
+ [],
|
|
|
+ );
|
|
|
+
|
|
|
+ let actual = parse_str::<Transition>(&input).unwrap();
|
|
|
+
|
|
|
+ assert_eq!(expected, actual);
|
|
|
+ }
|
|
|
+
|
|
|
+ #[test]
|
|
|
+ fn transition_parse_with_in_msg_reply_owned_states() {
|
|
|
+ const EXPECTED_IN_STATE: &str = "Catcher";
|
|
|
+ const EXPECTED_IN_MSG: &str = "Msg";
|
|
|
+ const EXPECTED_OWNED_STATES: [&str; 2] = ["First", "Second"];
|
|
|
+ const EXPECTED_OUT_STATE: &str = "End";
|
|
|
+ let input = format!(
|
|
|
+ "{EXPECTED_IN_STATE}?{EXPECTED_IN_MSG}::Reply[{}, {}] -> {EXPECTED_OUT_STATE}",
|
|
|
+ EXPECTED_OWNED_STATES[0], EXPECTED_OWNED_STATES[1]
|
|
|
+ );
|
|
|
+ let expected = Transition::new(
|
|
|
+ State::new(EXPECTED_IN_STATE, []),
|
|
|
+ Some(Message::new(EXPECTED_IN_MSG, true, EXPECTED_OWNED_STATES)),
|
|
|
+ [EXPECTED_OUT_STATE].map(State::new_empty_owned),
|
|
|
+ [],
|
|
|
+ );
|
|
|
+
|
|
|
+ let actual = parse_str::<Transition>(&input).unwrap();
|
|
|
+
|
|
|
+ assert_eq!(expected, actual);
|
|
|
+ }
|
|
|
+
|
|
|
+ #[test]
|
|
|
+ fn transition_parse_has_single_dest() {
|
|
|
+ const EXPECTED_IN_STATE: &str = "Catcher";
|
|
|
+ const EXPECTED_OUT_STATE: &str = "End";
|
|
|
+ const EXPECTED_DEST_STATE: &str = "Target";
|
|
|
+ const EXPECTED_DEST_MSG: &str = "DeathNote";
|
|
|
+ let input = format!("{EXPECTED_IN_STATE} -> {EXPECTED_OUT_STATE} >{EXPECTED_DEST_STATE}!{EXPECTED_DEST_MSG}");
|
|
|
+ let expected = Transition::new(
|
|
|
+ State::new(EXPECTED_IN_STATE, []),
|
|
|
+ None,
|
|
|
+ [EXPECTED_OUT_STATE].map(State::new_empty_owned),
|
|
|
+ [Dest::new(
|
|
|
+ DestinationState::Individual(State::new(EXPECTED_DEST_STATE, [])),
|
|
|
+ Message::new(EXPECTED_DEST_MSG, false, []),
|
|
|
+ )],
|
|
|
+ );
|
|
|
+
|
|
|
+ let actual = parse_str::<Transition>(&input).unwrap();
|
|
|
+
|
|
|
+ assert_eq!(expected, actual);
|
|
|
+ }
|
|
|
+
|
|
|
+ #[test]
|
|
|
+ fn transition_parse_multiple_out_states_and_out_msgs() {
|
|
|
+ const EXPECTED_IN_STATE: &str = "Catcher";
|
|
|
+ const EXPECTED_IN_MSG: &str = "Tap";
|
|
|
+ const EXPECTED_OUT_STATES: [&str; 2] = ["Out0", "Out1"];
|
|
|
+ const EXPECTED_DESTS: [(&str, &str); 2] = [("Dest0", "Msg0"), ("Dest1", "Msg1")];
|
|
|
+ let states_list = EXPECTED_OUT_STATES.join(", ");
|
|
|
+ let dest_list = EXPECTED_DESTS.map(|(l, r)| format!("{l}!{r}")).join(", ");
|
|
|
+ let input = format!("{EXPECTED_IN_STATE}?{EXPECTED_IN_MSG} -> {states_list} >{dest_list}");
|
|
|
+ let expected = Transition::new(
|
|
|
+ State::new(EXPECTED_IN_STATE, []),
|
|
|
+ Some(Message::new(EXPECTED_IN_MSG, false, [])),
|
|
|
+ EXPECTED_OUT_STATES.map(State::new_empty_owned),
|
|
|
+ EXPECTED_DESTS.map(|(l, r)| {
|
|
|
+ Dest::new(
|
|
|
+ DestinationState::Individual(State::new(l, [])),
|
|
|
+ Message::new(r, false, []),
|
|
|
+ )
|
|
|
+ }),
|
|
|
+ );
|
|
|
+
|
|
|
+ let actual = parse_str::<Transition>(&input).unwrap();
|
|
|
+
|
|
|
+ assert_eq!(expected, actual);
|
|
|
+ }
|
|
|
+
|
|
|
+ #[test]
|
|
|
+ fn state_parse_no_owned_states() {
|
|
|
+ const EXPECTED_TRAIT: &str = "Contained";
|
|
|
+ let expected = State::new(EXPECTED_TRAIT, []);
|
|
|
+
|
|
|
+ let actual = parse_str::<State>(EXPECTED_TRAIT).unwrap();
|
|
|
+
|
|
|
+ assert_eq!(expected, actual);
|
|
|
+ }
|
|
|
+
|
|
|
+ #[test]
|
|
|
+ fn state_parse_has_owned_states() {
|
|
|
+ const EXPECTED_TRAIT: &str = "Contained";
|
|
|
+ const EXPECTED_OWNED: [&str; 2] = ["First", "Second"];
|
|
|
+ let input = format!(
|
|
|
+ "{EXPECTED_TRAIT}[{}, {}]",
|
|
|
+ EXPECTED_OWNED[0], EXPECTED_OWNED[1]
|
|
|
+ );
|
|
|
+ let expected = State::new(EXPECTED_TRAIT, EXPECTED_OWNED);
|
|
|
+
|
|
|
+ let actual = parse_str::<State>(&input).unwrap();
|
|
|
+
|
|
|
+ assert_eq!(expected, actual);
|
|
|
+ }
|
|
|
+
|
|
|
+ #[test]
|
|
|
+ fn dest_parse() {
|
|
|
+ const EXPECTED_STATE: &str = "Opened";
|
|
|
+ const EXPECTED_MSG: &str = "Read";
|
|
|
+ let input = format!("{EXPECTED_STATE}!{EXPECTED_MSG}");
|
|
|
+ let expected = Dest::new(
|
|
|
+ DestinationState::Individual(State::new(EXPECTED_STATE, [])),
|
|
|
+ Message::new(EXPECTED_MSG, false, []),
|
|
|
+ );
|
|
|
+
|
|
|
+ let actual = parse_str::<Dest>(&input).unwrap();
|
|
|
+
|
|
|
+ assert_eq!(expected, actual);
|
|
|
+ }
|
|
|
+
|
|
|
+ #[test]
|
|
|
+ fn dest_list_trailing_comma_err() {
|
|
|
+ const INPUT: &str = "First!Msg0, Second!Msg1,";
|
|
|
+
|
|
|
+ let result = parse_str::<DestList>(INPUT);
|
|
|
+
|
|
|
+ assert!(result.is_err());
|
|
|
+ let err_str = result.err().unwrap().to_string();
|
|
|
+ assert_eq!(DestList::TRAILING_COMMA_ERR, err_str);
|
|
|
+ }
|
|
|
+
|
|
|
+ #[test]
|
|
|
+ fn destination_state_parse_regular() {
|
|
|
+ const EXPECTED_DEST_STATE: &str = "Listening";
|
|
|
+ let expected = DestinationState::Individual(State::new(EXPECTED_DEST_STATE, []));
|
|
|
+
|
|
|
+ let actual = parse_str::<DestinationState>(EXPECTED_DEST_STATE).unwrap();
|
|
|
+
|
|
|
+ assert_eq!(expected, actual);
|
|
|
+ }
|
|
|
+
|
|
|
+ #[test]
|
|
|
+ fn destination_state_parse_owned_states() {
|
|
|
+ const EXPECTED_DEST_STATE: &str = "Listening";
|
|
|
+ const EXPECTED_OWNED_STATES: [&str; 2] = ["First", "Second"];
|
|
|
+ let input = format!(
|
|
|
+ "{EXPECTED_DEST_STATE}[{}]",
|
|
|
+ EXPECTED_OWNED_STATES.join(", ")
|
|
|
+ );
|
|
|
+ let expected =
|
|
|
+ DestinationState::Individual(State::new(EXPECTED_DEST_STATE, EXPECTED_OWNED_STATES));
|
|
|
+
|
|
|
+ let actual = parse_str::<DestinationState>(&input).unwrap();
|
|
|
+
|
|
|
+ assert_eq!(expected, actual);
|
|
|
+ }
|
|
|
+
|
|
|
+ #[test]
|
|
|
+ fn destination_state_parse_service() {
|
|
|
+ const EXPECTED_DEST_STATE: &str = "Listening";
|
|
|
+ let expected = DestinationState::Service(State::new(EXPECTED_DEST_STATE, []));
|
|
|
+ let input = format!("service({EXPECTED_DEST_STATE})");
|
|
|
+
|
|
|
+ let actual = parse_str::<DestinationState>(&input).unwrap();
|
|
|
+
|
|
|
+ assert_eq!(expected, actual);
|
|
|
+ }
|
|
|
+
|
|
|
+ #[test]
|
|
|
+ fn destination_state_parse_service_no_state_err() {
|
|
|
+ let result = parse_str::<DestinationState>("service()");
|
|
|
+
|
|
|
+ assert!(result.is_err());
|
|
|
+ let err_str = result.err().unwrap().to_string();
|
|
|
+ assert_eq!(DestinationState::NO_STATE_ERR, err_str);
|
|
|
+ }
|
|
|
+
|
|
|
+ #[test]
|
|
|
+ fn destination_state_parse_service_multi_state_err() {
|
|
|
+ let result = parse_str::<DestinationState>("service(Left, Right)");
|
|
|
+
|
|
|
+ assert!(result.is_err());
|
|
|
+ let err_str = result.err().unwrap().to_string();
|
|
|
+ assert_eq!(DestinationState::MULTI_STATE_ERR, err_str);
|
|
|
+ }
|
|
|
+
|
|
|
+ #[test]
|
|
|
+ fn message_new() {
|
|
|
+ const EXPECTED_MSG_TYPE: &str = "Orders";
|
|
|
+ const EXPECTED_IS_REPLY: bool = true;
|
|
|
+ const EXPECTED_OWNED_STATES: [&str; 2] = ["First", "Second"];
|
|
|
+
|
|
|
+ let actual = Message::new(EXPECTED_MSG_TYPE, EXPECTED_IS_REPLY, EXPECTED_OWNED_STATES);
|
|
|
+
|
|
|
+ assert_eq!(actual.msg_type.as_ref(), EXPECTED_MSG_TYPE);
|
|
|
+ assert_eq!(actual.is_reply(), EXPECTED_IS_REPLY);
|
|
|
+ let idents = actual.owned_states.unwrap().idents;
|
|
|
+ assert_eq!(idents.len(), EXPECTED_OWNED_STATES.len());
|
|
|
+ assert_eq!(idents[0].as_ref(), EXPECTED_OWNED_STATES[0]);
|
|
|
+ assert_eq!(idents[1].as_ref(), EXPECTED_OWNED_STATES[1]);
|
|
|
+ }
|
|
|
+
|
|
|
+ #[test]
|
|
|
+ fn message_parse_regular() {
|
|
|
+ const EXPECTED_MSG: &str = "Write";
|
|
|
+ let expected = Message::new(EXPECTED_MSG, false, []);
|
|
|
+
|
|
|
+ let actual = parse_str::<Message>(EXPECTED_MSG).unwrap();
|
|
|
+
|
|
|
+ assert_eq!(expected, actual);
|
|
|
+ }
|
|
|
+
|
|
|
+ #[test]
|
|
|
+ fn message_parse_reply() {
|
|
|
+ const EXPECTED_MSG: &str = "ReadAttr";
|
|
|
+ let input = format!("{EXPECTED_MSG}::Reply");
|
|
|
+ let expected = Message::new(EXPECTED_MSG, true, []);
|
|
|
+
|
|
|
+ let actual = parse_str::<Message>(&input).unwrap();
|
|
|
+
|
|
|
+ assert_eq!(expected, actual);
|
|
|
+ }
|
|
|
+
|
|
|
+ #[test]
|
|
|
+ fn message_parse_reply_err() {
|
|
|
+ const EXPECTED_MSG: &str = "ReadAttr";
|
|
|
+ // Notice the intentional typo: "Rely" instead of "Reply".
|
|
|
+ let input = format!("{EXPECTED_MSG}::Rely");
|
|
|
+
|
|
|
+ let result = parse_str::<Message>(&input);
|
|
|
+
|
|
|
+ assert!(result.is_err());
|
|
|
+ let err_str = result.err().unwrap().to_string();
|
|
|
+ assert_eq!(MessageReplyPart::REPLY_ERR, err_str);
|
|
|
+ }
|
|
|
+
|
|
|
+ #[test]
|
|
|
+ fn message_parse_owned_states_not_reply() {
|
|
|
+ const EXPECTED_MSG: &str = "Open";
|
|
|
+ const OWNED_STATES: [&str; 2] = ["Handle0", "Handle1"];
|
|
|
+ let input = format!("{EXPECTED_MSG}[{}, {}]", OWNED_STATES[0], OWNED_STATES[1]);
|
|
|
+ let expected = Message::new(EXPECTED_MSG, false, OWNED_STATES);
|
|
|
+
|
|
|
+ let actual = parse_str::<Message>(&input).unwrap();
|
|
|
+
|
|
|
+ assert_eq!(expected, actual);
|
|
|
+ }
|
|
|
+
|
|
|
+ #[test]
|
|
|
+ fn message_parse_owned_states_is_reply() {
|
|
|
+ const EXPECTED_MSG: &str = "Open";
|
|
|
+ const OWNED_STATES: [&str; 2] = ["Handle0", "Handle1"];
|
|
|
+ let input = format!(
|
|
|
+ "{EXPECTED_MSG}::Reply[{}, {}]",
|
|
|
+ OWNED_STATES[0], OWNED_STATES[1]
|
|
|
+ );
|
|
|
+ let expected = Message::new(EXPECTED_MSG, true, OWNED_STATES);
|
|
|
+
|
|
|
+ let actual = parse_str::<Message>(&input).unwrap();
|
|
|
+
|
|
|
+ assert_eq!(expected, actual);
|
|
|
+ }
|
|
|
+}
|