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]
}
5 changes: 5 additions & 0 deletions modules/core/src-3/Newtype.scala
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ abstract class Newtype[A] extends HasId { self =>
opaque type Type = A

def apply(a: A): Type = a
final val make: Newtype.Make[A, Type] = apply(_: A)

extension (orig: Type) def value: A = orig

Expand All @@ -42,3 +43,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]
}
11 changes: 6 additions & 5 deletions modules/core/src/smithy4s/http/internals/MetaDecode.scala
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,9 @@ import HttpBinding._

private[http] sealed abstract class MetaDecode[+A] {
def map[B](to: A => B): MetaDecode[B] = this match {
case StringValueMetaDecode(f) => StringValueMetaDecode(f andThen to)
case StringListMetaDecode(f) => StringListMetaDecode(f andThen to)
case StringValueMetaDecode(f) => StringValueMetaDecode(f andThen to)
case StringCollectionMetaDecode(f) =>
StringCollectionMetaDecode(f andThen to)
case StringMapMetaDecode(f) => StringMapMetaDecode(f andThen to)
case StringListMapMetaDecode(f) => StringListMapMetaDecode(f andThen to)
case EmptyMetaDecode => EmptyMetaDecode
Expand Down Expand Up @@ -76,7 +77,7 @@ private[http] sealed abstract class MetaDecode[+A] {
putField(fieldName, f(values.head))
} else throw MetadataError.ArityError(fieldName, binding)
}
case (HeaderBinding(h), StringListMetaDecode(f)) =>
case (HeaderBinding(h), StringCollectionMetaDecode(f)) =>
lookupAndProcess(_.headers, h) { (values, fieldName, putField) =>
putField(fieldName, f(values.iterator))
}
Expand All @@ -86,7 +87,7 @@ private[http] sealed abstract class MetaDecode[+A] {
putField(fieldName, f(values.head))
} else throw MetadataError.ArityError(fieldName, binding)
}
case (QueryBinding(q), StringListMetaDecode(f)) =>
case (QueryBinding(q), StringCollectionMetaDecode(f)) =>
lookupAndProcess(_.query, q) { (values, fieldName, putField) =>
putField(fieldName, f(values.iterator))
}
Expand Down Expand Up @@ -154,7 +155,7 @@ private[http] object MetaDecode {

// format: off
final case class StringValueMetaDecode[A](f: String => A) extends MetaDecode[A]
final case class StringListMetaDecode[A](f: Iterator[String] => A) extends MetaDecode[A]
final case class StringCollectionMetaDecode[A](f: Iterator[String] => A) extends MetaDecode[A]
final case class StringMapMetaDecode[A](f: Iterator[(String, String)] => A) extends MetaDecode[A]
final case class StringListMapMetaDecode[A](f: Iterator[(String, Iterator[String])] => A) extends MetaDecode[A]
case object EmptyMetaDecode extends MetaDecode[Nothing]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import smithy4s.http.internals.MetaDecode.{
EmptyMetaDecode,
PutField,
StringListMapMetaDecode,
StringListMetaDecode,
StringCollectionMetaDecode,
StringMapMetaDecode,
StringValueMetaDecode,
StructureMetaDecode
Expand Down Expand Up @@ -98,29 +98,21 @@ private[http] class SchemaVisitorMetadataReader()
}
}

override def list[A](
override def collection[C[_], A](
shapeId: ShapeId,
hints: Hints,
tag: CollectionTag[C],
member: Schema[A]
): MetaDecode[List[A]] = {
): MetaDecode[C[A]] = {
self(member) match {
case MetaDecode.StringValueMetaDecode(f) =>
MetaDecode.StringListMetaDecode[List[A]](_.map(f).toList)
MetaDecode.StringCollectionMetaDecode[C[A]] { it =>
tag.fromIterator(it.map(f))
}
case _ => EmptyMetaDecode
}
}

override def set[A](
shapeId: ShapeId,
hints: Hints,
member: Schema[A]
): MetaDecode[Set[A]] =
self(member) match {
case MetaDecode.StringValueMetaDecode(f) =>
MetaDecode.StringListMetaDecode[Set[A]](_.map(f).toSet)
case _ => EmptyMetaDecode
}

override def map[K, V](
shapeId: ShapeId,
hints: Hints,
Expand All @@ -132,7 +124,7 @@ private[http] class SchemaVisitorMetadataReader()
StringMapMetaDecode[Map[K, V]](map =>
map.map { case (k, v) => (readK(k), readV(v)) }.toMap
)
case (StringValueMetaDecode(readK), StringListMetaDecode(readV)) =>
case (StringValueMetaDecode(readK), StringCollectionMetaDecode(readV)) =>
StringListMapMetaDecode[Map[K, V]](map =>
map.map { case (k, v) => (readK(k), readV(v)) }.toMap
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import smithy4s.http.HttpBinding
import smithy4s.http.internals.MetaEncode._
import smithy4s.schema.Alt.SchemaAndValue
import smithy4s.schema.{
CollectionTag,
EnumValue,
Field,
Primitive,
Expand Down Expand Up @@ -84,26 +85,15 @@ object SchemaVisitorMetadataWriter extends SchemaVisitor[MetaEncode] { self =>
}
}

override def list[A](
override def collection[C[_], A](
shapeId: ShapeId,
hints: Hints,
tag: CollectionTag[C],
member: Schema[A]
): MetaEncode[List[A]] = {
): MetaEncode[C[A]] = {
self(member) match {
case StringValueMetaEncode(f) =>
StringListMetaEncode[List[A]](listA => listA.map(f))
case _ => MetaEncode.empty
}
}

override def set[A](
shapeId: ShapeId,
hints: Hints,
member: Schema[A]
): MetaEncode[Set[A]] = {
self(member) match {
case StringValueMetaEncode(f) =>
StringListMetaEncode[Set[A]](set => set.map(f).toList)
StringListMetaEncode[C[A]](c => tag.iterator(c).map(f).toList)
case _ => MetaEncode.empty
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,17 +57,12 @@ object SchemaVisitorPathEncoder extends SchemaVisitor[MaybePathEncode] { self =>
}
}

override def list[A](
override def collection[C[_], A](
shapeId: ShapeId,
hints: Hints,
tag: CollectionTag[C],
member: Schema[A]
): MaybePathEncode[List[A]] = default

override def set[A](
shapeId: ShapeId,
hints: Hints,
member: Schema[A]
): MaybePathEncode[Set[A]] = default
): MaybePathEncode[C[A]] = default

override def map[K, V](
shapeId: ShapeId,
Expand Down
27 changes: 10 additions & 17 deletions modules/core/src/smithy4s/internals/SchematicDocumentDecoder.scala
Original file line number Diff line number Diff line change
Expand Up @@ -229,25 +229,18 @@ object SchematicDocumentDecoder extends Schematic[DocumentDecoderMake] {
()
})

def list[S](fs: DocumentDecoderMake[S]): DocumentDecoderMake[List[S]] =
def collection[C[_], S](
tag: CollectionTag[C],
fs: DocumentDecoderMake[S]
): DocumentDecoderMake[C[S]] =
fs.transform { fa =>
DocumentDecoder.instance("List", "Array", false) {
DocumentDecoder.instance(tag.name, "Array", false) {
case (pp, DArray(value)) =>
value.zipWithIndex.map { case (document, index) =>
val localPath = PayloadPath.Segment(index) :: pp
fa(localPath, document)
}.toList
}
}

def set[S](fs: DocumentDecoderMake[S]): DocumentDecoderMake[Set[S]] =
fs.transform { fa =>
DocumentDecoder.instance("Set", "Array", false) {
case (pp, DArray(value)) =>
value.zipWithIndex.map { case (document, index) =>
val localPath = PayloadPath.Segment(index) :: pp
fa(localPath, document)
}.toSet
tag.fromIterator(value.iterator.zipWithIndex.map {
case (document, index) =>
val localPath = PayloadPath.Segment(index) :: pp
fa(localPath, document)
})
}
}

Expand Down
19 changes: 7 additions & 12 deletions modules/core/src/smithy4s/internals/SchematicDocumentEncoder.scala
Original file line number Diff line number Diff line change
Expand Up @@ -113,19 +113,14 @@ object SchematicDocumentEncoder extends Schematic[DocumentEncoderMake] {

def unit: DocumentEncoderMake[Unit] = fromNotKey(_ => DObject(Map.empty))

def list[S](fs: DocumentEncoderMake[S]): DocumentEncoderMake[List[S]] =
def collection[C[_], S](
tag: CollectionTag[C],
fs: DocumentEncoderMake[S]
): DocumentEncoderMake[C[S]] =
fs.transform(encoderS =>
fromNotKey[List[S]](l => DArray(l.map(encoderS.apply).toIndexedSeq)).get
)

def set[S](fs: DocumentEncoderMake[S]): DocumentEncoderMake[Set[S]] =
fs.transform(encoderS =>
fromNotKey[Set[S]](s => DArray(s.map(encoderS.apply).toIndexedSeq)).get
)

def vector[S](fs: DocumentEncoderMake[S]): DocumentEncoderMake[Vector[S]] =
fs.transform(encoderS =>
fromNotKey[Vector[S]](v => DArray(v.map(encoderS.apply).toIndexedSeq)).get
fromNotKey[C[S]](c =>
DArray(tag.iterator(c).map(encoderS.apply).toIndexedSeq)
).get
)

def map[K, V](
Expand Down
130 changes: 130 additions & 0 deletions modules/core/src/smithy4s/schema/CollectionTag.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
/*
* Copyright 2021-2022 Disney Streaming
*
* Licensed under the Tomorrow Open Source Technology License, Version 1.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://disneystreaming.github.io/TOST-1.0.txt
*
* 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 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

def iterator[A](c: C[A]): Iterator[A]
def build[A](put: (A => Unit) => Unit): C[A]

def fromIterator[A](it: Iterator[A]): C[A] = build(put => it.foreach(put(_)))
def empty[A]: C[A] = build(_ => ())
}

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

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 = mut.ListBuffer.newBuilder[A]
put(builder += (_))
builder.result().toList
}

}

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 = Set.newBuilder[A]
put(builder.+=(_))
builder.result()
}
}

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 = 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[_, _]]) apply(schema).asInstanceOf[MaybeCT[B]]
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
}
5 changes: 2 additions & 3 deletions modules/core/src/smithy4s/schema/PassthroughSchematic.scala
Original file line number Diff line number Diff line change
Expand Up @@ -46,9 +46,8 @@ class PassthroughSchematic[F[_]](schematic: Schematic[F]) extends Schematic[F] {

def unit: F[Unit] = schematic.unit

def list[S](fs: F[S]): F[List[S]] = schematic.list(fs)

def set[S](fs: F[S]): F[Set[S]] = schematic.set(fs)
def collection[C[_], S](tag: CollectionTag[C], fs: F[S]): F[C[S]] =
schematic.collection(tag, fs)

def map[K, V](fk: F[K], fv: F[V]): F[Map[K, V]] = schematic.map(fk, fv)

Expand Down
Loading