-
Couldn't load subscription status.
- Fork 223
Subscript syntax for Variables #215
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
Conversation
|
Note: the reason I went with the |
Sources/Variable.swift
Outdated
| current = normalize(current) | ||
|
|
||
| // try to evaluate bit if it's an indirection | ||
| var resolvedBit = bit |
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 do for var bit and mutate bit itself
Sources/Variable.swift
Outdated
| var resolvedBit = bit | ||
| if bit.hasPrefix("$"), | ||
| let resolved = try? Variable(String(bit.dropFirst())).resolve(context), | ||
| let property = resolved { |
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.
maybe better to do resolved as? 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.
I had that first, but things break then if the resolved value is an Int for example (see the tests with array indirect lookup)
|
I love the idea. It's a shame that we can't use subscript for that though
if bit.hasPrefix("[") && bit.hasSuffix("]"),
let resolved = try? Variable(String(bit.dropFirst().dropLast())).resolve(context),
…
|
|
Jinja uses braces for lookup: |
|
Damn so that would still be better to find a way to do that with braces without dot, both because it feels more natural and because it's similar to Jinja… but I guess you tried to find a solution for that already? |
|
I'm honestly not sure how to tackle that without regular expressions, and without blowing this code up into some massive thing. |
03cf1db to
efc5ad3
Compare
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.
The fact that this solution uses o.$p and not o[p] also means that p can't be a compound expression 😢
So we can't express object[property.name|lowercase] for example with the $ syntax (except by using an intermediate {% set %} but that's not as pretty then)
|
I didn't test this code, but this might work: fileprivate func lookup() -> [String] {
return variable.split(separator: ".")
.flatMap({ $0.split(separator: "[", maxSplits: 1) })
.map({ String($0).trim(character: "]") })
}This will though only work on single brackets, like @AliSoftware good point about filter. |
|
Can also try |
|
@ilyapuchka Mmmh interesting idea to use a filter to extract the property. But that would still mean we couldn't apply a filter on that filter 😅 |
|
I really think the right syntax is to use the same syntax as Jinja, the question is how to implement it in Stencil without breaking all the code… |
|
@AliSoftware true. We can for that add support for brackets in filter expressions |
|
I'm working on a solution right now, might take a bit. I'm going to iterate over the characters (in the |
CHANGELOG.md
Outdated
| [Yonas Kolb](https://github.com/yonaskolb) | ||
| [#394](https://github.com/stencilproject/Stencil/pull/214) | ||
| [#214](https://github.com/stencilproject/Stencil/pull/214) | ||
| - Variables now support indirect evaluation. For example, if you have a variable `key = "name"`, and an |
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.
This would be a breaking change right? If so then it should go into ### Breaking section too.
For example, the following that works in Stencil 0.11 would no longer work?:
let schema = [
"$schema": "http://json-schema.org/draft-06/schema#",
"type": "object",
"description" "A geographical coordinate",
"properties": [
"latitude": ["type": "number"],
"longitude": ["type": "number"],
]
]<h2>{{ schema.description|default:"Schema" }}</h2>
<a href="{{ schema.$schema }}">View Schema Specification</a>
<code>
{{ schema|serialiseJSON }}
</code>Perhaps an alternative would be to make this a filter itself instead of overloading special sub-syntax into this, then the same can be achieved by the following syntax which perhaps it a little clearer for the reader of the template:
{{ item|valueForKey:"key" }}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 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.
Yeah, I'm rewriting it to use object[property] syntax like jinja.
258e876 to
3e6bbf2
Compare
|
Since the force push is hiding the commit comments:
We should define the syntax a bit more. To make things a bit easer:
|
Sources/LookupParser.swift
Outdated
| } | ||
|
|
||
| guard !partialBits.isEmpty else { | ||
| throw TemplateSyntaxError("Attempting to dereference empty object in variable '\(variable)'") |
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.
What does this mean? Maybe we can say the same simpler? I guess this handles .. case? The we should just say that this is invalid syntax.
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.
It's more than just .., it could be stuff.., or stuff..other.stuff, or other cases. It's already a TemplateSyntaxError, this just specifies what's wrong.
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.
If I understand code flow correctly here errors will all fall into unexpected '.' in \(variable) kind of description.
Sources/LookupParser.swift
Outdated
| struct LookupParser { | ||
| private var bits = [String]() | ||
| private var current = "" | ||
| private var partialBits = [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.
What partialBits stands for?
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.
In a lookup of my[a.b].stuff[ref][ref2].here, the parser will go through every bit, which might contain bracket syntax. So in this case, the partialBits would end up being:
my[a.b]stuff[ref][ref2]here
It's called bit because the result is a bunch of bits (old name, from before this PR), and partial, because it's still parsing the bit. Naming things is hard, and it's not like stuffBeforeDot is better?
Sources/LookupParser.swift
Outdated
|
|
||
| /// A structure used to represent a template variable, and to resolve it in a given context. | ||
| struct LookupParser { | ||
| private var bits = [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.
Maybe rename it to keyPath?
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.
That'd be more a question for @kylef, what's the reason for the bits naming?
Sources/LookupParser.swift
Outdated
| // when opening the first bracket, we must have a partial bit | ||
| private mutating func openBracket() throws { | ||
| guard !partialBits.isEmpty || !current.isEmpty else { | ||
| throw TemplateSyntaxError("Attempting to dereference an empty object in variable '\(variable)'") |
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.
same as before, if this handles [[ syntax then let's just say that in error message
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.
Same answer as before. [[ might be just one of the cases, we can't assume that.
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.
And here it can be probably unexpected '[' in '\(variable)'
Sources/LookupParser.swift
Outdated
| import Foundation | ||
|
|
||
| /// A structure used to represent a template variable, and to resolve it in a given context. | ||
| struct LookupParser { |
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.
Maybe VariableLookup will be a better name? I would say that's the main purpose of this type where parsing is secondary.
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.
Also probably this can be a class, so you can remove mutating attribute (which will also probably save from unnecessary memory allocations in runtime)
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.
Naming things is hard. Maybe KeypathParser, using your keypath suggestion from before?
Input from others is welcome, so we can reach a consensus on this.
| try context.push(dictionary: ["property": 5]) { | ||
| let variable = Variable("contacts[property]") | ||
| let result = try variable.resolve(context) as? String | ||
| try expect(result).to.beNil() |
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.
it is probably unrelated to this PR, but I wonder if it would be better to raise runtime exceptions in such cases, I find it strange that Stencil just returns nils in such cases, it can be really hard to debug templates with such behaviour. I.e. I think EJS will raise exception in this case, but I don't know what is the common approach in other templating languages.
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 think django/jinja just return nils, like here. It's up to the developer to check for empty cases and handle these accordingly. How would you otherwise check if something exists or not, without triggering an exception?
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 see it like that. Lookup function should return nil if variable is not defined in the context, but if it is array subscript or type introspection it should throw. But even in the first case I don't think nil lookup result should be rendered as empty strings, it should be exception. In Jinja I believe one can check if variable is not nil using is defined ({% if loop.previtem is defined and value > loop.previtem %}). But I'm not very well familiar with Jinja and Django to advocate for that and it is not related to this PR so lets leave it for another time.
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.
Quote from Jinja template docs:
If a variable or attribute does not exist, you will get back an undefined value. What you can do with that kind of value depends on the application configuration: the default behavior is to evaluate to an empty string if printed or iterated over, and to fail for every other operation.
So it returns an invalid value (doesn't throw), even for subscripts, that can be printed, iterated (and tested in ifs I assume), but other operations will fail (exception) for it.
| } | ||
| } | ||
|
|
||
| $0.it("can resolve multiple indirections") { |
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.
copy paste?
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.
derp
| } | ||
| } | ||
|
|
||
| $0.it("can resolve multiple indirections") { |
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 think in implementation you call it as references, I guess it should be used here too
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 not sure if it should be called indirections, references, subscripts, ... 😕
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.
For those with Swift background the most clear name would be probably subscript.
| } | ||
| } | ||
|
|
||
| $0.it("can resolve recursive indirections") { |
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.
nested references?
| "article.author[prop]]", | ||
| "article.author[]", | ||
| "article.author[[]]", | ||
| "article.author[prop][]", |
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.
also article.author[prop]comments (missing . after ])
8d76b79 to
e395a10
Compare
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 think this is awesome 👏
This allows users to refer to variables (and properties) using indirection.
Let's say for example we have the following context:
A user can then write:
To produce: