mod decode;
mod frozen;
mod historical;
#[cfg(test)]
mod tests;
mod transitions;
mod types;
mod versioned;
use crate::{
history::{AuthorizedHistory, RetainedField},
lint_attributes::{scoped_field_type_for_owner, with_lints},
model::RecordInput,
};
use proc_macro2::{Span, TokenStream};
use quote::{format_ident, quote};
use std::collections::BTreeSet;
use syn::{DeriveInput, Error, Ident, Path, Result, Type};
pub struct GenerationPaths {
pub support: Path,
pub error: Path,
pub helper_prefix: String,
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub enum PayloadTraits {
#[default]
Native,
Frontend,
}
#[derive(Debug, Clone, Default)]
pub struct GenerationOptions {
pub payload_traits: PayloadTraits,
pub observation_cfg: Option<Ident>,
}
pub struct GeneratedVersion {
pub number: u32,
pub name: Ident,
pub schema_expression: TokenStream,
pub shape_expression: TokenStream,
pub contract: DeriveInput,
}
pub struct GeneratedHistory {
pub items: TokenStream,
pub latest: Ident,
pub decoder: Ident,
pub versions: Vec<GeneratedVersion>,
}
pub fn generate_history(
input: &RecordInput,
stable_name: &str,
history: &AuthorizedHistory,
paths: &GenerationPaths,
) -> Result<GeneratedHistory> {
generate_history_with_options(
input,
stable_name,
history,
paths,
&GenerationOptions::default(),
)
}
pub fn generate_history_with_options(
input: &RecordInput,
stable_name: &str,
history: &AuthorizedHistory,
paths: &GenerationPaths,
options: &GenerationOptions,
) -> Result<GeneratedHistory> {
let expected = input
.fields
.iter()
.map(|field| field.name.to_string().trim_start_matches("r#").to_owned())
.collect::<BTreeSet<_>>();
let actual = input
.field_visibility
.keys()
.cloned()
.collect::<BTreeSet<_>>();
if expected != actual {
return Err(Error::new_spanned(
&input.name,
"field visibility must cover the complete authored history inventory",
));
}
let context = Context {
input,
history,
paths,
stable_name,
};
let (payloads, versions) = types::generate(&context, options)?;
let historical = historical::generate(&context, options);
let conversions = transitions::generate(&context);
let decoder = decode::generate(&context);
let assertions = frozen::generate(&context, options);
Ok(GeneratedHistory {
items: with_lints(
&input.attributes,
quote!(#payloads #conversions #historical #decoder #assertions),
)?,
latest: context.payload(history.head()),
decoder: context.helper("decode_history"),
versions,
})
}
pub fn generate_versioned(
input: &RecordInput,
stable_name: &str,
history: &AuthorizedHistory,
paths: &GenerationPaths,
) -> Result<TokenStream> {
let context = Context {
input,
history,
paths,
stable_name,
};
with_lints(&input.attributes, versioned::generate(&context))
}
struct Context<'a> {
input: &'a RecordInput,
history: &'a AuthorizedHistory,
paths: &'a GenerationPaths,
stable_name: &'a str,
}
impl Context<'_> {
fn field(&self, version: u32, name: &Ident) -> (usize, &RetainedField) {
let retained = self
.history
.versions()
.iter()
.find(|retained| retained.version == version)
.expect("generated retained version");
retained
.fields
.iter()
.enumerate()
.find(|(_, field)| &field.name == name)
.expect("generated retained field")
}
fn field_type(&self, version: u32, name: &Ident) -> Type {
let (index, field) = self.field(version, name);
scoped_field_type_for_owner(
&self.payload(version),
&self.input.name,
index,
&field.ty,
&field.attributes,
)
.0
}
fn payload(&self, version: u32) -> Ident {
format_ident!(
"{}V{}",
self.input.name,
version,
span = self.input.name.span()
)
}
fn helper(&self, suffix: &str) -> Ident {
format_ident!(
"{}_{suffix}",
self.paths.helper_prefix,
span = Span::mixed_site()
)
}
fn version(&self, value: u32) -> TokenStream {
let support = &self.paths.support;
let version = format_ident!("__type_history_version", span = Span::mixed_site());
quote! {
match #support::PayloadVersion::try_from_raw(#value) {
::core::result::Result::Ok(#version) => #version,
::core::result::Result::Err(_) => ::core::panic!("generated history has a non-positive retained version"),
}
}
}
fn stable_name(&self) -> TokenStream {
let support = &self.paths.support;
let stable_name = self.stable_name;
quote!(#support::StableName::new(#stable_name))
}
}