Skip to content
/ tarams Public

Cast and validate external data and request parameters for Elixir and Phoenix

License

Notifications You must be signed in to change notification settings

bluzky/tarams

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

79 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

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!