    /**
     * GraphQL Query builder.
     * Generates a GraphQL query string from a <@code>Function</@code>
     */
    public static class GQLQuery {

        private final ObjectMapper mapper = new ObjectMapper();
        private Function function;

        private GQLQuery() {
            mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
        }

        public static GQLQuery from(Function function) {
            GQLQuery query = new GQLQuery();
            query.function = function;
            return query;
        }

        public String toString() {
            String query =
                    "\"" + function.getType().getName() + "(" + function.getArguments().toQueryArgumentsString() + ")" +
                            "{ " + function.getName() + "( " + function.getArguments().toMethodArgumentsString() + ")" +
                            "{ " + function.getFragment().toString() + "} }\"";

            ObjectNode rootNode = mapper.createObjectNode();
            rootNode.putRawValue("operationName", null);
            rootNode.putRawValue("query", new RawValue(query));
            rootNode.putPOJO("variables", function.getArguments().toVariables());

            return rootNode.toPrettyString();
        }

        public TypeReference<?> getReturnType() {
            return function.getRturnType();
        }

        public String getName() {
            return function.getName();
        }

    }

    //--------------------------------------------------------------
    //---------QueryBuilder Inner classes---------------------------
    //--------------------------------------------------------------

    /**
     * Represents a function argument, and it's details.
     */
    public static class Argument {
        private String name;
        private String type;
        private Object value;
        private boolean optional;
        private boolean ignore;

        private Argument() {
        }

        private Argument(String name, Object value) {
            if (value == null) {//mark argument as ignorable
                ignore = true;
                return;

            } else if ((value instanceof Optional)) {
                if (!((Optional<?>) value).isPresent()) {
                    ignore = true;
                    return;
                }
                this.optional = true;
                value = ((Optional<?>) value).get();
            }

            this.name = name;
            this.value = value;
            this.type = this.value.getClass().getSimpleName();
        }

        public static Argument of(String name, Object value) {
            return new Argument(name, value);
        }

        public String getName() {
            return name;
        }

        public Object getValue() {
            return value;
        }

        public String getType() {
            return type;
        }

        public boolean isOptional() {
            return optional;
        }

        public boolean isIgnore() {
            return ignore;
        }
    }

    /**
     * A wrapper for the function arguments.
     */
    public static class Arguments {
        private List<Argument> arguments = new ArrayList<>();


        private Arguments() {
        }

        private Arguments(Argument... arguments) {
            this.arguments = Arrays.asList(arguments);
        }

        public static Arguments of(Argument... arguments) {
            return new Arguments(arguments);
        }

        /**
         * Builds the arguments for graphql FUNCTION.
         * Ex: getUser(firstName: $firstName, lastName: $lastName)...
         *
         * @return graphql function arguments with parameters as variables.
         */
        public String toMethodArgumentsString() {
            StringBuilder sb = new StringBuilder();
            arguments.forEach(arg -> {
                if (!arg.isIgnore()) { //do not build arg if marked as ignorable
                    sb.append(arg.getName()).append(": $").append(arg.getName()).append(", ");
                }
            });
            sb.deleteCharAt(sb.lastIndexOf(","));
            return sb.toString();
        }

        /**
         * Builds the arguments for graphql QUERY type.
         * Ex: mutation($firstName: String, $lastName: String)...
         *
         * @return graphql query arguments with type.
         */
        public String toQueryArgumentsString() {
            StringBuilder sb = new StringBuilder();

            arguments.forEach(arg -> {
                if (!arg.isIgnore()) {
                    sb.append("$")
                            .append(arg.getName())
                            .append(": ")
                            .append(arg.getType())
                            .append(arg.isOptional() ? "" : "!")
                            .append(", ");
                }
            });
            sb.deleteCharAt(sb.lastIndexOf(","));
            return sb.toString();
        }

        /**
         * Builds a map of variables that can be passed to GQL query in the variables JSON element.
         *
         * @return a map of variables.
         */
        public HashMap<String, Object> toVariables() {
            HashMap<String, Object> variables = new HashMap<>();
            arguments.forEach(arg -> {
                if (!arg.isIgnore()) {
                    variables.put(arg.getName(), arg.getValue());
                }
            });

            return variables;
        }
    }

    /**
     * Represents a filed information from a GraphQL fragment.
     */
    public static class FragmentField {
        private String name;
        private List<FragmentField> fieldList = new ArrayList<>();

        private FragmentField() {
        }

        public static FragmentField of(String name) {
            FragmentField fragmentField = new FragmentField();
            fragmentField.name = name;
            return fragmentField;
        }

        public static FragmentField of(String name, FragmentField... fields) {
            FragmentField fragmentField = new FragmentField();
            fragmentField.name = name;
            fragmentField.fieldList.addAll(Arrays.asList(fields));
            return fragmentField;
        }

        public String getName() {
            return name;
        }

        public List<FragmentField> getFieldList() {
            return fieldList;
        }
    }

    /**
     * GraphQL function types.
     */
    public enum FunctionType {
        Query("query"),
        Mutation("mutation");

        private final String name;

        FunctionType(final String name) {
            this.name = name;
        }

        public String getName() {
            return name;
        }
    }


    /**
     * This is a wrapper that represent a GraphQL function.
     * It contains all the information in order to build a query.
     */
    public static class Function {
        private TypeReference<?> resultType;
        private FunctionType type;
        private String name;
        private Arguments arguments;
        private ResultFragment resultFragment;

        private Function() {
        }

        public Function(FunctionType type, String name) {
            this.name = name;
            this.type = type;
        }

        public Function returnType(TypeReference<?> resultType) {
            this.resultType = resultType;
            return this;
        }

        public Function arguments(Argument... arguments) {
            this.arguments = Arguments.of(arguments);
            return this;
        }

        public Function resultFragment(FragmentField... fields) {
            this.resultFragment = ResultFragment.of(fields);
            return this;
        }


        public TypeReference<?> getRturnType() {
            return resultType;
        }

        public String getName() {
            return name;
        }

        public Arguments getArguments() {
            return arguments;
        }

        public ResultFragment getFragment() {
            return resultFragment;
        }

        public FunctionType getType() {
            return type;
        }
    }

    /**
     * GraphQL expected result type after the query is executed.
     * Note: This is pure informatory - as this type may or not be used by the user after the HTTP call.
     * However, it is useful to know the expected result type of <@code>Function</@code> that we want to execute.
     */
    public static class ResultFragment {
        private List<FragmentField> fields = new ArrayList<>();

        private ResultFragment() {
        }

        public static ResultFragment of(FragmentField... fields) {
            ResultFragment resultFragment = new ResultFragment();
            resultFragment.fields = Arrays.asList(fields);

            if (resultFragment.fields.size() == 0) {
                throw new IllegalArgumentException("Fragment should have at list one output field");
            }
            return resultFragment;
        }

        public String toString() {
            return getFieldString(fields);
        }

        private String getFieldString(List<FragmentField> fields) {
            StringBuilder sb = new StringBuilder();
            fields.forEach(field -> {
                sb.append(field.getName()).append(" ");
                if (field.getFieldList() != null && !field.getFieldList().isEmpty()) {
                    sb.append("{ ").append(getFieldString(field.getFieldList())).append(" } ");
                }
            });
            return sb.toString();
        }
    }

