Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

validate types/schemas inside other schemas? #44

Open
jtippett opened this issue Feb 13, 2024 · 5 comments
Open

validate types/schemas inside other schemas? #44

jtippett opened this issue Feb 13, 2024 · 5 comments

Comments

@jtippett
Copy link

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
Copy link
Owner

solnic commented Feb 13, 2024

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([]), "[email protected]")
# {:ok, "[email protected]"}

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

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

@jtippett
Copy link
Author

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
Copy link
Owner

solnic commented Feb 14, 2024

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
Copy link
Author

jtippett commented Mar 15, 2024

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
Copy link
Owner

solnic commented Mar 29, 2024

@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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants