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

sttp.tapir.internal.OneOfMacro.scala Maven / Gradle / Ivy

The newest version!
package sttp.tapir.internal

import sttp.tapir.Schema
import sttp.tapir.generic.Configuration

import scala.annotation.tailrec
import scala.reflect.macros.blackbox

private[tapir] object OneOfMacro {
  // http://onoffswitch.net/extracting-scala-method-names-objects-macros/
  def generateOneOfUsingField[E: c.WeakTypeTag, V: c.WeakTypeTag](
      c: blackbox.Context
  )(extractor: c.Expr[E => V], asString: c.Expr[V => String])(
      mapping: c.Expr[(V, Schema[_])]*
  )(conf: c.Expr[Configuration], discriminatorSchema: c.Expr[Schema[V]]): c.Expr[Schema[E]] = {
    import c.universe._

    @tailrec
    def resolveFunctionName(f: Function): String = {
      f.body match {
        // the function name
        case t: Select => t.name.decodedName.toString

        case t: Function =>
          resolveFunctionName(t)

        // an application of a function and extracting the name
        case t: Apply if t.fun.isInstanceOf[Select @unchecked] =>
          t.fun.asInstanceOf[Select].name.decodedName.toString

        // curried lambda
        case t: Block if t.expr.isInstanceOf[Function @unchecked] =>
          val func = t.expr.asInstanceOf[Function]

          resolveFunctionName(func)

        case _ =>
          throw new RuntimeException("Unable to resolve function name for expression: " + f.body)
      }
    }
    val weakTypeE = weakTypeOf[E]
    val weakTypeV = weakTypeOf[V]

    val name = resolveFunctionName(extractor.tree.asInstanceOf[Function])

    val schemaForE =
      q"""{
            val mappingAsList = _root_.scala.collection.immutable.List(..$mapping)
            val mappingAsMap: _root_.scala.collection.immutable.Map[$weakTypeV, _root_.sttp.tapir.Schema[_]] = mappingAsList.toMap
            
            val discriminatorName = _root_.sttp.tapir.FieldName($name, $conf.toEncodedName($name))
            // cannot use .collect because of a bug in ScalaJS (Trying to access the this of another class ... during phase: jscode)
            val discriminatorMapping = mappingAsMap.toList.flatMap { 
                case (k, _root_.sttp.tapir.Schema(_, _root_.scala.Some(fname), _, _, _, _, _, _, _, _, _)) => _root_.scala.collection.immutable.List($asString.apply(k) -> _root_.sttp.tapir.SchemaType.SRef(fname))
                case _ => _root_.scala.Nil
              }
              .toMap
            
            val sname = _root_.sttp.tapir.Schema.SName(${weakTypeE.typeSymbol.fullName},${extractTypeArguments(c)(weakTypeE)})
            // cast needed because of Scala 2.12
            val subtypes = mappingAsList.map(_._2)
            _root_.sttp.tapir.Schema((_root_.sttp.tapir.SchemaType.SCoproduct[$weakTypeE](subtypes, _root_.scala.None) { e =>
              val ee: $weakTypeV = $extractor(e)
              mappingAsMap.get(ee).map(m => _root_.sttp.tapir.SchemaType.SchemaWithValue(m.asInstanceOf[_root_.sttp.tapir.Schema[Any]], e))
            }).addDiscriminatorField(
              discriminatorName, $discriminatorSchema, discriminatorMapping
            ), _root_.scala.Some(sname))
          }"""

    Debug.logGeneratedCode(c)(weakTypeE.typeSymbol.fullName, schemaForE)
    c.Expr[Schema[E]](schemaForE)
  }

  def generateOneOfWrapped[E: c.WeakTypeTag](c: blackbox.Context)(conf: c.Expr[Configuration]): c.Expr[Schema[E]] = {
    import c.universe._

    val weakTypeE = weakTypeOf[E]

    val symbol = weakTypeE.typeSymbol.asClass
    if (!symbol.isClass || !symbol.isSealed) {
      c.abort(c.enclosingPosition, "Can only generate a coproduct schema for a sealed trait or class.")
    } else {
      val subclasses = symbol.knownDirectSubclasses.toList.sortBy(_.name.encodedName.toString)

      val subclassesSchemas = subclasses.map(subclass =>
        q"${subclass.name.encodedName.toString} -> _root_.sttp.tapir.Schema.wrapWithSingleFieldProduct(_root_.scala.Predef.implicitly[_root_.sttp.tapir.Schema[${subclass.asType}]])($conf)"
      )

      val subclassesSchemaCases = subclasses.map { subclass =>
        cq"""v: ${subclass.asType} => _root_.scala.Some(_root_.sttp.tapir.SchemaType.SchemaWithValue(subclassNameToSchemaMap(${subclass.name.encodedName.toString}).asInstanceOf[_root_.sttp.tapir.Schema[Any]], v))"""
      }

      val schemaForE = q"""{
        val subclassNameToSchema: _root_.scala.collection.immutable.List[(String, _root_.sttp.tapir.Schema[_])] = _root_.scala.collection.immutable.List($subclassesSchemas: _*)
        val subclassNameToSchemaMap: _root_.scala.collection.immutable.Map[String, _root_.sttp.tapir.Schema[_]] = subclassNameToSchema.toMap
        
        val sname = _root_.sttp.tapir.Schema.SName(${weakTypeE.typeSymbol.fullName},${extractTypeArguments(c)(weakTypeE)})
        // cast needed because of Scala 2.12
        val subtypes = subclassNameToSchema.map(_._2)
        _root_.sttp.tapir.Schema(
          schemaType = _root_.sttp.tapir.SchemaType.SCoproduct[$weakTypeE](subtypes, _root_.scala.None) { e =>
            e match {
              case ..$subclassesSchemaCases
            }
          },
          name = _root_.scala.Some(sname)
        )
      }"""

      Debug.logGeneratedCode(c)(weakTypeE.typeSymbol.fullName, schemaForE)
      c.Expr[Schema[E]](schemaForE)
    }
  }

  private def extractTypeArguments(c: blackbox.Context)(weakType: c.Type): List[String] = {
    def allTypeArguments(tn: c.Type): Seq[c.Type] = tn.typeArgs.flatMap(tn2 => tn2 +: allTypeArguments(tn2))
    allTypeArguments(weakType).map(_.typeSymbol.fullName).toList
  }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy