Created
May 16, 2022 06:39
-
-
Save kurtlawrence/a653c1d215ae8ce5041f43d684423991 to your computer and use it in GitHub Desktop.
`fit_kserd!` macro implementation used in daedalus
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// # Fitting kserd macro. | |
// | |
// The macro logic is hidden from users to avoid having the long list of pattern matching exposed. | |
// It also helps control the pattern state. | |
// | |
// Fitting is split into four aspects: | |
// 1. Parsing | |
// 2. Flattening | |
// 3. Documentation | |
// 4. Code generation | |
// | |
// ## Parsing | |
// Parsing is the first phase. The intention is to take the macro input tokens and transform them | |
// into an intermediary representation, validating the input along the way. | |
// The intermediary format is designed for a pull based reading system. At the highest level are | |
// the _cases_, these are a stream of pattern (or _case_) tokens linked to a body expression. | |
// | |
// A _case_ format consists of a tree of tokens, but in a streamed, depth-first, format, with tags | |
// denoting starts and ends of nesting. For example, take the case pattern: | |
// | |
// ``` | |
// Cntr { | |
// field1 = Bool(x), | |
// field2 = Cntr { | |
// field3 = Num(y) | |
// } | |
// field4 = Str(z) | |
// } | |
// ``` | |
// | |
// The goal of this pattern is to have 3 local variables, x, y, z, which would be a bool, f64, and | |
// &str respectively. That is if the Kserd matches that data form. It was found that the most simple | |
// way of making the codegen work was to convert the input into a list of _paths_ from the root | |
// Kserd to the local variable assignment. To achieve this the intermediary format constructs a | |
// stream that flags nesting (indentation and new lines are added for clarity): | |
// ``` | |
// @cntr_start | |
// @req(field1) "Bool" bool @local x | |
// @req(field2) @cntr_start | |
// @req(field3) "Num" float @local y | |
// @cntr_end | |
// @req(field4) "Str" str @local z | |
// @cntr_end | |
// ``` | |
// | |
// When read in a stream the @cntr tags are used to define the start and end of the nesting. Local | |
// assigments carry some metadata, like the local identifier, the function identifier used to get | |
// that function, and the id literal (this is used for error handling). | |
// | |
// Parsing also handles fitting types (denoted with the `field: Type` syntax), and optional fields | |
// (denoted with `[field]` syntax). Optional fields change the @req to @opt, while fitting fields | |
// use a specific `@fit(Type)` syntax. | |
// | |
// It is important to note that the parsed stream is still in a _nested_ format. | |
// | |
// ## Documentation | |
// After the parsing phase, documenting and flattening are forked. This is done because documenting | |
// works better with a nested structure, while the flattening alters the stream into what the | |
// codegen phase can work with. | |
// | |
// Documenting is fairly simple, it consumes the parsed token stream and concatenates an expression | |
// together. This expression (which will be of literals) is stored until written to a `#[doc = | |
// expr]` attribute on the trait impl. | |
// | |
// ## Flattening | |
// Flattening takes the parsed stream of tokens and _flattens_ the nested structure into one which | |
// has a **defined path from the root Kserd** for each local assignment. | |
// | |
// While simple in scope the implementation is complex. In a high level view a prefix of tokens is | |
// maintained, acting like a stack where path components are popped when consumed. However, as the | |
// macro pattern matching will only match tokens that _precede_ a tt stream (otherwise it would be | |
// ambiguous), the prefix is **maintained in reverse order**. This also makes the whole case | |
// matching stream be constructed in a reverse manner and has to be reversed before being sent to | |
// the codegen. | |
// | |
// > Note that it is only the _case_ that is reversed, so this is operating inside each case and | |
// > the body expression is store alongside each pattern. | |
// | |
// These implementation complexities make flattening the most complex phase and care must be taken | |
// in this area if added syntax is wanted. | |
// | |
// ## Code generation | |
// Codegen imagines the cases token stream structure to be in a flatten format, where each local | |
// assignment carries a path before it from the root Kserd. | |
// | |
// The algorithm used will consume case matching and slowly build up the code. Nested structures | |
// use temporary variables to get to the final getting function from a Kserd. The code is built in | |
// a way that handles optionality in a general manner. It makes the code messier but saves on | |
// duplicating macro code which would be far more maintenance. The concept is that even without | |
// optional fields, the Kserd is lumped into a Option, and the next get function is then applied on | |
// that. Optionality is transitive, if a field is optional, all further matches are optional as | |
// well. It is important to note that the fields **before** the optional field are **not** optional | |
// and must return an error. To handle this there is a variable identifier that is carried through, | |
// starting as required and flipping to optional on the first optional field that is encountered. | |
// | |
// Code generation also had to handle the multiple cases. The way the fitting is intended to work | |
// is that there might be multiple different patterns of a Kserd that could match. Each of these | |
// patterns (case) should have fast fail attributes (making use of the ? operator), but chaining | |
// each one together was difficult. Instead a bespoke function is defined for each case. There is a | |
// limit of 32 of these cases that can be defined which should be more than enough. Each function | |
// has a signature that handles the Result, and then chaining these functions together to carry the | |
// failure messages is quite simple. | |
#[doc(hidden)] | |
#[macro_export] | |
macro_rules! fit_kserd_internal { // INTEREST -- using an internal call and making it doc hidden | |
// #### PARSE ############################################################## | |
// No more cases: Finished parsing. | |
(@parse[$doc:expr, $($args:tt)*] | |
{} | |
() | |
() | |
($($cases:tt)*) | |
) => { | |
// compile_error!(concat!("Finished Parsing:\n", stringify!($($cases)*))); // turn on to | |
// get some debugging | |
$crate::fit_kserd_internal! { @flatten[$crate::fit_kserd_internal!(@doc-init, $doc, $($cases)*), $($args)*] $($cases)* } | |
}; | |
// No more input but last case hadn't been finished with expr | |
(@parse[$($args:tt)*] | |
{ $($casedocs:literal)* } | |
() | |
($($case:tt)*) | |
($($cases:tt)*) | |
) => { | |
compile_error!(concat!("expecting an expression associated with: ", stringify!($($case)*))); | |
}; | |
// Recognise a case documentation. | |
(@parse[$($args:tt)*] | |
{ $($casedocs:literal)* } | |
(#[doc = $doc:literal] $($tail:tt)*) | |
() | |
($($cases:tt)*) | |
) => { | |
$crate::fit_kserd_internal! { | |
@parse[$($args)*] | |
{ $($casedocs)* $doc } | |
($($tail)*) | |
() | |
($($cases)*) | |
} | |
}; | |
// ---- Variant(ident) pattern --------------------------------------------- | |
// Match a Bool variant | |
(@parse[$($args:tt)*] | |
{ $($casedocs:literal)* } | |
(Bool($local:ident) $($tail:tt)*) | |
($($case:tt)*) | |
($($cases:tt)*) | |
)=> { | |
$crate::fit_kserd_internal! { | |
@parse[$($args)*] | |
{ $($casedocs)* } | |
($($tail)*) | |
($($case)* "Bool" bool @local $local) | |
($($cases)*) | |
} | |
}; | |
// Match a Num variant | |
(@parse[$($args:tt)*] | |
{ $($casedocs:literal)* } | |
(Num($local:ident) $($tail:tt)*) | |
($($case:tt)*) | |
($($cases:tt)*) | |
)=> { | |
$crate::fit_kserd_internal! { | |
@parse[$($args)*] | |
{ $($casedocs)* } | |
($($tail)*) | |
($($case)* "Num" float @local $local) | |
($($cases)*) | |
} | |
}; | |
// Match a Str variant | |
(@parse[$($args:tt)*] | |
{ $($casedocs:literal)* } | |
(Str($local:ident) $($tail:tt)*) | |
($($case:tt)*) | |
($($cases:tt)*) | |
)=> { | |
$crate::fit_kserd_internal! { | |
@parse[$($args)*] | |
{ $($casedocs)* } | |
($($tail)*) | |
($($case)* "Str" str @local $local) | |
($($cases)*) | |
} | |
}; | |
// Match a Str variant | |
(@parse[$($args:tt)*] | |
{ $($casedocs:literal)* } | |
(Barr($local:ident) $($tail:tt)*) | |
($($case:tt)*) | |
($($cases:tt)*) | |
)=> { | |
$crate::fit_kserd_internal! { | |
@parse[$($args)*] | |
{ $($casedocs)* } | |
($($tail)*) | |
($($case)* "Barr" barr @local $local) | |
($($cases)*) | |
} | |
}; | |
// Match a Tuple variant | |
(@parse[$($args:tt)*] | |
{ $($casedocs:literal)* } | |
(Tuple($local:ident) $($tail:tt)*) | |
($($case:tt)*) | |
($($cases:tt)*) | |
)=> { | |
$crate::fit_kserd_internal! { | |
@parse[$($args)*] | |
{ $($casedocs)* } | |
($($tail)*) | |
($($case)* "Tuple" tuple @local $local) | |
($($cases)*) | |
} | |
}; | |
// Match a Cntr variant | |
(@parse[$($args:tt)*] | |
{ $($casedocs:literal)* } | |
(Cntr($local:ident) $($tail:tt)*) | |
($($case:tt)*) | |
($($cases:tt)*) | |
)=> { | |
$crate::fit_kserd_internal! { | |
@parse[$($args)*] | |
{ $($casedocs)* } | |
($($tail)*) | |
($($case)* "Cntr" cntr @local $local) | |
($($cases)*) | |
} | |
}; | |
// Match a Seq variant | |
(@parse[$($args:tt)*] | |
{ $($casedocs:literal)* } | |
(Seq($local:ident) $($tail:tt)*) | |
($($case:tt)*) | |
($($cases:tt)*) | |
)=> { | |
$crate::fit_kserd_internal! { | |
@parse[$($args)*] | |
{ $($casedocs)* } | |
($($tail)*) | |
($($case)* "Seq" seq @local $local) | |
($($cases)*) | |
} | |
}; | |
// Match a Tuple|Seq variant | |
(@parse[$($args:tt)*] | |
{ $($casedocs:literal)* } | |
(Tuple|Seq($local:ident) $($tail:tt)*) | |
($($case:tt)*) | |
($($cases:tt)*) | |
)=> { | |
$crate::fit_kserd_internal! { | |
@parse[$($args)*] | |
{ $($casedocs)* } | |
($($tail)*) | |
($($case)* "Tuple|Seq" tuple_or_seq @local $local) | |
($($cases)*) | |
} | |
}; | |
// Match a Map variant | |
(@parse[$($args:tt)*] | |
{ $($casedocs:literal)* } | |
(Map($local:ident) $($tail:tt)*) | |
($($case:tt)*) | |
($($cases:tt)*) | |
)=> { | |
$crate::fit_kserd_internal! { | |
@parse[$($args)*] | |
{ $($casedocs)* } | |
($($tail)*) | |
($($case)* "Map" map @local $local) | |
($($cases)*) | |
} | |
}; | |
// Match a Kserd variant | |
(@parse[$($args:tt)*] | |
{ $($casedocs:literal)* } | |
(Kserd($local:ident) $($tail:tt)*) | |
($($case:tt)*) | |
($($cases:tt)*) | |
)=> { | |
$crate::fit_kserd_internal! { | |
@parse[$($args)*] | |
{ $($casedocs)* } | |
($($tail)*) | |
($($case)* "Kserd" SPECIAL_KSERD @local $local) | |
($($cases)*) | |
} | |
}; | |
// It looks to match a `Variant(ident)` pattern but hasn't matched, print error | |
(@parse[$($args:tt)*] | |
{ $($casedocs:literal)* } | |
($variant:ident($local:ident) $($tail:tt)*) | |
($($case:tt)*) | |
($($cases:tt)*) | |
)=> { | |
compile_error!(concat!("unknown variant '", stringify!($variant), "'.")); | |
}; | |
// It looks to match a `Variant|Variant(ident)` pattern but hasn't matched, print error | |
(@parse[$($args:tt)*] | |
{ $($casedocs:literal)* } | |
($variant:ident|$variant2:ident($local:ident) $($tail:tt)*) | |
($($case:tt)*) | |
($($cases:tt)*) | |
)=> { | |
compile_error!(concat!("unknown variant '", stringify!($variant), "|", stringify!($variant2), "'. Did you mean Tuple|Seq?")); | |
}; | |
// ---- Variant { } pattern ------------------------------------------------ | |
// Match a nested Cntr variant. | |
(@parse[$($args:tt)*] | |
{ $($casedocs:literal)* } | |
(Cntr { $($nested:tt)+ } $($tail:tt)*) | |
($($case:tt)*) | |
($($cases:tt)*) | |
)=> { | |
$crate::fit_kserd_internal! { | |
@parse[$($args)*] | |
{ $($casedocs)* } | |
($($nested)* @cntr_end $($tail)*) | |
($($case)* @cntr_start) | |
($($cases)*) | |
} | |
}; | |
// It looks to match a `Variant { }` pattern but hasn't matched, print error | |
(@parse[$($args:tt)*] | |
{ $($casedocs:literal)* } | |
($variant:ident { $($nested:tt)+ } $($tail:tt)*) | |
($($case:tt)*) | |
($($cases:tt)*) | |
)=> { | |
compile_error!(concat!("unknown nested variant '", stringify!($variant), "'.")); | |
}; | |
// Match an _optional_ `[ident] = ` pattern | |
(@parse[$($args:tt)*] | |
{ $($casedocs:literal)* } | |
([$field:ident] = $($tail:tt)*) | |
($($case:tt)*) | |
($($cases:tt)*) | |
)=> { | |
$crate::fit_kserd_internal! { | |
@parse[$($args)*] | |
{ $($casedocs)* } | |
($($tail)*) | |
($($case)* @opt($field)) | |
($($cases)*) | |
} | |
}; | |
// Match an `ident = ` pattern | |
(@parse[$($args:tt)*] | |
{ $($casedocs:literal)* } | |
($field:ident = $($tail:tt)*) | |
($($case:tt)*) | |
($($cases:tt)*) | |
)=> { | |
$crate::fit_kserd_internal! { | |
@parse[$($args)*] | |
{ $($casedocs)* } | |
($($tail)*) | |
($($case)* @req($field)) | |
($($cases)*) | |
} | |
}; | |
// Match an _optional_ `ident: FitTy` pattern | |
(@parse[$($args:tt)*] | |
{ $($casedocs:literal)* } | |
([$field:ident]: $fit:ty, $($tail:tt)*) | |
($($case:tt)*) | |
($($cases:tt)*) | |
)=> { | |
$crate::fit_kserd_internal! { | |
@parse[$($args)*] | |
{ $($casedocs)* } | |
($($tail)*) | |
($($case)* @opt($field) @fit($fit)) | |
($($cases)*) | |
} | |
}; | |
// Match an `ident: FitTy` pattern | |
(@parse[$($args:tt)*] | |
{ $($casedocs:literal)* } | |
($field:ident: $fit:ty, $($tail:tt)*) | |
($($case:tt)*) | |
($($cases:tt)*) | |
)=> { | |
$crate::fit_kserd_internal! { | |
@parse[$($args)*] | |
{ $($casedocs)* } | |
($($tail)*) | |
($($case)* @req($field) @fit($fit)) | |
($($cases)*) | |
} | |
}; | |
// Print error if used a : but not with trailing comma | |
(@parse[$($args:tt)*] | |
{ $($casedocs:literal)* } | |
([$field:ident]: $e:tt $($tail:tt)*) | |
($($case:tt)*) | |
($($cases:tt)*) | |
)=> { | |
compile_error!(concat!("Expecting a trailing comma after a `Fit` type ascription> [", stringify!($field), "]: ", stringify!($e))); | |
}; | |
// Print error if used a : but not with trailing comma | |
(@parse[$($args:tt)*] | |
{ $($casedocs:literal)* } | |
($field:ident: $e:tt $($tail:tt)*) | |
($($case:tt)*) | |
($($cases:tt)*) | |
)=> { | |
compile_error!(concat!("Expecting a trailing comma after a `Fit` type ascription> ", stringify!($field), ": ", stringify!($e))); | |
}; | |
// End a Cntr | |
(@parse[$($args:tt)*] | |
{ $($casedocs:literal)* } | |
(@cntr_end $($tail:tt)*) | |
($($case:tt)*) | |
($($cases:tt)*) | |
)=> { | |
$crate::fit_kserd_internal! { | |
@parse[$($args)*] | |
{ $($casedocs)* } | |
($($tail)*) | |
($($case)* @cntr_end) | |
($($cases)*) | |
} | |
}; | |
// Skip trailing commas in fields | |
(@parse[$($args:tt)*] | |
{ $($casedocs:literal)* } | |
(, $($tail:tt)*) | |
($($case:tt)*) | |
($($cases:tt)*) | |
)=> { | |
$crate::fit_kserd_internal! { | |
@parse[$($args)*] | |
{ $($casedocs)* } | |
($($tail)*) | |
($($case)*) | |
($($cases)*) | |
} | |
}; | |
// ---- Body --------------------------------------------------------------- | |
// Case separated by comma | |
(@parse[$($args:tt)*] | |
{ $($casedocs:literal)* } | |
(=> $body:expr, $($tail:tt)*) | |
($($case:tt)*) | |
($($cases:tt)*) | |
) => { | |
$crate::fit_kserd_internal! { | |
@parse[$($args)*] | |
{} | |
($($tail)*) | |
() | |
($($cases)* ({ $($casedocs)* | $body } $($case)*),) | |
} | |
}; | |
// Print error if a semi colon was used | |
(@parse[$($args:tt)*] | |
{ $($casedocs:literal)* } | |
(=> $body:expr; $($tail:tt)*) | |
($($case:tt)*) | |
($($cases:tt)*) | |
) => { | |
compile_error!("found a semi-colon at the end of a body, did you mean to use a comma?"); | |
}; | |
// Don't require comma after proper block | |
(@parse[$($args:tt)*] | |
{ $($casedocs:literal)* } | |
(=> $body:block $($tail:tt)*) | |
($($case:tt)*) | |
($($cases:tt)*) | |
) => { | |
$crate::fit_kserd_internal! { | |
@parse[$($args)*] | |
{} | |
($($tail)*) | |
() | |
($($cases)* ({ $($casedocs)* | $body } $($case)*),) | |
} | |
}; | |
// No more tail, finish it off | |
(@parse[$($args:tt)*] | |
{ $($casedocs:literal)* } | |
(=> $body:expr) | |
($($case:tt)*) | |
($($cases:tt)*) | |
) => { | |
$crate::fit_kserd_internal! { | |
@parse[$($args)*] | |
{} | |
() | |
() | |
($($cases)* ({ $($casedocs)* | $body } $($case)*),) | |
} | |
}; | |
// #### FLATTEN ############################################################ | |
// Flattening uses a prefix stack. Fields are _always_ pushed onto the stack. | |
// They are popped when the field is 'consumed' is some way, that can be a local assigner, the | |
// cntr end, or the fitting ascription. | |
// ---- Case movements ----------------------------------------------------- | |
// No more cases to flatten. | |
(@flatten_next[$($args:tt)*] | |
($($completed:tt)*) | |
() | |
) => { | |
$crate::fit_kserd_internal! { @gen-init[$($args)*] $($completed)* } | |
}; | |
// Matched a case, start next case cycle. | |
(@flatten_next[$($args:tt)*] | |
($($completed:tt)*) | |
(({ $($body:tt)* } $($case:tt)*), $($tail:tt)*) | |
) => { | |
$crate::fit_kserd_internal! { | |
@flatten_case[$($args)*] [ { $($body)* } $($completed)* ] | |
() | |
($($case)* $($tail)*) | |
() | |
} | |
}; | |
// Flattening entry point. | |
(@flatten[$($args:tt)*] $($cases:tt)*) => { | |
$crate::fit_kserd_internal! { | |
@flatten_next[$($args)*] | |
() | |
($($cases)*) | |
} | |
}; | |
// ---- Case building ------------------------------------------------------ | |
// Recognised that the next sequence is the next case. Run the reversing logic. | |
(@flatten_case[$($args:tt)*] [ $($store:tt)* ] | |
($($case:tt)*) | |
($( ({ $($body:tt)* } $($tmp:tt)*), )*) | |
($($prefix:tt)*) | |
) => { | |
$crate::fit_kserd_internal! { | |
@flatten_rev[$($args)*] [ $($store)* ] [ $( ({ $($body)* } $($tmp)*), )* ] | |
($($case)*) | |
() | |
} | |
}; | |
// Recognise the beginning of a cntr. | |
(@flatten_case[$($args:tt)*] [ $($store:tt)* ] | |
($($case:tt)*) | |
(@cntr_start $($tail:tt)*) | |
($($prefix:tt)*) | |
) => { | |
$crate::fit_kserd_internal! { | |
@flatten_case[$($args)*] [ $($store)* ] | |
($($case)*) | |
($($tail)*) | |
(cntr@ $($prefix)*) | |
} | |
}; | |
// Recognise the end of a cntr. Consumes the last field assigner as well. | |
(@flatten_case[$($args:tt)*] [ $($store:tt)* ] | |
($($case:tt)*) | |
(@cntr_end $($tail:tt)*) | |
(cntr@ ($field:ident)$f:ident@ $($prefix:tt)*) | |
) => { | |
$crate::fit_kserd_internal! { | |
@flatten_case[$($args)*] [ $($store)* ] | |
($($case)*) | |
($($tail)*) | |
($($prefix)*) | |
} | |
}; | |
// Recognise the end of a cntr. The prefix just has a cntr (this is the _last_ cntr_end) | |
(@flatten_case[$($args:tt)*] [ $($store:tt)* ] | |
($($case:tt)*) | |
(@cntr_end $($tail:tt)*) | |
(cntr@ $($prefix:tt)*) | |
) => { | |
$crate::fit_kserd_internal! { | |
@flatten_case[$($args)*] [ $($store)* ] | |
($($case)*) | |
($($tail)*) | |
($($prefix)*) | |
} | |
}; | |
// Add the fitting to a field. | |
(@flatten_case[$($args:tt)*] [ $($store:tt)* ] | |
($($case:tt)*) | |
(@fit($fit:ty) $($tail:tt)*) | |
(($field:ident)$f:ident@ $($prefix:tt)*) | |
) => { | |
$crate::fit_kserd_internal! { | |
@flatten_case[$($args)*] [ $($store)* ] | |
(($field, $fit)fit@ ($field)$f@ $($prefix)* $($case)*) | |
($($tail)*) | |
($($prefix)*) | |
} | |
}; | |
// Recognise a NESTED local assignment. Eg f1 = Bool(x). Here the field is in the prefix. | |
// Notice the field prefix is consumed. | |
(@flatten_case[$($args:tt)*] [ $($store:tt)* ] | |
($($case:tt)*) | |
($id:tt $get:ident @local $local:ident $($tail:tt)*) | |
(($field:ident)$f:ident@ $($prefix:tt)*) | |
) => { | |
$crate::fit_kserd_internal! { | |
@flatten_case[$($args)*] [ $($store)* ] | |
($local $get $id ($field)$f@ $($prefix)* $($case)*) | |
($($tail)*) | |
($($prefix)*) | |
} | |
}; | |
// Recognise a ROOT local assignment. Eg Bool(x) => "Bool" bool @local x | |
(@flatten_case[$($args:tt)*] [ $($store:tt)* ] | |
($($case:tt)*) | |
($id:tt $get:ident @local $local:ident $($tail:tt)*) | |
($($prefix:tt)*) | |
) => { | |
$crate::fit_kserd_internal! { | |
@flatten_case[$($args)*] [ $($store)* ] | |
($local $get $id $($prefix)* $($case)*) | |
($($tail)*) | |
($($prefix)*) | |
} | |
}; | |
// A field, this gets pushed onto the prefix stack. | |
(@flatten_case[$($args:tt)*] [ $($store:tt)* ] | |
($($case:tt)*) | |
(@$f:ident($field:ident) $($tail:tt)*) | |
($($prefix:tt)*) | |
) => { | |
$crate::fit_kserd_internal! { | |
@flatten_case[$($args)*] [ $($store)* ] | |
($($case)*) | |
($($tail)*) | |
(($field)$f@ $($prefix)*) | |
} | |
}; | |
// Print error if not recognised. | |
(@flatten_case[$($args:tt)*] [ $($store:tt)* ] | |
($($case:tt)*) | |
($($tokens:tt)*) | |
($($prefix:tt)*) | |
) => { | |
compile_error!( | |
concat!("internal error: unrecognised flatten_case:\n", | |
stringify!($($tokens)*), | |
"\nPrefix:\n", | |
stringify!($($prefix)*) | |
) | |
); | |
}; | |
// ---- Reversing ---------------------------------------------------------- | |
// Finished reversing; Rebuild case and add to completed. Then begin the next seq. | |
(@flatten_rev[$($args:tt)*] [ { $($body:tt)* } $($completed:tt)* ] [$($nextcase:tt)*] | |
() | |
($($rev:tt)*) | |
) => { | |
$crate::fit_kserd_internal! { | |
@flatten_next[$($args)*] | |
($($completed)* ({ $($body)* } $($rev)*),) | |
($($nextcase)*) | |
} | |
}; | |
// Start/continue the flattening, pop a single token and push it back. | |
// This consumes many recusrion cycles, it is common to have to increase recursion limit. | |
(@flatten_rev[$($args:tt)*] [ $($store:tt)* ] [ $($nextcase:tt)* ] | |
($token:tt $($tail:tt)*) | |
($($rev:tt)*) | |
) => { | |
$crate::fit_kserd_internal! { | |
@flatten_rev[$($args)*] [ $($store)* ] [ $($nextcase)* ] | |
($($tail)*) | |
($token $($rev)*) | |
} | |
}; | |
// #### CODE GENERATION #################################################### | |
// Generate the conditional invocation code. This invokes a case function, prepends the | |
// previous error. Only invokes a case function if the previous one returned an error. | |
(@gen[$out:ty, $kserd:ident, $cx:ident, $cxty:ty] | |
() | |
($($rem:tt)*) | |
($first:ident, $($fns:ident,)*) | |
) => { | |
#[allow(unused_mut)] | |
let mut r = $first($kserd, $cx); | |
$( | |
if let Err(a) = r { | |
r = $fns($kserd, $cx).map_err(|b| a.append(b)); | |
} | |
)* | |
r | |
}; | |
// Generate each internal function. This means the use of ? can be used, significantly | |
// simplifying control flow. | |
(@gen[$out:ty, $kserd:ident, $cx:ident, $cxty:ty] | |
(({ $($discard:literal)* | $body:expr } $($locals:tt)* ), $($cases:tt)*) | |
($name:ident, $($fns:ident,)*) | |
($($named:tt)*) | |
) => { | |
fn $name(__kserd: &$crate::Kserd, $cx: $cxty) | |
-> ::std::result::Result<$out, $crate::fit::FitError> | |
{ | |
$crate::fit_kserd_internal!(@locals[Some(__kserd), __kserd, $cx] (@req) $($locals)*); | |
$body | |
} | |
$crate::fit_kserd_internal! { | |
@gen[$out, $kserd, $cx, $cxty] | |
($($cases)*) | |
($($fns,)*) | |
($($named)* $name,) | |
} | |
}; | |
// Print error if cases does not match expected front. | |
(@gen[$($args:tt)*] | |
($($cases:tt)*) | |
($($fns:tt)*) | |
($($named:tt)*) | |
) => { | |
compile_error!(concat!("internal codegen error: unrecognised case representation: ", stringify!($($cases)*))) | |
}; | |
// Print error if run out of fn names | |
(@gen[$($args:tt)*] | |
($($cases:tt)*) | |
() | |
($($named:tt)*) | |
) => { | |
compile_error!("reached maxiumum 32 cases supported.") | |
}; | |
// Generate code for each case. Initialisation vector. | |
(@gen-init[$docs:expr, $on:ty, $out:ty, $cx:ident, $cxty:ty] $($cases:tt)*) => { | |
#[doc = $docs] | |
impl $crate::fit::Fit<&$crate::Kserd<'_>, $cxty, $out> for $on { | |
fn fit(__kserd: &$crate::Kserd, $cx: $cxty) | |
-> ::std::result::Result<$out, $crate::fit::FitError> | |
{ | |
if __kserd.id().map(|id| !id.eq_ignore_ascii_case(stringify!($on))).unwrap_or(false) { | |
return Err($crate::fit::FitError::lazy(|| | |
$crate::fit::FitErrorEntry { | |
desc: concat!("Expecting an id of `", stringify!($on), "`.").into(), | |
body: None | |
})); | |
} | |
// INTEREST | |
// since concatenating tokens is impossible (without `paste`), just specifies 32 | |
// potential function names | |
$crate::fit_kserd_internal! { | |
@gen[$out, __kserd, $cx, $cxty] | |
($($cases)*) | |
( | |
_oper00, _oper01, _oper02, _oper03, _oper04, _oper05, _oper06, _oper07, | |
_oper08, _oper09, _oper10, _oper11, _oper12, _oper13, _oper14, _oper15, | |
_oper16, _oper17, _oper18, _oper19, _oper20, _oper21, _oper22, _oper23, | |
_oper24, _oper25, _oper26, _oper27, _oper28, _oper29, _oper30, _oper31, | |
) | |
() | |
} | |
} | |
} | |
}; | |
// Print error if args don't match expected pattern; internal repr error. | |
(@gen-init[$($args:tt)*] $($cases:tt)*) => { | |
compile_error!(concat!("internal codegen error: did not recognise arguments: ", stringify!($($args)*))); | |
}; | |
// Recognise that the field is optional and flip the optional flag. | |
(@locals[$($args:tt)*] | |
(@req) @cntr @opt $($tail:tt)* | |
) => { | |
$crate::fit_kserd_internal! { | |
@locals[$($args)*] (@opt) @cntr @opt $($tail)* | |
} | |
}; | |
// Start the next kserd match cycle. | |
(@locals[$kserdopt:expr, $($args:tt)*] | |
(@$req:ident) @cntr @$ignore:ident($field:ident) $($tail:tt)* | |
) => { | |
let _tmp_kserd = match $kserdopt { | |
Some(_x) => $crate::fit_kserd_internal!(@locals @$req, _x, $field), | |
None => None, | |
}; | |
$crate::fit_kserd_internal! { @locals[_tmp_kserd, $($args)*] (@$req) $($tail)* } | |
}; | |
// Local assignment. Not optional so unwraps the kserdopt. | |
// Specialised Kserd variant, notice the get function. | |
(@locals[$kserdopt:expr, $kserd:ident, $($args:tt)*] | |
(@req) $id:literal SPECIAL_KSERD $local:ident $($tail:tt)* | |
) => { | |
let $local = $kserdopt.expect("internal fit_kserd! macro error: req case is always Some"); | |
$crate::fit_kserd_internal! { @locals[Some($kserd), $kserd, $($args)*] (@req) $($tail)* } | |
}; | |
// Local assignment. Not optional so unwraps the kserdopt. | |
(@locals[$kserdopt:expr, $kserd:ident, $($args:tt)*] | |
(@req) $id:literal $get:ident $local:ident $($tail:tt)* | |
) => { | |
let _tmp_kserd = $kserdopt.expect("internal fit_kserd! macro error: req case is always Some"); | |
let $local = _tmp_kserd.$get().ok_or_else(|| $crate::fit::FitError::kserd_unexp_ty($id, _tmp_kserd))?; | |
$crate::fit_kserd_internal! { @locals[Some($kserd), $kserd, $($args)*] (@req) $($tail)* } | |
}; | |
// Local assignment. Optional. Specialised Kserd variant, notice the get function. | |
// Always succeeds as just getting the Kserd | |
(@locals[$kserdopt:expr, $kserd:ident, $($args:tt)*] | |
(@opt) $id:literal SPECIAL_KSERD $local:ident $($tail:tt)* | |
) => { | |
let $local = $kserdopt; | |
$crate::fit_kserd_internal! { @locals[Some($kserd), $kserd, $($args)*] (@req) $($tail)* } | |
}; | |
// Local assignment. Optional. | |
// The type of the field is non-optional and will throw an error if not matching | |
(@locals[$kserdopt:expr, $kserd:ident, $($args:tt)*] | |
(@opt) $id:literal $get:ident $local:ident $($tail:tt)* | |
) => { | |
let $local = match $kserdopt { | |
Some(_x) => Some(_x.$get().ok_or_else(|| $crate::fit::FitError::kserd_unexp_ty($id, _x))?), | |
None => None, | |
}; | |
$crate::fit_kserd_internal! { @locals[Some($kserd), $kserd, $($args)*] (@req) $($tail)* } | |
}; | |
// Fitting assignment. Not optional so unwraps the kserdopt. | |
(@locals[$kserdopt:expr, $kserd:ident, $cx:ident] | |
(@req) @fit($local:ident, $fit:ty) $($tail:tt)* | |
) => { | |
let $local = <$fit>::fit($kserdopt.expect("internal fit_kserd! macro error: req case is always Some"), $cx).nest($crate::fit::FitError::kserd_field_ty(stringify!($local), stringify!($fit)))?; | |
$crate::fit_kserd_internal! { @locals[Some($kserd), $kserd, $cx] (@req) $($tail)* } | |
}; | |
// Fitting assignment. Optional. | |
// Follows similar logic to local assignment, a field existed with that name, then fitting | |
// fails propogate the error, NOT silently letting it through. | |
(@locals[$kserdopt:expr, $kserd:ident, $cx:ident] | |
(@opt) @fit($local:ident, $fit:ty) $($tail:tt)* | |
) => { | |
let $local = match $kserdopt { | |
Some(_kserd) => Some(<$fit>::fit(_kserd, $cx).nest($crate::fit::FitError::kserd_field_ty(stringify!($local), stringify!($fit)))?), | |
None => None | |
}; | |
$crate::fit_kserd_internal! { @locals[Some($kserd), $kserd, $cx] (@req) $($tail)* } | |
}; | |
// Inner kserd matching logic. Not optional so any failure makes use of the ? operator which | |
// will break out of the match sequence and return early. | |
(@locals @req, $kserd:ident, $field:ident) => {{ | |
let _cntr = $kserd.cntr().ok_or_else(|| $crate::fit::FitError::kserd_unexp_ty("Cntr", $kserd))?; | |
let _kserd = _cntr.get(stringify!($field)).ok_or_else(|| $crate::fit::FitError::kserd_exp_field(stringify!($field), $kserd))?; | |
Some(_kserd) | |
}}; | |
// Inner kserd matching logic. Optional so just propogates any non found values. | |
// Container types are NOT optional, only if the field does not exist. | |
(@locals @opt, $kserd:ident, $field:ident) => {{ | |
let _cntr = $kserd.cntr().ok_or_else(|| $crate::fit::FitError::kserd_unexp_ty("Cntr", $kserd))?; | |
_cntr.get(stringify!($field)) | |
}}; | |
// No locals remain, do nothing. | |
(@locals[$($args:tt)*] (@$discard:ident)) => { }; | |
// Print error if the @case pattern match fails | |
(@locals[$($args:tt)*] | |
$($tokens:tt)* | |
) => { | |
compile_error!(concat!("internal codegen error, unrecognised locals representation:\n", stringify!($($tokens)*))); | |
}; | |
// #### DOCUMENTATION ###################################################### | |
// Add the case documentation. | |
(@doc, $doc:expr, $( ({ $($casedocs:literal)* | $body:expr } $($case:tt)* ), )*) => { | |
concat!($doc, | |
$( | |
$( "//", $casedocs, "\n", )* | |
$crate::fit_kserd_internal!(@doc | |
("") | |
() | |
($($case)*) | |
), | |
)* | |
) | |
}; | |
// No more docs. | |
(@doc ($doc:expr) ($($prefix:literal)*) | |
() | |
) => { $doc }; | |
// Start of a container. Increment indent. | |
(@doc ($doc:expr) ($($prefix:literal)*) | |
(@cntr_start $($tail:tt)*) | |
) => { | |
$crate::fit_kserd_internal!(@doc | |
(concat!($doc, "Cntr {\n")) | |
($($prefix)* " ") | |
($($tail)*) | |
) | |
}; | |
// End of a container. New line after. | |
(@doc ($doc:expr) ($discard:literal $($prefix:literal)*) | |
(@cntr_end $($tail:tt)*) | |
) => { | |
$crate::fit_kserd_internal!(@doc | |
(concat!($doc, $($prefix,)* "}\n")) | |
($($prefix)*) | |
($($tail)*) | |
) | |
}; | |
// The variant id, eg Bool, Str, etc. Always new line after. | |
(@doc ($doc:expr) ($($prefix:literal)*) | |
($id:literal $get:ident @local $local:ident $($tail:tt)*) | |
) => { | |
$crate::fit_kserd_internal!(@doc | |
(concat!($doc, $id, "\n")) | |
($($prefix)*) | |
($($tail)*) | |
) | |
}; | |
// Required fitting: `field: Type` | |
(@doc ($doc:expr) ($($prefix:literal)*) | |
(@req($field:ident) @fit($fit:ty) $($tail:tt)*) | |
) => { | |
$crate::fit_kserd_internal!(@doc | |
(concat!($doc, $($prefix,)* stringify!($field), ": ", stringify!($fit), "\n")) | |
($($prefix)*) | |
($($tail)*) | |
) | |
}; | |
// Optional fitting: `[field]: Type` | |
(@doc ($doc:expr) ($($prefix:literal)*) | |
(@opt($field:ident) @fit($fit:ty) $($tail:tt)*) | |
) => { | |
$crate::fit_kserd_internal!(@doc | |
(concat!($doc, $($prefix,)* "[", stringify!($field), "]: ", stringify!($fit), "\n")) | |
($($prefix)*) | |
($($tail)*) | |
) | |
}; | |
// Required field assigner: `field = ` | |
(@doc ($doc:expr) ($($prefix:literal)*) | |
(@req($field:ident) $($tail:tt)*) | |
) => { | |
$crate::fit_kserd_internal!(@doc | |
(concat!($doc, $($prefix,)* stringify!($field), " = ")) | |
($($prefix)*) | |
($($tail)*) | |
) | |
}; | |
// Optional field assigner: `[field] = ` | |
(@doc ($doc:expr) ($($prefix:literal)*) | |
(@opt($field:ident) $($tail:tt)*) | |
) => { | |
$crate::fit_kserd_internal!(@doc | |
(concat!($doc, $($prefix,)* "[", stringify!($field), "] = ")) | |
($($prefix)*) | |
($($tail)*) | |
) | |
}; | |
// Entry point. Prefixes the supplied docs with a # Grammar section. | |
(@doc-init, $doc:expr, $($tokens:tt)*) => { | |
$crate::fit_kserd_internal!(@doc, concat!($doc, "\n\n# Grammar\n\n```text\n"), $($tokens)*) | |
}; | |
// #### ENTRY POINTS ####################################################### | |
($docs:literal, $on:ty, $out:ty, $cx:ident, $cxty:ty >>) => { compile_error!("empty grammar"); }; | |
($docs:literal, $on:ty, $out:ty, $cx:ident, $cxty:ty >> $($grammar:tt)*) => { | |
$crate::fit_kserd_internal! { | |
@parse[$docs, $on, $out, $cx, $cxty] {} ($($grammar)*) () () | |
} | |
}; | |
} | |
/// Macro to define [`Fit`] implementations using a grammar like pattern matching mechanism. | |
/// | |
/// To increase the visibility of a [`Fit`] implementation (that is fitting a [`Kserd`]), the fit | |
/// macro accepts a pattern grammar that closely matches the variants of [`kserd::Value`]. The | |
/// macro parses this grammar and constructs the required rust code to assign the pattern locals. | |
/// It is the recommended way of fitting [`Kserd`] structures as it standardises the documentation, | |
/// errors, and matching behaviour. | |
/// | |
/// # Example usage | |
/// The macro is used on `daedalus` defined items. As an example, to define a fit to a Table one | |
/// might use the macro like so: | |
/// ```rust | |
/// # use repr::*; | |
/// struct Table; // dummy struct | |
/// fit_kserd!("Some documentation on the fit", Table, cx is &str >> | |
/// /// Documentation for case | |
/// Tuple|Seq(rows) => { | |
/// // here `rows` will be &Vec<Kserd> | |
/// todo!() | |
/// }, | |
/// Cntr { | |
/// [header] = Tuple(header), | |
/// data = Seq(rows) | |
/// } => { | |
/// // `header` would be optional: Option<&Vec<Kserd>>, | |
/// // `data` would be &Vec<Kserd> | |
/// todo!() | |
/// } | |
/// ); | |
/// ``` | |
/// | |
/// Each pattern has a body expression associated with it. The body must return a `Result`, the ok | |
/// type will be defined either as the input structure, or another type if the alternate overload | |
/// is used. The error type is [`FitError`]. | |
/// | |
/// # Usage Notes | |
/// ## Recursion Limits | |
/// The macro sometimes reaches recursion limits, especially in nested patterns. Increasing the | |
/// crate defined `#![recursion_limit="256"]` usually assuages the issue. | |
/// | |
/// ## `?` Operator | |
/// The `?` operator can (and should) be used in body expressions. This can make using [`FitError`] | |
/// more ergonomic. | |
/// | |
/// ## Optionality | |
/// Nested fields can be optional. The syntax to use is `[field_name]`. | |
/// | |
/// ## Identifiers | |
/// Fields and variable identifiers have to be valid rust identifiers, this means that `field-name` | |
/// is illegal. | |
/// | |
/// ## Fitting | |
/// Using the syntax `field: Type,` tells the macro to invoke a fitting mechanism on the kserd | |
/// data at that field. This is used to compose fitting together. Fits can also be optional with | |
/// `[field]: Type`. `Type` needs to support fitting by implementing [`Fit`]. | |
/// | |
/// ## Overloads | |
/// The macro has 4 variants. The most basic signature is `T >> /* patterns */`. This applies the | |
/// [`Fit`] trait to `T` and the result is also of type `T`. If the result type needs to be | |
/// something other than `T` that be specified with `T, U >> /* patterns */`. Both of these | |
/// signatures can also have a literal doc string as the first argument. The doc string gets | |
/// prepended to the grammar docs. | |
/// | |
/// ## Documentation | |
/// Along with a preliminary doc string, each case can be documented using the `///` syntax _above | |
/// the case_. | |
/// | |
/// ## Context and working directory | |
/// The action context and working directory are defined _before_ the grammar, using identifiers | |
/// specified bracketed by paranthese: `(cx, wd)`. These identifiers are arbitary and enable access | |
/// to the context and working directory defined on [`Fit`]. | |
/// | |
/// # Grammar Syntax | |
/// Most variants except `Unit` are supported. There is support for `Tuple|Seq` where it will match | |
/// on a tuple _or_ a sequence. `Cntr` variants have nesting syntax with curly braces, each field | |
/// defined with the variant or type it will match against. | |
/// | |
/// ## All Variants | |
/// Required: | |
/// ```rust | |
/// #![recursion_limit="256"] | |
/// # use repr::*; | |
/// use std::collections::BTreeMap; | |
/// use std::path::Path; | |
/// | |
/// struct A; | |
/// fit_kserd! { A, cx is () >> | |
/// Cntr { | |
/// a = Bool(a), | |
/// b = Num(b), | |
/// c = Str(c), | |
/// d = Barr(d), | |
/// e = Tuple(e), | |
/// f = Cntr(f), | |
/// g = Seq(g), | |
/// h = Tuple|Seq(h), | |
/// i = Map(i), | |
/// } => { | |
/// let a: bool = a; | |
/// let b: f64 = b; | |
/// let c: &str = c; | |
/// let d: &[u8] = d; | |
/// let e: &Vec<Kserd> = e; | |
/// // f is an Accessor here: see kserd docs for more info | |
/// let g: &Vec<Kserd> = g; | |
/// let h: &Vec<Kserd> = h; | |
/// let i: &BTreeMap<Kserd, Kserd> = i; | |
/// | |
/// // access the context | |
/// let cx: () = cx; | |
/// | |
/// Ok(A) | |
/// } | |
/// } | |
/// ``` | |
/// | |
/// Optional: | |
/// ```rust | |
/// #![recursion_limit="256"] | |
/// # use repr::*; | |
/// use std::collections::BTreeMap; | |
/// | |
/// struct A; | |
/// fit_kserd! { A, cx is () >> | |
/// Cntr { | |
/// [a] = Bool(a), | |
/// [b] = Num(b), | |
/// [c] = Str(c), | |
/// [d] = Barr(d), | |
/// [e] = Tuple(e), | |
/// [f] = Cntr(f), | |
/// [g] = Seq(g), | |
/// [h] = Tuple|Seq(h), | |
/// [i] = Map(i), | |
/// } => { | |
/// let a: Option<bool> = a; | |
/// let b: Option<f64> = b; | |
/// let c: Option<&str> = c; | |
/// let d: Option<&[u8]> = d; | |
/// let e: Option<&Vec<Kserd>> = e; | |
/// // f is an Accessor here: see kserd docs for more info | |
/// let g: Option<&Vec<Kserd>> = g; | |
/// let h: Option<&Vec<Kserd>> = h; | |
/// let i: Option<&BTreeMap<Kserd, Kserd>> = i; | |
/// | |
/// Ok(A) | |
/// } | |
/// } | |
/// ``` | |
/// | |
/// [`Fit`]: crate::fit::Fit | |
/// [`FitError`]: crate::fit::FitError | |
/// [`Kserd`]: ::kserd::Kserd | |
/// [`kserd::Value`]: ::kserd::Value | |
#[macro_export] | |
macro_rules! fit_kserd { | |
($docs:literal, $on:ty, $out:ty, $cx:ident is $cxty:ty >> $($grammar:tt)*) => { | |
$crate::fit_kserd_internal! { $docs, $on, $out, $cx, $cxty >> $($grammar)* } | |
}; | |
($on:ty, $out:ty, $cx:ident is $cxty:ty >> $($grammar:tt)*) => { | |
$crate::fit_kserd!("", $on, $out, $cx is $cxty >> $($grammar)*); | |
}; | |
($docs:literal, $out:ty, $cx:ident is $cxty:ty >> $($grammar:tt)*) => { | |
$crate::fit_kserd!($docs, $out, $out, $cx is $cxty >> $($grammar)*); | |
}; | |
($out:ty, $cx:ident is $cxty:ty >> $($grammar:tt)*) => { | |
$crate::fit_kserd!("", $out, $out, $cx is $cxty >> $($grammar)*); | |
}; | |
} | |
#[cfg(test)] | |
mod tests { | |
use kserd::{ds::Accessor, Kserd, Kstr}; | |
use std::collections::BTreeMap; | |
// Tests the 4 entry points and all variants for compilation. | |
#[test] | |
fn fit_macro_compilation_test1() { | |
struct A; | |
fit_kserd!(A, _cx is () >> Bool(_x) => { Ok(A) }); | |
} | |
#[test] | |
fn fit_macro_compilation_test2() { | |
struct A; | |
fit_kserd!("Doc", A, _cx is () >> Num(_x) => Ok(A)); | |
} | |
#[test] | |
fn fit_macro_compilation_test3() { | |
struct A; | |
struct B; | |
fit_kserd!(A, B, _cx is () >> Str(_x) => { Ok(B) }); | |
(A, ()).1; | |
} | |
#[test] | |
fn fit_macro_compilation_test4() { | |
struct A; | |
struct B; | |
fit_kserd!("Doc", A, B, _cx is () >> Barr(_x) => Ok(B)); | |
(A, ()).1; | |
} | |
#[test] | |
fn fit_macro_compilation_every_variant() { | |
struct A; | |
fit_kserd! { A, cx is () >> | |
Cntr { | |
a = Bool(a), | |
b = Num(b), | |
c = Str(c), | |
d = Barr(d), | |
e = Tuple(e), | |
f = Cntr(f), | |
g = Seq(g), | |
h = Tuple|Seq(h), | |
i = Map(i), | |
j = Kserd(j), | |
} => { | |
let _a: bool = a; | |
let _b: f64 = b; | |
let _c: &str = c; | |
let _d: &[u8] = d; | |
let _e: &Vec<Kserd> = e; | |
let _f: Accessor<&BTreeMap<Kstr, Kserd>> = f; | |
let _g: &Vec<Kserd> = g; | |
let _h: &Vec<Kserd> = h; | |
let _i: &BTreeMap<Kserd, Kserd> = i; | |
let _j: &Kserd = j; | |
let _cx: () = cx; | |
Ok(A) | |
} | |
} | |
} | |
#[test] | |
fn fit_macro_compilation_every_variant_opt() { | |
struct A; | |
fit_kserd! { A, _cx is () >> | |
Cntr { | |
[a] = Bool(a), | |
[b] = Num(b), | |
[c] = Str(c), | |
[d] = Barr(d), | |
[e] = Tuple(e), | |
[f] = Cntr(f), | |
[g] = Seq(g), | |
[h] = Tuple|Seq(h), | |
[i] = Map(i), | |
[j] = Kserd(j), | |
} => { | |
let _a: Option<bool> = a; | |
let _b: Option<f64> = b; | |
let _c: Option<&str> = c; | |
let _d: Option<&[u8]> = d; | |
let _e: Option<&Vec<Kserd>> = e; | |
let _f: Option<Accessor<&BTreeMap<Kstr, Kserd>>> = f; | |
let _g: Option<&Vec<Kserd>> = g; | |
let _h: Option<&Vec<Kserd>> = h; | |
let _i: Option<&BTreeMap<Kserd, Kserd>> = i; | |
let _j: Option<&Kserd> = j; | |
Ok(A) | |
} | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment