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

Make :attr equivalent to required(:attr) in schemas #60

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
87 changes: 49 additions & 38 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ Elixir `Drops` is a collection of small modules that provide useful extensions a

## Installation

If [available in Hex](https://hex.pm/docs/publish), the package can be installed by adding `drops` to your list of dependencies in `mix.exs`:
This package can be installed by adding `drops` to your list of dependencies in `mix.exs`:

```elixir
def deps do
Expand All @@ -15,7 +15,7 @@ def deps do
end
```

Documentation can be generated with [ExDoc](https://github.com/elixir-lang/ex_doc) and published on [HexDocs](https://hexdocs.pm). Once published, the docs can be found at <https://hexdocs.pm/drops>.
Documentation can be found at <https://hexdocs.pm/drops>.

## Contracts

Expand All @@ -29,8 +29,8 @@ defmodule UserContract do

schema do
%{
required(:name) => string(),
required(:email) => string()
name: string(),
email: string()
}
end
end
Expand Down Expand Up @@ -76,7 +76,7 @@ Contract's schemas are a powerful way of defining the exact shape of the data yo

### Required and optional keys

A schema must explicitly define which keys are required and which are optional. This is done by using `required` and `optional` functions. Here's an example:
Schema attributes are required by defaults and attributes that are optional must be marked explicitly using `optional`. Here's an example:

```elixir
defmodule UserContract do
Expand All @@ -85,7 +85,7 @@ defmodule UserContract do
schema do
%{
optional(:name) => string(),
required(:email) => string()
:name => string()
}
end
end
Expand All @@ -97,6 +97,17 @@ UserContract.conform(%{name: "Jane", email: "janedoe.org"})
# {:ok, %{name: "Jane", email: "janedoe.org"}}
```

If preferred, you can also use `required` to be more explicit. This schema is equivalent to the above:

```elixir
schema do
%{
optional(:name) => string(),
required(:name) => string()
}
end
```

### Types

You can define the expected types of the values using `string`, `integer`, `float`, `boolean`, `atom`, `map`, `list`, `any` and `maybe` functions. Here's an example:
Expand All @@ -107,12 +118,12 @@ defmodule UserContract do

schema do
%{
required(:name) => string(),
required(:age) => integer(),
required(:active) => boolean(),
required(:tags) => list(:string),
required(:settings) => map(:string),
required(:address) => maybe(:string)
name: string(),
age: integer(),
active: boolean(),
tags: list(:string),
settings: map(:string),
address: maybe(:string)
}
end
end
Expand All @@ -128,8 +139,8 @@ defmodule UserContract do

schema do
%{
required(:name) => string(:filled?),
required(:age) => integer(gt?: 18)
name: string(:filled?),
age: integer(gt?: 18)
}
end
end
Expand Down Expand Up @@ -169,18 +180,18 @@ defmodule UserContract do

schema do
%{
required(:user) => %{
required(:name) => string(:filled?),
required(:age) => integer(),
required(:address) => %{
required(:city) => string(:filled?),
required(:street) => string(:filled?),
required(:zipcode) => string(:filled?)
user: %{
name: string(:filled?),
age: integer(),
address: %{
city: string(:filled?),
street: string(:filled?),
zipcode: string(:filled?)
},
required(:tags) =>
tags:
list(%{
required(:name) => string(:filled?),
required(:created_at) => integer()
name: string(:filled?),
created_at: integer()
})
}
}
Expand Down Expand Up @@ -255,7 +266,7 @@ defmodule UserContract do

schema do
%{
required(:count) => cast(:string) |> integer(gt?: 0)
count: cast(:string) |> integer(gt?: 0)
}
end
end
Expand Down Expand Up @@ -301,7 +312,7 @@ defmodule UserContract do

schema do
%{
required(:text) => cast(:string, caster: CustomCaster) |> string()
text: cast(:string, caster: CustomCaster) |> string()
}
end
end
Expand All @@ -320,11 +331,11 @@ defmodule UserContract do

schema(atomize: true) do
%{
required(:name) => string(),
required(:age) => integer(),
required(:tags) =>
name: string(),
age: integer(),
tags:
list(%{
required(:name) => string()
name: string()
})
}
end
Expand Down Expand Up @@ -367,8 +378,8 @@ defmodule UserContract do

schema do
%{
required(:name) => Types.Name,
required(:age) => Types.Age
name: Types.Name,
age: Types.Age
}
end
end
Expand All @@ -390,8 +401,8 @@ You can also define reusable schemas, since they are represented as map type:
```elixir
defmodule Types.User do
use Drops.Type, %{
required(:name) => string(:filled?),
required(:age) => integer(gteq?: 0)
name: string(:filled?),
age: integer(gteq?: 0)
}
end

Expand All @@ -400,7 +411,7 @@ defmodule UserContract do

schema do
%{
required(:user) => Types.User
user: Types.User
}
end
end
Expand Down Expand Up @@ -429,7 +440,7 @@ defmodule ProductContract do

schema do
%{
required(:price) => Types.Price
price: Types.Price
}
end
end
Expand Down Expand Up @@ -463,8 +474,8 @@ defmodule UserContract do

schema do
%{
required(:email) => maybe(:string),
required(:login) => maybe(:string)
email: maybe(:string),
login: maybe(:string)
}
end

Expand Down
8 changes: 4 additions & 4 deletions lib/drops/casters.ex
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ defmodule Drops.Casters do
...> use Drops.Contract
...>
...> schema do
...> %{required(:age) => cast(:string) |> type(:integer)}
...> %{age: cast(:string) |> type(:integer)}
...> end
...> end
iex> UserContract.conform(%{age: "20"})
Expand All @@ -27,7 +27,7 @@ defmodule Drops.Casters do
...> use Drops.Contract
...>
...> schema do
...> %{required(:num) => cast(:string) |> type(:float)}
...> %{num: cast(:string) |> type(:float)}
...> end
...> end
iex> UserContract.conform(%{num: "20.5"})
Expand All @@ -39,7 +39,7 @@ defmodule Drops.Casters do
...> use Drops.Contract
...>
...> schema do
...> %{required(:id) => cast(:integer) |> type(:string)}
...> %{id: cast(:integer) |> type(:string)}
...> end
...> end
iex> UserContract.conform(%{id: 312})
Expand All @@ -51,7 +51,7 @@ defmodule Drops.Casters do
...> use Drops.Contract
...>
...> schema do
...> %{required(:date) => cast(:integer) |> type(:date_time)}
...> %{date: cast(:integer) |> type(:date_time)}
...> end
...> end
iex> UserContract.conform(%{date: 1614556800})
Expand Down
40 changes: 20 additions & 20 deletions lib/drops/contract.ex
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,8 @@ defmodule Drops.Contract do
...>
...> schema do
...> %{
...> required(:name) => type(:string),
...> required(:age) => type(:integer)
...> name: type(:string),
...> age: type(:integer)
...> }
...> end
...> end
Expand Down Expand Up @@ -126,8 +126,8 @@ defmodule Drops.Contract do
...>
...> schema do
...> %{
...> required(:name) => type(:string),
...> required(:age) => type(:integer)
...> name: type(:string),
...> age: type(:integer)
...> }
...> end
...> end
Expand All @@ -148,13 +148,13 @@ defmodule Drops.Contract do
...>
...> schema(atomize: true) do
...> %{
...> required(:user) => %{
...> required(:name) => type(:string, [:filled?]),
...> required(:age) => type(:integer),
...> required(:address) => %{
...> required(:city) => type(:string, [:filled?]),
...> required(:street) => type(:string, [:filled?]),
...> required(:zipcode) => type(:string, [:filled?])
...> user: %{
...> name: type(:string, [:filled?]),
...> age: type(:integer),
...> address: %{
...> city: type(:string, [:filled?]),
...> street: type(:string, [:filled?]),
...> zipcode: type(:string, [:filled?])
...> }
...> }
...> }
Expand Down Expand Up @@ -200,18 +200,18 @@ defmodule Drops.Contract do
...>
...> schema(:address) do
...> %{
...> required(:street) => string(:filled?),
...> required(:city) => string(:filled?),
...> required(:zip) => string(:filled?),
...> required(:country) => string(:filled?)
...> street: string(:filled?),
...> city: string(:filled?),
...> zip: string(:filled?),
...> country: string(:filled?)
...> }
...> end
...>
...> schema do
...> %{
...> required(:name) => string(),
...> required(:age) => integer(),
...> required(:address) => @schemas.address
...> name: string(),
...> age: integer(),
...> address: @schemas.address
...> }
...> end
...> end
Expand Down Expand Up @@ -271,8 +271,8 @@ defmodule Drops.Contract do
...>
...> schema do
...> %{
...> required(:email) => maybe(:string),
...> required(:login) => maybe(:string)
...> email: maybe(:string),
...> login: maybe(:string)
...> }
...> end
...>
Expand Down
12 changes: 6 additions & 6 deletions lib/drops/type.ex
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ defmodule Drops.Type do
...>
...> schema do
...> %{
...> required(:email) => Email
...> email: Email
...> }
...> end
...> end
Expand Down Expand Up @@ -51,7 +51,7 @@ defmodule Drops.Type do
...>
...> schema do
...> %{
...> required(:email) => FilledEmail
...> email: FilledEmail
...> }
...> end
...> end
Expand All @@ -73,8 +73,8 @@ defmodule Drops.Type do

defmodule User do
use Drops.Type, %{
required(:name) => string(),
required(:email) => string()
name: string(),
email: string()
}
end

Expand All @@ -83,7 +83,7 @@ defmodule Drops.Type do
...>
...> schema do
...> %{
...> required(:user) => User
...> user: User
...> }
...> end
...> end
Expand Down Expand Up @@ -112,7 +112,7 @@ defmodule Drops.Type do
...>
...> schema do
...> %{
...> required(:unit_price) => Price
...> unit_price: Price
...> }
...> end
...> end
Expand Down
8 changes: 6 additions & 2 deletions lib/drops/type/compiler.ex
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,12 @@ defmodule Drops.Type.Compiler do

def visit(%{} = spec, opts) do
keys =
Enum.map(spec, fn {{presence, name}, type_spec} ->
%Key{path: [name], presence: presence, type: visit(type_spec, opts)}
Enum.map(spec, fn
{key, type_spec} when is_atom(key) ->
%Key{path: [key], presence: :required, type: visit(type_spec, opts)}

{{presence, name}, type_spec} ->
%Key{path: [name], presence: presence, type: visit(type_spec, opts)}
end)

Map.new(keys, opts)
Expand Down
6 changes: 6 additions & 0 deletions lib/drops/type/dsl.ex
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,12 @@ defmodule Drops.Type.DSL do
%{
required(:email) => type(:string)
}

Note that attributes are required by default, so the above is equivalent to:

%{
email: type(:string)
}
"""
@doc since: "0.1.0"
@spec required(atom()) :: {:required, atom()}
Expand Down
Loading
Loading