-
Notifications
You must be signed in to change notification settings - Fork 78
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 indexed access to context values #141
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for doing this. I really appreciate it (been putting off having to figure out the parser.
Feel welcome to have a conversation about "not now" on the comments I made.
src/path.rs
Outdated
value | ||
} else { | ||
return value; | ||
}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Isn't this just
let value = value?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nope, it's an early return from fold when value is error.
https://play.rust-lang.org/?gist=46038d66fc075362393e0eb30d9748dc&version=stable
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Dropped, sorry I missed that.
src/path.rs
Outdated
// because zero is not counted normal | ||
if (*x != 0f32 && !x.is_normal()) || *x < 0f32 || | ||
x.round() > (::std::usize::MAX as f32) { | ||
return Error::renderer("bad array index"); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could you make the error include the index?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
More thanks
src/path.rs
Outdated
// at the first condition only is_normal is not enough | ||
// because zero is not counted normal | ||
if (*x != 0f32 && !x.is_normal()) || *x < 0f32 || | ||
x.round() > (::std::usize::MAX as f32) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Does liquid support negative indexing like Python? Does it support slicing?
(and this code is one of the reasons why I'm considering native integer support)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Neither of docs I see say this explicitly, but a simple test in jekyll shows that negative indexing is allowed, ranges(I've tried [0..1]
[0:1]
[0-1]
) are not
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for looking into this!
src/path.rs
Outdated
let value = | ||
value | ||
.get(idx) | ||
.ok_or_else(|| Error::Render("index out of range".to_string()))?; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you include the index and value's length?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yet again, thanks!
src/path.rs
Outdated
.ok_or_else(|| Error::Render("index out of range".to_string()))?; | ||
Ok(value) | ||
} | ||
(&Value::Array(_), _) => Error::renderer("bad array index type"), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can the error include a {:?}
of value?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Again, thanks!
src/path.rs
Outdated
|
||
match result { | ||
Ok(result) => result.render(context), | ||
Err(e) => Error::renderer(&format!("rendering error: {}", e)), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You can use chain_err
for this
result.chain_err(|| "Failed to render expression")
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Dropped, ?
is much better choice, the chained error wouldn't have added much value.
.clone(); | ||
|
||
let mut counter = self.indexes.len(); | ||
let result = self.indexes.iter().fold(Ok(&value), |value, index| { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
- There a reason you are using
counter
rather thanself.indexes.iter().enumerate()
? - Long term I'm wanting to switch it so
Context
to have a trait for the user-provided variables. Ideally, this would allow lazy lookups. To get maximize this with indexing, the trait would have some kind of lookup object (slice of Enum(string, index).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
- I was affraid to make iterator less readable because of more nesting.
counter
is an internal variable made to check the depth correctness. It's not used in template and is local to current function. I see no point to place it inside context.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
- Sorry I wasn't clear. I was referring to the whole handling of value lookups and not counter. Both comments applied to the exact same line of code, making it harder.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm sorry, but I can't understand what you mean here. Could you explain a bit more please?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Context.get_val
implements some of the parsing algorithm for looking up a value
- Ideally we only implement the parsing algorithm in one place
- I want to copy the minimal part of a value as possible. Other clients wants to load data from a database (we'd change Context to have a trait to make this possible) and want to load the minimal amount from the database.
So for
{{ posts[5].custom.obj.arr[5] }}
The current implementation requires copying all of posts
before indexing into it.
What'd be amazing is if we could do this
enum PathPart {
ObjectIndex(&str),
ArrayIndex(usize),
}
impl Context {
fn get_val(&self, path: &[PathPart]) -> Option<Value> {
...
}
}
This would allow only the leaf to be copied.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Another use case for when copying will involve a lot of data: cobalt's site.data
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh, I got it. This is a complicated thing actualy. I think we could implement this in current model - where we are not using subexpressions.
But imagine some perfect case, when we could use subexpressions, like posts[ content[2*3] ]. In this case the subexpression should be rendered with the context as a parameter first. This is a main problem Rust is protecting us from: changing the context using the value from inside the context itself. I'm not sure if introducing a trait could solve it.
I see another kind of a solution here: read-only rendering, i.e. when Context doesn't change. Probably even a separate RenderMut
should be introduced maybe to do non-readonly rendering where it's required explicitly and which I beleive is a more rare case than immutable one.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I feel I'm missing something. I'm not seeing why posts[ content[2*3] ]
is a problem, particularly that Rust is protecting us from and what a read-only Context gives us.
My rough run through of how this would work:
For now, this just involves turning the Identifier
path into &[PathPart]
and passing that to Context
.
Long term, when/if we add more complex lookups, we should parse the inner expression first, render it, and make a PathPart
out of it.
This effectively transforms
posts[ content[2*3] ]
into
{% assign _x = 2*3 %}
{% assign _y = content[_x] %}
posts[_y]
(forgive my rough approximation of liquid syntax).
Note: these variables shouldn't actually need to be created in the Context, they should be able to live only in Rust.
and I don't see any problems with doing variable based lookups. When constructing a PathPart
, IdentifierPath
would just do a lookup on the current value.
src/path.rs
Outdated
let options = LiquidOptions::with_known_blocks(); | ||
let template = concat!("array: {{ test_a[0] }}\n", | ||
"complex_dot: {{ test_a[0].test_h }}\n", | ||
"complex_string: {{ test_a[0][\"test_h\"] }}\n"); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could you make these separate tests?
Am I reading this right that the cases can be summarized as:
- identifier_path_array_index
- identifier_path_object_dot
- identifier_path_object_index
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed
src/path.rs
Outdated
assert_eq!(template.render(&mut context).unwrap(), | ||
Some(concat!("array: test_h: 5\n", | ||
"complex_dot: 5\n", | ||
"complex_string: 5\n") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(random place)
Should we make it so all context.get
s are IdentifierPath's?
The reason I'm considering this is we then have a central place of handling identifiers to ensure we are always consistent about how they are handled.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Dup of the first
src/path.rs
Outdated
fn identifier_path() { | ||
let options = LiquidOptions::with_known_blocks(); | ||
let template = concat!("array: {{ test_a[0] }}\n", | ||
"complex_dot: {{ test_a[0].test_h }}\n", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
is test_a[0][var]
a failure?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Whats the status of this?
src/path.rs
Outdated
use token::Token::*; | ||
use error::{Error, Result}; | ||
|
||
#[derive(Debug)] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Clone, Eq, etc?
src/path.rs
Outdated
|
||
match result { | ||
Ok(result) => result.render(context), | ||
Err(e) => Error::renderer(&format!("rendering error: {}", e)), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Dropped, ?
is much better choice, the chained error wouldn't have added much value.
src/path.rs
Outdated
(value, _) if counter == 0 => Ok(value), | ||
(value, _) => { | ||
Error::renderer( | ||
&format!("expected indexable element, but founr '{:?}'", value) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
founr -> found :)
src/path.rs
Outdated
Ok(value) | ||
} | ||
(&Value::Object(_), _) => Error::renderer("bad object index"), | ||
(value, _) if counter == 0 => Ok(value), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for explaining.
I think I missed that this counts down. So you are doing special processing on the last element.
So looking back, it sounds like the outstanding items are:
|
My current fresh rustfmt (0.9.0) refuses to format the lexer.rs as travis requires. The warning will probably will go away as Tavis updates rustfmt. I can fix it to a custom formatting if you want, but it'll be a real pain to maintain this file with newer rustfmt installed. I've also added the I'm sorry, but I currently don't have too much time to make subexpression support and I really think it should be done after grammar-based parser is implemented. My main arguments for deferring it are (I may be wrong here, feel free to discuss it):
|
Huh, for some reason I( thought 0.9.0 used nightly before they decided to switch it over to rustfmt-nightly. Would you prefer to comply with 0.8.6 first and I do the upgrade afterwords or for me to do the upgrade now, possibly causing some rebase pain for you?
I'm sorry if I gave you the impression that you should! I know it'd be complex with the current parser and I'm grateful for what you have done! I don't mean questions to be a passive aggressive way of saying "do this" but as a means to make sure implicit assumptions become explicit so people know why you did what you did and what limitations might exist. |
Preformed the formatting with 0.8.6. And of course I didn't in any way imply that anyone is forcing me to do something and I'm totally no offence about this whole PR. The apology above is more about my personal feeling about work not done as it could be done if I had more time. |
Glad to hear. I just want to make sure I've been communicating clearly to avoid burning people out and driving away contributors. |
Thank you for leaving each commit for me to see the differences between each round of feedback. At this point, could you clean up the commit history and post here when done? Thanks for getting this done! |
Do you mean squashing commits? |
The exact format I leave to you; you know your changes best. My recommendation
So for example, when a commit is made to satisfy the CI, it could probably be squashed. I can go either way on improving error messages and negative indexing being separate commits or squashed, as long as the commit messages are descriptive enough. Exceptions are gladly granted if trying to re-arrange commit history becomes messy due to conflicts. |
Went ahead and did a squash+merge because I'm going to do a release soon. |
This fixes #127 adding feature to access identifiers via index
arr[0]
are supportedobj["child"]
are supported and interchangeable withobj.child
Checklist
cargo test
succeedsrustup run nightly cargo clippy
succeedscargo fmt -- --write-mode=diff
succeeds