zio-schema
zio-schema copied to clipboard
Dynamic optics with generic records and enums
Although the AccessorBuilder
machinery lets library authors build optics using their own data types, a generic mechanism would be extremely useful so that library authors have to do less work.
There are three parts to this problem:
-
ADT for generic, refiied optic. Creating a
sealed trait DynamicOptic[-From, +To]
hierarchy, which simply stores the information that is available viaAccessorBuilder
(i.e. the field / case and the types). This is truly a "reified optic", as it merely stores path information. This would live inzio.schema.optics
. -
Macro-powered type refinements. Using macros to create a type refinement containing the names and types of the fields of a record, and the cases of an enum. For example:
This generic refinement could be stored on thetype GenericRefinement= AnyRef { def name: String def age: Int }
Schema
of the appropriate type (e.g.Record#GenericRefinement
). -
Library-focused accessor support. We can introduce types like
GenericRecord
andGenericEnum
, which extendDynamic
(Scala 2) orSelectable
(Scala 3), and whose constructors merely require the ability to lookup aDynamicOptic
from a value with the specifiedSchema
, e.g.:
This is only an approximate design because cases of enums and elements in collections will require a different signature. In order to be maximally useful, thetrait AccessorLookup[Underlying] { def lookupLens[S, A](value: Underlying, schema: Schema[A], lens: DynamicOptic.Lens[S, A]): Either[String, A] ... }
lookup
functions cannot return a rawA
value, because that would preclude applications in libraries like ZIO Flow, but they could return aF[A]
for some accessor-lookup specified wrapper type (or maybe,F[String, A]
). Given a library suppliedAccessorLookup
, we could have constructors forGenericRecord
andGenericEnum
:
With this, a user can now create generic records and generic enums, and lookup and maybe even modify terms of records and enums, as per the interfaces defined above, all in a type-safe way, and without having to write any boilerplate themselves.class GenericRecord(accessorLookup: AccessorLookup) extends Selectable { ... } object GenericRecord { def make[Underlying](underlying: Underlying, lookup: AccessorLookup[Underlying], schema: Schema.Record[_]): schema.GenericRefinement = ??? }