Skip to content

Latest commit

 

History

History
317 lines (234 loc) · 7.39 KB

README.md

File metadata and controls

317 lines (234 loc) · 7.39 KB

Tarams

Phoenix request params validation library.

Build Status Coverage Status Hex Version docs

Warning: Tarams v1.0.0 APIs is not back compatible

Why Tarams

- Reduce code boilerplate 
- Shorter schema definition
- Default function which generate value each casting time
- Custom validation functions
- Custom parse functions

Installation

Available in Hex, the package can be installed by adding tarams to your list of dependencies in mix.exs:

def deps do
  [
    {:tarams, "~> 1.0.0"}
  ]
end

Usage

Process order

Cast data -> validate casted data -> transform data

@index_params_schema  %{
    keyword: :string,
    status: [type: :string, required: true],
    group_id: [type: :integer, number: [greater_than: 0]],
    name: [type: :string, from: :another_field]
  }

def index(conn, params) do
    with {:ok, better_params} <- Tarams.cast(params, @index_params_schema) do
        # do anything with your params
    else
        {:error, errors} -> # return params error
    end
end

Define schema

Schema is just a map and it can be nested. Each field is defined as

<field_name>: [<field_spec>, ...]

Or short form

<field_name>: <type>

Field specs is a keyword list thay may include:

  • type is required, Tarams support same data type as Ecto. I borrowed code from Ecto
  • default: default value or default function
  • cast_func: custom cast function
  • number, format, length, in, not_in, func, required, each are available validations
  • from: use value from another field
  • as: alias key you will receive from Tarams.cast if casting is succeeded

Default value

You can define a default value for a field if it's missing from the params.

schema = %{
    status: [type: :string, default: "pending"]
}

Or you can define a default value as a function. This function is evaluated when Tarams.cast gets invoked.

schema = %{
    date: [type: :utc_datetime, default: &Timex.now/0]
}

Custom cast function

You can define your own casting function, tarams provide cast_func option. Your cast_func must follows this spec

1. Custom cast fuction accept value only

fn(any) :: {:ok, any} | {:error, binary} | :error
def my_array_parser(value) do
    if is_binary(value) do
        ids = 
            String.split(value, ",")
            |> Enum.map(&String.to_integer(&1))
        
        {:ok, ids}
    else
        {:error, "Invalid string"
    end
end

schema = %{
    user_id: [type: {:array, :integer}, cast_func: &my_array_parser/1]
}

Tarams.cast(%{user_id: "1,2,3"}, schema)

This is a demo parser function.

2. Custom cast function accept value and current object

data = %{
   name: "tada",
   bold: true
}

schema = %{
    name: [type: :string, cast_func: fn value, data -> 
        {:ok, (if data.bold, do: String.upcase(value), else: value)}
    end]
}

Tarams.cast(data, schema)

# > %{name: "TADA"}

3.Custom cast function accept tuple {M, f}

Your cast function must accept 2 arguments

defmodule MyModule do
    def upcase(value, data) do
        {:ok, (if data.bold, do: String.upcase(value), else: value)}
    end
end
data = %{
   name: "tada",
   bold: true
}

schema = %{
    name: [type: :string, cast_func: {MyModule, :upcase}]
}

Tarams.cast(data, schema)

# > %{name: "TADA"}

Nested schema

With Tarams you can parse and validate nested map and list easily

@my_schema %{
    status: :string,
    pagination: %{
        page: [type: :integer, number: [min: 1]],
        size: [type: :integer, number: [min: 10, max: 100"]]
    }
}

Or nested list schema

@user_schema %{
    name: :string,
    email: [type: :string, required: true]
    addresses: [type: {:array, %{
        street: :string,
        district: :string,
        city: :string
    }}]
}

Validation

Tarams uses Valdi validation library. You can read more about Valdi here Basically it supports following validation

  • validate inclusion/exclusion

  • validate length for string and enumerable types

  • validate number

  • validate string format/pattern

  • validate custom function

  • validate required(not nil) or not

  • validate each array item

    product_schema = %{
      sku: [type: :string, required: true, length: [min: 6, max: 20]]
      name: [type: :string, required: true],
      quantity: [type: :integer, number: [min: 0]],
      type: [type: :string, in: ~w(physical digital)],
      expiration_date: [type: :naive_datetime, func: &my_validation_func/1],
      # dynamic required
      width: [type: :integer, required: fn value, data -> data.type == "physical" end],
      # validate each array item
      tags: [type: {:array, :string}, each: [length: [max: 50]]]
    }

Dynamic required

  • Can accept function or {module, function} tuple
  • Only support 2 arity function
def require_email?(value, data), do: is_nil(email.phone)

....

%{
    phone: :string
    name: [type: :string, required: fn value, data -> true end],
    email: [type: :string, required: {__MODULE__, :require_email?}]
}

Validate array item

Support validate array item with :each option, each accept a list of validators

%{
    values: [type: {:array, :number}, each: [number: [min: 20, max: 50]]]
  }

Transform data

Field name alias

You can set alias name for schema fields

data = %{
   name: "tada"
}

schema = %{
    name: [type: :string, as: :full_name]
}

Tarams.cast(data, schema)

# > %{full_name: "tada"}

Convert data

You can specify a function similar to cast_func to manipulate data after casted. However data object passed to transform function is original data before casting.

data = %{status: 10}

schema = %{
    name: [type: :string, into: fn value -> {:ok, "name: #{value}}" end]
}

Tarams.cast(data, schema)
# > %{name: "name: tada"}
  • Transform function can return tuple {:ok, value}, {:error, message} or value directly.
schema = %{
    value: [type: :integer, into: &to_string/1]
}

Contributors

If you find a bug or want to improve something, please send a pull request. Thank you!