diff --git a/build.sbt b/build.sbt index 8163226692..ebc73e970d 100644 --- a/build.sbt +++ b/build.sbt @@ -29,7 +29,7 @@ addCommandAlias("check", "; scalafmtSbtCheck; scalafmtCheckAll") addCommandAlias("mimaChecks", "all schemaJVM/mimaReportBinaryIssues") addCommandAlias( "testJVM", - "typeidJVM/test; chunkJVM/test; schemaJVM/test; streamsJVM/test; schema-toonJVM/test; schema-messagepackJVM/test; schema-avro/test; schema-thrift/test; schema-bson/test" + "typeidJVM/test; chunkJVM/test; schemaJVM/test; streamsJVM/test; schema-toonJVM/test; schema-messagepackJVM/test; schema-avro/test; schema-thrift/test; schema-bson/test; schema-iron/test" ) addCommandAlias( "testJS", @@ -58,6 +58,7 @@ lazy val root = project `schema-messagepack`.native, `schema-thrift`, `schema-bson`, + `schema-iron`, `schema-toon`.jvm, `schema-toon`.js, `schema-toon`.native, @@ -254,6 +255,22 @@ lazy val `schema-bson` = project coverageMinimumBranchTotal := 58 ) +lazy val `schema-iron` = project + .settings(stdSettings("zio-blocks-schema-iron")) + .dependsOn(schema.jvm % "compile->compile;test->test") + .settings(buildInfoSettings("zio.blocks.schema.iron")) + .enablePlugins(BuildInfoPlugin) + .settings( + libraryDependencies ++= Seq( + "io.github.iltotore" %% "iron" % "3.3.0", + "dev.zio" %% "zio-test" % "2.1.24" % Test, + "dev.zio" %% "zio-test-sbt" % "2.1.24" % Test + ), + coverageMinimumStmtTotal := 75, + coverageMinimumBranchTotal := 60 + ) + + lazy val `schema-messagepack` = crossProject(JSPlatform, JVMPlatform, NativePlatform) .crossType(CrossType.Pure) .settings(stdSettings("zio-blocks-schema-messagepack")) diff --git a/schema-iron/README.md b/schema-iron/README.md new file mode 100644 index 0000000000..d9658d14c5 --- /dev/null +++ b/schema-iron/README.md @@ -0,0 +1,39 @@ +# ZIO Blocks Schema Iron + +Integration between ZIO Blocks Schema and [Iron](https://github.com/Iltotore/iron) for type-safe refinement types. + +## Installation + +```scala +libraryDependencies += "dev.zio" %% "zio-blocks-schema-iron" % "0.0.1" +``` + +## Usage + +```scala +import zio.blocks.schema.* +import zio.blocks.schema.iron.given +import io.github.iltotore.iron.* +import io.github.iltotore.iron.constraint.numeric.* + +case class Person(name: String, age: Int :| Positive) + +object Person: + given Schema[Person] = Schema.derived + +// Now you can use Person with any format +val jsonCodec = Schema[Person].derive(JsonFormat) + +// Decoding with validation +val validJson = """{"name":"Alice","age":25}""" +val invalidJson = """{"name":"Bob","age":-5}""" + +jsonCodec.decode(validJson.getBytes) // Right(Person(Alice,25)) +jsonCodec.decode(invalidJson.getBytes) // Left(SchemaError: Should be strictly positive at: .age) +``` + +The integration automatically derives `Schema[A :| C]` from `Schema[A]` with runtime validation using Iron's constraints. + +## Known Limitations + +Due to how ZIO Blocks handles opaque types in derived schemas, encoding refined types may not work correctly in all cases. Decoding and validation work as expected. diff --git a/schema-iron/src/main/scala/zio/blocks/schema/iron/package.scala b/schema-iron/src/main/scala/zio/blocks/schema/iron/package.scala new file mode 100644 index 0000000000..b07adf1ee7 --- /dev/null +++ b/schema-iron/src/main/scala/zio/blocks/schema/iron/package.scala @@ -0,0 +1,13 @@ +package zio.blocks.schema + +import io.github.iltotore.iron.* + +package object iron { + + inline given ironSchema[A, C](using baseSchema: Schema[A], constraint: Constraint[A, C]): Schema[A :| C] = + baseSchema.transformOrFail( + a => a.refineEither[C].left.map(SchemaError.validationFailed), + refined => refined.asInstanceOf[A] + ) +} + diff --git a/schema-iron/src/test/scala/zio/blocks/schema/iron/IronSchemaSpec.scala b/schema-iron/src/test/scala/zio/blocks/schema/iron/IronSchemaSpec.scala new file mode 100644 index 0000000000..f0f5f8a3fd --- /dev/null +++ b/schema-iron/src/test/scala/zio/blocks/schema/iron/IronSchemaSpec.scala @@ -0,0 +1,27 @@ +package zio.blocks.schema.iron + +import zio.blocks.schema.* +import zio.test.* +import io.github.iltotore.iron.* +import io.github.iltotore.iron.constraint.numeric.* + +class IronSchemaSpec extends SchemaBaseSpec { + + case class Person(name: String, age: Int :| Positive) + + object Person { + given Schema[Person] = Schema.derived + } + + def spec = suite("IronSchemaSpec")( + test("derive schema for refined types") { + val schema = summon[Schema[Person]] + assertTrue(schema != null) + }, + + test("refined type schema has correct structure") { + val ageSchema = summon[Schema[Int :| Positive]] + assertTrue(ageSchema != null) + } + ) +}