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

Add support for "runtime" values #311

Merged
merged 2 commits into from
Jan 23, 2025

Conversation

GuillaumeGomez
Copy link
Contributor

Fixes #48.

We can now provide values at runtime. It'll be useful for me for switching mdBook to rinja (or askama soon enough) as they need to allow to parse some files after mdBook is compiled if users provide more (not sure it'll be enough though, we'll see).

Funnily enough, it forced me to support the call::<> syntax with references and all.

@GuillaumeGomez GuillaumeGomez force-pushed the runtime-values branch 2 times, most recently from 7bc47be to 4dc727a Compare January 10, 2025 13:19
@GuillaumeGomez
Copy link
Contributor Author

Finally fixed CI. Sorry for the noise.

/// feature is enabled, it uses the [`BTreeMap`](alloc::collections::BTreeMap) type, otherwise
/// it uses an empty type and does nothing.
#[derive(Default)]
pub struct Values(#[cfg(feature = "alloc")] HashMap<String, Box<dyn Any>>);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I kinda dislike the use of a HashMap in here. Unless there are quiet a lot key-value pairs, a simple slice &[(&str, &dyn Any)] will be excessively much faster. My guess is that a HashMap with many intermediate allocations won't be faster for less than 20 values. (I did not benchmark my guess, though. :) )

Maybe Values could be a trait instead?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not using a map sounds good to me. I suppose by making Values a trait, we would let it up to users to implement it on whatever they want? But then the question is: should we implement on some types by default as well (like &(&str, &dyn Any) or HashMap<String, Box<dyn Any>>)?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it would be useful to implement it for some types, so it works "out of the box". But I would not seal the trait.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What do you mean by "seal the trait"?

Copy link
Collaborator

@Kijewski Kijewski Jan 12, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

mod private { // ← not `pub`, so you can't implement this trait outside of the crate
    pub trait Sealed {}
}

pub mod MyTrait: private::Sealed {}

https://predr.ag/blog/definitive-guide-to-sealed-traits-in-rust/#what-are-sealed-traits

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah so making the trait impossible to implement outside of the crate. I didn't know there was a name for that. TIL, thanks!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Big question: making the Values trait fully generic over the key creates a lot of issues:

  • DynTemplate is not dyn-compatible anymore
  • We get unsolvable errors like:
    4  | #[derive(Template)]
    |          ^^^^^^^^
    |          |
    |          expected `&K`, found `&str`
    |          arguments to this method are incorrect
    

Would it be ok to force the key to be AsRef<str>?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I think it's a good idea to force the key to be str'ish. Maybe K: Borrow<str> would be the best solution? So the interface is the same as HashMap::get(), and because Borrow<str> implies that K's hash is the same as the borrowed str's hash.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds good to me.

@Kijewski
Copy link
Collaborator

I am not 100% sure that I see the advantage over adding a field values?

@GuillaumeGomez
Copy link
Contributor Author

GuillaumeGomez commented Jan 12, 2025

Mostly because then, you don't need to add a values field to all impacted type. For example in docs.rs, we have a csrp csrf (EDIT: typo fix) thing we pass when rendering sometimes, but that means all docs.rs types need to have this field, even though so they can't set it and have no use for it.

@Kijewski
Copy link
Collaborator

Ah, yes, a csrf token sounds like a good use case.

@GuillaumeGomez
Copy link
Contributor Author

Turned Values into a trait as discussed and implemented on some types. Sadly, even with the simplified version, there are still some frustrating limitations. For example, we cannot use &str as key for HashMap otherwise Values is not implemented. And when I tried to remove the for<'a> limitation, I encountered some lifetime issues I couldn't solve.

I'm very curious to see if you will be able to see a way to get around them.

Also, since DynTemplate cannot use generics, I currently commented out the _with_values methods on it.

@GuillaumeGomez GuillaumeGomez force-pushed the runtime-values branch 2 times, most recently from 86701f7 to fb8a28f Compare January 15, 2025 16:04
@Kijewski Kijewski force-pushed the runtime-values branch 2 times, most recently from 15ee05e to f16dcef Compare January 15, 2025 19:10
@Kijewski
Copy link
Collaborator

I rebased the PR on the current master.

I implemented the idea somewhat differently. Please feel free to unpick the newest commit if you don't like it!

Instead of adding a VALUES variable, I implemented a "key" | value(ty) filter, and made the argument &dyn Values instead of being generic.

rinja/src/lib.rs Outdated
@@ -83,6 +84,7 @@ pub use rinja_derive::Template;
pub use crate as shared;
pub use crate::error::{Error, Result};
pub use crate::helpers::PrimitiveType;
pub use crate::values::{NO_VALUES, Value, Values};
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would also reexport get_value since people can always call rinja::get_value in their templates or even in their code (who knows?).

@@ -1167,7 +1167,8 @@ impl<'a> Generator<'a, '_> {
idx
};
lines.write(format_args!(
"(&&rinja::filters::Writable(expr{idx})).rinja_write(__rinja_writer)?;",
"(&&&rinja::filters::Writable(expr{idx})).\
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why adding a new reference here btw?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added the values as new argument to WriteWritable. This way it gets automagically passed on to any {{ template }}s. To make this work I need to tell Templates other FastWritables apart.

@GuillaumeGomez
Copy link
Contributor Author

I think it's a much simpler approach, love it! Shouldn't have tried to force too much the trait argument approach... My turn now (some docs to update and some edges I'd like to soften). Thanks!

@@ -584,6 +586,7 @@ impl<'a> Generator<'a, '_> {
return Ok(DisplayWrap::Unwrapped);
}
}
#[allow(clippy::literal_string_with_formatting_args)]
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh that's a new lint I added haha.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you, I like the lint! I forgot more than once to surround the string with format!(…). :)

@Kijewski
Copy link
Collaborator

My turn now (some docs to update and some edges I'd like to soften)

I'm happy that you like my approach, and I'm interested to see what you make out of it! :)

@GuillaumeGomez
Copy link
Contributor Author

Seems like we broke rustc. :o

@Kijewski
Copy link
Collaborator

Haha, oops.

Can you open an issue? I guess you have more experience with that than me. (And I just noticed that it's 20:40 already and that I have not eaten all day.)

@GuillaumeGomez
Copy link
Contributor Author

Off you go then. Enough computer for today for you! Eat well.

And yes, gonna do that. :)

@GuillaumeGomez
Copy link
Contributor Author

While trying the new changes and in particular the new filter, I'm not a bit fan of the incoherence introduced: the argument is not argument an argument but a generic. What do you think if we instead added support for generics on filters? Or simpler (although I like the filter): we don't make this filter.

@Kijewski
Copy link
Collaborator

Yes, having a type as a function argument is a bit odd. Something like "key" | value::<u32> should be readable enough. Should we make the :: optional, so you can write "key" | value<u32>, too?

I wouldn't like a free function value("key") very much, because it shadows the method Self::value().

@GuillaumeGomez
Copy link
Contributor Author

Yes, having a type as a function argument is a bit odd. Something like "key" | value::<u32> should be readable enough. Should we make the :: optional, so you can write "key" | value<u32>, too?

I'd prefer to keep the syntax Rust compatible as much as possible (and also I already implemented to parsing of ::<> so less work hehe).

I wouldn't like a free function value("key") very much, because it shadows the method Self::value().

Me neither. Using rinja::get_value sounds good enough to me.

If it sounds good to you, I'll make the changes.

@Kijewski
Copy link
Collaborator

If it sounds good to you, I'll make the changes.

Sure, go ahead! :)

@GuillaumeGomez
Copy link
Contributor Author

Finally done! I special-cased the rinja::get_value path. I also needed to add even more generics support (for Expr::Call) so I think we support generics pretty much everywhere now. Talk about side-effects 😆 I also extended them to all them to be recursive which allows things like HashMap<String, Vec<u32>>.

I think I did pretty much everything I thought was missing. Thanks a lot for your help getting it to the finish line! :)

@Kijewski
Copy link
Collaborator

Would it be difficult to extract the turbofish implementation into its own PR, and then rebase this PR on it?

@GuillaumeGomez
Copy link
Contributor Author

Hum, I think it should be mostly fine. The git history will be completely broken so I'm gonna have a good time. :')

@GuillaumeGomez
Copy link
Contributor Author

Much smaller change now. Now two commits only and just value related changes. :)

Comment on lines 711 to 741
fn _visit_ty(
&mut self,
ctx: &Context<'_>,
buf: &mut Buffer,
ty: &WithSpan<'_, Expr<'a>>,
) -> Result<(), CompileError> {
match &**ty {
Expr::Var(ty) => {
buf.write(normalize_identifier(ty));
}
Expr::Path(ty) => {
self.visit_path(buf, ty);
}
Expr::Unary("&", rhs) => {
buf.write('&');
self._visit_ty(ctx, buf, rhs.as_ref())?;
}
Expr::BinOp(token @ ("<" | ">"), lhs, rhs) => {
self._visit_ty(ctx, buf, lhs.as_ref())?;
buf.write(token);
self._visit_ty(ctx, buf, rhs.as_ref())?;
}
_ => {
return Err(
ctx.generate_error("expected a type name, not an expression", ty.span())
);
}
}
Ok(())
}

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
fn _visit_ty(
&mut self,
ctx: &Context<'_>,
buf: &mut Buffer,
ty: &WithSpan<'_, Expr<'a>>,
) -> Result<(), CompileError> {
match &**ty {
Expr::Var(ty) => {
buf.write(normalize_identifier(ty));
}
Expr::Path(ty) => {
self.visit_path(buf, ty);
}
Expr::Unary("&", rhs) => {
buf.write('&');
self._visit_ty(ctx, buf, rhs.as_ref())?;
}
Expr::BinOp(token @ ("<" | ">"), lhs, rhs) => {
self._visit_ty(ctx, buf, lhs.as_ref())?;
buf.write(token);
self._visit_ty(ctx, buf, rhs.as_ref())?;
}
_ => {
return Err(
ctx.generate_error("expected a type name, not an expression", ty.span())
);
}
}
Ok(())
}

The method is not used anymore.

@Kijewski
Copy link
Collaborator

Kijewski commented Jan 23, 2025

I wonder why neither rustc nor clippy have a problem with the unused method? It calls itself, but nobody else calls it.

Except for the one comment, it look great!

@GuillaumeGomez
Copy link
Contributor Author

It's super strange indeed... Gonna report that. In the meantime, fixed it.

Copy link
Collaborator

@Kijewski Kijewski left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

@GuillaumeGomez GuillaumeGomez merged commit cae09c5 into rinja-rs:master Jan 23, 2025
36 checks passed
@GuillaumeGomez GuillaumeGomez deleted the runtime-values branch January 23, 2025 23:08
@GuillaumeGomez
Copy link
Contributor Author

I wonder why neither rustc nor clippy have a problem with the unused method? It calls itself, but nobody else calls it.

It's because it starts with an underscore. XD

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Add a render_with_values method?
2 participants