diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index 504bb24d..57c3599e 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -26,7 +26,7 @@ If applicable, add screenshots to help explain your problem. **please complete the following information:** -- `rustc --version`: [e.g. 1.46.0] +- `rustc --version`: [e.g. 1.50] - Crate version (if applicable): [e.g. 0.0.2] **Additional context** diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9b13e98a..ba6f46a1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -22,7 +22,7 @@ jobs: strategy: matrix: os: [macos, ubuntu, windows] - rust: [1.46.0, stable, beta, nightly] + rust: ['1.50', stable, beta, nightly] include: - os: ubuntu target: wasm32-unknown-unknown diff --git a/CHANGELOG.md b/CHANGELOG.md index b5074525..8639dac5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,7 @@ TODO: Date * **Breaking:** - * Increased minimum supported Rust version from 1.45.0 to 1.46.0. + * Increased minimum supported Rust version from 1.45.0 to 1.50. * Removed "rhizome" features (always enabled now) * Removed "styles" and "topiary" features. CSS scoping will be enabled through more general means. * Reworked generated component interface @@ -39,6 +39,15 @@ TODO: Date ``` * Optional arguments: `pattern?: Type` + * Repeat arguments: `item_name/pattern*: Type` or `item_name/pattern+: Type` + + The `item_name/` is optional. + + Repeat arguments work in combination with `?` and/or defaults. + + When using `*?`, you can flatten the resulting `Option>` into just `Vec<_>` by writing `*?.flatten`. + The default value defaults to `Vec::default()` in this case. + * Default parameters: `pattern: Type = default` * Conditional attributes: `."attribute-name"? = {Option<&'bump str>}` * Conditional parameters (like conditional attributes) diff --git a/Cargo.toml b/Cargo.toml index 161a90d0..00b6584c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -39,7 +39,8 @@ lignin = "0.0.5" #public lignin-schema = { version = "0.0.4", features = ["bumpalo-collections"] } rhizome = { version = "0.0.1", features = ["macros"] } # public static_assertions = "1.1.0" -typed-builder = "0.9.0" # semi-public +typed-builder = { git = "https://github.com/Tamschi/rust-typed-builder.git", branch = "patched/for-Asteracea/repeat-arguments" } # semi-public +vec1 = "1.6.0" #public [dev-dependencies] cargo-husky = "1.5.0" diff --git a/README.md b/README.md index 239a0bb6..1a1cb518 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ [![Crates.io](https://img.shields.io/crates/v/asteracea)](https://crates.io/crates/asteracea) [![Docs.rs](https://docs.rs/asteracea/badge.svg)](https://docs.rs/crates/asteracea) -![Rust 1.46.0](https://img.shields.io/static/v1?logo=Rust&label=&message=1.46.0&color=grey) +![Rust 1.50](https://img.shields.io/static/v1?logo=Rust&label=&message=1.50&color=grey) [![CI](https://github.com/Tamschi/Asteracea/workflows/CI/badge.svg?branch=develop)](https://github.com/Tamschi/Asteracea/actions?query=workflow%3ACI+branch%3Adevelop) ![Crates.io - License](https://img.shields.io/crates/l/asteracea/0.0.2) diff --git a/book/tests/meta_constants_.rs b/book/tests/meta_constants_.rs index bc4d1247..7cee5ba0 100644 --- a/book/tests/meta_constants_.rs +++ b/book/tests/meta_constants_.rs @@ -3,4 +3,4 @@ pub const BRANCH: &str = "develop"; pub const USER: &str = "Tamschi"; pub const REPOSITORY: &str = "Asteracea"; -pub const RUST_VERSION: &str = "1.46.0"; +pub const RUST_VERSION: &str = "1.50"; diff --git a/proc-macro-definitions/src/component_declaration/arguments.rs b/proc-macro-definitions/src/component_declaration/arguments.rs index 77a1ab02..f42d8f6d 100644 --- a/proc-macro-definitions/src/component_declaration/arguments.rs +++ b/proc-macro-definitions/src/component_declaration/arguments.rs @@ -1,13 +1,20 @@ -use quote::ToTokens; +use quote::{quote, quote_spanned, ToTokens}; use syn::{ parse::{Parse, ParseStream}, - Attribute, Expr, PatType, Result, Token, Visibility, + parse2, Attribute, Error, Expr, Ident, PatType, Result, Token, Type, Visibility, }; use unquote::unquote; +use wyz::Pipe; + +use crate::asteracea_ident; + +pub mod kw { + syn::custom_keyword!(flatten); +} pub struct ConstructorArgument { pub capture: Capture, - pub argument: Argument, + pub argument: ValidatedArgument, } pub enum Capture { @@ -40,59 +47,161 @@ impl ToTokens for Capture { } } -pub struct Argument { +struct Argument { + pub item_name: Option<(Ident, Token![/])>, + pub fn_arg: PatType, + pub repeat_mode: RepeatMode, + pub optional: Option, + pub flatten: Option<(Token![.], kw::flatten)>, + pub default: Option<(Token![=], Expr)>, +} +impl Argument { + pub fn validate(self) -> Result { + if self.item_name.is_some() && self.repeat_mode == RepeatMode::Single { + Err(Error::new( + self.optional + .map(|o| o.span) + .unwrap_or_else(|| self.fn_arg.colon_token.span), + "Expected repeat mode `*` or `+`, since an item name was specified", + )) + } else if self.flatten.is_some() + && !(matches!(self.repeat_mode, RepeatMode::AnyNumber(_)) && self.optional.is_some()) + { + let (stop, flatten) = self.flatten.unwrap(); + Err(Error::new_spanned( + quote!(#stop #flatten), + "`.flatten` is only available here following argument mode `*?`", + )) + } else { + let Argument { + item_name, + fn_arg, + repeat_mode, + optional, + flatten, + default, + } = self; + Ok(ValidatedArgument { + item_name, + fn_arg, + repeat_mode, + optional, + flatten, + default, + }) + } + } +} +pub struct ValidatedArgument { + pub item_name: Option<(Ident, Token![/])>, pub fn_arg: PatType, - pub question: Option, + pub repeat_mode: RepeatMode, + pub optional: Option, + pub flatten: Option<(Token![.], kw::flatten)>, pub default: Option<(Token![=], Expr)>, } +impl ValidatedArgument { + pub fn effective_type(&self) -> Type { + effective_type( + self.fn_arg.ty.as_ref().clone(), + self.repeat_mode, + self.optional, + &self.flatten, + ) + } +} + +pub fn effective_type( + ty: Type, + repeat_mode: RepeatMode, + optional: Option, + flatten: &Option<(Token![.], kw::flatten)>, +) -> Type { + match repeat_mode { + RepeatMode::Single => ty, + RepeatMode::AtLeastOne(token) => { + let asteracea = asteracea_ident(token.span); + parse2(quote_spanned!(token.span=> ::#asteracea::vec1::Vec1<#ty>)) + .expect("parameter helper definitions at-least-one type") + } + RepeatMode::AnyNumber(token) => parse2(quote_spanned!(token.span=> ::std::vec::Vec<#ty>)) + .expect("parameter helper definitions any-number type"), + } + .pipe(|ty| { + if flatten.is_some() { + assert!(repeat_mode != RepeatMode::Single); + assert!(optional.is_some()); + ty + } else if let Some(question) = optional { + parse2(quote_spanned!(question.span=> ::core::option::Option<#ty>)) + .expect("parameter helper definitions optional type") + } else { + ty + } + }) +} impl Parse for ConstructorArgument { fn parse(input: ParseStream) -> Result { unquote!(input, #do let Attributes::parse_outer => attrs #let capture + #do let ItemName::parse => item_name #let pat - #let question + #let repeat_mode + #let optional + #do let Flatten::parse => flatten #let colon_token #let ty #do let DefaultParameter::parse => default ); Ok(Self { argument: Argument { + item_name: item_name.into_inner(), fn_arg: PatType { attrs: attrs.into_inner(), pat, colon_token, ty, }, - question, + repeat_mode, + optional, + flatten: flatten.into_inner(), default: default.into_inner(), - }, + } + .validate()?, capture, }) } } -impl Parse for Argument { +impl Parse for ValidatedArgument { fn parse(input: ParseStream) -> Result { unquote!(input, #do let Attributes::parse_outer => attrs + #do let ItemName::parse => item_name #let pat - #let question + #let repeat_mode + #let optional + #do let Flatten::parse => flatten #let colon_token #let ty #do let DefaultParameter::parse => default ); - Ok(Self { + Argument { + item_name: item_name.into_inner(), fn_arg: PatType { attrs: attrs.into_inner(), pat, colon_token, ty, }, - question, + repeat_mode, + optional, + flatten: flatten.into_inner(), default: default.into_inner(), - }) + } + .validate() } } @@ -113,6 +222,83 @@ impl ToTokens for Attributes { } } +struct ItemName(Option<(Ident, Token![/])>); +impl ItemName { + pub fn into_inner(self) -> Option<(Ident, Token![/])> { + self.0 + } +} +impl Parse for ItemName { + fn parse(input: ParseStream) -> Result { + input + .peek2(Token![/]) + .then(|| Result::Ok((input.parse()?, input.parse()?))) + .transpose()? + .pipe(Self) + .pipe(Ok) + } +} +impl ToTokens for ItemName { + fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) { + if let Some(item_name) = &self.0 { + item_name.0.to_tokens(tokens); + item_name.1.to_tokens(tokens); + } + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum RepeatMode { + Single, + AtLeastOne(Token![+]), + AnyNumber(Token![*]), +} +impl Parse for RepeatMode { + fn parse(input: ParseStream) -> Result { + if let Some(plus) = input.parse().unwrap() { + Self::AtLeastOne(plus) + } else if let Some(asterisk) = input.parse().unwrap() { + Self::AnyNumber(asterisk) + } else { + Self::Single + } + .pipe(Ok) + } +} +impl ToTokens for RepeatMode { + fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) { + match self { + Self::Single => (), + Self::AtLeastOne(plus) => plus.to_tokens(tokens), + Self::AnyNumber(asterisk) => asterisk.to_tokens(tokens), + } + } +} + +struct Flatten(Option<(Token![.], kw::flatten)>); +impl Flatten { + fn into_inner(self) -> Option<(Token![.], kw::flatten)> { + self.0 + } +} +impl Parse for Flatten { + fn parse(input: ParseStream) -> Result { + input + .peek(Token![.]) // This is slightly imprecise and only works because (`.flatten`) is the only legal match here, but it results in better errors. + .then(|| Ok((input.parse()?, input.parse()?))) + .transpose() + .map(Self) + } +} +impl ToTokens for Flatten { + fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) { + if let Some((eq, expr)) = self.0.as_ref() { + eq.to_tokens(tokens); + expr.to_tokens(tokens); + } + } +} + struct DefaultParameter(Option<(Token![=], Expr)>); impl DefaultParameter { fn into_inner(self) -> Option<(Token![=], Expr)> { diff --git a/proc-macro-definitions/src/component_declaration/mod.rs b/proc-macro-definitions/src/component_declaration/mod.rs index a81c2044..27c72a30 100644 --- a/proc-macro-definitions/src/component_declaration/mod.rs +++ b/proc-macro-definitions/src/component_declaration/mod.rs @@ -1,5 +1,5 @@ use self::{ - arguments::{Argument, ConstructorArgument}, + arguments::{ConstructorArgument, ValidatedArgument}, parameter_helper_definitions::{CustomArgument, ParameterHelperDefintions}, }; use crate::{ @@ -21,8 +21,8 @@ use syn::{ punctuated::Punctuated, spanned::Spanned, token::Paren, - Attribute, Error, Generics, Ident, Item, Lifetime, Pat, PatIdent, PatType, ReturnType, Token, - Type, Visibility, WhereClause, WherePredicate, + Attribute, Error, Generics, Ident, Item, Lifetime, Pat, PatIdent, ReturnType, Token, Type, + Visibility, WhereClause, WherePredicate, }; use syn_mid::Block; use unquote::unquote; @@ -47,7 +47,7 @@ pub struct ComponentDeclaration { render_attributes: Vec, render_generics: Generics, render_paren: Paren, - render_args: Punctuated, + render_args: Punctuated, render_type: ReturnType, constructor_block: Option<(kw::new, kw::with, Block)>, body: Part, @@ -236,9 +236,10 @@ impl Parse for ComponentDeclaration { for constructor_argument in constructor_args.iter() { if let ConstructorArgument { capture: arguments::Capture::Yes(visibility), - argument: Argument { fn_arg, .. }, + argument, } = constructor_argument { + let fn_arg = &argument.fn_arg; let span = match visibility { Visibility::Inherited => fn_arg.span(), visibility => visibility.span(), @@ -246,9 +247,8 @@ impl Parse for ComponentDeclaration { let attrs = &fn_arg.attrs; let pat = &fn_arg.pat; let arg = { - let PatType { - colon_token, ty, .. - } = fn_arg; + let colon_token = fn_arg.colon_token; + let ty = argument.effective_type(); quote!(#pat#colon_token #ty) }; call2_strict( @@ -350,11 +350,14 @@ impl ComponentDeclaration { .iter() .map(|arg| Ok(CustomArgument { attrs: arg.argument.fn_arg.attrs.as_slice(), + item_name: &arg.argument.item_name, ident: match &*arg.argument.fn_arg.pat { Pat::Ident(PatIdent{ ident, .. }) => ident, other => {return Err(Error::new_spanned(other, "Component parameters must be named. Bind this pattern to an identifier by prefixing it with `identifier @`."))} }, - optional: arg.argument.question, + repeat_mode: arg.argument.repeat_mode, + optional: arg.argument.optional, + flatten: &arg.argument.flatten, ty: &*arg.argument.fn_arg.ty, default: &arg.argument.default, })) @@ -364,11 +367,14 @@ impl ComponentDeclaration { .iter() .map(|arg| Ok(CustomArgument { attrs: arg.fn_arg.attrs.as_slice(), + item_name: &arg.item_name, ident: match &*arg.fn_arg.pat { Pat::Ident(PatIdent{ ident, .. }) => ident, other => {return Err(Error::new_spanned(other, "Component parameters must be named. Bind this pattern to an identifier by prefixing it with `identifier @`."))} }, - optional: arg.question, + repeat_mode: arg.repeat_mode, + optional: arg.optional, + flatten: &arg.flatten, ty: &*arg.fn_arg.ty, default: &arg.default, })) @@ -381,6 +387,7 @@ impl ComponentDeclaration { for_function_args: new_args_generic_args, on_builder_function: new_args_builder_generics, for_builder_function_return: new_args_builder_generic_args, + has_impl_generics: _new_has_impl_generics, } = ParameterHelperDefintions::new( &component_generics, &parse2(quote_spanned!(constructor_paren.span=> <'a: '_>)).unwrap(), @@ -396,6 +403,7 @@ impl ComponentDeclaration { for_function_args: render_args_generic_args, on_builder_function: render_args_builder_generics, for_builder_function_return: render_args_builder_generic_args, + has_impl_generics: _render_has_impl_generics, } = ParameterHelperDefintions::new( &component_generics, &parse2(quote_spanned!(render_paren.span=> <'a, 'bump: '_>)).unwrap(), @@ -404,6 +412,15 @@ impl ComponentDeclaration { &render_lifetime, ); + // FIXME: + // let constructor_allow_non_camel_case_types = new_has_impl_generics.then(|| { + // quote!(#[allow(non_camel_case_types)]) + // }); + + // let render_allow_non_camel_case_types = render_has_impl_generics.then(|| { + // quote!(#[allow(non_camel_case_types)]) + // }); + let constructor_args_field_patterns = constructor_args .into_iter() .map(|arg| match *arg.argument.fn_arg.pat { @@ -474,11 +491,13 @@ impl ComponentDeclaration { //TODO: Doc comment referring to associated type. #[derive(#asteracea::__Asteracea__implementation_details::typed_builder::TypedBuilder)] #[builder(doc)] + // FIXME: #constructor_allow_non_camel_case_types #visibility struct #new_args_name#new_args_generics #new_args_body //TODO: Doc comment referring to associated type. #[derive(#asteracea::__Asteracea__implementation_details::typed_builder::TypedBuilder)] #[builder(doc)] + // FIXME: #render_allow_non_camel_case_types #visibility struct #render_args_name#render_args_generics #render_args_body #(#struct_definition)* diff --git a/proc-macro-definitions/src/component_declaration/parameter_helper_definitions.rs b/proc-macro-definitions/src/component_declaration/parameter_helper_definitions.rs index 329eca36..568284f1 100644 --- a/proc-macro-definitions/src/component_declaration/parameter_helper_definitions.rs +++ b/proc-macro-definitions/src/component_declaration/parameter_helper_definitions.rs @@ -1,10 +1,10 @@ -use crate::syn_ext::*; +use crate::{asteracea_ident, syn_ext::*}; use call2_for_syn::call2_strict; use proc_macro2::Span; use quote::quote_spanned; use std::{iter, mem}; use syn::{ - parse2, parse_quote, + parse_quote, punctuated::Punctuated, spanned::Spanned as _, token::{Brace, Paren}, @@ -16,6 +16,8 @@ use syn::{ }; use wyz::Tap as _; +use super::arguments::{self, RepeatMode}; + fn transform_lifetime( existing_lifetime: &mut Lifetime, lifetime: &Lifetime, @@ -245,16 +247,30 @@ pub struct ParameterHelperDefintions { pub for_function_args: AngleBracketedGenericArguments, pub on_builder_function: Generics, pub for_builder_function_return: AngleBracketedGenericArguments, + pub has_impl_generics: bool, } -#[derive(Debug, Copy, Clone)] +#[derive(Debug, Clone, Copy)] pub struct CustomArgument<'a> { pub attrs: &'a [Attribute], + pub item_name: &'a Option<(Ident, Token![/])>, pub ident: &'a Ident, + pub repeat_mode: RepeatMode, pub optional: Option, + pub flatten: &'a Option<(Token![.], arguments::kw::flatten)>, pub ty: &'a Type, pub default: &'a Option<(Token![=], Expr)>, } +impl<'a> CustomArgument<'a> { + fn effective_type(&self) -> Type { + arguments::effective_type( + self.ty.clone(), + self.repeat_mode, + self.optional, + self.flatten, + ) + } +} #[allow(clippy::needless_collect)] // Inaccurate lint, apparently. impl ParameterHelperDefintions { @@ -269,15 +285,8 @@ impl ParameterHelperDefintions { let argument_types = custom_arguments .iter() .map(|arg| { - let ty = arg - .ty - .clone() - .tap_mut(|ty| transform_type(ty, transient_lifetime, &mut impl_generics, true)); - if let Some(question) = arg.optional { - parse2(quote_spanned!(question.span()=> Option<#ty>)).unwrap() - } else { - ty - } + arg.effective_type() + .tap_mut(|ty| transform_type(ty, transient_lifetime, &mut impl_generics, true)) }) .collect::>(); @@ -372,8 +381,11 @@ impl ParameterHelperDefintions { |( &CustomArgument { attrs, + item_name, ident, + repeat_mode, optional, + flatten, ty: _, default, }, @@ -384,29 +396,54 @@ impl ParameterHelperDefintions { //TODO?: Better optionals. Something like `ident?: Type` to express `ident: Option = None` but with the Option stripped for the setter? // Of course the counter-argument here is that I'd like to transition to native Rust named and default parameters eventually, // and it's unlikely that the language will get an option-stripping workaround that doesn't interfere with generic type inference. - attrs: iter::once( - call2_strict( - match (optional, default) { - (None, None) => { - quote_spanned!(ident.span()=> #[builder()]) - } - (None, Some((eq, default))) => { - quote_spanned!(eq.span=> #[builder(default = #default)]) + attrs: iter::once({ + let default = match (optional, default) { + (None, None) => { + quote_spanned!(ident.span()=>) + } + (None, Some((eq, default))) => { + quote_spanned!(eq.span=> default #eq #default,) + } + (Some(optional), None) => { + quote_spanned!(optional.span=> default,) + } + (Some(optional), Some((eq, default))) => { + if let Some((_, flatten)) = flatten { + quote_spanned!(flatten.span=> default #eq #default) + } else { + let some = quote_spanned!(optional.span=> ::core::option::Option::Some(#default)); + quote_spanned!(eq.span=> default #eq #some,) } - (Some(optional), None) => { - quote_spanned!(optional.span()=> #[builder(setter(strip_option), default)]) - } - (Some(optional), Some((eq, default))) => { - quote_spanned! {optional.span.join(eq.span).unwrap_or(optional.span)=> - #[builder(setter(strip_option), default = Some(#default))] - } - } - }, + } + }; + let strip_option = optional.and_then(|optional| { + if flatten.is_some() { + None + } else { + Some(quote_spanned!(optional.span=> strip_option,)) + } + }); + let item_name = item_name.as_ref().map(|(item_name, slash)| { + assert!(repeat_mode != RepeatMode::Single); + quote_spanned!(slash.span=> item_name = #item_name,) + }); + let extend = match repeat_mode { + RepeatMode::Single => {None} + RepeatMode::AtLeastOne(token) => { + let asteracea = asteracea_ident(token.span); + Some(quote_spanned!(token.span=> extend(from_first = |first| ::#asteracea::vec1::vec1![first], #item_name),)) + } + RepeatMode::AnyNumber(token) => { + Some(quote_spanned!(token.span=> extend(from_first, from_iter, #item_name),)) + } + }; + call2_strict( + quote_spanned!(ident.span()=> #[builder(#default setter(#strip_option #extend))]), Attribute::parse_outer, ) .unwrap() - .unwrap(), - ) + .unwrap() + }) .flatten() .chain(attrs.iter().cloned()) .collect(), @@ -499,6 +536,7 @@ impl ParameterHelperDefintions { }, gt_token: ]>::default(), }, + has_impl_generics: !impl_generics.is_empty(), } } } diff --git a/rustfmt.toml b/rustfmt.toml index 3f309b78..cc9988cb 100644 --- a/rustfmt.toml +++ b/rustfmt.toml @@ -4,4 +4,4 @@ use_field_init_shorthand = true use_try_shorthand = true unstable_features = true -merge_imports = true +imports_granularity = "Crate" diff --git a/src/lib.rs b/src/lib.rs index 15d8a88a..7dd39707 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -26,6 +26,7 @@ pub use asteracea_proc_macro_definitions::{bump_format, component, fragment, trace_escalations}; pub use lignin; pub use rhizome; +pub use vec1; #[cfg(doctest)] pub mod readme { diff --git a/tests/meta_constants_.rs b/tests/meta_constants_.rs index bc4d1247..7cee5ba0 100644 --- a/tests/meta_constants_.rs +++ b/tests/meta_constants_.rs @@ -3,4 +3,4 @@ pub const BRANCH: &str = "develop"; pub const USER: &str = "Tamschi"; pub const REPOSITORY: &str = "Asteracea"; -pub const RUST_VERSION: &str = "1.46.0"; +pub const RUST_VERSION: &str = "1.50"; diff --git a/tests/multi_constructor_args.rs b/tests/multi_constructor_args.rs new file mode 100644 index 00000000..c07ba819 --- /dev/null +++ b/tests/multi_constructor_args.rs @@ -0,0 +1,82 @@ +use rhizome::Node; +use std::iter; +use vec1::{vec1, Vec1}; + +asteracea::component! { + Any( + pub flattened*?.flatten: usize, + pub sometimes*?: usize, + pub some/many+?: usize, + pub one/any*: usize, + pub always+: usize, + )() + + // Just to show the types: + new with { + let _: &Vec = &flattened; + let _: &Option> = &sometimes; + let _: &Option> = &many; + let _: &Vec = &any; + let _: &Vec1 = &always; + } + + [] +} + +#[test] +fn check_values() { + asteracea::component! { + Outer()() + + <*Any pub any + *flattened_item = {0} + *sometimes = {iter::once(1)} + *sometimes_item = {2} + *some = {3} + *many = {iter::once(4)} + *any = {iter::once(5)} + *one = {6} + *always_item = {7} + *always = {iter::once(8)} + > + } + + let any = Outer::new( + &Node::new_for::<()>().into_arc(), + Outer::new_args_builder().build(), + ) + .unwrap() + .any; + + assert_eq!(any.flattened, vec![0]); + assert_eq!(any.sometimes, Some(vec![1, 2])); + assert_eq!(any.many, Some(vec1![3, 4])); + assert_eq!(any.any, vec![5, 6]); + assert_eq!(any.always, vec1![7, 8]); +} + +#[test] +fn can_omit_optional() { + asteracea::component! { + #[allow(dead_code)] + Outer()() + + <*Any pub any + *one = {6} + *always_item = {7} + > + }; + + let any = Outer::new( + &Node::new_for::<()>().into_arc(), + Outer::new_args_builder().build(), + ) + .unwrap() + .any; + + assert_eq!(any.flattened, vec![]); + assert_eq!(any.sometimes, None); + assert_eq!(any.many, None); + assert_eq!(any.any, vec![6]); + assert_eq!(any.always, vec1![7]); +} diff --git a/tests/unincluded_meta_workflows_ci.rs b/tests/unincluded_meta_workflows_ci.rs index 8d4f28a2..1c1d3b98 100644 --- a/tests/unincluded_meta_workflows_ci.rs +++ b/tests/unincluded_meta_workflows_ci.rs @@ -8,6 +8,6 @@ use constants::*; fn rust_version() { version_sync::assert_contains_regex!( ".github/workflows/ci.yml", - &format!(r"^\s*rust: \[{},", RUST_VERSION) + &format!(r"^\s*rust: \['{}',", RUST_VERSION) ); }