[dry-schema] Evolve support

Currently, and understandably, dry-schema generated schemas are immutable. A use case I have is versioned schemas to where I want to do things like update, add, or remove fields and keep them semantically locked, and while I do not think that entire feature is worth supporting for this core library there is a concept from the Haskell / Rambda world around “evolve” which is basically a fancier “merge” operation.

What I’m proposing is that if we have a schema, say:

UserSchema = Dry::Schema.Params do
  required(:name).filled(:string)
  required(:email).filled(:string)

  required(:age).maybe(:integer)
end

…that we could evolve it into this:

UserSchemaV2 = UserSchema.evolve do
  optional(:email).filled(:string)
  remove(:age)
end

…in which the original attributes are preserved but it introduces deltas from the previous schema. I’m not stuck on the name mind, as much as the concept.

Other nice-to-haves here might be things like tracking those deltas and marking it as an evolved schema to aid in aforementioned potential versioning.

I may be willing to build a prototype for this but wanted to run it by as an idea first.

I have some qualms about mutating schema in this way, from personal experience: the more alterations you pile up, the harder it becomes to understand what the final product will be. For this reason I prefer a more declarative approach to schema versioning.

Are you aware of schema composition as an option?

module UserSchema
  Baseline = Dry::Schema.Params do
    required(:name).filled(:string)
    required(:email).filled(:string)
  end

  V1 = Dry::Schema.Params(parent: Baseline) do
    required(:age).maybe(:integer)
  end

  V2 = Dry::Schema.Params(parent: Baseline) do
    optional(:email).filled(:string)
  end
end

The parent: option supports multiple inheritance by passing an array of schemas.

The only thing this would not support is removal of a key; but you can model this differently by moving the definition of the key, or isolating it into a separate schema that is inherited into a subset of versions.