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

gql.goi.Goi.scala Maven / Gradle / Ivy

The newest version!
/*
 * Copyright 2024 Valdemar Grange
 *
 * 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.
 */
package gql.goi

import gql.ast._
import cats.effect._
import java.util.Base64
import java.nio.charset.StandardCharsets
import cats.implicits._
import cats.data._
import cats._
import gql.SchemaShape
import gql.resolver.Resolver
import gql.dsl.all._
import cats.mtl._

trait Node {
  def value: Any = this
  def typename: String
}

object Node {
  def apply(value: Any, typename: String): Node = NodeImpl(value, typename)

  def unapply(node: Node): Option[Any] = Some(node.value)

  private case class NodeImpl(override val value: Any, typename: String) extends Node

  implicit val nodeInterface: Interface[fs2.Pure, Node] = interface[fs2.Pure, Node](
    "Node",
    "id" -> abst[fs2.Pure, ID[String]]
  )
}

object Goi {
  def makeId[F[_]](typename: String, id: String)(implicit F: Sync[F]): F[String] =
    F.delay(new String(Base64.getEncoder.encode(s"$typename:$id".getBytes()), StandardCharsets.UTF_8))

  def makeImpl[A](specify: Node => Option[A]) =
    gql.ast.Implementation(Eval.now(Node.nodeInterface))(specify.andThen(_.rightIor))

  def addIdWith[F[_], A, B](
      t: Type[F, A],
      resolver: Resolver[F, A, B],
      fromIds: NonEmptyList[B] => IorT[F, String, Map[B, Ior[String, A]]],
      specify: Node => Option[A]
  )(implicit F: Sync[F], idCodec: IDCodec[B]): Type[F, A] =
    t
      .copy(implementations = makeImpl[A](specify) :: t.implementations)
      .addFields(
        "id" -> build.from(resolver.evalMap(b => makeId[F](t.name, encodeString[B](b)).map(ID(_))))
      )
      .addAttributes(GoiAttribute(idCodec, fromIds))

  def addId[F[_], A, B](
      t: Type[F, A],
      resolver: Resolver[F, A, B],
      fromIds: NonEmptyList[B] => IorT[F, String, Map[B, Ior[String, A]]]
  )(implicit
      F: Sync[F],
      idCodec: IDCodec[B]
  ): Type[F, A] =
    addIdWith[F, A, B](
      t,
      resolver,
      fromIds,
      x =>
        if (x.typename === t.name) Some(x.value.asInstanceOf[A])
        else None
    )

  def addId[F[_], A](t: Interface[F, A]): Interface[F, A] =
    t.addFields("id" -> abst[F, ID[String]])

  def decodeInput[A](typename: String, codec: IDCodec[A], elems: Array[String]) = {
    val xs = codec.codecs
    if (xs.size =!= elems.size.toLong)
      s"Invalid Global object identifier size. Expected ${xs.size + 1} id parts seperated by :, but got ${elems.size + 1} parts. The types must be ${xs
          .prepend("typename(string)")
          .mkString_(":")}. The provided id parts were ${(typename :: elems.toList).mkString(":")}.".invalidNec
    else codec.decode(elems)
  }

  def encodeString[A](a: A)(implicit idCodec: IDCodec[A]): String = idCodec.encode(a).mkString_(":")

  def getId[F[_]](id: String)(implicit F: Sync[F]): IorT[F, String, (String, List[String])] = IorT {
    F.delay(new String(Base64.getDecoder().decode(id), StandardCharsets.UTF_8)).map { fullId =>
      if (fullId.isEmpty) s"Empty id".leftIor
      else
        fullId.split(":").toList match {
          case Nil            => s"Empty id".leftIor
          case typename :: ys => (typename -> ys).rightIor
        }
    }
  }

  final case class DecodedIds[F[_], V, K](gid: CollectedAttribute[F, V, K], keys: NonEmptyList[(String, K)])
  def decodeIds[F[_]: Sync, G[_]: NonEmptyTraverse](
      ids: G[String],
      lookup: Map[String, CollectedAttribute[F, ?, ?]],
      schemaTypes: Set[String]
  ): IorT[F, String, List[DecodedIds[F, ?, ?]]] =
    ids
      .nonEmptyTraverse[IorT[F, String, *], (String, (String, List[String]))](id => getId(id) tupleLeft id)
      .subflatMap { outs =>
        val m = outs.toNonEmptyList.groupMap { case (_, (k, _)) => k } { case (id, (_, v)) => (id, v) }
        m.toList.traverse { case (typename, keys) =>
          lookup.get(typename) match {
            case None if schemaTypes.contains(typename) =>
              s"Typename `$typename` does not have a global object identitifaction defined for it.".leftIor
            case None => s"Typename `$typename` does not exist in this schema.".leftIor
            case Some(gid) =>
              gid match {
                case gid: CollectedAttribute[F, v, k] =>
                  keys
                    .traverse[Ior[String, *], (String, k)] { case (id, key) =>
                      decodeInput(typename, gid.attribute.codec, key.toArray).toEither
                        .leftMap(_.mkString_("\n"))
                        .toIor
                        .map(id -> _)
                    }
                    .map(DecodedIds[F, v, k](gid, _))
              }
          }
        }
      }

  def runIds[F[_]: Parallel, G[_]: NonEmptyTraverse](
      ids: G[String],
      lookup: Map[String, CollectedAttribute[F, ?, ?]],
      schemaTypes: Set[String]
  )(implicit F: Sync[F]): IorT[F, String, G[Option[Ior[String, Node]]]] =
    decodeIds[F, G](ids, lookup, schemaTypes)
      .flatMap(_.parFlatTraverse { case di: DecodedIds[F, v, k] =>
        val ks = (di.keys: NonEmptyList[(String, k)])
        val reverseMapping: Map[k, String] = ks.map { case (id, key) => key -> id }.toList.toMap
        val resultsF: IorT[F, String, Map[k, Ior[String, v]]] = di.gid.attribute.fromIds(ks.map { case (_, key) => key })
        resultsF.map(_.toList.mapFilter { case (k, v) => reverseMapping.get(k) tupleRight v.map(Node(_, di.gid.typename)) })
      })
      .map(_.toMap)
      .map(lookup => ids.map(lookup.get))

  def node[F[_]: Parallel, Q, M, S](shape: SchemaShape[F, Q, M, S], xs: List[CollectedAttribute[F, ?, ?]])(implicit
      F: Sync[F]
  ): SchemaShape[F, Q, M, S] = {
    val lookup = xs.map(x => x.typename -> x).toMap
    val sts = shape.discover.toplevels.keySet
    builder[F, Q] { b =>
      implicit lazy val optIorNode: Out[F, Option[Ior[String, Node]]] =
        OutOpt[F, Ior[String, Node], Node](Node.nodeInterface, Resolver.id[F, Ior[String, Node]].rethrow)

      def nodeField[G[_]](out: Out[F, G[Option[Ior[String, Node]]]])(implicit
          a: In[G[ID[String]]],
          G: NonEmptyTraverse[G]
      ): Field[F, Q, G[Option[Ior[String, Node]]]] =
        b.from(
          arged(arg[G[ID[String]]]("id"))
            .evalMap(id => runIds[F, G](id.map(_.value), lookup, sts).value)
            .rethrow
        )(out)

      shape.copy(query =
        shape.query.copy(
          fields = shape.query.fields ::: fields[F, Q](
            "node" -> nodeField[Id](optIorNode),
            "nodes" ->
              nodeField[NonEmptyList](gql.ast.gqlOutArrForTraversable(NonEmptyTraverse[NonEmptyList], optIorNode))
          )
        )
      )
    }
  }

  final case class CollectedAttribute[F[_], A, B](
      typename: String,
      attribute: GoiAttribute[F, A, B]
  )
  def collectAttributes[F[_]](
      shape: SchemaShape[F, ?, ?, ?]
  ): Either[NonEmptyChain[String], List[CollectedAttribute[F, _, _]]] = {
    type State = List[CollectedAttribute[F, ?, ?]]
    type Effect[A] = EitherT[Eval, NonEmptyChain[String], A]
    def go[H[_]: Defer](implicit H: Monad[H], R: Raise[H, NonEmptyChain[String]]): H[State] =
      shape.visitOnce[H, State] { case SchemaShape.VisitNode.OutNode(t: Type[F, a]) =>
        val fas = t.attributes.collect { case g: GoiAttribute[F, a, ?] => g }
        val n = t.name

        fas match {
          case Nil => H.pure(Nil)
          case (x: GoiAttribute[F, a, id]) :: xs =>
            if (xs.nonEmpty) R.raise(NonEmptyChain.one(s"More than one goi attribute found on `$n`"))
            else H.pure(List(CollectedAttribute[F, a, id](n, x)))
        }
      }

    go[Effect].value.value
  }

  def addSchemaGoi[F[_]: Parallel: Sync, Q, M, S](schema: SchemaShape[F, Q, M, S]): SchemaShape[F, Q, M, S] =
    node(schema, collectAttributes(schema).getOrElse(Nil))

  def validateFor[F[_], Q, M, S](shape: SchemaShape[F, Q, M, S], instances: List[CollectedAttribute[F, ?, ?]]): List[String] = {
    val instanceSet = instances.map(_.typename).toSet
    val all = shape.discover.outputs.keySet
    // Parameters that have typenames do not occur in the schema
    val notImplemented = instanceSet -- all
    // Paramerters that do occur in the schema
    val implemented = instanceSet -- notImplemented

    // Parameters that are duplicated
    val duplicates = instances.groupBy(_.typename).toList.collect { case (k, v) if v.size > 1 => (k, v.size) }

    val nodeInstances = shape.discover.implementations.get(Node.nodeInterface.name)
    // All the typenames that implement the Node interface
    val implementing = nodeInstances.map(_.keySet).getOrElse(Set.empty)
    // Does occur but does not extend the Node interface
    val doesNotExtend = implemented -- implementing
    // Does extend the Node interface but does not occur in the parameters
    val missing = implementing -- implemented

    notImplemented.toList.map(n =>
      s"Type `$n` does not occur in the schema. Hint: GlobalID provides smart constructors for creating types that satisfy the rules of GOI."
    ) ++
      doesNotExtend.toList.map(n => s"Type `$n` was declared as a node GlobalID instance but did not extend the `Node` type.") ++
      missing.toList.map(n =>
        s"Type `$n` extends the `Node` type but was not declared as a node GlobalID instance. Hint: You might have forgot to include your GlobalID instance for `$n`."
      ) ++
      duplicates.toList.map { case (n, size) =>
        s"Type `$n` was declared $size times as a GlobalID instance. Hint: Ensure that the GlobalID instance for `$n` is only added once."
      }
  }

}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy