Skip to content

Commit

Permalink
feat(list-validation): Added validation functions and test cases for …
Browse files Browse the repository at this point in the history
…List data type (#16)
  • Loading branch information
shraddha1704 committed Aug 6, 2018
1 parent 7bf2f4c commit d197727
Show file tree
Hide file tree
Showing 7 changed files with 381 additions and 5 deletions.
1 change: 1 addition & 0 deletions .dialyzer_ignore
Expand Up @@ -9,3 +9,4 @@ Unknown function 'Elixir.Litmus.Type.PID':'__impl__'/1
Unknown function 'Elixir.Litmus.Type.Port':'__impl__'/1
Unknown function 'Elixir.Litmus.Type.Reference':'__impl__'/1
Unknown function 'Elixir.Litmus.Type.Tuple':'__impl__'/1
Call to missing or unexported function 'Elixir.Litmus.Type.List':'__impl__'/1
36 changes: 34 additions & 2 deletions README.md
Expand Up @@ -38,11 +38,15 @@ iex> schema = %{
...> "new_user" => %Litmus.Type.Boolean{
...> truthy: ["1"],
...> falsy: ["0"]
...> },
...> "account_ids" => %Litmus.Type.List{
...> max_length: 3,
...> type: :number
...> }
...> }
iex> params = %{"id" => 1, "username" => "user@123", "pin" => 1234, "new_user" => "1"}
iex> params = %{"id" => 1, "username" => "user@123", "pin" => 1234, "new_user" => "1", "account_ids" => [1, 3, 9]}
iex> Litmus.validate(params, schema)
{:ok, %{"id" => 1, "new_user" => true, "pin" => 1234, "username" => "user@123"}}
{:ok, %{"id" => 1, "new_user" => true, "pin" => 1234, "username" => "user@123", "account_ids" => [1, 3, 9]}}

iex> schema = %{"id" => %Litmus.Type.Any{}}
iex> params = %{"password" => 1}
Expand All @@ -54,6 +58,7 @@ Currently, we support the following data types:

* [**Any**](#module-litmus-type-any)
* [**Boolean**](#module-litmus-type-boolean)
* [**List**](#module-litmus-type-list)
* [**Number**](#module-litmus-type-number)
* [**String**](#module-litmus-type-string)

Expand Down Expand Up @@ -95,6 +100,33 @@ iex> Litmus.validate(params, schema)
{:error, "new_user must be a boolean"}
```

### Litmus.Type.List

The `List` module contains options that will validate List data types. It supports the following options:
* `:min_length` - Specifies the minimum list length. Allowed values are non-negative integers.
* `:max_length` - Specifies the maximum list length. Allowed values are non-negative integers.
* `:length` - Specifies the exact list length. Allowed values are non-negative integers.
* `:type` - Specifies the data type of elements in the list. Allowed values are are atoms `:atom, :boolean, :number and :string`. Default value is `nil`. If `nil`, any element type is allowed in the list.

```
iex> schema = %{
...> "ids" => %Litmus.Type.List{
...> min_length: 1,
...> max_length: 5
...> },
...> "course_numbers" => %Litmus.Type.List{
...> length: 3,
...> type: :number
...> }
...> }
iex> params = %{"ids" => [1, "a"], "course_numbers" => [500, 523, 599]}
iex> Litmus.validate(params, schema)
{:ok, %{"ids" => [1, "a"], "course_numbers" => [500, 523, 599]}}
iex> params = %{"ids" => [1, "a"], "course_numbers" => [500, "523", 599]}
iex> Litmus.validate(params, schema)
{:error, "course_numbers must be a list of numbers"}
```

### Litmus.Type.Number

The `Number` module contains options that will validate Number data types. It converts "stringified" numerical values to numbers. It supports the following options:
Expand Down
2 changes: 1 addition & 1 deletion lib/type.ex
Expand Up @@ -3,7 +3,7 @@ defprotocol Litmus.Type do

alias Litmus.Type

@type t :: Type.Any.t() | Type.Boolean.t() | Type.Number.t() | Type.String.t()
@type t :: Type.Any.t() | Type.Boolean.t() | Type.List.t() | Type.Number.t() | Type.String.t()

@spec validate(t(), String.t(), map) :: {:ok, map} | {:error, String.t()}
def validate(type, field, data)
Expand Down
149 changes: 149 additions & 0 deletions lib/type/list.ex
@@ -0,0 +1,149 @@
defmodule Litmus.Type.List do
@moduledoc false

alias Litmus.Required
alias Litmus.Type

defstruct [
:min_length,
:max_length,
:length,
:type,
required: false
]

@type t :: %__MODULE__{
min_length: non_neg_integer | nil,
max_length: non_neg_integer | nil,
length: non_neg_integer | nil,
type: String.t() | atom | nil,
required: boolean
}

@spec validate_field(t, String.t(), map) :: {:ok, map} | {:error, String.t()}
def validate_field(type, field, data) do
with {:ok, data} <- Required.validate(type, field, data),
{:ok, data} <- validate_list(type, field, data),
{:ok, data} <- type_validate(type, field, data),
{:ok, data} <- min_length_validate(type, field, data),
{:ok, data} <- max_length_validate(type, field, data),
{:ok, data} <- length_validate(type, field, data) do
{:ok, data}
else
{:error, msg} -> {:error, msg}
end
end

@spec validate_list(t, String.t(), map) :: {:ok, map} | {:error, String.t()}
defp validate_list(%__MODULE__{}, field, params) do
cond do
!Map.has_key?(params, field) ->
{:ok, params}

is_list(params[field]) ->
{:ok, params}

true ->
{:error, "#{field} must be a list"}
end
end

@spec min_length_validate(t, String.t(), map) :: {:ok, map} | {:error, String.t()}
defp min_length_validate(%__MODULE__{min_length: nil}, _field, params) do
{:ok, params}
end

defp min_length_validate(%__MODULE__{min_length: min_length}, field, params)
when is_integer(min_length) and min_length >= 0 do
if Map.has_key?(params, field) && length(params[field]) < min_length do
{:error, "#{field} must not be below length of #{min_length}"}
else
{:ok, params}
end
end

@spec max_length_validate(t, String.t(), map) :: {:ok, map} | {:error, String.t()}
defp max_length_validate(%__MODULE__{max_length: nil}, _field, params) do
{:ok, params}
end

defp max_length_validate(%__MODULE__{max_length: max_length}, field, params)
when is_integer(max_length) and max_length >= 0 do
if Map.has_key?(params, field) && length(params[field]) > max_length do
{:error, "#{field} must not exceed length of #{max_length}"}
else
{:ok, params}
end
end

@spec length_validate(t, String.t(), map) :: {:ok, map} | {:error, String.t()}
defp length_validate(%__MODULE__{length: nil}, _field, params) do
{:ok, params}
end

defp length_validate(%__MODULE__{length: length}, field, params)
when is_integer(length) and length >= 0 do
if Map.has_key?(params, field) && length(params[field]) != length do
{:error, "#{field} length must be of #{length} length"}
else
{:ok, params}
end
end

@spec type_validate(t, String.t() | atom, map) :: {:ok, map} | {:error, String.t()}
defp type_validate(%__MODULE__{type: nil}, _field, params) do
{:ok, params}
end

defp type_validate(%__MODULE__{type: type}, field, params) do
case type do
:atom -> validate_atom(params, field)
:boolean -> validate_boolean(params, field)
:number -> validate_number(params, field)
:string -> validate_string(params, field)
end
end

@spec validate_atom(map, String.t()) :: {:ok, map} | {:error, String.t()}
defp validate_atom(params, field) do
if Enum.all?(params[field], &is_atom/1) do
{:ok, params}
else
{:error, "#{field} must be a list of atoms"}
end
end

@spec validate_boolean(map, String.t()) :: {:ok, map} | {:error, String.t()}
defp validate_boolean(params, field) do
if Enum.all?(params[field], &is_boolean/1) do
{:ok, params}
else
{:error, "#{field} must be a list of boolean"}
end
end

@spec validate_number(map, String.t()) :: {:ok, map} | {:error, String.t()}
defp validate_number(params, field) do
if Enum.all?(params[field], &is_number/1) do
{:ok, params}
else
{:error, "#{field} must be a list of numbers"}
end
end

@spec validate_string(map, String.t()) :: {:ok, map} | {:error, String.t()}
defp validate_string(params, field) do
if Enum.all?(params[field], &is_binary/1) do
{:ok, params}
else
{:error, "#{field} must be a list of strings"}
end
end

defimpl Litmus.Type do
alias Litmus.Type

@spec validate(Type.t(), String.t(), map) :: {:ok, map} | {:error, String.t()}
def validate(type, field, data), do: Type.List.validate_field(type, field, data)
end
end
2 changes: 1 addition & 1 deletion lib/type/string.ex
Expand Up @@ -37,7 +37,7 @@ defmodule Litmus.Type.String do
end
end

@spec convert(t, String.t(), map) :: {:ok, map}
@spec convert(t, String.t(), map) :: {:ok, map} | {:error, String.t()}
defp convert(%__MODULE__{}, field, params) do
cond do
!Map.has_key?(params, field) ->
Expand Down
8 changes: 7 additions & 1 deletion test/litmus_test.exs
Expand Up @@ -32,6 +32,11 @@ defmodule LitmusTest do
"remember_me" => %Litmus.Type.Boolean{
truthy: [1],
falsy: [0]
},
"account_ids" => %Litmus.Type.List{
type: :number,
min_length: 2,
max_length: 5
}
}

Expand All @@ -40,7 +45,8 @@ defmodule LitmusTest do
"password" => " 1234 ",
"user" => "qwerty",
"pin" => 3636,
"remember_me" => 1
"remember_me" => 1,
"account_ids" => [523, 524, 599]
}

modified_params = Map.replace!(req_params, "password", String.trim(req_params["password"]))
Expand Down

0 comments on commit d197727

Please sign in to comment.