All Downloads are FREE. Search and download functionalities are using the official Maven repository.

io.basestar.graphql.GraphQLUtils Maven / Gradle / Ivy

package io.basestar.graphql;

/*-
 * #%L
 * basestar-graphql
 * %%
 * Copyright (C) 2019 - 2020 Basestar.IO
 * %%
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 *      http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 * #L%
 */

import com.google.common.collect.ImmutableSet;
import com.google.common.io.BaseEncoding;
import graphql.GraphQLContext;
import graphql.execution.ExecutionContext;
import graphql.language.*;
import io.basestar.auth.Caller;
import io.basestar.expression.Expression;
import io.basestar.schema.*;
import io.basestar.schema.use.*;
import io.basestar.util.Path;

import java.util.*;
import java.util.stream.Collectors;

public class GraphQLUtils {

    public static final String MAP_KEY = Reserved.PREFIX + "key";

    public static final String MAP_VALUE = Reserved.PREFIX + "value";

    private static final SelectionSet EMPTY_SELECTION = SelectionSet.newSelectionSet().build();

    public static Set paths(final InstanceSchema schema, final SelectionSet selections) {

        return selections.getSelections().stream()
                .flatMap(v -> paths(schema, v).stream())
                .collect(Collectors.toSet());
    }

    public static Set paths(final InstanceSchema schema, final Selection selection) {

        if(selection instanceof Field) {
            final Field field = (Field)selection;
            final String name = field.getName();

            if(schema.metadataSchema().containsKey(name)) {
                return Collections.singleton(Path.of(name));
            }

            if(schema instanceof Link.Resolver) {
                final Link link = ((Link.Resolver) schema).getLink(name, true);
                if(link != null) {
                    return paths(link.getSchema(), field.getSelectionSet()).stream()
                            .map(v -> Path.of(name).with(v)).collect(Collectors.toSet());
                }
            }
            final Property property = schema.requireProperty(name, true);
            return paths(property.getType(), Path.of(name), field.getSelectionSet());

        } else{
            throw new IllegalStateException();
        }
    }

    public static Path path(final String name) {

        // FIXME: if we do this properly then we don't need to use the reserved prefix in map key/value
        return Path.of(Arrays.stream(name.split("/"))
                .map(v -> {
                    if(v.equals(GraphQLUtils.MAP_VALUE)) {
                        return "*";
                    } else {
                        return v;
                    }
                }).filter(v -> !v.startsWith(Reserved.PREFIX))
                .toArray(String[]::new));
    }

    protected static Set paths(final Use type, final Path parent, final SelectionSet selections) {

        return type.visit(new Use.Visitor>() {
            @Override
            public Set visitBoolean(final UseBoolean type) {

                return ImmutableSet.of(parent);
            }

            @Override
            public Set visitInteger(final UseInteger type) {

                return ImmutableSet.of(parent);
            }

            @Override
            public Set visitNumber(final UseNumber type) {

                return ImmutableSet.of(parent);
            }

            @Override
            public Set visitString(final UseString type) {

                return ImmutableSet.of(parent);
            }

            @Override
            public Set visitEnum(final UseEnum type) {

                return ImmutableSet.of(parent);
            }

            @Override
            public Set visitRef(final UseRef type) {

                return paths(type.getSchema(), selections).stream()
                        .map(parent::with).collect(Collectors.toSet());
            }

            @Override
            public  Set visitArray(final UseArray type) {

                return paths(type.getType(), parent, selections);
            }

            @Override
            public  Set visitSet(final UseSet type) {

                return paths(type.getType(), parent, selections);
            }

            @Override
            public  Set visitMap(final UseMap type) {

                final Set paths = new HashSet<>();
                for(final Selection selection : selections.getSelections()) {
                    final Field field = (Field)selection;
                    final String name = field.getName();
                    if(MAP_KEY.equals(name)) {
                        paths.add(parent.with("*"));
                    } else if(MAP_VALUE.equals(name)) {
                        paths.addAll(paths(type.getType(), parent.with("*"), field.getSelectionSet()));
                    }
                }
                return paths;
            }

            @Override
            public Set visitStruct(final UseStruct type) {

                return paths(type.getSchema(), selections).stream()
                        .map(parent::with).collect(Collectors.toSet());
            }

            @Override
            public Set visitBinary(final UseBinary type) {

                return ImmutableSet.of(parent);
            }
        });
    }

    public static Map fromRequest(final InstanceSchema schema, final Map input) {

        if(input == null) {
            return null;
        } else {
            final Map result = new HashMap<>();
            schema.getAllProperties().forEach((k, prop) -> result.put(k, fromRequest(prop.getType(), input.get(k))));
            return result;
        }
    }

    protected static Object fromRequest(final Use type, final Object value) {

        if(value == null) {
            return null;
        } else {
            return type.visit(new Use.Visitor() {
                @Override
                public Object visitBoolean(final UseBoolean type) {

                    return type.create(value);
                }

                @Override
                public Object visitInteger(final UseInteger type) {

                    return type.create(value);
                }

                @Override
                public Object visitNumber(final UseNumber type) {

                    return type.create(value);
                }

                @Override
                public Object visitString(final UseString type) {

                    return type.create(value);
                }

                @Override
                public Object visitEnum(final UseEnum type) {

                    return type.create(value);
                }

                @Override
                public Object visitRef(final UseRef type) {

                    return type.create(value);
                }

                @Override
                public  Object visitArray(final UseArray type) {

                    return ((Collection)value).stream()
                            .map(v -> fromRequest(type.getType(), v))
                            .collect(Collectors.toList());
                }

                @Override
                public  Object visitSet(final UseSet type) {

                    return ((Collection)value).stream()
                            .map(v -> fromRequest(type.getType(), v))
                            .collect(Collectors.toSet());
                }

                @Override
                @SuppressWarnings("unchecked")
                public  Object visitMap(final UseMap type) {

                    final Map result = new HashMap<>();
                    ((Collection>)value).forEach(v -> {
                        final String key = (String)v.get(GraphQLUtils.MAP_KEY);
                        final Object value = fromRequest(type.getType(), v.get(GraphQLUtils.MAP_VALUE));
                        result.put(key, value);
                    });
                    return result;
                }

                @Override
                @SuppressWarnings("unchecked")
                public Object visitStruct(final UseStruct type) {

                    return fromRequest(type.getSchema(), (Map)value);
                }

                @Override
                public Object visitBinary(final UseBinary type) {

                    return type.create(BaseEncoding.base64().decode(value.toString()));
                }
            });
        }
    }

    @SuppressWarnings("unchecked")
    public static Map toResponse(final InstanceSchema schema, final Map input) {

        if(input == null) {
            return null;
        } else {
            final Map result = new HashMap<>();
            schema.metadataSchema().forEach((k, use) -> result.put(k, toResponse(use, input.get(k))));
            schema.getAllProperties().forEach((k, prop) -> result.put(k, toResponse(prop.getType(), input.get(k))));
            if (schema instanceof Link.Resolver) {
                ((Link.Resolver) schema).getAllLinks().forEach((k, link) -> {
                    final List> values = (List>) input.get(k);
                    if (values != null) {
                        result.put(k, values.stream().map(value -> toResponse(link.getSchema(), value))
                                .collect(Collectors.toList()));
                    }
                });
            }
            return result;
        }
    }

    public static Object toResponse(final Use type, final Object value) {

        if(value == null) {
            return null;
        } else {
            return type.visit(new Use.Visitor() {
                @Override
                public Object visitBoolean(final UseBoolean type) {

                    return type.create(value);
                }

                @Override
                public Object visitInteger(final UseInteger type) {

                    return type.create(value);
                }

                @Override
                public Object visitNumber(final UseNumber type) {

                    return type.create(value);
                }

                @Override
                public Object visitString(final UseString type) {

                    return type.create(value);
                }

                @Override
                public Object visitEnum(final UseEnum type) {

                    return type.create(value);
                }

                @Override
                @SuppressWarnings("unchecked")
                public Object visitRef(final UseRef type) {

                    return toResponse(type.getSchema(), (Map)value);
                }

                @Override
                public  Object visitArray(final UseArray type) {

                    return ((Collection)value).stream()
                            .map(v -> toResponse(type.getType(), v))
                            .collect(Collectors.toList());
                }

                @Override
                public  Object visitSet(final UseSet type) {

                    return ((Collection)value).stream()
                            .map(v -> toResponse(type.getType(), v))
                            .collect(Collectors.toSet());
                }

                @Override
                @SuppressWarnings("unchecked")
                public  Object visitMap(final UseMap type) {

                    return ((Map)value).entrySet().stream()
                            .map(e -> {
                                final Map result = new HashMap<>();
                                result.put(MAP_KEY, e.getKey());
                                result.put(MAP_VALUE, toResponse(type.getType(), e.getValue()));
                                return result;
                            })
                            .collect(Collectors.toList());
                }

                @Override
                @SuppressWarnings("unchecked")
                public Object visitStruct(final UseStruct type) {

                    return toResponse(type.getSchema(), (Map)value);
                }

                @Override
                public Object visitBinary(final UseBinary type) {

                    return BaseEncoding.base64().encode(type.create(value));
                }
            });
        }
    }

    public static Map fromInput(final ExecutionContext context, final InstanceSchema schema, final ObjectValue input) {

        if(input == null) {
            return null;
        } else {
            final Map result = new HashMap<>();
            schema.getAllProperties().forEach((k, prop) -> result.put(k, fromInput(context, prop.getType(), get(input, k))));
            return result;
        }
    }

    @SuppressWarnings("unchecked")
    public static  T fromInput(final ExecutionContext context, final Use type, final Value value) {

        if(value == null) {
            return null;
        } else if(value instanceof VariableReference) {

            final String name = ((VariableReference) value).getName();
            return fromInput(type, context.getVariables().get(name));

        } else {

            return (T)type.visit(new Use.Visitor() {
                @Override
                public Object visitBoolean(final UseBoolean type) {

                    if(value instanceof BooleanValue) {
                        return ((BooleanValue) value).isValue();
                    } else {
                        throw new IllegalStateException();
                    }
                }

                @Override
                public Object visitInteger(final UseInteger type) {

                    if(value instanceof IntValue) {
                        return ((IntValue) value).getValue().longValue();
                    } else if(value instanceof FloatValue) {
                        return ((FloatValue) value).getValue().longValue();
                    } else {
                        throw new IllegalStateException();
                    }
                }

                @Override
                public Object visitNumber(final UseNumber type) {

                    if(value instanceof IntValue) {
                        return ((IntValue) value).getValue().doubleValue();
                    } else if(value instanceof FloatValue) {
                        return ((FloatValue) value).getValue().doubleValue();
                    } else {
                        throw new IllegalStateException();
                    }
                }

                @Override
                public Object visitString(final UseString type) {

                    if(value instanceof StringValue) {
                        return ((StringValue) value).getValue();
                    } else {
                        throw new IllegalStateException();
                    }
                }

                @Override
                public Object visitEnum(final UseEnum type) {

                    if(value instanceof EnumValue) {
                        return ((EnumValue) value).getName();
                    } else {
                        throw new IllegalStateException();
                    }
                }

                @Override
                public Object visitRef(final UseRef type) {

                    if(value instanceof ObjectValue) {
                        final String id = fromInput(context, UseString.DEFAULT, get((ObjectValue)value, Reserved.ID));
                        return ObjectSchema.ref(id);
                    } else {
                        throw new IllegalStateException();
                    }
                }

                @Override
                public  Object visitArray(final UseArray type) {

                    if(value instanceof ArrayValue) {
                        return ((ArrayValue)value).getValues().stream()
                                .map(v -> fromInput(context, type.getType(), v))
                                .collect(Collectors.toList());
                    } else {
                        throw new IllegalStateException();
                    }
                }

                @Override
                public  Object visitSet(final UseSet type) {

                    if(value instanceof ArrayValue) {
                        return ((ArrayValue)value).getValues().stream()
                                .map(v -> fromInput(context, type.getType(), v))
                                .collect(Collectors.toList());
                    } else {
                        throw new IllegalStateException();
                    }
                }

                @Override
                public  Object visitMap(final UseMap type) {

                    if(value instanceof ArrayValue) {
                        final Map result = new HashMap<>();
                        ((ArrayValue)value).getValues().forEach(v -> {
                            final ObjectValue entry = (ObjectValue)v;
                            final Value keyValue = get(entry, GraphQLUtils.MAP_KEY);
                            final String key = ((StringValue)keyValue).getValue();
                            final Object value = fromInput(context, type.getType(), get(entry, GraphQLUtils.MAP_VALUE));
                            result.put(key, value);
                        });
                        return result;
                    } else {
                        throw new IllegalStateException();
                    }
                }

                @Override
                public Object visitStruct(final UseStruct type) {

                    if(value instanceof ObjectValue) {
                        return fromInput(context, type.getSchema(), (ObjectValue) value);
                    } else {
                        throw new IllegalStateException();
                    }
                }

                @Override
                public Object visitBinary(final UseBinary type) {

                    if(value instanceof StringValue) {
                        return BaseEncoding.base64().decode(((StringValue) value).getValue());
                    } else {
                        throw new IllegalStateException();
                    }
                }
            });
        }
    }

    public static Map fromInput(final InstanceSchema schema, final Map input) {

        if(input == null) {
            return null;
        } else {
            final Map result = new HashMap<>();
            schema.getAllProperties().forEach((k, prop) -> result.put(k, fromInput(prop.getType(), input.get(k))));
            return result;
        }
    }

    @SuppressWarnings("unchecked")
    public static  T fromInput(final Use type, final Object value) {

        if(value == null) {
            return null;
        } else {

            return (T)type.visit(new Use.Visitor() {
                @Override
                public Object visitBoolean(final UseBoolean type) {

                    return type.create(value);
                }

                @Override
                public Object visitInteger(final UseInteger type) {

                    return type.create(value);
                }

                @Override
                public Object visitNumber(final UseNumber type) {

                    return type.create(value);
                }

                @Override
                public Object visitString(final UseString type) {

                    return type.create(value);
                }

                @Override
                public Object visitEnum(final UseEnum type) {

                    return type.create(value);
                }

                @Override
                @SuppressWarnings("unchecked")
                public Object visitRef(final UseRef type) {

                    if(value instanceof Map) {
                        return ObjectSchema.ref(Instance.getId((Map) value));
                    } else {
                        throw new IllegalStateException();
                    }
                }

                @Override
                public  Object visitArray(final UseArray type) {

                    if(value instanceof Collection) {
                        return ((Collection)value).stream()
                                .map(v -> fromInput(type.getType(), v))
                                .collect(Collectors.toList());
                    } else {
                        throw new IllegalStateException();
                    }
                }

                @Override
                public  Object visitSet(final UseSet type) {

                    if(value instanceof Collection) {
                        return ((Collection)value).stream()
                                .map(v -> fromInput(type.getType(), v))
                                .collect(Collectors.toList());
                    } else {
                        throw new IllegalStateException();
                    }
                }

                @Override
                @SuppressWarnings("unchecked")
                public  Object visitMap(final UseMap type) {

                    if(value instanceof Collection) {
                        final Map result = new HashMap<>();
                        ((Collection>)value).forEach(entry -> {
                            final String key = (String)entry.get(GraphQLUtils.MAP_KEY);
                            final Object value = fromInput(type.getType(), entry.get(GraphQLUtils.MAP_VALUE));
                            result.put(key, value);
                        });
                        return result;
                    } else {
                        throw new IllegalStateException();
                    }
                }

                @Override
                @SuppressWarnings("unchecked")
                public Object visitStruct(final UseStruct type) {

                    if(value instanceof Map) {
                        return fromInput(type.getSchema(), (Map) value);
                    } else {
                        throw new IllegalStateException();
                    }
                }

                @Override
                public Object visitBinary(final UseBinary type) {

                    if(value instanceof String) {
                        return BaseEncoding.base64().decode((String) value);
                    } else {
                        throw new IllegalStateException();
                    }
                }
            });
        }
    }

    @SuppressWarnings("unchecked")
    public static Map toResponse(final InstanceSchema schema, final SelectionSet selections, final Map input) {

        if(input == null) {
            return null;
        } else {
            final Map result = new HashMap<>();

            schema.metadataSchema().forEach((k, use) -> {
                if(selected(selections, k)) {
                    result.put(k, toResponse(use, select(selections, k), input.get(k)));
                }
            });
            schema.getAllProperties().forEach((k, prop) -> {
                if(selected(selections, k)) {
                    result.put(k, toResponse(prop.getType(), select(selections, k), input.get(k)));
                }
            });
            if (schema instanceof Link.Resolver) {
                ((Link.Resolver) schema).getAllLinks().forEach((k, link) -> {
                    if(selected(selections, k)) {
                        final List> values = (List>) input.get(k);
                        if (values != null) {
                            result.put(k, values.stream().map(value -> toResponse(link.getSchema(), select(selections, k), value))
                                    .collect(Collectors.toList()));
                        }
                    }
                });
            }
            return result;
        }
    }

    public static Object toResponse(final Use type, final SelectionSet selections, final Object value) {

        if(value == null) {
            return null;
        } else {
            return type.visit(new Use.Visitor() {
                @Override
                public Object visitBoolean(final UseBoolean type) {

                    return type.create(value);
                }

                @Override
                public Object visitInteger(final UseInteger type) {

                    return type.create(value);
                }

                @Override
                public Object visitNumber(final UseNumber type) {

                    return type.create(value);
                }

                @Override
                public Object visitString(final UseString type) {

                    return type.create(value);
                }

                @Override
                public Object visitEnum(final UseEnum type) {

                    return type.create(value);
                }

                @Override
                @SuppressWarnings("unchecked")
                public Object visitRef(final UseRef type) {

                    return toResponse(type.getSchema(), selections, (Map)value);
                }

                @Override
                public  Object visitArray(final UseArray type) {

                    return ((Collection)value).stream()
                            .map(v -> toResponse(type.getType(), selections, v))
                            .collect(Collectors.toList());
                }

                @Override
                public  Object visitSet(final UseSet type) {

                    return ((Collection)value).stream()
                            .map(v -> toResponse(type.getType(), selections, v))
                            .collect(Collectors.toSet());
                }

                @Override
                @SuppressWarnings("unchecked")
                public  Object visitMap(final UseMap type) {

                    return ((Map)value).entrySet().stream()
                            .map(e -> {
                                final Map result = new HashMap<>();
                                if(selected(selections, MAP_KEY)) {
                                    result.put(MAP_KEY, e.getKey());
                                }
                                if(selected(selections, MAP_VALUE)) {
                                    result.put(MAP_VALUE, toResponse(type.getType(), select(selections, MAP_VALUE), e.getValue()));
                                }
                                return result;
                            })
                            .collect(Collectors.toList());
                }

                @Override
                @SuppressWarnings("unchecked")
                public Object visitStruct(final UseStruct type) {

                    return toResponse(type.getSchema(), selections, (Map)value);
                }

                @Override
                public Object visitBinary(final UseBinary type) {

                    return BaseEncoding.base64().encode(type.create(value));
                }
            });
        }
    }

    public static  T argValue(final ExecutionContext context, final Use type, final Field field, final String name) {

        return fromInput(context, type, argValue(field, name));
    }

    public static Map argInput(final ExecutionContext context, final InstanceSchema schema, final Field field, final String name) {

        return fromInput(context, schema, (ObjectValue)argValue(field, name));
    }

    public static Map argInputExpr(final ExecutionContext context, final ObjectSchema schema, final Field field, final String name) {

        final ObjectValue object = (ObjectValue)argValue(field, name);
        if(object == null) {
            return null;
        } else {
            final Map result = new HashMap<>();
            schema.getAllProperties().forEach((k, prop) -> {
                final Value value = get(object, k);
                if(value instanceof VariableReference) {
                    final String var = ((VariableReference) value).getName();
                    final String str = (String)context.getVariables().get(var);
                    final Expression expr = Expression.parse(str);
                    result.put(k, expr);
                } else if(value instanceof StringValue) {
                    final Expression expr = Expression.parse(((StringValue) value).getValue());
                    result.put(k, expr);
                }
            });
            return result;
        }
    }

    public static Value argValue(final Field field, final String name) {

        final Argument arg = arg(field, name);
        return arg == null ? null : arg.getValue();
    }

    public static Argument arg(final Field field, final String name) {

        return field.getArguments().stream().filter(v -> v.getName().equals(name))
                .findFirst().orElse(null);
    }

    public static Value get(final ObjectValue input, final String name) {

        return input.getObjectFields().stream()
                .filter(v -> v.getName().equals(name))
                .map(ObjectField::getValue)
                .findFirst().orElse(null);
    }

    private static SelectionSet select(final SelectionSet selections, final String k) {

        for(final Selection v : selections.getSelections()) {
            if(v instanceof Field && ((Field) v).getName().equals(k)) {
                return ((Field) v).getSelectionSet();
            }
        }
        return EMPTY_SELECTION;
    }

    private static boolean selected(final SelectionSet selections, final String k) {

        if(selections == null) {
            return false;
        } else {
            return selections.getSelections().stream().anyMatch(v -> {
                if (v instanceof Field) {
                    return k.equals(((Field) v).getName());
                } else {
                    return false;
                }
            });
        }
    }

    public static String ucFirst(final String name) {

        return name.isEmpty() ? name : name.substring(0, 1).toUpperCase() + name.substring(1);
    }

    public static Caller caller(final GraphQLContext context) {

        if(context != null) {
            final Caller caller = context.get("caller");
            if(caller != null) {
                return caller;
            }
        }
        // FIXME
        return Caller.SUPER;
    }
}