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

Union data type #37

Closed ilyabayel closed 8 months ago

ilyabayel commented 9 months ago

Hey, @solnic. How're you doing? Does this library support union types?

I would like to validate a map with union data type. Something like form with lots of Node where Node = Field | Section. We could think of it like:

defmodule Field do
  ...
end

defmodule Section do
  ...
end

defmodule Node do
  @type t :: Field.t() | Section.t()
end

I see this union type describer as a function named union which accepts array of types.

  union([float(), integer(), string()])

For my specific case I could use it like:

defmodule FormContract do
  use Drops.Contract

  schema do
    %{
      required(:title) => string(:filled?),
      required(:nodes) =>
        list(
          union([
            %{
              required(:__type) => "Field",
              required(:name) => string(:filled?),
              required(:value) => union([integer(), float(), string(:filled?), boolean()])
            },
            %{
              required(:__type) => "Section",
              required(:name) => string(:filled?),
              required(:fields) =>
                list(%{
                  required(:__type) => "Field",
                  required(:name) => string(:filled?),
                  required(:value) => union([integer(), float(), string(:filled?), boolean()])
                })
            }
          ])
        )
    }
  end
end

With your new Custom types it will be way easier to read:

defmodule FormContract do
  use Drops.Contract

  defmodule Field do
    use Drops.Type, %{
      required(:__type) => "Field",
      required(:name) => string(:filled?),
      required(:value) => union([integer(), float(), string(:filled?), boolean()])
    }
  end

  defmodule Section do
    use Drops.Type, %{
      required(:__type) => "Section",
      required(:name) => string(:filled?),
      required(:fields) => list(Field)
    }
  end

  # Node = Field | Section
  defmodule Node do
    use Drops.Type, union([Field, Section])
  end

  schema do
    %{
      required(:title) => string(:filled?),
      required(:nodes) => list(Node)
    }
  end
end

What do you think? I could try to implement it if you don't mind

ilyabayel commented 9 months ago

we could also implement number type as union([float(), integer()])

solnic commented 8 months ago

hey sorry I missed the notification about this issue! Yes, lemme first finish custom types PR and we can get back to your proposal :)

solnic commented 8 months ago

@ilyabayel hey there :) I'm more-or-less done with custom types. One thing I'd like to add is support for the syntax you showed here use Drops.Type, %{...} which defines a custom map type. This shouldn't be too difficult to do now.

Please let me know if you're still interested in adding union function. Also notice that there's type function which supports passing an array with types as it's first argument, but I think union will be better as it's more explicit. I guess we should also rename Types.Sum to Types.Union.