bkiers / Liqp

An ANTLR based 'Liquid Template' parser and rendering engine.
MIT License
165 stars 94 forks source link

Breaking change in release 0.7.9 caused by new renderer helper #162

Closed pmenhart closed 3 years ago

pmenhart commented 5 years ago

I am scratching my head about solving the problem #132 by copying the whole map to JSON and back to map, as implemented in #136 (details on line 347-348 ).

Performance impact aside (double transformation is expensive, especially if only a small subset of values is needed in the template), this change has serious compatibility consequences.

In the old version, scalar values are retrieved using Map.get() method (in liqp.nodes.LookupNode.Indexable#get).

Since PR #136, this mechanism is replaced with calling parseSettings.mapper.convertValue(), dumping all variables to Jackson representation. Under the hood, Map.entrySet() is called. Any key not returned by entrySet is assigned an empty value.

What are the consequences? Any instance of a class implementing Map interface can be added to template variables. For example, we are using this approach with wrappers around a large and deep hierarchy of domain classes. Each map-wrapper has a get() method to provide proxied or dynamically calculated values, possibly returning other map instances at a lower level of hierarchy. Some get() calculations are very costly, but only the values that are really needed in rendering will be called. Returning all values via entrySet() is prohibitively expensive, making release 0.7.9 unusable.

Instead of double dumping the whole map hierarchy, wouldn't it be much simpler to enhance liqp.nodes.LookupNode.Indexable to deal with POJO objects? If value.class is neither Map nor TemplateContext, then try a getter method or a public field to obtain the "index".

This would elegantly solve the MapWithPojos problem given in #132. Unfortunately there wasn't much discussion in that ticket - please let me know whether I am missing something.

msangel commented 5 years ago

Have only one opinion: It's better to keep everything as Lists and Maps in data models, so all the stuff around (filters for arrays, and so on) will have limited types of entities to work with.

Regarding that fact that this is breaking change - agree, but anycase before it haven't worked as expected.

msangel commented 5 years ago

For example: open implementation of map filter. And now assume that we must handle raw properties of POJOs... When all is map or list/array - its much simple here.

pmenhart commented 5 years ago

If I understand your opinion correctly, POJOs should not be allowed in data models. Instead of library being responsible for that, the caller has to explicitly convert the POJO object to a map (or wrap the object with a Map wrapper, as I mentioned above).

I rewrote example from #132 with an explicit user conversion. For convenience, I am using com.fasterxml.jackson.databind.ObjectMapper already available as liqp dependency:

    static class Foo {
        public String a = "A";
    }

    private Map pojo2Map(Object o) {
        ObjectMapper objectMapper = new ObjectMapper();
        ObjectNode oNode = objectMapper.convertValue(o, ObjectNode.class);
        return objectMapper.convertValue(oNode, Map.class);
    }

    @Test
    public void renderMapWithPojosExistedThrowError() {

        Map<String, Object> data = new HashMap<String, Object>();
        data.put("foo", pojo2Map(new Foo()));
        String fooA = Template.parse("{{foo.a}}").render(data);

        assertThat(fooA, is("A"));
    }

Is that what you meant by "keep everything as Lists and Maps in data models"?

msangel commented 5 years ago

In my opinion, the view objects should not contain anything that is not serializable(i.e. cannot be converted by mapper to data tree). And this looks essential for me. Noone tries to put, for example, the Thread object to Spring MVC Model, or mentioned above map with lazy computing.

That MR do not rejecting POJOs, it's making opposite: allow POJOs in data model - reading its properties, iterate it's iterable fields, etc.

The POJOs must be in model, but they must be just data containers, not more, they must be logic-less.

bkiers commented 4 years ago

Hey all, I now just see these open issues... Will look into them this week.

msangel commented 4 years ago

@pmenhart Ok, I think I found tradeoff for solving your problem (allow lazy objects in data model) and another my problem(real object may contain more fields then possible/need to serialize) and easiness of support custom types in filters/nodes. Here's sample implementation: https://github.com/msangel/Liqp/commit/2ba7a2ddac041b1881af9c10763d52a5d4e5b549

In general: 1) I still serialize everything to JSON tree as this way we can go safe and deep properties access from all filters/nodes. 2) The parsed tree is inspected for specific pattern. 3) Based on the pattern I do replace plain Map object with specific type. You can use your own type. It just needs to extend LiquidSupport class. And your corresponding object will be created. 3) This LiquidSupport class support lazy computations. 4) The engine need to be aware only about one new possible type only: LiquidSupport. Example of change in mentioned LookupNode here

Test case with all the above: LiquidSupportTest.java

If this is ok, then whats needs to be added: 1) a bit batter custom type creation (I don't like constructor with Map parameter, probably will use factory/builder) 2) add new type support to all nodes/filters (this might tale a while but still doable in comparison of support all possible types everywhere) 3) tests

msangel commented 4 years ago

Also if you are good in manipulate Jackson internals, you can alter my code and replace abstract class with interface.


Another possible simplification is keep in the interface only Map toLiquid method and allow lazyness to be implemented on that.

pmenhart commented 4 years ago

Core of the problem is "I still serialize everything to JSON tree". The library is trying to outsmart the user, bringing incompatibility and considerable performance penalty as a consequence. My suggestion is to leave the decision to the user. If their data need transformation or wrappers, then let the user to massage the data. The library could contain support for such transformation as utility methods (with examples and documentation), but should not perform the transformation by default.

User may decide to use JSON serialization, or your proposed enrichment, or a custom wrapper - their choice. They may decide to transform the whole map, or only selected keys - based on their specific needs. Sanity of data is their responsibility. The library can offer them crutches, but not walk for them.

msangel commented 4 years ago

So your idea is to reject access POJOs properties in nodes/filters forcing users to use data model only with java.util.Map if they will want such possibility?

pmenhart commented 4 years ago

Well, I suggested to support POJO properties in nodes, see above. You opposed: "It's better to keep everything as Lists and Maps in data models". Perhaps we just misunderstood each other here.

Core difference between our approaches is lazy vs. eager evaluation of data model. If @bkiers or broader community wants eager evaluation unconditionally baked into the library, then such code should be released as 0.8.X. I'll prepare a PR to keep versions 0.7.X compatible with lazy evaluation prior to 0.7.9. If data transformation is needed, then the user has to transform the model outside the library. JSON serialization, our MapWrapper, or your LiquidSupport enhancer should be added to the library as helpers - but the user has to choose them consciously and explicitly (based on their needs).

msangel commented 4 years ago

Even if the eager evaluation of data model is preferable and looks essential for me, it has own drawback: everything is serialized. And the trick is that not everything can be serialized. So I agree with you in that that the serialization should be matter of choice(and lets the user takes the responsibility for serialization problem).

And 2 options for this: 1) add rendering options ('lazy' vs 'eager') 2) add marker interface for objects that should use eager evaluation (and the LiquidSupport in for I have proposed will extend that one)

pmenhart commented 4 years ago

I like the idea of rendering options ('lazy' vs 'eager'). Note that I created PR #163 with a hope it will be useful regardless of which decision will be made here.

msangel commented 4 years ago

Still don't clear how to implement support of 'map' filter/property access for POJO in case of 'lazy'. The Jackson convert the data tree entire, so we cannot to postpone the POJO's child serialization to later time.

So that's why I vote for marker interface. As we can make a mix of: 1)objects that will not be serialized and so inspected; 2) java.util.Map - these will be inspected via their API; 3) marked objects that will be converted to Jackson tree once accessed and further processed as java.util.Map

msangel commented 4 years ago

I found even better solution: https://github.com/msangel/Liqp/commit/bfd8c8f736e80ab545914eff080a1da8b0088d14 This will allow: 1) Object to be inspectable by marking them Inspectable. These can be at any place of data model and only these objects will be converted to data tree. 2) LiquidSupport that extends Inspectable but do not make any conversions, so the result of toLiquid will not be converted to data tree. That fact that the result is java.util.Map is enough for top-level properties access. 3) new rendering option: enum EvaluationMode {LAZY, EAGER} as discussed above 4) Default EvaluationMode is LAZY so full backward compatibility.

The new interface must have appropriate support in nodes/filters. So if this solution ok, I will prepare new MR to this master and will make changes in my existed MRs (as these can rely on the broken compatibility)

And unexpected question to @bkiers : why the mapper is inside of ParseSettings object but not RenderSettings? Data converting/processing happened on render step... The parse step should only care about input template. It's making sense in some future (because this is breaking change too) to move the mapper into more appropriate place.

bkiers commented 4 years ago

If @bkiers or broader community wants eager evaluation unconditionally baked into the library, then such code should be released as 0.8.X.

~Yes, I agree with that. @msangel are you OK if I merge @pmenhart's PR https://github.com/bkiers/Liqp/pull/163 and you base your lazy/eager rendering options then on develop?~

EDIT: oh, now I see https://github.com/bkiers/Liqp/pull/164

@pmenhart I guess that PR will make your PR https://github.com/bkiers/Liqp/pull/163 redundant, right?

[...] why the mapper is inside of ParseSettings object but not RenderSettings?

No particular reason. RenderSettings was introduced later and I never thought about needing a mapper in there 🤷‍♂

msangel commented 3 years ago

default behavior was reverted in master