cobalt-org / liquid-rust

Liquid templating for Rust
docs.rs/liquid
MIT License
473 stars 79 forks source link

Question: supporting object literals with unknown keys on custom filters #507

Open juanazam opened 1 year ago

juanazam commented 1 year ago

Hello!

I'm working on supporting Shopify's t filter (translation filter).

Here there is a simple a example of how it works. If we have the following translations file

/locales/en.default.json

{
  "layout": {
    "header": {
      "hello_user": "Hello {{ name }}!"
    }
  }
}

And the following liquid template:

<h1>{{ 'layout.header.hello_user' | t: name: customer.first_name }}</h1>

We would expect this example to render (assuming the first name of the customer is John):

<h1>Hello John</h1>

I'm running into the following issue when implementing this as a custom filter. This is how I'm defining the filter's parameters:

#[derive(Debug, FilterParameters)]
struct TArgs {
    #[parameter(
        description = "Variables to be used for revaluating liquid once translation is resolved.",
    )]
    variables: Option<Expression>,
}

#[derive(Clone, ParseFilter, FilterReflection)]
#[filter(
    name = "t",
    description = "Access the desired translation for the recipient local",
    parameters(TArgs),
    parsed(TFilter)
)]
pub struct T;

#[derive(Debug, FromFilterParameters, Display_filter)]
#[name = "t"]
struct TFilter {
    #[parameters]
    args: TArgs,
}

But the problem is that when I run the same example I get the following error:

 "liquid: Unexpected named argument `name`\nfrom: Filter parsing error\n  with:\n    filter=t: name: customer.name\n"}

I haven't been able to get the filter working with object literals with arbitrary keys. What's the best way to accomplish this?

juanazam commented 1 year ago

Hi @epage, sorry to bother you, do you have any insights on how I might be able to achieve this? Tagging you directly because you seem to be the person with the most knowledge con this crate, thanks in advance!

juanazam commented 1 year ago

Hi @epage, sorry to ping you directly again here, do you know if there are any easy workarounds for this issue?

TomzBench commented 1 year ago

I made a custom json filter by importing serde. Since the liquid model implements Serialize/deserialize you can use it to get a serde_json::Value and loop through keys...

use liquid_core::{Display_filter, Filter, FilterReflection, ParseFilter};
use liquid_core::{Error, Result, Runtime};
use liquid_core::{Value, ValueView};

#[derive(Clone, ParseFilter, FilterReflection)]
#[filter(
    name = "json",
    description = "Convert a JSON string into a liquid object",
    parsed(JsonFilter)
)]
pub struct Json;

#[derive(Debug, Default, Display_filter)]
#[name = "json"]
pub struct JsonFilter {}
impl Filter for JsonFilter {
    fn evaluate(&self, input: &dyn ValueView, _: &dyn Runtime) -> Result<Value> {
        serde_json::from_str(&input.to_kstr().as_str()).map_err(|e| Error::with_msg(e.to_string()))
    }
}

Usage in template:

{% assign obj = '{"name": "john"}' | json %}
juanazam commented 1 year ago

Hi @TomzBench, thanks for your reply! I will give this a try!

juanazam commented 1 year ago

Hey @TomzBench this won't work as I would like because of two things.

  1. The first argument of the t filter must be a string.
  2. The second argument must be a key/value pair, and not a JSON serialized object

The example I shared above captures what I need:

{{ 'layout.header.hello_user' | t: name: customer.first_name }}
TomzBench commented 1 year ago

per the t filter docs it does not appear that you can use templates in the local file.

If you have this JSON local file.

{
  "blog": {
    "comment": {
      "email": "Su correo electrónico"
    }
  }
}

You could do

{{ assign local = '{ "blog": { "comment": ... }' | json }}
{{ "blog.comment.email" | t : local }}

If you dont want to pass the local as an argument to the t filter then you need some other way to initialize the t filter with the local file. Shopify uses a global. so you could do it that way.

You need another scheme to parse the dot notation on the input.

juanazam commented 1 year ago

I'm pretty sure you can, the example I shared above came from the same docs on the interpolation section (link).

Regarding the file, I'm already passing it as a global, that's not the issue. The problem is that when defining the filter, it seems there is no way of passing a literal object as the second parameter of the filter. (the first argument is the lookup path to be used for the file).

Using liquid-rust you can either define parameters as positional or named. If you define them as named, you need to know all keywords in advance which doesn't for me, because the object literal should support any arbitrary object literal.

I haven't been able to figure out if there is a way of bypassing this restriction.

TomzBench commented 1 year ago

I'm pretty sure you can, the example I shared above came from the same docs on the interpolation section (link).

woops, looks like you're right

Using liquid-rust you can either define parameters as positional or named. If you define them as named, you need to know all keywords in advance which doesn't for me, because the object literal should support any arbitrary object literal.

I haven't been able to figure out if there is a way of bypassing this restriction.

I couldn't find in the docs where you can even pass named parameters. I have been restricting myself to positional and json objects when I have complex args. So if you absolutely need dynamically keyed values and can't use a json string then I think this crate can't do that is my guess. The maintainer doesn't seem available for comment though. Good luck!