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

org.scanamo.generic.Derivation.scala Maven / Gradle / Ivy

/*
 * Copyright 2019 Scanamo
 *
 * 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 org.scanamo.generic

import cats.data.NonEmptyChain
import cats.syntax.bifunctor.*
import cats.syntax.parallel.*
import magnolia1.*
import org.scanamo.*

private[scanamo] trait Derivation {
  type Typeclass[A] = DynamoFormat[A]

  type FieldName = String
  type Valid[A] = Either[NonEmptyChain[(FieldName, DynamoReadError)], A]

  // Derivation for case classes: generates an encoding that is isomorphic to the
  // isomorphic n-tuple for type `T`. For case objects, they are encoded as strings.
  // 1. we look up the class field in the object
  // 2. if not found, we check if the field has a default value
  // 3. if not, we try to decode a null value and see if the formatter produces something
  //    if yes, we just produce that value as a success
  // 4. otherwise, we decode the found value
  // 5. finally, we wrap errors in [[cats.data.NonEmptyChain]] so multiple decoding errors can
  //    be accumulated
  def join[T](cc: CaseClass[Typeclass, T]): Typeclass[T] = {
    def decodeField(o: DynamoObject, param: Param[Typeclass, T]): Valid[param.PType] =
      o(param.label)
        .fold[Either[DynamoReadError, param.PType]] {
          param.default.fold(param.typeclass.read(DynamoValue.nil).leftMap(_ => MissingProperty))(Right(_))
        }(param.typeclass.read(_))
        .leftMap(e => NonEmptyChain.one(param.label -> e))

    // case objects are inlined as strings
    if (cc.isObject)
      new DynamoFormat[T] {
        private[this] val _cachedAttribute = DynamoValue.fromString(cc.typeName.short)
        private[this] val _cachedHit = Right(cc.rawConstruct(Nil))

        def read(dv: DynamoValue): Either[DynamoReadError, T] =
          dv.asString
            .filter(_ == cc.typeName.short)
            .fold[Either[DynamoReadError, T]](Left(NoPropertyOfType("S", dv)))(_ => _cachedHit)

        def write(t: T): DynamoValue = _cachedAttribute
      }
    else
      new DynamoFormat.ObjectFormat[T] {
        def readObject(o: DynamoObject): Either[DynamoReadError, T] =
          cc.parameters.toList
            .parTraverse(decodeField(o, _))
            .bimap(
              es => InvalidPropertiesError(es.toNonEmptyList),
              xs => cc.rawConstruct(xs)
            )

        def writeObject(t: T): DynamoObject =
          DynamoObject(cc.parameters.foldLeft(List.empty[(String, DynamoValue)]) { case (xs, p) =>
            val v = p.typeclass.write(p.dereference(t))
            if (v.isNull) xs else (p.label -> v) :: xs
          }: _*)
      }
  }

  // Derivation for ADTs, they are encoded as an object of one property, the key being the case name
  def split[T](st: SealedTrait[Typeclass, T]): Typeclass[T] = {
    def decode(o: DynamoObject): Either[DynamoReadError, T] =
      (for {
        subtype <- st.subtypes.find(sub => o.contains(sub.typeName.short))
        value <- o(subtype.typeName.short)
      } yield subtype.typeclass.read(value)).getOrElse(Left(NoPropertyOfType("M", DynamoValue.nil)))

    new DynamoFormat.ObjectFormat[T] {
      def readObject(o: DynamoObject): Either[DynamoReadError, T] = decode(o)

      def writeObject(t: T): DynamoObject =
        st.split(t) { subtype =>
          DynamoObject.singleton(subtype.typeName.short, subtype.typeclass.write(subtype.cast(t)))
        }
    }
  }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy