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 indexed access to context values #141

Merged
merged 6 commits into from
Nov 7, 2017
Merged

Add indexed access to context values #141

merged 6 commits into from
Nov 7, 2017

Conversation

Albibek
Copy link
Contributor

@Albibek Albibek commented Oct 27, 2017

This fixes #127 adding feature to access identifiers via index

  • array indexes arr[0] are supported
  • object indexes obj["child"] are supported and interchangeable with obj.child
  • index expressions are not yet supported.

Checklist

  • Tests created for any new feature or regression tests for bugfixes.
  • cargo test succeeds
  • rustup run nightly cargo clippy succeeds
  • cargo fmt -- --write-mode=diff succeeds

Copy link
Member

@epage epage left a 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;
};
Copy link
Member

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?

Copy link
Contributor Author

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

Copy link
Member

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");
Copy link
Member

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?

Copy link
Member

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) {
Copy link
Member

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)

Copy link
Contributor Author

@Albibek Albibek Oct 27, 2017

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

Copy link
Member

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()))?;
Copy link
Member

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?

Copy link
Member

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"),
Copy link
Member

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?

Copy link
Member

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)),
Copy link
Member

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")

Copy link
Member

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| {
Copy link
Member

Choose a reason for hiding this comment

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

  1. There a reason you are using counter rather than self.indexes.iter().enumerate()?
  2. 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).

Copy link
Contributor Author

@Albibek Albibek Oct 27, 2017

Choose a reason for hiding this comment

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

  1. I was affraid to make iterator less readable because of more nesting.
  2. 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.

Copy link
Member

Choose a reason for hiding this comment

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

  1. 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.

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'm sorry, but I can't understand what you mean here. Could you explain a bit more please?

Copy link
Member

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

  1. Ideally we only implement the parsing algorithm in one place
  2. 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.

Copy link
Member

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

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, 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.

Copy link
Member

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");
Copy link
Member

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

Copy link
Member

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")
Copy link
Member

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.gets 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.

Copy link
Member

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",
Copy link
Member

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?

Copy link
Member

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)]
Copy link
Member

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)),
Copy link
Member

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)
Copy link
Member

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),
Copy link
Member

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.

@epage
Copy link
Member

epage commented Oct 30, 2017

So looking back, it sounds like the outstanding items are:

  • Fix travis
  • Fix typo while at it :)
  • Respond to is test_a[0][var] a failure?
  • (not blocking submission) Consolidate regular path parsing and indexed path parsing
  • (not blocking submission) Switch to constructing a &[PathPart] and passing that to Context.

@Albibek
Copy link
Contributor Author

Albibek commented Nov 2, 2017

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 should_panic test case to use when subexpressions arrive.

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):

  • I'd like to avoid writing code doing same things twice since expression based parser would make lots of code to be rewritten
  • I'd like to avoid increasing amount of custom token parsing code to force ourselves for parser to happen earlier

@epage
Copy link
Member

epage commented Nov 2, 2017

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.

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, 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.

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.

@Albibek
Copy link
Contributor Author

Albibek commented Nov 3, 2017

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.

@epage
Copy link
Member

epage commented Nov 3, 2017

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.

Glad to hear. I just want to make sure I've been communicating clearly to avoid burning people out and driving away contributors.

@epage
Copy link
Member

epage commented Nov 3, 2017

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?
(force pushes don't always cause email notifications)?

Thanks for getting this done!

@Albibek
Copy link
Contributor Author

Albibek commented Nov 3, 2017

Do you mean squashing commits?

@epage
Copy link
Member

epage commented Nov 3, 2017

Do you mean squashing commits?

The exact format I leave to you; you know your changes best.

My recommendation

  • Each commit should stand on its own
  • Each commit should build cleanly (don't stress trying to test each one)

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.

@epage epage merged commit da06c73 into cobalt-org:master Nov 7, 2017
@epage
Copy link
Member

epage commented Nov 7, 2017

Went ahead and did a squash+merge because I'm going to do a release soon.

epage added a commit that referenced this pull request Nov 8, 2017
* **syntax:** Add `arr[0]` and `obj["name"]` indexing (PR #141, fixes #127)
* **value:**  Add nil value to support foreign data (PR #140)
@epage epage mentioned this pull request Apr 10, 2018
4 tasks
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.

Bug: Array Indexes
2 participants