solnic / drops

🛠️ Tools for working with data effectively - data contracts using types, schemas, domain validation rules, type-safe casting, and more.
Other
251 stars 4 forks source link

validate types/schemas inside other schemas? #44

Open jtippett opened 7 months ago

jtippett commented 7 months ago

Firstly, thanks for this awesome library. Big fan of dry-rb and great to see you in elixir-land!

I'm curious about one thing - it would be ideal to be able to individually create/validate subschemas/types, a little more than pure maps so I can add more checks around the place prior to validating the "master" contract. I have previously used DB-less Ecto quite a bit for this, or just pattern matching on structs at the argument level.

It would be helpful to be able to either call "conform" on a type, or otherwise use a structs-like interface to be able to create/match on types. Either that or make a contract embed-able into another.

Any plans (or recommendations) along these lines?

Thanks once again.

solnic commented 7 months ago

Hey James! Thank you :) I'll be improving Validator protocol so that it can be used to validate contracts too, so yes, you'll be able to re-use a contract in another contract's schema, ie:

defmodule UserContract do
  # ...
end

defmodule AccountContract do
  schema do
    %{user: UserContract}
  end
end

Notice that types can be already validated individually too using the protocol, but it's a bit cumbersome to do because you need to create type structs manually at the moment (I'll make it nicer eventually):

defmodule Email do
  use Drops.Type, string(:filled?)
end

import Drops.Type.Validator

validate(Email.new([]), "hello@world.org")
# {:ok, "hello@world.org"}

validate(Email.new([]), 312)
# {:error, [input: 312, predicate: :type?, args: [:string, 312]]}

validate(Email.new([]), "")
# {:error, [input: "", predicate: :filled?, args: [""]]}
jtippett commented 7 months ago

Wow, thanks for the prompt reply! The proposed re-use contract feature looks perfect, can't wait. Until then, thanks for the trick validating a type by itself. I'd tried to figure it out from the source but couldn't quite get it..

thanks again!

solnic commented 7 months ago

I'd tried to figure it out from the source but couldn't quite get it..

Oh that's not a good sign. This is the very first iteration of the validator protocol and types, so the code could be simplified. Could you tell me which parts were confusing for you?

Thanks!

jtippett commented 6 months ago

Sorry for the late reply, was pulled away. Oh it's not a code problem I'm sure. I was in a big hurry and had only the most cursory look. I tend to just look at the tests to see what's "possible" and if there's nothing there, assume it's not..

I do have one follow-up question which is probably going to be important to many. How do you envisage this fitting in with pattern matching? I like to build rudimentary "type checking" into function signatures all over the place just as a sanity check. We can currently use this checking pre-conform, but I think it's most useful post-conform, and currently conform spits out a raw map.

For now I'm working around this with basically a secondary struct which i construct from the post-conform, but obviously that's a bit of a hack. Or is this way outside your envisaged use case 😅

An alternative approach could be to store the "full" struct elsewhere and leave a usable, keys-only struct available for struct, like https://github.com/saleyn/typedstruct does. Unfortunately it's currently impossible to use both, as Drops.Contract expects new/2 to exist.

solnic commented 6 months ago

@jtippett generating type-safe structs is on the roadmap. I'm probably going to add a way of inferring a typed struct from a schema definition and that should be enough, something like:

defmodule UserContract do
  use Drops.Contract

  schema do
    %{required(:name) => string()}
  end
end

defmodule User do
  use Drops.Struct

  defstruct_from_schema(UserContract)
end