Skip to content

Conversation

@andimarek
Copy link
Member

@andimarek andimarek commented Dec 1, 2018

The goal of this PR to make all Nodes immutable, but "modifying" the Ast should be still easy to.

This means that you can easily get a new tree when you modify a Node, even if the Node is deep inside the tree.

contains contribution from @felipe-gdr :-)

@andimarek andimarek added this to the 12.0 milestone Dec 1, 2018
add NodePosition to general Traverser
AstTransformer WIP
renamed ChildContainer to NodeChildContainer
general named children in Traverser
@andimarek andimarek added the breaking change requires a new major version to be relased label Dec 2, 2018
@andimarek
Copy link
Member Author

andimarek commented Dec 2, 2018

There still might be some improvements, but I expect that all major parts for that change are in place:

Every Node is now immutable.

It introduces the concept of a Zipper: basically a pointer to a specific Node in a immutable Tree which also keeps track of where where the current Node is in respect to the parent. (see here )

Some background details: the problem with an immutable Ast is, that it is very easy to change a Node and all the children, but is very hard then also to change the parents referencing the node you just changed, because a Node just knows about its children, nothing about it parent. Zipper is a general concept which helps with that. I can recommend http://learnyouahaskell.com/zippers as a more detailed explanation. I also took inspiration in implementing it from this book. (Warning: it actually talks about Haskell and not Java)

Additionally there is the class AstMultiZipper which is a list of Zippers and allows keeping track of multiple changes in a Tree at the same time.

AstTransformer now allows for modifying the Ast (Document) and getting a new Tree back in a very convenient way by leveraging AstMultiZipper.

It will look like this:

def document = TestUtil.parseQuery("{ root { foo { midA { leafA } midB { leafB } } bar { midC { leafC } midD { leafD } } } }")
AstTransformer astTransformer = new AstTransformer()
def visitor = new NodeVisitorStub() {
@Override
TraversalControl visitField(Field node, TraverserContext<Node> context) {
if (!node.name.startsWith("mid")) {
return TraversalControl.CONTINUE
}
String newName = node.name + "-modified"
Field changedField = node.transform({ builder -> builder.name(newName) })
changeNode(context, changedField)
return TraversalControl.CONTINUE
}
}
when:
def newDocument = astTransformer.transform(document, visitor)

Feedback welcome.

Also: Now that every Node is immutable you can't change a query anymore at any point. If you have some use cases which requires changing the query after it was create, please comment here. We wanna offer specific hooks where a query can be modified.

@andimarek
Copy link
Member Author

Ping @tinnou for feedback. thanks

}
ExecutionResultNode newNode = parent.withNewChildren(newChildren);
return new ExecutionResultZipper(newNode, restBreadcrumbs);
}
Copy link
Member

Choose a reason for hiding this comment

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

TODO ^

Copy link
Member Author

Choose a reason for hiding this comment

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

done

private Map<ExecutionResultNode, List<ExecutionResultZipper>> zipperWithSameParent(List<ExecutionResultZipper> zippers) {
//todo: change to LinkedHashMap
return zippers.stream().collect(Collectors.groupingBy(ExecutionResultZipper::getParent));
}
Copy link
Member

Choose a reason for hiding this comment

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

TODO ^

I think you put in a supplier of the Map

Copy link
Member Author

Choose a reason for hiding this comment

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

done

Copy link
Member

Choose a reason for hiding this comment

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

Yeah I reckon create its own helper method

curZippers.removeAll(deepestZippers);
curZippers.addAll(newZippers);
}
assertTrue(curZippers.size() == 1, "illegal state");
Copy link
Member

Choose a reason for hiding this comment

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

better assertion message - eg state the precondition better or have no message

Copy link
Member Author

Choose a reason for hiding this comment

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

done

private final String name;
private final List<Argument> arguments = new ArrayList<>();

private static final String CHILD_ARGUMENTS = "arguments";
Copy link
Member

Choose a reason for hiding this comment

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

not sure on the CHILD_ smurfing

The thing that represents "arguments" could be called arguments

throw new IllegalArgumentException("Cannot pass non-empty newChildren to Node that doesn't hold children");
}

return this;
Copy link
Member

Choose a reason for hiding this comment

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

This one does not seem to be finished.

Is this a BUG?

Copy link
Member Author

Choose a reason for hiding this comment

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

well: you can't add children to a FloatValue, so we only allow empty children here

Copy link
Member

Choose a reason for hiding this comment

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

I would create a helper method for these checks

@Override
    public FloatValue withNewChildren(NodeChildrenContainer newChildren) {
        NodeChildrenContainer.assertEmpty(newChildren);
        return this;
    }

Copy link
Member Author

Choose a reason for hiding this comment

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

done via helper method

}

return this;
}
Copy link
Member

Choose a reason for hiding this comment

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

Yeah - defintely common method if this is needed

@Override
public NullValue withNewChildren(NodeChildrenContainer newChildren) {
       return NodeChildrenContainer.assertNotEmpty(newChildren);
}

Copy link
Member Author

Choose a reason for hiding this comment

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

refactored it

/**
* PRIVATE: Used by {@link Traverser}
*/
void setCurAccValue(Object curAccValue) {
Copy link
Member

Choose a reason for hiding this comment

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

bad Javadoc - its going to lead to warnings and set of my OCD on having a clean command line compile / build

Copy link
Member Author

Choose a reason for hiding this comment

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

changed it no java doc

}
NodePosition that = (NodePosition) o;
return index == that.index &&
Objects.equals(name, that.name);

This comment was marked as outdated.

def midA = rootField.selectionSet.children[0] as Field
def leafA = midA.selectionSet.children[0] as Field
def leafB = midA.selectionSet.children[0] as Field

Copy link
Member

Choose a reason for hiding this comment

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

I think the indices here are wrong.

 def leafB = midA.selectionSet.children[1] as Field

here and below

Copy link
Member Author

Choose a reason for hiding this comment

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

fixed


def midB = rootField.selectionSet.children[1] as Field
def leafC = midB.selectionSet.children[0] as Field
def leafD = midB.selectionSet.children[0] as Field
Copy link
Member

Choose a reason for hiding this comment

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

indices are wrong I think

Copy link
Member Author

Choose a reason for hiding this comment

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

fixed

return NodeChildrenContainer.newNodeChildrenContainer()
.child(CHILD_VALUE, value)
.build();
}
Copy link
Member

Choose a reason for hiding this comment

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

I would do static import in all the places to reduce the boiler plate

@Override
    public NodeChildrenContainer getNamedChildren() {
        return newNodeChildrenContainer()
                .child(CHILD_VALUE, value)
                .build();
    }

Copy link
Member Author

Choose a reason for hiding this comment

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

done

import java.util.Objects;

@PublicApi
public class AstBreadcrumb {
Copy link
Member

Choose a reason for hiding this comment

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

I woukld still love to see JavaDoc at the top to outline the class.

Copy link
Member Author

Choose a reason for hiding this comment

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

added

import static java.util.stream.Collectors.toList;

@PublicApi
public class AstMultiZipper {
Copy link
Member

Choose a reason for hiding this comment

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

JavaDoc

Copy link
Member Author

Choose a reason for hiding this comment

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

added

@@ -0,0 +1,85 @@
package graphql.language;
Copy link
Member

Choose a reason for hiding this comment

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

Should all this transformation code be in graphql.language or one down in say graphql.language.transformation ?

I think the latter

Copy link
Member Author

Choose a reason for hiding this comment

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

AstZipper and AstMultiZipper is actually not coupled to transformations: it is a general concept.

the only two transformation classes are AstTransformer and AstTransformerUtil. Extra package for them?

}

public AstZipper moveUp() {
return null;
Copy link
Member

Choose a reason for hiding this comment

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

is this not done???

Copy link
Member

Choose a reason for hiding this comment

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

and hence not tested??

Copy link
Member Author

Choose a reason for hiding this comment

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

I removed all not used methods for now.


@Override
public IntValue withNewChildren(NodeChildrenContainer newChildren) {
assertNewChildrenAreEmpty(newChildren);
Copy link
Member

Choose a reason for hiding this comment

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

Are you have one. Do it on FLoatValue

Copy link
Member Author

Choose a reason for hiding this comment

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

done ... I forgot FloatValue somehow

*/
List<Node> getChildren();

NodeChildrenContainer getNamedChildren();
Copy link
Member

Choose a reason for hiding this comment

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

JavaDoc

Copy link
Member Author

Choose a reason for hiding this comment

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

added

import static graphql.Assert.assertNotNull;

@PublicApi
public class NodeChildrenContainer {
Copy link
Member

Choose a reason for hiding this comment

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

JavaDoc?

Maybe a seperate PR for the JavaDoc say

Copy link
Member Author

Choose a reason for hiding this comment

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

added

leave: { TraverserContext context ->
TraversalControl.CONTINUE
}
] as TraverserVisitor
Copy link
Member

Choose a reason for hiding this comment

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

what is this crazy syntax?? I didnt know you could do this! :)

Copy link
Member Author

Choose a reason for hiding this comment

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

a groovy map with closures/functions as value ... not so crazy, or?

@andimarek andimarek merged commit 44627e4 into master Dec 15, 2018
@kaqqao
Copy link
Contributor

kaqqao commented Dec 19, 2018

Now that every Node is immutable you can't change a query anymore at any point. If you have some use cases which requires changing the query after it was create, please comment here.

Perhaps I'm too late, but I'm aware of users that allow queries for objects without a subselection, by modifying the query on the fly to include all the direct fields in such cases. This was done via an instrumentation.
Is this use case still possible?

@kaqqao
Copy link
Contributor

kaqqao commented Dec 20, 2018

Ah, just seen #1345 which caters exactly to this use-case :)

@andimarek andimarek deleted the zipper2 branch January 14, 2019 04:14
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

breaking change requires a new major version to be relased

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants