From ce3a315ced8904310f329e08b07a4dfc87eb4250 Mon Sep 17 00:00:00 2001 From: Nabil Abdel-Hafeez <7283535+987Nabil@users.noreply.github.com> Date: Wed, 14 Aug 2024 17:31:55 +0200 Subject: [PATCH] Non empty collection schemas (#717) (#723) --- build.sbt | 3 +- .../scala/zio/schema/DynamicValueGen.scala | 2 + .../scala/zio/schema/codec/AvroCodec.scala | 30 +++++- .../zio/schema/codec/AvroSchemaCodec.scala | 28 ++++-- .../schema/codec/AvroSchemaCodecSpec.scala | 5 +- .../zio/schema/codec/BsonSchemaCodec.scala | 68 +++++++------- .../schema/codec/BsonSchemaCodecSpec.scala | 4 +- .../main/scala/zio/schema/CachedDeriver.scala | 12 ++- .../scala/zio/schema/codec/JsonCodec.scala | 35 ++++--- .../zio/schema/optics/ZioOpticsBuilder.scala | 42 ++++++++- .../zio/schema/codec/ProtobufCodec.scala | 26 +++-- .../scala/zio/schema/codec/ThriftCodec.scala | 27 +++--- .../src/main/scala/zio/schema/Differ.scala | 10 +- .../MutableSchemaBasedValueBuilder.scala | 81 +++++++++++++++- .../MutableSchemaBasedValueProcessor.scala | 55 +++++++++++ .../src/main/scala/zio/schema/Schema.scala | 94 ++++++++++++++++++- .../schema/meta/ExtensibleMetaSchema.scala | 17 ++++ 17 files changed, 433 insertions(+), 106 deletions(-) diff --git a/build.sbt b/build.sbt index 7c77d3458..478857687 100644 --- a/build.sbt +++ b/build.sbt @@ -39,8 +39,7 @@ inThisBuild( ThisBuild / publishTo := sonatypePublishToBundle.value scalacOptions ++= Seq("-scalajs") -addCommandAlias("prepare", "fix; fmt") -addCommandAlias("fmt", "all scalafmtSbt scalafmtAll") +addCommandAlias("fmt", "all scalafmtSbt scalafmtAll;fix") addCommandAlias("fmtCheck", "all scalafmtSbtCheck scalafmtCheckAll") addCommandAlias("fix", "scalafixAll") addCommandAlias("fixCheck", "scalafixAll --check") diff --git a/tests/shared/src/test/scala/zio/schema/DynamicValueGen.scala b/tests/shared/src/test/scala/zio/schema/DynamicValueGen.scala index d17941483..0a311fae7 100644 --- a/tests/shared/src/test/scala/zio/schema/DynamicValueGen.scala +++ b/tests/shared/src/test/scala/zio/schema/DynamicValueGen.scala @@ -74,7 +74,9 @@ object DynamicValueGen { case Schema.Enum22(_, case1, case2, case3, case4, case5, case6, case7, case8, case9, case10, case11, case12, case13, case14, case15, case16, case17, case18, case19, case20, case21, case22, _) => anyDynamicValueOfEnum(Chunk(case1, case2, case3, case4, case5, case6, case7, case8, case9, case10, case11, case12, case13, case14, case15, case16, case17, case18, case19, case20, case21, case22)) case Schema.EnumN(_, cases, _) => anyDynamicValueOfEnum(Chunk.fromIterable(cases.toSeq)) case Schema.Sequence(schema, _, _, _, _) => Gen.chunkOfBounded(0, 2)(anyDynamicValueOfSchema(schema)).map(DynamicValue.Sequence(_)) + case Schema.NonEmptySequence(schema, _, _, _, _) => Gen.chunkOfBounded(1, 2)(anyDynamicValueOfSchema(schema)).map(DynamicValue.Sequence(_)) case Schema.Map(ks, vs, _) => Gen.chunkOfBounded(0, 2)(anyDynamicValueOfSchema(ks).zip(anyDynamicValueOfSchema(vs))).map(DynamicValue.Dictionary(_)) + case Schema.NonEmptyMap(ks, vs, _) => Gen.chunkOfBounded(1, 2)(anyDynamicValueOfSchema(ks).zip(anyDynamicValueOfSchema(vs))).map(DynamicValue.Dictionary(_)) case Schema.Set(schema, _) => Gen.setOfBounded(0, 2)(anyDynamicValueOfSchema(schema)).map(DynamicValue.SetValue(_)) case Schema.Optional(schema, _) => Gen.oneOf(anyDynamicSomeValueOfSchema(schema), Gen.const(DynamicValue.NoneValue)) case Schema.Tuple2(left, right, _) => anyDynamicTupleValue(left, right) diff --git a/zio-schema-avro/src/main/scala/zio/schema/codec/AvroCodec.scala b/zio-schema-avro/src/main/scala/zio/schema/codec/AvroCodec.scala index e1bca4ff5..a1b790e88 100644 --- a/zio-schema-avro/src/main/scala/zio/schema/codec/AvroCodec.scala +++ b/zio-schema-avro/src/main/scala/zio/schema/codec/AvroCodec.scala @@ -19,6 +19,7 @@ import org.apache.avro.io.{ DecoderFactory, EncoderFactory } import org.apache.avro.util.Utf8 import org.apache.avro.{ Conversions, LogicalTypes, Schema => SchemaAvro } +import zio.prelude.NonEmptyMap import zio.schema.{ Fallback, FieldSet, Schema, StandardType, TypeId } import zio.stream.ZPipeline import zio.{ Chunk, Unsafe, ZIO } @@ -201,9 +202,20 @@ object AvroCodec { case record: Schema.Record[_] => decodeRecord(raw, record).map(_.asInstanceOf[A]) case Schema.Sequence(element, f, _, _, _) => decodeSequence(raw, element.asInstanceOf[Schema[Any]]).map(f.asInstanceOf[Chunk[Any] => A]) + case nes @ Schema.NonEmptySequence(element, _, _, _, _) => + decodeSequence(raw, element.asInstanceOf[Schema[Any]]).map(nes.fromChunk.asInstanceOf[Chunk[Any] => A]) case Schema.Set(element, _) => decodeSequence(raw, element.asInstanceOf[Schema[Any]]).map(_.toSet.asInstanceOf[A]) case mapSchema: Schema.Map[_, _] => decodeMap(raw, mapSchema.asInstanceOf[Schema.Map[Any, Any]]).map(_.asInstanceOf[A]) + case mapSchema: Schema.NonEmptyMap[_, _] => + decodeMap( + raw, + Schema.Map( + mapSchema.keySchema.asInstanceOf[Schema[Any]], + mapSchema.valueSchema.asInstanceOf[Schema[Any]], + mapSchema.annotations + ) + ).map(mapSchema.asInstanceOf[Schema.NonEmptyMap[Any, Any]].fromMap(_).asInstanceOf[A]) case Schema.Transform(schema, f, _, _, _) => decodeValue(raw, schema).flatMap( a => f(a).left.map(msg => DecodeError.MalformedFieldWithPath(Chunk.single("Error"), msg)) @@ -662,12 +674,22 @@ object AvroCodec { c21, c22 ) - case Schema.GenericRecord(typeId, structure, _) => encodeGenericRecord(a, typeId, structure) - case Schema.Primitive(standardType, _) => encodePrimitive(a, standardType) - case Schema.Sequence(element, _, g, _, _) => encodeSequence(element, g(a)) - case Schema.Set(element, _) => encodeSet(element, a) + case Schema.GenericRecord(typeId, structure, _) => encodeGenericRecord(a, typeId, structure) + case Schema.Primitive(standardType, _) => encodePrimitive(a, standardType) + case Schema.Sequence(element, _, g, _, _) => encodeSequence(element, g(a)) + case Schema.NonEmptySequence(element, _, g, _, _) => encodeSequence(element, g(a)) + case Schema.Set(element, _) => encodeSet(element, a) case mapSchema: Schema.Map[_, _] => encodeMap(mapSchema.asInstanceOf[Schema.Map[Any, Any]], a.asInstanceOf[scala.collection.immutable.Map[Any, Any]]) + case mapSchema: Schema.NonEmptyMap[_, _] => + encodeMap( + Schema.Map( + mapSchema.keySchema.asInstanceOf[Schema[Any]], + mapSchema.valueSchema.asInstanceOf[Schema[Any]], + mapSchema.annotations + ), + a.asInstanceOf[NonEmptyMap[Any, Any]].toMap + ) case Schema.Transform(schema, _, g, _, _) => g(a).map(encodeValue(_, schema)).getOrElse(throw new Exception("Transform failed.")) case Schema.Optional(schema, _) => encodeOption(schema, a) diff --git a/zio-schema-avro/src/main/scala/zio/schema/codec/AvroSchemaCodec.scala b/zio-schema-avro/src/main/scala/zio/schema/codec/AvroSchemaCodec.scala index 95cf24b4c..7652f9d33 100644 --- a/zio-schema-avro/src/main/scala/zio/schema/codec/AvroSchemaCodec.scala +++ b/zio-schema-avro/src/main/scala/zio/schema/codec/AvroSchemaCodec.scala @@ -277,12 +277,14 @@ object AvroSchemaCodec extends AvroSchemaCodec { private def toAvroSchema(schema: Schema[_]): scala.util.Either[String, SchemaAvro] = { schema match { - case e: Enum[_] => toAvroEnum(e) - case record: Record[_] => toAvroRecord(record) - case map: Schema.Map[_, _] => toAvroMap(map) - case seq: Schema.Sequence[_, _, _] => toAvroSchema(seq.elementSchema).map(SchemaAvro.createArray) - case set: Schema.Set[_] => toAvroSchema(set.elementSchema).map(SchemaAvro.createArray) - case Transform(codec, _, _, _, _) => toAvroSchema(codec) + case e: Enum[_] => toAvroEnum(e) + case record: Record[_] => toAvroRecord(record) + case map: Schema.Map[_, _] => toAvroMap(map) + case map: Schema.NonEmptyMap[_, _] => toAvroMap(map) + case seq: Schema.Sequence[_, _, _] => toAvroSchema(seq.elementSchema).map(SchemaAvro.createArray) + case seq: Schema.NonEmptySequence[_, _, _] => toAvroSchema(seq.elementSchema).map(SchemaAvro.createArray) + case set: Schema.Set[_] => toAvroSchema(set.elementSchema).map(SchemaAvro.createArray) + case Transform(codec, _, _, _, _) => toAvroSchema(codec) case Primitive(standardType, _) => standardType match { case StandardType.UnitType => Right(SchemaAvro.create(SchemaAvro.Type.NULL)) @@ -624,6 +626,18 @@ object AvroSchemaCodec extends AvroSchemaCodec { toAvroSchema(tupleSchema).map(SchemaAvro.createArray) } + private[codec] def toAvroMap(map: NonEmptyMap[_, _]): scala.util.Either[String, SchemaAvro] = + map.keySchema match { + case p: Schema.Primitive[_] if p.standardType == StandardType.StringType => + toAvroSchema(map.valueSchema).map(SchemaAvro.createMap) + case _ => + val tupleSchema = Schema + .Tuple2(map.keySchema, map.valueSchema) + .annotate(AvroAnnotations.name("Tuple")) + .annotate(AvroAnnotations.namespace("scala")) + toAvroSchema(tupleSchema).map(SchemaAvro.createArray) + } + private[codec] def toAvroDecimal(schema: Schema[_]): scala.util.Either[String, SchemaAvro] = { val scale = schema.annotations.collectFirst { case AvroAnnotations.scale(s) => s } .getOrElse(AvroAnnotations.scale().scale) @@ -820,7 +834,9 @@ object AvroSchemaCodec extends AvroSchemaCodec { case c: Dynamic => Right(c) case c: GenericRecord => Right(c) case c: Map[_, _] => Right(c) + case c: NonEmptyMap[_, _] => Right(c) case c: Sequence[_, _, _] => Right(c) + case c: NonEmptySequence[_, _, _] => Right(c) case c: Set[_] => Right(c) case c: Fail[_] => Right(c) case c: Lazy[_] => Right(c) diff --git a/zio-schema-avro/src/test/scala/zio/schema/codec/AvroSchemaCodecSpec.scala b/zio-schema-avro/src/test/scala/zio/schema/codec/AvroSchemaCodecSpec.scala index bc19b6b82..7306bbce1 100644 --- a/zio-schema-avro/src/test/scala/zio/schema/codec/AvroSchemaCodecSpec.scala +++ b/zio-schema-avro/src/test/scala/zio/schema/codec/AvroSchemaCodecSpec.scala @@ -1918,10 +1918,7 @@ object AssertionHelper { def recordFields(assertion: Assertion[Iterable[Schema.Field[_, _]]]): Assertion[Schema.Record[_]] = Assertion.assertionRec[Schema.Record[_], Chunk[Field[_, _]]]("hasRecordField")( assertion - ) { - case r: Schema.Record[_] => Some(r.fields) - case _ => None - } + )((r: Schema.Record[_]) => Some(r.fields)) def hasSequenceElementSchema[A](assertion: Assertion[Schema[A]]): Assertion[Schema.Sequence[_, A, _]] = Assertion.hasField("schemaA", _.elementSchema, assertion) diff --git a/zio-schema-bson/src/main/scala/zio/schema/codec/BsonSchemaCodec.scala b/zio-schema-bson/src/main/scala/zio/schema/codec/BsonSchemaCodec.scala index bf3298813..2194993ec 100644 --- a/zio-schema-bson/src/main/scala/zio/schema/codec/BsonSchemaCodec.scala +++ b/zio-schema-bson/src/main/scala/zio/schema/codec/BsonSchemaCodec.scala @@ -457,22 +457,24 @@ object BsonSchemaCodec { //scalafmt: { maxColumn = 400, optIn.configStyleArguments = false } private[codec] def schemaEncoder[A](schema: Schema[A]): BsonEncoder[A] = schema match { - case Schema.Primitive(standardType, _) => primitiveCodec(standardType).encoder - case Schema.Sequence(schema, _, g, _, _) => chunkEncoder(schemaEncoder(schema)).contramap(g) - case Schema.Map(ks, vs, _) => mapEncoder(ks, vs) - case Schema.Set(s, _) => chunkEncoder(schemaEncoder(s)).contramap(m => Chunk.fromIterable(m)) - case Schema.Transform(c, _, g, _, _) => transformEncoder(c, g) - case Schema.Tuple2(l, r, _) => tuple2Encoder(schemaEncoder(l), schemaEncoder(r)) - case Schema.Optional(schema, _) => BsonEncoder.option(schemaEncoder(schema)) - case Schema.Fail(_, _) => unitEncoder.contramap(_ => ()) - case Schema.GenericRecord(_, structure, _) => genericRecordEncoder(structure.toChunk) - case Schema.Either(left, right, _) => eitherEncoder(schemaEncoder(left), schemaEncoder(right)) - case Schema.Fallback(left, right, _, _) => fallbackEncoder(schemaEncoder(left), schemaEncoder(right)) - case l @ Schema.Lazy(_) => schemaEncoder(l.schema) - case r: Schema.Record[A] => caseClassEncoder(r) - case e: Schema.Enum[A] => enumEncoder(e, e.cases) - case d @ Schema.Dynamic(_) => dynamicEncoder(d) - case null => throw new Exception(s"A captured schema is null, most likely due to wrong field initialization order") + case Schema.Primitive(standardType, _) => primitiveCodec(standardType).encoder + case Schema.Sequence(schema, _, g, _, _) => chunkEncoder(schemaEncoder(schema)).contramap(g) + case Schema.NonEmptySequence(schema, _, g, _, _) => chunkEncoder(schemaEncoder(schema)).contramap(g) + case Schema.Map(ks, vs, _) => mapEncoder(ks, vs) + case Schema.NonEmptyMap(ks, vs, _) => mapEncoder(ks, vs).contramap(_.toMap) + case Schema.Set(s, _) => chunkEncoder(schemaEncoder(s)).contramap(m => Chunk.fromIterable(m)) + case Schema.Transform(c, _, g, _, _) => transformEncoder(c, g) + case Schema.Tuple2(l, r, _) => tuple2Encoder(schemaEncoder(l), schemaEncoder(r)) + case Schema.Optional(schema, _) => BsonEncoder.option(schemaEncoder(schema)) + case Schema.Fail(_, _) => unitEncoder.contramap(_ => ()) + case Schema.GenericRecord(_, structure, _) => genericRecordEncoder(structure.toChunk) + case Schema.Either(left, right, _) => eitherEncoder(schemaEncoder(left), schemaEncoder(right)) + case Schema.Fallback(left, right, _, _) => fallbackEncoder(schemaEncoder(left), schemaEncoder(right)) + case l @ Schema.Lazy(_) => schemaEncoder(l.schema) + case r: Schema.Record[A] => caseClassEncoder(r) + case e: Schema.Enum[A] => enumEncoder(e, e.cases) + case d @ Schema.Dynamic(_) => dynamicEncoder(d) + case null => throw new Exception(s"A captured schema is null, most likely due to wrong field initialization order") } //scalafmt: { maxColumn = 120, optIn.configStyleArguments = true } @@ -773,22 +775,24 @@ object BsonSchemaCodec { //scalafmt: { maxColumn = 400, optIn.configStyleArguments = false } private[codec] def schemaDecoder[A](schema: Schema[A]): BsonDecoder[A] = schema match { - case Schema.Primitive(standardType, _) => primitiveCodec(standardType).decoder - case Schema.Optional(codec, _) => BsonDecoder.option(schemaDecoder(codec)) - case Schema.Tuple2(left, right, _) => tuple2Decoder(schemaDecoder(left), schemaDecoder(right)) - case Schema.Transform(codec, f, _, _, _) => schemaDecoder(codec).mapOrFail(f) - case Schema.Sequence(codec, f, _, _, _) => chunkDecoder(schemaDecoder(codec)).map(f) - case Schema.Map(ks, vs, _) => mapDecoder(ks, vs) - case Schema.Set(s, _) => chunkDecoder(schemaDecoder(s)).map(entries => entries.toSet) - case Schema.Fail(message, _) => failDecoder(message) - case Schema.GenericRecord(_, structure, _) => recordDecoder(structure.toChunk) - case Schema.Either(left, right, _) => eitherDecoder(schemaDecoder(left), schemaDecoder(right)) - case Schema.Fallback(left, right, _, _) => fallbackDecoder(schemaDecoder(left), schemaDecoder(right)) - case l @ Schema.Lazy(_) => schemaDecoder(l.schema) - case s: Schema.Record[A] => caseClassDecoder(s) - case e: Schema.Enum[A] => enumDecoder(e) - case d @ Schema.Dynamic(_) => dynamicDecoder(d) - case null => throw new Exception(s"Missing a handler for decoding of schema $schema.") + case Schema.Primitive(standardType, _) => primitiveCodec(standardType).decoder + case Schema.Optional(codec, _) => BsonDecoder.option(schemaDecoder(codec)) + case Schema.Tuple2(left, right, _) => tuple2Decoder(schemaDecoder(left), schemaDecoder(right)) + case Schema.Transform(codec, f, _, _, _) => schemaDecoder(codec).mapOrFail(f) + case Schema.Sequence(codec, f, _, _, _) => chunkDecoder(schemaDecoder(codec)).map(f) + case s @ Schema.NonEmptySequence(codec, _, _, _, _) => chunkDecoder(schemaDecoder(codec)).map(s.fromChunk) + case Schema.Map(ks, vs, _) => mapDecoder(ks, vs) + case s @ Schema.NonEmptyMap(ks, vs, _) => mapDecoder(ks, vs).map(s.fromMap) + case Schema.Set(s, _) => chunkDecoder(schemaDecoder(s)).map(entries => entries.toSet) + case Schema.Fail(message, _) => failDecoder(message) + case Schema.GenericRecord(_, structure, _) => recordDecoder(structure.toChunk) + case Schema.Either(left, right, _) => eitherDecoder(schemaDecoder(left), schemaDecoder(right)) + case Schema.Fallback(left, right, _, _) => fallbackDecoder(schemaDecoder(left), schemaDecoder(right)) + case l @ Schema.Lazy(_) => schemaDecoder(l.schema) + case s: Schema.Record[A] => caseClassDecoder(s) + case e: Schema.Enum[A] => enumDecoder(e) + case d @ Schema.Dynamic(_) => dynamicDecoder(d) + case _ => throw new Exception(s"Missing a handler for decoding of schema $schema.") } //scalafmt: { maxColumn = 120, optIn.configStyleArguments = true } diff --git a/zio-schema-bson/src/test/scala/zio/schema/codec/BsonSchemaCodecSpec.scala b/zio-schema-bson/src/test/scala/zio/schema/codec/BsonSchemaCodecSpec.scala index 941dc1450..e3c743c50 100644 --- a/zio-schema-bson/src/test/scala/zio/schema/codec/BsonSchemaCodecSpec.scala +++ b/zio-schema-bson/src/test/scala/zio/schema/codec/BsonSchemaCodecSpec.scala @@ -40,10 +40,10 @@ object BsonSchemaCodecSpec extends ZIOSpecDefault { implicit lazy val schema: Schema[Tree] = DeriveSchema.gen implicit lazy val codec: BsonCodec[Tree] = BsonSchemaCodec.bsonCodec(schema) - private val genLeaf = Gen.int.map(Leaf) + private val genLeaf = Gen.int.map(Leaf.apply) lazy val gen: Gen[Any, Tree] = Gen.sized { i => - if (i >= 2) Gen.oneOf(genLeaf, Gen.suspend(gen.zipWith(gen)(Branch)).resize(i / 2)) + if (i >= 2) Gen.oneOf(genLeaf, Gen.suspend(gen.zipWith(gen)(Branch.apply)).resize(i / 2)) else genLeaf } } diff --git a/zio-schema-derivation/shared/src/main/scala/zio/schema/CachedDeriver.scala b/zio-schema-derivation/shared/src/main/scala/zio/schema/CachedDeriver.scala index 53d516a65..3dd37ac99 100644 --- a/zio-schema-derivation/shared/src/main/scala/zio/schema/CachedDeriver.scala +++ b/zio-schema-derivation/shared/src/main/scala/zio/schema/CachedDeriver.scala @@ -126,18 +126,22 @@ private[schema] object CachedDeriver { final case class Tuple2[A, B](leftKey: CacheKey[A], rightKey: CacheKey[B]) extends CacheKey[(A, B)] final case class Set[A](element: CacheKey[A]) extends CacheKey[Set[A]] final case class Map[K, V](key: CacheKey[K], valuew: CacheKey[V]) extends CacheKey[Map[K, V]] + final case class NonEmptyMap[K, V](key: CacheKey[K], valuew: CacheKey[V]) extends CacheKey[NonEmptyMap[K, V]] final case class Misc[A](schema: Schema[A]) extends CacheKey[A] def fromStandardType[A](st: StandardType[A]): CacheKey[A] = Primitive(st) def fromSchema[A](schema: Schema[A]): CacheKey[A] = schema match { - case e: Schema.Enum[_] => WithId(e.id) - case record: Schema.Record[_] => WithId(record.id) - case seq: Schema.Sequence[_, _, _] => WithIdentityObject(fromSchema(seq.elementSchema), seq.identity) - case set: Schema.Set[_] => Set(fromSchema(set.elementSchema)).asInstanceOf[CacheKey[A]] + case e: Schema.Enum[_] => WithId(e.id) + case record: Schema.Record[_] => WithId(record.id) + case seq: Schema.Sequence[_, _, _] => WithIdentityObject(fromSchema(seq.elementSchema), seq.identity) + case seq: Schema.NonEmptySequence[_, _, _] => WithIdentityObject(fromSchema(seq.elementSchema), seq.identity) + case set: Schema.Set[_] => Set(fromSchema(set.elementSchema)).asInstanceOf[CacheKey[A]] case map: Schema.Map[_, _] => Map(fromSchema(map.keySchema), fromSchema(map.valueSchema)).asInstanceOf[CacheKey[A]] + case map: Schema.NonEmptyMap[_, _] => + Map(fromSchema(map.keySchema), fromSchema(map.valueSchema)).asInstanceOf[CacheKey[A]] case Schema.Transform(inner, _, _, _, identity) => WithIdentityObject(fromSchema(inner), identity) case Schema.Primitive(standardType, _) => fromStandardType(standardType) case optional: Schema.Optional[_] => Optional(fromSchema(optional.schema)).asInstanceOf[CacheKey[A]] diff --git a/zio-schema-json/shared/src/main/scala/zio/schema/codec/JsonCodec.scala b/zio-schema-json/shared/src/main/scala/zio/schema/codec/JsonCodec.scala index 23a5f5635..93e9113ac 100644 --- a/zio-schema-json/shared/src/main/scala/zio/schema/codec/JsonCodec.scala +++ b/zio-schema-json/shared/src/main/scala/zio/schema/codec/JsonCodec.scala @@ -17,6 +17,7 @@ import zio.json.{ JsonFieldDecoder, JsonFieldEncoder } +import zio.prelude.NonEmptyMap import zio.schema._ import zio.schema.annotation._ import zio.schema.codec.DecodeError.ReadError @@ -182,9 +183,11 @@ object JsonCodec { //scalafmt: { maxColumn = 400, optIn.configStyleArguments = false } private[codec] def schemaEncoder[A](schema: Schema[A], cfg: Config, discriminatorTuple: DiscriminatorTuple = Chunk.empty): ZJsonEncoder[A] = schema match { - case Schema.Primitive(standardType, _) => primitiveCodec(standardType).encoder - case Schema.Sequence(schema, _, g, _, _) => ZJsonEncoder.chunk(schemaEncoder(schema, cfg, discriminatorTuple)).contramap(g) - case Schema.Map(ks, vs, _) => mapEncoder(ks, vs, discriminatorTuple, cfg) + case Schema.Primitive(standardType, _) => primitiveCodec(standardType).encoder + case Schema.Sequence(schema, _, g, _, _) => ZJsonEncoder.chunk(schemaEncoder(schema, cfg, discriminatorTuple)).contramap(g) + case Schema.NonEmptySequence(schema, _, g, _, _) => ZJsonEncoder.chunk(schemaEncoder(schema, cfg, discriminatorTuple)).contramap(g) + case Schema.Map(ks, vs, _) => mapEncoder(ks, vs, discriminatorTuple, cfg) + case Schema.NonEmptyMap(ks: Schema[kt], vs: Schema[vt], _) => mapEncoder(ks, vs, discriminatorTuple, cfg).contramap[NonEmptyMap[kt, vt]](_.toMap.asInstanceOf[Map[kt, vt]]).asInstanceOf[ZJsonEncoder[A]] case Schema.Set(s, _) => ZJsonEncoder.chunk(schemaEncoder(s, cfg, discriminatorTuple)).contramap(m => Chunk.fromIterable(m)) case Schema.Transform(c, _, g, a, _) => transformEncoder(a.foldLeft(c)((s, a) => s.annotate(a)), g, cfg) @@ -544,18 +547,20 @@ object JsonCodec { //scalafmt: { maxColumn = 400, optIn.configStyleArguments = false } private[codec] def schemaDecoder[A](schema: Schema[A], discriminator: Int = -1): ZJsonDecoder[A] = schema match { - case Schema.Primitive(standardType, _) => primitiveCodec(standardType).decoder - case Schema.Optional(codec, _) => option(schemaDecoder(codec, discriminator)) - case Schema.Tuple2(left, right, _) => ZJsonDecoder.tuple2(schemaDecoder(left, -1), schemaDecoder(right, -1)) - case Schema.Transform(c, f, _, a, _) => schemaDecoder(a.foldLeft(c)((s, a) => s.annotate(a)), discriminator).mapOrFail(f) - case Schema.Sequence(codec, f, _, _, _) => ZJsonDecoder.chunk(schemaDecoder(codec, -1)).map(f) - case Schema.Map(ks, vs, _) => mapDecoder(ks, vs) - case Schema.Set(s, _) => ZJsonDecoder.chunk(schemaDecoder(s, -1)).map(entries => entries.toSet) - case Schema.Fail(message, _) => failDecoder(message) - case Schema.GenericRecord(_, structure, _) => recordDecoder(structure.toChunk, schema.annotations.contains(rejectExtraFields())) - case Schema.Either(left, right, _) => ZJsonDecoder.either(schemaDecoder(left, -1), schemaDecoder(right, -1)) - case s @ Schema.Fallback(_, _, _, _) => fallbackDecoder(s) - case l @ Schema.Lazy(_) => schemaDecoder(l.schema, discriminator) + case Schema.Primitive(standardType, _) => primitiveCodec(standardType).decoder + case Schema.Optional(codec, _) => option(schemaDecoder(codec, discriminator)) + case Schema.Tuple2(left, right, _) => ZJsonDecoder.tuple2(schemaDecoder(left, -1), schemaDecoder(right, -1)) + case Schema.Transform(c, f, _, a, _) => schemaDecoder(a.foldLeft(c)((s, a) => s.annotate(a)), discriminator).mapOrFail(f) + case Schema.Sequence(codec, f, _, _, _) => ZJsonDecoder.chunk(schemaDecoder(codec, -1)).map(f) + case s @ Schema.NonEmptySequence(codec, _, _, _, _) => ZJsonDecoder.chunk(schemaDecoder(codec, -1)).map(s.fromChunk) + case Schema.Map(ks, vs, _) => mapDecoder(ks, vs) + case Schema.NonEmptyMap(ks, vs, _) => mapDecoder(ks, vs).mapOrFail(m => NonEmptyMap.fromMapOption(m).toRight("NonEmptyMap expected")) + case Schema.Set(s, _) => ZJsonDecoder.chunk(schemaDecoder(s, -1)).map(entries => entries.toSet) + case Schema.Fail(message, _) => failDecoder(message) + case Schema.GenericRecord(_, structure, _) => recordDecoder(structure.toChunk, schema.annotations.contains(rejectExtraFields())) + case Schema.Either(left, right, _) => ZJsonDecoder.either(schemaDecoder(left, -1), schemaDecoder(right, -1)) + case s @ Schema.Fallback(_, _, _, _) => fallbackDecoder(s) + case l @ Schema.Lazy(_) => schemaDecoder(l.schema, discriminator) //case Schema.Meta(_, _) => astDecoder case s @ Schema.CaseClass0(_, _, _) => caseClass0Decoder(discriminator, s) case s @ Schema.CaseClass1(_, _, _, _) => caseClass1Decoder(discriminator, s) diff --git a/zio-schema-optics/shared/src/main/scala/zio/schema/optics/ZioOpticsBuilder.scala b/zio-schema-optics/shared/src/main/scala/zio/schema/optics/ZioOpticsBuilder.scala index 6da76d0d8..73bf1b27f 100644 --- a/zio-schema-optics/shared/src/main/scala/zio/schema/optics/ZioOpticsBuilder.scala +++ b/zio-schema-optics/shared/src/main/scala/zio/schema/optics/ZioOpticsBuilder.scala @@ -3,6 +3,7 @@ package zio.schema.optics import scala.collection.immutable.ListMap import zio.optics._ +import zio.prelude.NonEmptyMap import zio.schema._ import zio.{ Chunk, ChunkBuilder } @@ -44,14 +45,24 @@ object ZioOpticsBuilder extends AccessorBuilder { collection match { case seq @ Schema.Sequence(_, _, _, _, _) => ZTraversal[S, S, A, A]( - ZioOpticsBuilder.makeSeqTraversalGet(seq), + ZioOpticsBuilder.makeSeqTraversalGet(seq.toChunk), ZioOpticsBuilder.makeSeqTraversalSet(seq) ) + case seq @ Schema.NonEmptySequence(_, _, _, _, _) => + ZTraversal[S, S, A, A]( + ZioOpticsBuilder.makeSeqTraversalGet(seq.toChunk), + ZioOpticsBuilder.makeNonEmptySeqTraversalSet(seq) + ) case Schema.Map(_: Schema[k], _: Schema[v], _) => ZTraversal( ZioOpticsBuilder.makeMapTraversalGet[k, v], ZioOpticsBuilder.makeMapTraversalSet[k, v] ) + case Schema.NonEmptyMap(_: Schema[k], _: Schema[v], _) => + ZTraversal( + ZioOpticsBuilder.makeMapTraversalGet[k, v], + ZioOpticsBuilder.makeNonEmptyMapTraversalSet[k, v] + ) case Schema.Set(_, _) => ZTraversal( ZioOpticsBuilder.makeSetTraversalGet[A], @@ -103,9 +114,9 @@ object ZioOpticsBuilder extends AccessorBuilder { } private[optics] def makeSeqTraversalGet[S, A]( - collection: Schema.Sequence[S, A, _] + toChunk: S => Chunk[A] ): S => Either[(OpticFailure, S), Chunk[A]] = { (whole: S) => - Right(collection.toChunk(whole)) + Right(toChunk(whole)) } private[optics] def makeSeqTraversalSet[S, A]( @@ -123,15 +134,40 @@ object ZioOpticsBuilder extends AccessorBuilder { } Right(collection.fromChunk(builder.result())) } + private[optics] def makeNonEmptySeqTraversalSet[S, A]( + collection: Schema.NonEmptySequence[S, A, _] + ): Chunk[A] => S => Either[(OpticFailure, S), S] = { (piece: Chunk[A]) => (whole: S) => + val builder = ChunkBuilder.make[A]() + val leftIterator = collection.toChunk(whole).iterator + val rightIterator = piece.iterator + while (leftIterator.hasNext && rightIterator.hasNext) { + val _ = leftIterator.next() + builder += rightIterator.next() + } + while (leftIterator.hasNext) { + builder += leftIterator.next() + } + Right(collection.fromChunk(builder.result())) + } private[optics] def makeMapTraversalGet[K, V](whole: Map[K, V]): Either[(OpticFailure, Map[K, V]), Chunk[(K, V)]] = Right(Chunk.fromIterable(whole)) + private[optics] def makeMapTraversalGet[K, V]( + whole: NonEmptyMap[K, V] + ): Either[(OpticFailure, NonEmptyMap[K, V]), Chunk[(K, V)]] = + Right(Chunk.fromIterable(whole.toMap)) + private[optics] def makeMapTraversalSet[K, V] : Chunk[(K, V)] => Map[K, V] => Either[(OpticFailure, Map[K, V]), Map[K, V]] = { (piece: Chunk[(K, V)]) => (whole: Map[K, V]) => Right(whole ++ piece.toList) } + private[optics] def makeNonEmptyMapTraversalSet[K, V] + : Chunk[(K, V)] => NonEmptyMap[K, V] => Either[(OpticFailure, NonEmptyMap[K, V]), NonEmptyMap[K, V]] = { + (piece: Chunk[(K, V)]) => (whole: NonEmptyMap[K, V]) => + Right(whole ++ piece.toList) + } private[optics] def makeSetTraversalGet[A](whole: Set[A]): Either[(OpticFailure, Set[A]), Chunk[A]] = Right(Chunk.fromIterable(whole)) diff --git a/zio-schema-protobuf/shared/src/main/scala/zio/schema/codec/ProtobufCodec.scala b/zio-schema-protobuf/shared/src/main/scala/zio/schema/codec/ProtobufCodec.scala index 77a87e90f..c51bafeef 100644 --- a/zio-schema-protobuf/shared/src/main/scala/zio/schema/codec/ProtobufCodec.scala +++ b/zio-schema-protobuf/shared/src/main/scala/zio/schema/codec/ProtobufCodec.scala @@ -54,16 +54,17 @@ object ProtobufCodec { */ @scala.annotation.tailrec private[codec] def canBePacked(schema: Schema[_]): Boolean = schema match { - case Schema.Sequence(element, _, _, _, _) => canBePacked(element) - case Schema.Transform(codec, _, _, _, _) => canBePacked(codec) - case Schema.Primitive(standardType, _) => canBePacked(standardType) - case _: Schema.Tuple2[_, _] => false - case _: Schema.Optional[_] => false - case _: Schema.Fail[_] => false - case _: Schema.Either[_, _] => false - case _: Schema.Fallback[_, _] => false - case lzy @ Schema.Lazy(_) => canBePacked(lzy.schema) - case _ => false + case Schema.Sequence(element, _, _, _, _) => canBePacked(element) + case Schema.NonEmptySequence(element, _, _, _, _) => canBePacked(element) + case Schema.Transform(codec, _, _, _, _) => canBePacked(codec) + case Schema.Primitive(standardType, _) => canBePacked(standardType) + case _: Schema.Tuple2[_, _] => false + case _: Schema.Optional[_] => false + case _: Schema.Fail[_] => false + case _: Schema.Either[_, _] => false + case _: Schema.Fallback[_, _] => false + case lzy @ Schema.Lazy(_) => canBePacked(lzy.schema) + case _ => false } private def canBePacked(standardType: StandardType[_]): Boolean = standardType match { @@ -181,8 +182,6 @@ object ProtobufCodec { encodeKey(rightWireType, Some(2)) ++ rightDecoder.remainder encodeKey(WireType.LengthDelimited(data.size), Some(seqIndex)) ++ data - case other => - throw new IllegalStateException(s"Invalid state in processDictionary: $other") } }.flatten val data = encodeKey( @@ -370,7 +369,7 @@ object ProtobufCodec { byteBuffer.order(ByteOrder.LITTLE_ENDIAN) byteBuffer.putDouble(v) encodeKey(WireType.Bit64, fieldNumber) ++ Chunk.fromArray(byteBuffer.array) - case (StandardType.BinaryType, bytes: Chunk[Byte]) => + case (StandardType.BinaryType, bytes: Chunk[Byte] @unchecked) => encodeKey(WireType.LengthDelimited(bytes.length), fieldNumber) ++ bytes case (StandardType.CharType, c: Char) => encodePrimitive(fieldNumber, StandardType.StringType, c.toString) @@ -780,7 +779,6 @@ object ProtobufCodec { override protected def finishedCreatingOneSequenceElement( context: DecoderContext, - schema: Schema.Sequence[_, _, _], index: Int ): Boolean = state.length(context) > 0 diff --git a/zio-schema-thrift/src/main/scala/zio/schema/codec/ThriftCodec.scala b/zio-schema-thrift/src/main/scala/zio/schema/codec/ThriftCodec.scala index bfc928d58..06ab301aa 100644 --- a/zio-schema-thrift/src/main/scala/zio/schema/codec/ThriftCodec.scala +++ b/zio-schema-thrift/src/main/scala/zio/schema/codec/ThriftCodec.scala @@ -432,18 +432,20 @@ object ThriftCodec { @tailrec final private def getType[A](schema: Schema[A]): Byte = schema match { - case _: Schema.Record[A] => TType.STRUCT - case Schema.Sequence(_, _, _, _, _) => TType.LIST - case Schema.Map(_, _, _) => TType.MAP - case Schema.Set(_, _) => TType.SET - case Schema.Transform(schema, _, _, _, _) => getType(schema) - case Schema.Primitive(standardType, _) => getPrimitiveType(standardType) - case Schema.Tuple2(_, _, _) => TType.STRUCT - case Schema.Optional(schema, _) => getType(schema) - case Schema.Either(_, _, _) => TType.STRUCT - case Schema.Lazy(lzy) => getType(lzy()) - case _: Schema.Enum[A] => TType.STRUCT - case _ => TType.VOID + case _: Schema.Record[A] => TType.STRUCT + case Schema.Sequence(_, _, _, _, _) => TType.LIST + case Schema.NonEmptySequence(_, _, _, _, _) => TType.LIST + case Schema.Map(_, _, _) => TType.MAP + case Schema.NonEmptyMap(_, _, _) => TType.MAP + case Schema.Set(_, _) => TType.SET + case Schema.Transform(schema, _, _, _, _) => getType(schema) + case Schema.Primitive(standardType, _) => getPrimitiveType(standardType) + case Schema.Tuple2(_, _, _) => TType.STRUCT + case Schema.Optional(schema, _) => getType(schema) + case Schema.Either(_, _, _) => TType.STRUCT + case Schema.Lazy(lzy) => getType(lzy()) + case _: Schema.Enum[A] => TType.STRUCT + case _ => TType.VOID } } @@ -691,7 +693,6 @@ object ThriftCodec { override protected def finishedCreatingOneSequenceElement( context: DecoderContext, - schema: Schema.Sequence[_, _, _], index: Int ): Boolean = context.expectedCount.map(_ - (index + 1)).exists(_ > 0) diff --git a/zio-schema/shared/src/main/scala/zio/schema/Differ.scala b/zio-schema/shared/src/main/scala/zio/schema/Differ.scala index 5796e53aa..132a01832 100644 --- a/zio-schema/shared/src/main/scala/zio/schema/Differ.scala +++ b/zio-schema/shared/src/main/scala/zio/schema/Differ.scala @@ -25,6 +25,7 @@ import java.util.{ Currency, UUID } import scala.annotation.nowarn import scala.collection.immutable.ListMap +import zio.prelude.NonEmptyMap import zio.schema.diff.Edit import zio.{ Chunk, ChunkBuilder } @@ -263,12 +264,13 @@ object Differ { Right(Currency.getInstance(s)) } catch { case e: Throwable => Left(s"$s is not a valid Currency: ${e.getMessage}") } ) - case Schema.Tuple2(leftSchema, rightSchema, _) => fromSchema(leftSchema) <*> fromSchema(rightSchema) - case Schema.Optional(schema, _) => fromSchema(schema).optional - case Schema.Sequence(schema, g, f, _, _) => - fromSchema(schema).chunk.transform(f, g) + case Schema.Tuple2(leftSchema, rightSchema, _) => fromSchema(leftSchema) <*> fromSchema(rightSchema) + case Schema.Optional(schema, _) => fromSchema(schema).optional + case Schema.Sequence(schema, g, f, _, _) => fromSchema(schema).chunk.transform(f, g) + case s @ Schema.NonEmptySequence(schema, _, f, _, _) => fromSchema(schema).chunk.transform(f, s.fromChunk) case Schema.Set(s, _) => set(s) case Schema.Map(k, v, _) => map(k, v) + case s @ Schema.NonEmptyMap(k: Schema[kt], v: Schema[vt], _) => map(k, v).transform[NonEmptyMap[kt, vt]](_.toMap.asInstanceOf[Map[kt, vt]], s.fromMap).asInstanceOf[Differ[A]] case Schema.Either(leftSchema, rightSchema, _) => either(fromSchema(leftSchema), fromSchema(rightSchema)) case Schema.Fallback(leftSchema, rightSchema, _, _) => fallback(fromSchema(leftSchema), fromSchema(rightSchema)) case s @ Schema.Lazy(_) => fromSchema(s.schema) diff --git a/zio-schema/shared/src/main/scala/zio/schema/MutableSchemaBasedValueBuilder.scala b/zio-schema/shared/src/main/scala/zio/schema/MutableSchemaBasedValueBuilder.scala index 9234ffbfd..fab014698 100644 --- a/zio-schema/shared/src/main/scala/zio/schema/MutableSchemaBasedValueBuilder.scala +++ b/zio-schema/shared/src/main/scala/zio/schema/MutableSchemaBasedValueBuilder.scala @@ -2,6 +2,7 @@ package zio.schema import scala.util.control.NonFatal +import zio.prelude.NonEmptyMap import zio.schema.MutableSchemaBasedValueBuilder.{ CreateValueFromSchemaError, ReadingFieldResult } import zio.{ Chunk, ChunkBuilder } @@ -63,7 +64,6 @@ trait MutableSchemaBasedValueBuilder[Target, Context] { * is called. */ protected def finishedCreatingOneSequenceElement( context: Context, - schema: Schema.Sequence[_, _, _], index: Int ): Boolean @@ -542,7 +542,37 @@ trait MutableSchemaBasedValueBuilder[Target, Context] { elems += elem contextStack = contextStack.tail - val continue = finishedCreatingOneSequenceElement(contextStack.head, s, index) + val continue = finishedCreatingOneSequenceElement(contextStack.head, index) + + if (continue) { + currentSchema = elementSchema + pushContext(startCreatingOneSequenceElement(contextStack.head, s)) + readOne(index + 1) + } else { + contextStack = contextStack.tail + finishWith(createSequence(contextStack.head, s, elems.result())) + } + } + + currentSchema = elementSchema + startCreatingSequence(currentContext, s) match { + case Some(startingState) => + pushContext(startingState) + pushContext(startCreatingOneSequenceElement(startingState, s)) + readOne(0) + case None => + finishWith(createSequence(currentContext, s, Chunk.empty)) + } + case nes @ Schema.NonEmptySequence(elementSchema, _, _, _, _) => + val s = Schema.Sequence(elementSchema, nes.fromChunk, nes.toChunk, nes.annotations, nes.identity) + val elems = ChunkBuilder.make[Target]() + + def readOne(index: Int): Unit = + push { elem => + elems += elem + + contextStack = contextStack.tail + val continue = finishedCreatingOneSequenceElement(contextStack.head, index) if (continue) { currentSchema = elementSchema @@ -600,6 +630,53 @@ trait MutableSchemaBasedValueBuilder[Target, Context] { case None => finishWith(createDictionary(contextStack.head, s, Chunk.empty)) } + case nem @ Schema.NonEmptyMap(ks: Schema[k], vs: Schema[v], _) => + val s = Schema.Map(ks, vs, nem.annotations) + val elems = ChunkBuilder.make[(Target, Target)]() + + def readOne(index: Int): Unit = + push { key => + currentSchema = vs + pushContext(startCreatingOneDictionaryValue(currentContext, s)) + + push { value => + val elem = (key, value) + elems += elem + + contextStack = contextStack.tail.tail + val continue = finishedCreatingOneDictionaryElement(contextStack.head, s, index) + + if (continue) { + currentSchema = ks + pushContext(startCreatingOneDictionaryElement(contextStack.head, s)) + readOne(index + 1) + } else { + val state = contextStack.head + contextStack = contextStack.tail + finishWith( + NonEmptyMap + .fromMapOption(createDictionary(state, s, elems.result()).asInstanceOf[Map[k, v]]) + .getOrElse(throw new IllegalStateException("NonEmpty map requires at least on element")) + .asInstanceOf[Target] + ) + } + } + } + + startCreatingDictionary(currentContext, s) match { + case Some(startingState) => + currentSchema = ks + pushContext(startingState) + pushContext(startCreatingOneDictionaryElement(startingState, s)) + readOne(0) + case None => + finishWith( + NonEmptyMap + .fromMapOption(createDictionary(contextStack.head, s, Chunk.empty).asInstanceOf[Map[k, v]]) + .getOrElse(throw new IllegalStateException("NonEmpty map requires at least on element")) + .asInstanceOf[Target] + ) + } case s @ Schema.Set(as: Schema[a], _) => val elems = ChunkBuilder.make[Target]() diff --git a/zio-schema/shared/src/main/scala/zio/schema/MutableSchemaBasedValueProcessor.scala b/zio-schema/shared/src/main/scala/zio/schema/MutableSchemaBasedValueProcessor.scala index dfc890104..734f86e44 100644 --- a/zio-schema/shared/src/main/scala/zio/schema/MutableSchemaBasedValueProcessor.scala +++ b/zio-schema/shared/src/main/scala/zio/schema/MutableSchemaBasedValueProcessor.scala @@ -2,6 +2,7 @@ package zio.schema import scala.collection.immutable.ListMap +import zio.prelude.NonEmptyMap import zio.{ Chunk, ChunkBuilder } /** Base trait for mutable value processors, processing a value with a known schema. An example @@ -760,6 +761,29 @@ trait MutableSchemaBasedValueProcessor[Target, Context] { } } + startProcessingSequence(currentContext, s, inputChunk.size) + pushContext(contextForSequence(currentContext, s, 0)) + processNext(0) + case nes @ Schema.NonEmptySequence(schema, _, _, _, _) => + val s = Schema.Sequence(schema, nes.fromChunk, nes.toChunk, nes.annotations, nes.identity) + val inputChunk = nes.toChunk.asInstanceOf[Any => Chunk[Any]](currentValue) + val resultChunk = ChunkBuilder.make[Target](inputChunk.size) + + def processNext(inputIdx: Int): Unit = { + contextStack = contextStack.tail + if (inputIdx == inputChunk.size) { + finishWith(processSequence(currentContext, s, resultChunk.result())) + } else { + currentSchema = schema + currentValue = inputChunk(inputIdx) + pushContext(contextForSequence(currentContext, s, inputIdx)) + push { dv => + resultChunk += dv + processNext(inputIdx + 1) + } + } + } + startProcessingSequence(currentContext, s, inputChunk.size) pushContext(contextForSequence(currentContext, s, 0)) processNext(0) @@ -768,6 +792,37 @@ trait MutableSchemaBasedValueProcessor[Target, Context] { val inputChunk = Chunk.fromIterable(currentValue.asInstanceOf[Map[k, v]]) val resultChunk = ChunkBuilder.make[(Target, Target)](inputChunk.size) + def processNext(inputIdx: Int): Unit = + if (inputIdx == inputChunk.size) { + finishWith(processDictionary(currentContext, s, resultChunk.result())) + } else { + currentSchema = ks + val currentTuple = inputChunk(inputIdx) + currentValue = currentTuple._1 + + pushContext(contextForMap(currentContext, s, inputIdx)) + push { (a: Target) => + contextStack = contextStack.tail + + currentSchema = vs + currentValue = currentTuple._2 + pushContext(contextForMap(currentContext, s, inputIdx)) + push { (b: Target) => + contextStack = contextStack.tail + val pair = (a, b) + resultChunk += pair + processNext(inputIdx + 1) + } + } + } + + startProcessingDictionary(currentContext, s, inputChunk.size) + processNext(0) + case nem @ Schema.NonEmptyMap(ks: Schema[k], vs: Schema[v], _) => + val s = Schema.Map(ks, vs, nem.annotations) + val inputChunk = Chunk.fromIterable(currentValue.asInstanceOf[NonEmptyMap[k, v]].toMap) + val resultChunk = ChunkBuilder.make[(Target, Target)](inputChunk.size) + def processNext(inputIdx: Int): Unit = if (inputIdx == inputChunk.size) { finishWith(processDictionary(currentContext, s, resultChunk.result())) diff --git a/zio-schema/shared/src/main/scala/zio/schema/Schema.scala b/zio-schema/shared/src/main/scala/zio/schema/Schema.scala index ec792b7ff..6b722ce8e 100644 --- a/zio-schema/shared/src/main/scala/zio/schema/Schema.scala +++ b/zio-schema/shared/src/main/scala/zio/schema/Schema.scala @@ -6,11 +6,12 @@ import java.time.temporal.ChronoUnit import scala.annotation.tailrec import scala.collection.immutable.ListMap +import zio.prelude.NonEmptySet import zio.schema.annotation._ import zio.schema.internal.SourceLocation import zio.schema.meta._ import zio.schema.validation._ -import zio.{ Chunk, Unsafe } +import zio.{ Chunk, NonEmptyChunk, Unsafe, prelude } /** * A `Schema[A]` describes the structure of some data type `A`, in terms of case classes, @@ -196,6 +197,8 @@ object Schema extends SchemaPlatformSpecific with SchemaEquality { schema match { case Sequence(schema, _, toChunk, _, _) => toChunk(value).flatMap(value => loop(value, schema)) + case nes @ NonEmptySequence(schema, _, _, _, _) => + nes.toChunk(value).flatMap(value => loop(value, schema)) case Transform(schema, _, g, _, _) => g(value) match { case Right(value) => loop(value, schema) @@ -211,6 +214,10 @@ object Schema extends SchemaPlatformSpecific with SchemaEquality { loop(tuple.extract1(value), left) ++ loop(tuple.extract2(value), right) case l @ Lazy(_) => loop(value, l.schema) + case Schema.NonEmptyMap(ks, vs, _) => + Chunk.fromIterable(value.toMap.keys).flatMap(loop(_, ks)) ++ Chunk + .fromIterable(value.values) + .flatMap(loop(_, vs)) case Schema.Map(ks, vs, _) => Chunk.fromIterable(value.keys).flatMap(loop(_, ks)) ++ Chunk.fromIterable(value.values).flatMap(loop(_, vs)) case set @ Schema.Set(as, _) => @@ -299,6 +306,30 @@ object Schema extends SchemaPlatformSpecific with SchemaEquality { implicit def chunk[A](implicit schemaA: Schema[A]): Schema[Chunk[A]] = Schema.Sequence[Chunk[A], A, String](schemaA, identity, identity, Chunk.empty, "Chunk") + implicit def nonEmptyChunk[A](implicit schemaA: Schema[A]): Schema[NonEmptyChunk[A]] = + Schema.NonEmptySequence[NonEmptyChunk[A], A, String]( + schemaA, + NonEmptyChunk.fromChunk, + _.toChunk, + Chunk.empty, + "NonEmptyChunk" + ) + + implicit def nonEmptySet[A](implicit schemaA: Schema[A]): Schema[NonEmptySet[A]] = + Schema.NonEmptySequence[NonEmptySet[A], A, String]( + schemaA, + chunk => NonEmptySet.fromSetOption(chunk.toSet), + _.toNonEmptyChunk.toChunk, + Chunk.empty, + "NonEmptySet" + ) + + implicit def nonEmptyMap[K, V]( + implicit keySchema: Schema[K], + valueSchema: Schema[V] + ): Schema[prelude.NonEmptyMap[K, V]] = + Schema.NonEmptyMap[K, V](keySchema, valueSchema, Chunk.empty) + implicit def map[K, V]( implicit keySchema: Schema[K], valueSchema: Schema[V] @@ -788,6 +819,67 @@ object Schema extends SchemaPlatformSpecific with SchemaEquality { b.makeTraversal(self, keySchema <*> valueSchema) } + final case class NonEmptyMap[K, V]( + keySchema: Schema[K], + valueSchema: Schema[V], + override val annotations: Chunk[Any] = Chunk.empty + ) extends Collection[prelude.NonEmptyMap[K, V], (K, V)] { + self => + override type Accessors[Lens[_, _, _], Prism[_, _, _], Traversal[_, _]] = + Traversal[prelude.NonEmptyMap[K, V], (K, V)] + + override def annotate(annotation: Any): NonEmptyMap[K, V] = + copy(annotations = (annotations :+ annotation).distinct) + + override def defaultValue: scala.Either[String, prelude.NonEmptyMap[K, V]] = + keySchema.defaultValue.flatMap( + defaultKey => valueSchema.defaultValue.map(defaultValue => prelude.NonEmptyMap(defaultKey -> defaultValue)) + ) + + def fromChunk(chunk: Chunk[(K, V)]): prelude.NonEmptyMap[K, V] = + fromChunkOption(chunk).getOrElse(throw new IllegalArgumentException("NonEmptyMap cannot be empty")) + + def fromChunkOption(chunk: Chunk[(K, V)]): Option[prelude.NonEmptyMap[K, V]] = + NonEmptyChunk.fromChunk(chunk).map(prelude.NonEmptyMap.fromNonEmptyChunk) + + def fromMap(map: scala.collection.immutable.Map[K, V]): prelude.NonEmptyMap[K, V] = + prelude.NonEmptyMap + .fromMapOption(map) + .getOrElse(throw new IllegalArgumentException("NonEmptyMap cannot be empty")) + + def toChunk(map: prelude.NonEmptyMap[K, V]): Chunk[(K, V)] = + Chunk.fromIterable(map.toList) + + override def makeAccessors(b: AccessorBuilder): b.Traversal[prelude.NonEmptyMap[K, V], (K, V)] = + b.makeTraversal(self, keySchema <*> valueSchema) + } + + final case class NonEmptySequence[Col, Elm, I]( + elementSchema: Schema[Elm], + fromChunkOption: Chunk[Elm] => Option[Col], + toChunk: Col => Chunk[Elm], + override val annotations: Chunk[Any] = Chunk.empty, + identity: I + ) extends Collection[Col, Elm] { + self => + override type Accessors[Lens[_, _, _], Prism[_, _, _], Traversal[_, _]] = Traversal[Col, Elm] + + val fromChunk: Chunk[Elm] => Col = (chunk: Chunk[Elm]) => + fromChunkOption(chunk).getOrElse( + throw new IllegalArgumentException(s"NonEmptySequence $identity cannot be empty") + ) + + override def annotate(annotation: Any): NonEmptySequence[Col, Elm, I] = + copy(annotations = (annotations :+ annotation).distinct) + + override def defaultValue: scala.util.Either[String, Col] = + elementSchema.defaultValue.map(fromChunk.compose(Chunk(_))) + + override def makeAccessors(b: AccessorBuilder): b.Traversal[Col, Elm] = b.makeTraversal(self, elementSchema) + + override def toString: String = s"NonEmptySequence($elementSchema, $identity)" + } + final case class Set[A](elementSchema: Schema[A], override val annotations: Chunk[Any] = Chunk.empty) extends Collection[scala.collection.immutable.Set[A], A] { self => diff --git a/zio-schema/shared/src/main/scala/zio/schema/meta/ExtensibleMetaSchema.scala b/zio-schema/shared/src/main/scala/zio/schema/meta/ExtensibleMetaSchema.scala index 5e8f9f2aa..3bee800a3 100644 --- a/zio-schema/shared/src/main/scala/zio/schema/meta/ExtensibleMetaSchema.scala +++ b/zio-schema/shared/src/main/scala/zio/schema/meta/ExtensibleMetaSchema.scala @@ -573,12 +573,20 @@ object ExtensibleMetaSchema { ) case Schema.Sequence(schema, _, _, _, _) => ListNode(item = subtree(NodePath.root / "item", Lineage.empty, schema), NodePath.root) + case Schema.NonEmptySequence(schema, _, _, _, _) => + ListNode(item = subtree(NodePath.root / "item", Lineage.empty, schema), NodePath.root) case Schema.Map(ks, vs, _) => Dictionary( keys = subtree(NodePath.root / "keys", Lineage.empty, ks), values = subtree(NodePath.root / "values", Lineage.empty, vs), NodePath.root ) + case Schema.NonEmptyMap(ks, vs, _) => + Dictionary( + keys = subtree(NodePath.root / "keys", Lineage.empty, ks), + values = subtree(NodePath.root / "values", Lineage.empty, vs), + NodePath.root + ) case Schema.Set(schema, _) => ListNode(item = subtree(NodePath.root / "item", Lineage.empty, schema), NodePath.root) case Schema.Transform(schema, _, _, _, _) => subtree(NodePath.root, Lineage.empty, schema) @@ -642,6 +650,8 @@ object ExtensibleMetaSchema { ) case Schema.Sequence(schema, _, _, _, _) => ListNode(item = subtree(path / "item", lineage, schema, optional = false), path, optional) + case Schema.NonEmptySequence(schema, _, _, _, _) => + ListNode(item = subtree(path / "item", lineage, schema, optional = false), path, optional) case Schema.Map(ks, vs, _) => Dictionary( keys = subtree(path / "keys", Lineage.empty, ks, optional = false), @@ -649,6 +659,13 @@ object ExtensibleMetaSchema { path, optional ) + case Schema.NonEmptyMap(ks, vs, _) => + Dictionary( + keys = subtree(path / "keys", Lineage.empty, ks, optional = false), + values = subtree(path / "values", Lineage.empty, vs, optional = false), + path, + optional + ) case Schema.Set(schema @ _, _) => ListNode(item = subtree(path / "item", lineage, schema, optional = false), path, optional) case Schema.Transform(schema, _, _, _, _) => subtree(path, lineage, schema, optional)