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

org.opentripplanner.apis.gtfs.GraphQLScalars Maven / Gradle / Ivy

The newest version!
package org.opentripplanner.apis.gtfs;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import graphql.language.FloatValue;
import graphql.language.IntValue;
import graphql.language.StringValue;
import graphql.relay.Relay;
import graphql.schema.Coercing;
import graphql.schema.CoercingParseLiteralException;
import graphql.schema.CoercingParseValueException;
import graphql.schema.CoercingSerializeException;
import graphql.schema.GraphQLScalarType;
import java.text.ParseException;
import java.time.OffsetDateTime;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.util.Optional;
import org.locationtech.jts.geom.Geometry;
import org.opentripplanner.framework.graphql.scalar.DateScalarFactory;
import org.opentripplanner.framework.graphql.scalar.DurationScalarFactory;
import org.opentripplanner.framework.json.ObjectMappers;
import org.opentripplanner.framework.model.Cost;
import org.opentripplanner.framework.model.Grams;
import org.opentripplanner.utils.time.OffsetDateTimeParser;

public class GraphQLScalars {

  private static final ObjectMapper GEOJSON_MAPPER = ObjectMappers.geoJson();
  public static final GraphQLScalarType DURATION_SCALAR =
    DurationScalarFactory.createDurationScalar();

  public static final GraphQLScalarType POLYLINE_SCALAR = GraphQLScalarType.newScalar()
    .name("Polyline")
    .description(
      "List of coordinates in an encoded polyline format (see https://developers.google.com/maps/documentation/utilities/polylinealgorithm). The value appears in JSON as a string."
    )
    .coercing(
      new Coercing() {
        @Override
        public String serialize(Object input) {
          return input == null ? null : input.toString();
        }

        @Override
        public String parseValue(Object input) {
          return serialize(input);
        }

        @Override
        public String parseLiteral(Object input) {
          if (!(input instanceof StringValue)) {
            return null;
          }
          return ((StringValue) input).getValue();
        }
      }
    )
    .build();

  public static final GraphQLScalarType OFFSET_DATETIME_SCALAR = GraphQLScalarType.newScalar()
    .name("OffsetDateTime")
    .coercing(
      new Coercing() {
        @Override
        public String serialize(Object dataFetcherResult) throws CoercingSerializeException {
          if (dataFetcherResult instanceof ZonedDateTime zdt) {
            return zdt.format(DateTimeFormatter.ISO_OFFSET_DATE_TIME);
          } else if (dataFetcherResult instanceof OffsetDateTime odt) {
            return odt.format(DateTimeFormatter.ISO_OFFSET_DATE_TIME);
          } else {
            throw new CoercingSerializeException(
              "Cannot serialize object of class %s".formatted(
                  dataFetcherResult.getClass().getSimpleName()
                )
            );
          }
        }

        @Override
        public OffsetDateTime parseValue(Object input) throws CoercingParseValueException {
          if (input instanceof CharSequence cs) {
            try {
              return OffsetDateTimeParser.parseLeniently(cs);
            } catch (ParseException e) {
              int errorOffset = e.getErrorOffset();
              throw new CoercingParseValueException(
                "Cannot parse %s into an OffsetDateTime. Error at character index %s".formatted(
                    input,
                    errorOffset
                  )
              );
            }
          }
          throw new CoercingParseValueException(
            "Cannot parse %s into an OffsetDateTime. Must be a string."
          );
        }

        @Override
        public OffsetDateTime parseLiteral(Object input) throws CoercingParseLiteralException {
          if (input instanceof StringValue sv) {
            try {
              return OffsetDateTimeParser.parseLeniently(sv.getValue());
            } catch (ParseException e) {
              throw new CoercingSerializeException();
            }
          }
          throw new CoercingParseLiteralException();
        }
      }
    )
    .build();

  public static final GraphQLScalarType COORDINATE_VALUE_SCALAR = GraphQLScalarType.newScalar()
    .name("CoordinateValue")
    .coercing(
      new Coercing() {
        private static final String VALIDATION_ERROR_MESSAGE = "Not a valid WGS84 coordinate value";

        @Override
        public Double serialize(Object dataFetcherResult) throws CoercingSerializeException {
          if (dataFetcherResult instanceof Double doubleValue) {
            return doubleValue;
          } else if (dataFetcherResult instanceof Float floatValue) {
            return floatValue.doubleValue();
          } else {
            throw new CoercingSerializeException(
              "Cannot serialize object of class %s as a coordinate number".formatted(
                  dataFetcherResult.getClass().getSimpleName()
                )
            );
          }
        }

        @Override
        public Double parseValue(Object input) throws CoercingParseValueException {
          if (input instanceof Double doubleValue) {
            return validateCoordinate(doubleValue).orElseThrow(() ->
              new CoercingParseValueException(VALIDATION_ERROR_MESSAGE)
            );
          }
          if (input instanceof Integer intValue) {
            return validateCoordinate(intValue).orElseThrow(() ->
              new CoercingParseValueException(VALIDATION_ERROR_MESSAGE)
            );
          }
          throw new CoercingParseValueException(
            "Expected a number, got %s %s".formatted(input.getClass().getSimpleName(), input)
          );
        }

        @Override
        public Double parseLiteral(Object input) throws CoercingParseLiteralException {
          if (input instanceof FloatValue coordinate) {
            return validateCoordinate(coordinate.getValue().doubleValue()).orElseThrow(() ->
              new CoercingParseLiteralException(VALIDATION_ERROR_MESSAGE)
            );
          }
          if (input instanceof IntValue coordinate) {
            return validateCoordinate(coordinate.getValue().doubleValue()).orElseThrow(() ->
              new CoercingParseLiteralException(VALIDATION_ERROR_MESSAGE)
            );
          }
          throw new CoercingParseLiteralException(
            "Expected a number, got: " + input.getClass().getSimpleName()
          );
        }

        private static Optional validateCoordinate(double coordinate) {
          if (coordinate >= -180.001 && coordinate <= 180.001) {
            return Optional.of(coordinate);
          }
          return Optional.empty();
        }
      }
    )
    .build();

  public static final GraphQLScalarType COST_SCALAR = GraphQLScalarType.newScalar()
    .name("Cost")
    .coercing(
      new Coercing() {
        private static final int MAX_COST = 1000000;
        private static final String VALIDATION_ERROR_MESSAGE =
          "Cost cannot be negative or greater than %d".formatted(MAX_COST);

        @Override
        public Integer serialize(Object dataFetcherResult) throws CoercingSerializeException {
          if (dataFetcherResult instanceof Integer intValue) {
            return intValue;
          } else if (dataFetcherResult instanceof Cost costValue) {
            return costValue.toSeconds();
          } else {
            throw new CoercingSerializeException(
              "Cannot serialize object of class %s as a cost".formatted(
                  dataFetcherResult.getClass().getSimpleName()
                )
            );
          }
        }

        @Override
        public Cost parseValue(Object input) throws CoercingParseValueException {
          if (input instanceof Integer intValue) {
            return validateCost(intValue).orElseThrow(() ->
              new CoercingParseValueException(VALIDATION_ERROR_MESSAGE)
            );
          }
          throw new CoercingParseValueException(
            "Expected an integer, got %s %s".formatted(input.getClass().getSimpleName(), input)
          );
        }

        @Override
        public Cost parseLiteral(Object input) throws CoercingParseLiteralException {
          if (input instanceof IntValue intValue) {
            var value = intValue.getValue().intValue();
            return validateCost(value).orElseThrow(() ->
              new CoercingParseLiteralException(VALIDATION_ERROR_MESSAGE)
            );
          }
          throw new CoercingParseLiteralException(
            "Expected an integer, got: " + input.getClass().getSimpleName()
          );
        }

        private static Optional validateCost(int cost) {
          if (cost >= 0 && cost <= MAX_COST) {
            return Optional.of(Cost.costOfSeconds(cost));
          }
          return Optional.empty();
        }
      }
    )
    .build();

  public static final GraphQLScalarType LOCAL_DATE_SCALAR =
    DateScalarFactory.createGtfsDateScalar();

  public static final GraphQLScalarType GEOJSON_SCALAR = GraphQLScalarType.newScalar()
    .name("GeoJson")
    .description("Geographic data structures in JSON format. See: https://geojson.org/")
    .coercing(
      new Coercing() {
        @Override
        public JsonNode serialize(Object dataFetcherResult) throws CoercingSerializeException {
          if (dataFetcherResult instanceof Geometry) {
            var geom = (Geometry) dataFetcherResult;
            return GEOJSON_MAPPER.valueToTree(geom);
          }
          return null;
        }

        @Override
        public Geometry parseValue(Object input) throws CoercingParseValueException {
          return null;
        }

        @Override
        public Geometry parseLiteral(Object input) throws CoercingParseLiteralException {
          return null;
        }
      }
    )
    .build();

  public static final GraphQLScalarType GRAPHQL_ID_SCALAR = GraphQLScalarType.newScalar()
    .name("ID")
    .coercing(
      new Coercing() {
        @Override
        public String serialize(Object dataFetcherResult) throws CoercingSerializeException {
          if (dataFetcherResult instanceof Relay.ResolvedGlobalId) {
            Relay.ResolvedGlobalId globalId = (Relay.ResolvedGlobalId) dataFetcherResult;
            return new Relay().toGlobalId(globalId.getType(), globalId.getId());
          }
          throw new CoercingSerializeException(
            "Unknown type " + dataFetcherResult.getClass().getSimpleName()
          );
        }

        @Override
        public Relay.ResolvedGlobalId parseValue(Object input) throws CoercingParseValueException {
          if (input instanceof String) {
            return new Relay().fromGlobalId((String) input);
          }
          throw new CoercingParseValueException(
            "Unexpected type " + input.getClass().getSimpleName()
          );
        }

        @Override
        public Relay.ResolvedGlobalId parseLiteral(Object input)
          throws CoercingParseLiteralException {
          if (input instanceof StringValue) {
            return new Relay().fromGlobalId(((StringValue) input).getValue());
          }
          throw new CoercingParseLiteralException(
            "Unexpected type " + input.getClass().getSimpleName()
          );
        }
      }
    )
    .build();

  public static final GraphQLScalarType GRAMS_SCALAR = GraphQLScalarType.newScalar()
    .name("Grams")
    .coercing(
      new Coercing() {
        @Override
        public Double serialize(Object dataFetcherResult) throws CoercingSerializeException {
          if (dataFetcherResult instanceof Grams) {
            var grams = (Grams) dataFetcherResult;
            return Double.valueOf(grams.asDouble());
          }
          return null;
        }

        @Override
        public Grams parseValue(Object input) throws CoercingParseValueException {
          if (input instanceof Double doubleValue) {
            return new Grams(doubleValue);
          }
          if (input instanceof Integer intValue) {
            return new Grams(intValue);
          }
          throw new CoercingParseValueException(
            "Expected a number, got %s %s".formatted(input.getClass().getSimpleName(), input)
          );
        }

        @Override
        public Grams parseLiteral(Object input) throws CoercingParseLiteralException {
          if (input instanceof FloatValue coordinate) {
            return new Grams(coordinate.getValue().doubleValue());
          }
          if (input instanceof IntValue coordinate) {
            return new Grams(coordinate.getValue().doubleValue());
          }
          throw new CoercingParseLiteralException(
            "Expected a number, got: " + input.getClass().getSimpleName()
          );
        }
      }
    )
    .build();

  public static final GraphQLScalarType RATIO_SCALAR = GraphQLScalarType.newScalar()
    .name("Ratio")
    .coercing(
      new Coercing() {
        private static final String VALIDATION_ERROR_MESSAGE =
          "Value is under 0 or greater than 1.";

        @Override
        public Double serialize(Object dataFetcherResult) throws CoercingSerializeException {
          var validationException = new CoercingSerializeException(VALIDATION_ERROR_MESSAGE);
          if (dataFetcherResult instanceof Double doubleValue) {
            return validateRatio(doubleValue).orElseThrow(() -> validationException);
          } else if (dataFetcherResult instanceof Float floatValue) {
            return validateRatio(floatValue.doubleValue()).orElseThrow(() -> validationException);
          } else {
            throw new CoercingSerializeException(
              "Cannot serialize object of class %s as a ratio".formatted(
                  dataFetcherResult.getClass().getSimpleName()
                )
            );
          }
        }

        @Override
        public Double parseValue(Object input) throws CoercingParseValueException {
          if (input instanceof Double doubleValue) {
            return validateRatio(doubleValue).orElseThrow(() ->
              new CoercingParseValueException(VALIDATION_ERROR_MESSAGE)
            );
          }
          if (input instanceof Integer intValue) {
            return validateRatio(intValue).orElseThrow(() ->
              new CoercingParseValueException(VALIDATION_ERROR_MESSAGE)
            );
          }
          throw new CoercingParseValueException(
            "Expected a number, got %s %s".formatted(input.getClass().getSimpleName(), input)
          );
        }

        @Override
        public Double parseLiteral(Object input) throws CoercingParseLiteralException {
          if (input instanceof FloatValue ratio) {
            return validateRatio(ratio.getValue().doubleValue()).orElseThrow(() ->
              new CoercingParseLiteralException(VALIDATION_ERROR_MESSAGE)
            );
          }
          if (input instanceof IntValue ratio) {
            return validateRatio(ratio.getValue().doubleValue()).orElseThrow(() ->
              new CoercingParseLiteralException(VALIDATION_ERROR_MESSAGE)
            );
          }
          throw new CoercingParseLiteralException(
            "Expected a number, got: " + input.getClass().getSimpleName()
          );
        }

        private static Optional validateRatio(double ratio) {
          if (ratio >= -0.001 && ratio <= 1.001) {
            return Optional.of(ratio);
          }
          return Optional.empty();
        }
      }
    )
    .build();

  public static final GraphQLScalarType RELUCTANCE_SCALAR = GraphQLScalarType.newScalar()
    .name("Reluctance")
    .coercing(
      new Coercing() {
        private static final double MIN_Reluctance = 0.1;
        private static final double MAX_Reluctance = 100000;
        private static final String VALIDATION_ERROR_MESSAGE =
          "Reluctance needs to be between %s and %s".formatted(MIN_Reluctance, MAX_Reluctance);

        @Override
        public Double serialize(Object dataFetcherResult) throws CoercingSerializeException {
          if (dataFetcherResult instanceof Double doubleValue) {
            return doubleValue;
          } else if (dataFetcherResult instanceof Float floatValue) {
            return floatValue.doubleValue();
          } else {
            throw new CoercingSerializeException(
              "Cannot serialize object of class %s as a reluctance".formatted(
                  dataFetcherResult.getClass().getSimpleName()
                )
            );
          }
        }

        @Override
        public Double parseValue(Object input) throws CoercingParseValueException {
          if (input instanceof Double doubleValue) {
            return validateReluctance(doubleValue).orElseThrow(() ->
              new CoercingParseValueException(VALIDATION_ERROR_MESSAGE)
            );
          }
          if (input instanceof Integer intValue) {
            return validateReluctance(intValue).orElseThrow(() ->
              new CoercingParseValueException(VALIDATION_ERROR_MESSAGE)
            );
          }
          throw new CoercingParseValueException(
            "Expected a number, got %s %s".formatted(input.getClass().getSimpleName(), input)
          );
        }

        @Override
        public Double parseLiteral(Object input) throws CoercingParseLiteralException {
          if (input instanceof FloatValue reluctance) {
            return validateReluctance(reluctance.getValue().doubleValue()).orElseThrow(() ->
              new CoercingParseLiteralException(VALIDATION_ERROR_MESSAGE)
            );
          }
          if (input instanceof IntValue reluctance) {
            return validateReluctance(reluctance.getValue().doubleValue()).orElseThrow(() ->
              new CoercingParseLiteralException(VALIDATION_ERROR_MESSAGE)
            );
          }
          throw new CoercingParseLiteralException(
            "Expected a number, got: " + input.getClass().getSimpleName()
          );
        }

        private static Optional validateReluctance(double reluctance) {
          if (reluctance >= MIN_Reluctance - 0.001 && reluctance <= MAX_Reluctance + 0.001) {
            return Optional.of(reluctance);
          }
          return Optional.empty();
        }
      }
    )
    .build();
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy