Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Repeat arguments #42

Draft
wants to merge 14 commits into
base: develop
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/ISSUE_TEMPLATE/bug_report.md
Original file line number Diff line number Diff line change
Expand Up @@ -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**
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
11 changes: 10 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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<Vec<_>>` 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)
Expand Down
3 changes: 2 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
2 changes: 1 addition & 1 deletion book/tests/meta_constants_.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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";
212 changes: 199 additions & 13 deletions proc-macro-definitions/src/component_declaration/arguments.rs
Original file line number Diff line number Diff line change
@@ -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 {
Expand Down Expand Up @@ -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<Token![?]>,
pub flatten: Option<(Token![.], kw::flatten)>,
pub default: Option<(Token![=], Expr)>,
}
impl Argument {
pub fn validate(self) -> Result<ValidatedArgument> {
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<Token![?]>,
pub repeat_mode: RepeatMode,
pub optional: Option<Token![?]>,
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<Token![?]>,
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<Self> {
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<Self> {
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()
}
}

Expand All @@ -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<Self> {
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<Self> {
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<Self> {
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)> {
Expand Down
Loading