StencilSwiftKit icon indicating copy to clipboard operation
StencilSwiftKit copied to clipboard

Map only works with strings

Open cornr opened this issue 6 years ago • 12 comments

I am trying to filter an array using map. In this case I filter Sourcery variables:

{% map type.allVariables into vars using variable %}{% if variable.readAccess != "private" and variable.readAccess != "fileprivate" and not variable.isComputed %}{{ variable }}{% endif %}{% endmap %}

Afterwards I want to print these filtered vars. {% for variable in vars where variable != "" %}{{ variable.name }}: {{ variable.name }}{% if not forloop.last %}, {% endif %}{% endfor %}

A get following Exception:

Terminating app due to uncaught exception 'NSUnknownKeyException', reason: '[<TtGCs19_SwiftStringStorageVs6UInt16 0x7fb2a2df64f0> valueForUndefinedKey:]: this class is not key value coding-compliant for the key name.'

My guess is the problem lies here: https://github.com/SwiftGen/StencilSwiftKit/blob/d5f90b5d33f277e7cce5b51bd1694734fa5f941f/Sources/StencilSwiftKit/MapNode.swift#L71

The MapNode expects the mapped 0utput to be a String. I could dig into this and provide a fix if you recognize it as an issue.

cornr avatar Jan 10 '19 13:01 cornr

The thing is that for Stencil, everything that you output is a String. The result of a map iteration will always be a String. What you're doing here is actually mapping your array of variables to a list of variable descriptions and empty strings (for those that don't match your if)

What you could do instead is filter in the for loop:

{% for variable in vars where variable.readAccess != "private" and variable.readAccess != "fileprivate" %}{{ variable.name }}: {{ variable.name }}{% if not forloop.last %}, {% endif %}{% endfor %}

djbe avatar Jan 10 '19 14:01 djbe

Ok. The filter in the for loop is what I am doing right now. And sure it does work. But I use this for loop and therefore the filter several times and the condition is a lot more complex. Thats why I want to filter the array once and then iterate over it at different places.

cornr avatar Jan 10 '19 14:01 cornr

What's useful then is to separate that filter logic into a macro, which returns "true" or "false", and set a variable to that result inside your for loop. Then check the value with an if test:

{% macro myMacro variable %}{% filter removeNewlines:"all" %}
{% if some long test %}true{%else %}false{% endif %}
{% endfilter %}{% endmacro %}

...

{% for variable in vars %}{% filter removeNewlines:"leading" %}
  {% set shouldShow %}{% call myMacro variable %}{% endset %}
  {% if shouldShow == "true" %}
    {{ variable.name }}: {{ variable.name }}
    {% if not forloop.last %}, {% endif %}{% endfor %}
  {% endif %}
{% endfilter %}{% endfor %}

djbe avatar Jan 10 '19 14:01 djbe

Thanks for that pattern. I am gonna try this tomorrow.

cornr avatar Jan 10 '19 14:01 cornr

PS: just missing an {% endfilter %} in the macro and for blocks of your example @djbe 😉

AliSoftware avatar Jan 10 '19 14:01 AliSoftware

@AliSoftware I don't know about anything :trollface:

djbe avatar Jan 10 '19 14:01 djbe

@djbe You can't hide 😄 image

AliSoftware avatar Jan 10 '19 14:01 AliSoftware

This works. Thanks. But the {% if not forloop.last %}, {% endif %}does not work anymore since we don't now if the last variable is filtered by the macro.

PS: just an {% endfor %} after {% if not forloop.last %}, {% endif %} too much in your example @djbe 😉

cornr avatar Jan 11 '19 09:01 cornr

One idea I pitched in our Slack was to create a new filter named call in StencilSwiftKit, that would take as a parameter the name of a 1-arity macro defined in your template, and call it like the call tag does.

That would allow you to use the filter syntax to call macros that take one parameter, like this {{ myvariable|call:"myMacro" }} … as an alternative way of using the set + call tags syntax {% set shouldShow %}{% call myMacro myvariable %}{% endset %}

note that this filter would only accept macros that have exactly one parameter then, and should throw a TemplateError otherwise

If we decide to add such a convenience filter in StencilSwiftKit, that means the code suggested by @djbe would become something like this:

{% for variable in vars where variable|call:"myMacro" == "true" %}{% filter removeNewlines:"all" %}
    {{ variable.name }}: {{ variable.name }}
{% if not forloop.last %}, {% endif %}
{% endfilter %}{% endfor %}

And given that you'd be able to use the "for where" syntax in the for tag, I think that would fix the issue you mention with the forloop.last too!

If you feel like this addition would be interesting and worth it, we'd welcome a PR to add it 😉

AliSoftware avatar Jan 11 '19 10:01 AliSoftware

That would a nice addition as the filtering is would ne named and separated from the template. I am going to experiment with this

cornr avatar Jan 11 '19 14:01 cornr

I think this can not be implemented as a filter since

protocol FilterType {
    func invoke(value: Any?, arguments: [Any?]) throws -> Any?
}

does not get the context to resolve the CallableBlock from the macro tag.

cornr avatar Jan 11 '19 16:01 cornr

@cornr https://github.com/stencilproject/Stencil/pull/203 was merged a while ago that adds a context: Context parameter to the invoke function, so that shouldn't be an issue. It hasn't been released yet, but you can test the latest "master" branch of Stencil.

djbe avatar Jan 11 '19 22:01 djbe

@djbe @cornr it's been a long time but do you remember whether you got this working? I read in the Stencil docs, "The equality operator only supports numerical, string and boolean types." So when I attempt to evaluate something like, {* if myVariable == "true" *} it doesn't ever evaluate to true because myVariable isn't a string. Suggestions?

claire-lynch-okcupid avatar Mar 23 '23 18:03 claire-lynch-okcupid

Sorry @claire-lynch-okcupid I don't know what was the outcome of this. Unfortunately I did not work with StencilKit lately.

cornr avatar Mar 24 '23 07:03 cornr