Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Rework of the concept of collection in the Schema abstraction #290

Merged
merged 14 commits into from
Jul 7, 2022
2 changes: 1 addition & 1 deletion modules/codegen/src/smithy4s/codegen/Renderer.scala
Original file line number Diff line number Diff line change
Expand Up @@ -597,7 +597,7 @@ private[codegen] class Renderer(compilationUnit: CompilationUnit) { self =>
renderHintsVal(hints),
line"val underlyingSchema : $Schema_[$tpe] = ${tpe.schemaRef}$trailingCalls",
lines(
s"implicit val schema : $Schema_[$name] = bijection(underlyingSchema, $name(_), (_ : $name).value)"
s"implicit val schema : $Schema_[$name] = bijection(underlyingSchema, $name.make, (_ : $name).value)"
)
)
).addImports(imports)
Expand Down
5 changes: 5 additions & 0 deletions modules/core/src-2/Newtype.scala
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ abstract class Newtype[A] extends HasId { self =>
type Type <: Base with _Tag

@inline final def apply(a: A): Type = a.asInstanceOf[Type]
final val make: Newtype.Make[A, Type] = apply(_: A)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With this change, is there ever a case where someone should use the apply method rather than make? If not, should the apply method be private?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Users should very much keep using the apply method to construct instances of newtypes. This is very much a kind of hack to ensure we have a way of detecting whether a bijection is actually coming from a newtype. I'll try to hide it further and make the Make construct package private.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.


@inline final def value(x: Type): A =
x.asInstanceOf[A]
Expand Down Expand Up @@ -51,3 +52,7 @@ abstract class Newtype[A] extends HasId { self =>
def unapply(h: Hints): Option[Type] = h.get(tag)
}
}

object Newtype {
trait Make[A, B] extends Function[A, B]
}
81 changes: 72 additions & 9 deletions modules/core/src/smithy4s/schema/CollectionTag.scala
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@
package smithy4s
package schema

import scala.reflect.ClassTag

sealed trait CollectionTag[C[_]] {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

def name: String

Expand All @@ -28,40 +30,101 @@ sealed trait CollectionTag[C[_]] {
}

object CollectionTag {
import scala.collection.{immutable => cols}
import scala.collection.compat.{immutable => cols}
import scala.collection.{mutable => mut}

case object List extends CollectionTag[List] {
case object ListTag extends CollectionTag[List] {
override def name: String = "List"

override def iterator[A](c: List[A]): Iterator[A] = c.iterator

override def build[A](put: (A => Unit) => Unit): List[A] = {
val builder = cols.List.newBuilder[A]
put(builder.+=(_))
builder.result()
val builder = mut.ListBuffer.newBuilder[A]
put(builder += (_))
builder.result().toList
}

}

case object Set extends CollectionTag[Set] {
case object SetTag extends CollectionTag[Set] {
override def name: String = "Set"
override def iterator[A](c: Set[A]): Iterator[A] = c.iterator

override def build[A](put: (A => Unit) => Unit): Set[A] = {
val builder = cols.Set.newBuilder[A]
val builder = Set.newBuilder[A]
put(builder.+=(_))
builder.result()
}
}

case object Vector extends CollectionTag[Vector] {
case object VectorTag extends CollectionTag[Vector] {
override def name: String = "Vector"
override def iterator[A](c: Vector[A]): Iterator[A] = c.iterator

override def build[A](put: (A => Unit) => Unit): Vector[A] = {
val builder = cols.Vector.newBuilder[A]
val builder = Vector.newBuilder[A]
put(builder.+=(_))
builder.result()
}
}

case object IndexedSeqTag extends CollectionTag[IndexedSeq] {

override def name: String = "IndexedSeq"
override def iterator[A](c: IndexedSeq[A]): Iterator[A] = c.iterator

override def build[A](put: (A => Unit) => Unit): Vector[A] = {
val builder = Vector.newBuilder[A]
put(builder.+=(_))
builder.result()
}

/**
* Returns a builder that may be able to store the elements in an unboxed
* fashion
*/
private[smithy4s] def compactBuilder[A](
schema: Schema[A]
): ((A => Unit) => Unit) => IndexedSeq[A] =
schema.compile(CTSchemaVisitor) match {
case Some(ct) => { (put: (A => Unit) => Unit) =>
val builder = cols.ArraySeq.newBuilder(ct)
put(builder.+=(_))
builder.result()
}
case None => { (put: (A => Unit) => Unit) =>
val builder = IndexedSeq.newBuilder[A]
put(builder.+=(_))
builder.result()
}
}
}

private[this] type MaybeCT[A] = Option[ClassTag[A]]

/**
* Retrieves a ClassTag whenever possible.
*/
// format: off
private[this] object CTSchemaVisitor extends SchemaVisitor[MaybeCT]{
val primitiveCT = Primitive.deriving[ClassTag]
def primitive[P](shapeId: ShapeId, hints: Hints, tag: Primitive[P]): MaybeCT[P] = Some(primitiveCT(tag))
def collection[C[_], A](shapeId: ShapeId, hints: Hints, tag: CollectionTag[C], member: Schema[A]): MaybeCT[C[A]] = tag match {
case ListTag => Some(implicitly[ClassTag[List[A]]])
case SetTag => Some(implicitly[ClassTag[Set[A]]])
case VectorTag => Some(implicitly[ClassTag[Vector[A]]])
case IndexedSeqTag => Some(implicitly[ClassTag[IndexedSeq[A]]])
}
def map[K, V](shapeId: ShapeId, hints: Hints, key: Schema[K], value: Schema[V]): MaybeCT[Map[K,V]] = Some(implicitly[ClassTag[Map[K, V]]])
def enumeration[E](shapeId: ShapeId, hints: Hints, values: List[EnumValue[E]], total: E => EnumValue[E]): MaybeCT[E] = None
def struct[S](shapeId: ShapeId, hints: Hints, fields: Vector[SchemaField[S, _]], make: IndexedSeq[Any] => S): MaybeCT[S] = None
def union[U](shapeId: ShapeId, hints: Hints, alternatives: Vector[SchemaAlt[U, _]], dispatch: U => Alt.SchemaAndValue[U, _]): MaybeCT[U] = None
def biject[A, B](schema: Schema[A], to: A => B, from: B => A): MaybeCT[B] = {
if (to.isInstanceOf[Newtype.Make[A, B]]) apply(schema).asInstanceOf[MaybeCT[B]]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a hacky but when we have newtypes of primitives, we should attempt to retain compaction

else None
}
def surject[A, B](schema: Schema[A], to: Refinement[A,B], from: B => A): MaybeCT[B] = None
def lazily[A](suspend: Lazy[Schema[A]]): MaybeCT[A] = None
}
// format: off
}
7 changes: 5 additions & 2 deletions modules/core/src/smithy4s/schema/Schema.scala
Original file line number Diff line number Diff line change
Expand Up @@ -137,8 +137,11 @@ object Schema {

private val placeholder: ShapeId = ShapeId("placeholder", "Placeholder")

def list[A](a: Schema[A]): Schema[List[A]] = Schema.CollectionSchema[List, A](placeholder, Hints.empty, CollectionTag.List, a)
def set[A](a: Schema[A]): Schema[Set[A]] = Schema.CollectionSchema[Set, A](placeholder, Hints.empty, CollectionTag.Set, a)
def list[A](a: Schema[A]): Schema[List[A]] = Schema.CollectionSchema[List, A](placeholder, Hints.empty, CollectionTag.ListTag, a)
def set[A](a: Schema[A]): Schema[Set[A]] = Schema.CollectionSchema[Set, A](placeholder, Hints.empty, CollectionTag.SetTag, a)
def vector[A](a: Schema[A]): Schema[Vector[A]] = Schema.CollectionSchema[Vector, A](placeholder, Hints.empty, CollectionTag.VectorTag, a)
def indexedSeq[A](a: Schema[A]): Schema[IndexedSeq[A]] = Schema.CollectionSchema[IndexedSeq, A](placeholder, Hints.empty, CollectionTag.IndexedSeqTag, a)

def map[K, V](k: Schema[K], v: Schema[V]): Schema[Map[K, V]] = Schema.MapSchema(placeholder, Hints.empty, k, v)
def recursive[A](s : => Schema[A]) : Schema[A] = Schema.LazySchema(Lazy(s))

Expand Down
52 changes: 49 additions & 3 deletions modules/json/src/smithy4s/http/json/SchemaVisitorJCodec.scala
Original file line number Diff line number Diff line change
Expand Up @@ -517,6 +517,51 @@ private[smithy4s] class SchemaVisitorJCodec(maxArity: Int)
out.encodeError("Cannot use vectors as keys")
}

private def indexedSeq[A](
member: Schema[A]
): JCodec[IndexedSeq[A]] = new JCodec[IndexedSeq[A]] {
private[this] val a = apply(member)
def expecting: String = "list"

override def canBeKey: Boolean = false

def decodeValue(cursor: Cursor, in: JsonReader): IndexedSeq[A] =
if (in.isNextToken('[')) {
if (in.isNextToken(']')) Vector.empty
else {
in.rollbackToken()
CollectionTag.IndexedSeqTag.compactBuilder(member) { put =>
var i = 0
while ({
if (i >= maxArity)
throw cursor.payloadError(
this,
s"input $expecting exceeded max arity of `$maxArity`"
)
put(cursor.under(i)(cursor.decode(a, in)))
i += 1
in.isNextToken(',')
}) ()
if (!in.isCurrentToken(']')) {
in.arrayEndOrCommaError()
}
}
}
} else in.decodeError("Expected JSON array")

def encodeValue(xs: IndexedSeq[A], out: JsonWriter): Unit = {
out.writeArrayStart()
xs.foreach(x => a.encodeValue(x, out))
out.writeArrayEnd()
}

def decodeKey(in: JsonReader): IndexedSeq[A] =
in.decodeError("Cannot use vectors as keys")

def encodeKey(xs: IndexedSeq[A], out: JsonWriter): Unit =
out.encodeError("Cannot use vectors as keys")
}

private def set[A](
member: Schema[A]
): JCodec[Set[A]] = new JCodec[Set[A]] {
Expand Down Expand Up @@ -632,9 +677,10 @@ private[smithy4s] class SchemaVisitorJCodec(maxArity: Int)
member: Schema[A]
): JCodec[C[A]] = {
tag match {
case CollectionTag.List => listImpl(member)
case CollectionTag.Set => set(member)
case CollectionTag.Vector => vector(member)
case CollectionTag.ListTag => listImpl(member)
case CollectionTag.SetTag => set(member)
case CollectionTag.VectorTag => vector(member)
case CollectionTag.IndexedSeqTag => indexedSeq(member)
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,8 @@ class SchemaGenerator(maxWidth: Int) {
Vector(
recurse.map(Schema.list(_)),
recurse.map(Schema.set(_)),
recurse.map(Schema.vector(_)),
recurse.map(Schema.indexedSeq(_)),
Gen.zip(recurse, recurse).map { case (k, v) => map(k, v) },
genStruct,
genUnion
Expand Down