Giter Club home page Giter Club logo

params's Introduction

Params

Looking for maintainer

Build Status Hex Version Hex Docs Total Download License Last Updated

Easily define parameter structure and validate/cast with Ecto.Schema

Installation

Available in Hex, the package can be installed as:

Add params to your list of dependencies in mix.exs:

def deps do
  [
    {:params, "~> 2.0"}
  ]
end

About

If you've been doing Ecto based applications lately, you know Ecto provides a very easy way to populate structs with data coming from request parameters, validating and casting their values along the way.

All this thanks to the Ecto.Schema and Ecto.Changeset modules. The first specifies the fields your model has (typically the same as your db table) and the later provides an easy way to convert potentially unsafe data and validate stuff via changesets.

So for example, in a typical Phoenix application, a User model would look like:

defmodule MyApp.User do
   use MyApp.Web, :model

   schema "users" do
     field :name, :string
     field :age,  :integer
   end

   @required [:name]
   @optional [:age]

   def changeset(changeset_or_model, params) do
     cast(changeset_or_model, params, @required ++ @optional)
     |> validate_required(@required)
   end
end

Normally, changesets are related to some data that will be persisted into a database, and your controller would use the User.changeset method like:

# UserController.ex
def create(conn, params) do
  ch = User.changeset(%User{}, params)
  if ch.valid? do
    ...
end

However, you can use Ecto.Schema for validating/casting data that won't necessarily be persisted into a database. All you need is just specify a module and define your schema, Ecto.Changeset will be happy to work with it.

This comes handy when you have certain parameter structure you want to enforce for example when creating a REST API.

Some Rails developers might be right now wondering where their strong parameters can be defined. On Elixir land, there's no need for such a thing, as we will see, just using an Ecto.Schema with Ecto.Changeset can be much more flexible. Using schemas allows not only specifying which fields we want, but changesets let use type cast, perform validations on values, etc.

So, for example, suppose your Phoenix based API performs a search for kittens looking for a home and expects something like:

{
  "breed": "Russian Blue",
  "age_min": 0,
  "age_max": 5,
  "near_location": {
     "latitude": 92.1,
     "longitude": -82.1
  }
}

You'd like to validate that your controller has received the correct params structure, all you need to do is create a couple of modules:

defmodule MyApi.Params.Location
  use Ecto.Schema
  import Ecto.Changeset

  @required ~w(latitude longitude)
  @optional ~w()

  schema "location params" do
    field :latitude, :float
    field :longitude, :float
  end

  def changeset(ch, params) do
    cast(ch, params, @required ++ @optional)
    |> validate_required(@required)
  end
end

defmodule MyAPI.Params.KittenSearch
  use Ecto.Schema
  import Ecto.Changeset

  @required ~w(breed)
  @optional ~w(age_min age_max)

  schema "params for kitten search" do
    field :breed, :string
    field :age_min, :integer
    field :age_max, :integer
    field :age_max, :integer
    field :color, Ecto.Enum, values: [:brown, :black, :grey, :unknown],
    embeds_one :near_location, Location
  end

  def changeset(ch, params) do
    cast(ch, params, @required ++ @optional)
    |> validate_required(@required)
    |> cast_embed(:near_location, required: true)
  end
end

# On your controller:
def search(conn, params) do
  alias MyAPI.Params.KittenSearch
  changeset = KittenSearch.changeset(%KittenSearch{}, params)
  if changeset.valid? do
    ...
end

That would allow you to take only valid params as you'd normally have with any other Ecto.Schema module.

However it's still a lot of code, most of it defining the the changeset, specifying the optional and required fields, etc.

Params is just a simple Ecto.Schema wrapper for reducing all this boilerplate, while still leting you create custom changesets for parameter processing.

Usage

The previous example could be written like:

defmodule MyAPI.KittenController do

  use Params

  defparams kitten_search %{
    breed!: :string,
    age_max: :integer,
    age_min: [field: :integer, default: 1],
    color: [field: Ecto.Enum, values: [:brown, :black, :grey, :unknown]],
    near_location!: %{
      latitude!: :float, longitude!: :float
    },
    tags: [:string]
  }

  def index(conn, params) do
    changeset = kitten_search(params)
    if changeset.valid? do
      search = Params.data changeset
      IO.puts search.near_location.latitude
    ...
  end
end

The defparams macro generates a module for processing a params schema

By default all fields are optional. You can mark required fields by ending them with a !, of course the bang is removed from the field definition and is only used to mark which fields are required by default.

You can also create a module and define your schema or custom changesets in it:

defmodule UserSearch do
  use Params.Schema, %{name: :string, age: :integer}
  import Ecto.Changeset, only: [cast: 3, validate_inclusion: 3]

  def child(ch, params) do
    cast(ch, params, ~w(name age))
    |> validate_inclusion(:age, 1..6)
  end
end

defmodule MyApp.UserController do

  def index(conn, params) do
    changeset = UserSearch.from(params, with: &UserSearch.child/2)
    if changeset.valid? do
      # age in 1..6
  end

end

The Params.data and Params.to_map can be useful for obtaining a struct or map from a changeset.

Note that Params.data and Params.to_map have different behaviour: data returns a struct which will include all valid params. to_map returns a map that only includes the submitted keys and keys with default values:

defmodule UserUpdateParams do
  use Params.Schema, %{
    name: :string,
    age: :integer,
    auditlog: [field: :boolean, default: true]
  }
end

changeset = UserUpdateParams.from(%{name: "John"})

Params.data(changeset) # => %UserUpdateParams{name: "John", age: nil, auditlog: true}
Params.to_map(changeset) # => %{name: "John", auditlog: true}

API Documentation

API Documentation

Contributors

Here's a list of awesome people who have contributed code to this project.

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

Copyright and License

Copyright (c) 2016 Victor Hugo Borja

Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at https://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.

params's People

Contributors

accua avatar andykingking avatar astery avatar greg-rychlewski avatar iamjarvo avatar jgautsch avatar johnhamelink avatar kianmeng avatar lasseebert avatar mahcloud avatar nirev avatar take-five avatar tiagopog avatar vic avatar ziinc avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

params's Issues

Compile error when using defparams macro in module that uses Phoenix.Component

Phoenix.Component overrides the def and defp macros to annotate function heads with some pattern matching.
I believe because defparams creates a new method by calling Module.eval_quoted/2 this is somehow evaluating the ast returned by Phoenix.Component's def macro prematurely(? - tbh, I don't grok this perfectly yet)

But the error can be seen with code like the following:

defmodule MyMod do
  use Phoenix.Component
  use Params

  defparams fleeno %{some: :string}

  def hi do
    IO.puts("Hello")
  end
end
error: cannot invoke remote function Phoenix.Component.Declarative.__pattern__!/2 inside a match
  nofile:1: MyMod.util_2/1

Is there a reason I'm not understanding that the defparams macro is using Module.eval_quoted/2 rather than just defining the method directly?

In other words, can't

  Module.eval_quoted(__MODULE__, quote do
     def unquote(name)(params) do
       unquote(module_name).from(params)
     end
  end)

Just be written as

  def unquote(name)(params) do
    unquote(module_name).from(params)
  end

In my own experiments it seems to work fine, but I'm not sure if I'm missing something.

(Of course, I could just call the macro before I use Phoenix.Component, but perhaps there's a more comprehensive fix)

Default value for time/dates?

Is there any way to specify a default value for a field of type time? e..g Set ~T[16:30:00] as a default value?

I tried:

[field: :time, default: ~T[16:30:00]]

But it throws an error: ** (CompileError) nofile: invalid quoted expression: ~T[16:30:00]

Restructure README

In my opinion, the README contains too much back story that you have to wade through before getting to the basic usage of the package. Everybody is interested in the basic usage, but only some people are interested in the back story. I suggest moving the "Usage" section to near the top of the document.

I'd be happy to make this into a PR.

Release latest version to hex

Hi,

i'd like to use #30 in our codebase. For this it would be great to have a new release on hex.
The last one there is 2.1.1 which is from january 2019.

Thanks in advance.

No longer working with Ecto 2.0-beta

Hey there,

The breaking changes introduced in 2.0-beta have caused params to break (thankfully its tests also break in the same way). I'm working on a PR to fix this right now, but in the meantime, here's the sort of errors I'm experiencing:

Compiled lib/params/def.ex
** (Protocol.UndefinedError) protocol Enumerable not implemented for :id
    (elixir) lib/enum.ex:1: Enumerable.impl_for!/1
    (elixir) lib/enum.ex:116: Enumerable.reduce/3
    (elixir) lib/enum.ex:1486: Enum.reduce/3
    test/params_test.exs:10: (module)
    test/params_test.exs:8: (module)
== Compilation error on file lib/params.ex ==
** (CompileError) lib/params.ex:77: unknown key :model for struct Ecto.Changeset
    (elixir) src/elixir_map.erl:185: :elixir_map."-assert_struct_keys/5-lc$^0/1-0-"/5
    (elixir) src/elixir_map.erl:62: :elixir_map.translate_struct/4
    (stdlib) lists.erl:1353: :lists.mapfoldl/3

Elixir 1.11 Compiler Warning

When compiling this library using Elixir v 1.11 you get the following warning:

warning: Ecto.Changeset.validate_required/2 defined in application :ecto is used by the current application but the current application does not depend on :ecto. To fix this, you must do one of:
  1. If :ecto is part of Erlang/Elixir, you must include it under :extra_applications inside "def application" in your mix.exs
  2. If :ecto is a dependency, make sure it is listed under "def deps" in your mix.exs
  3. In case you don't want to add a requirement to :ecto, you may optionally skip this warning by adding [xref: [exclude: [Ecto.Changeset]]] to your "def project" in mix.exs
  lib/params.ex:145: Params.changeset/2

This can be solved by changing

def application do
    [applications: [:logger]]
  end

to

def application do
    [extra_applications: [:logger]]
  end

in mix.exs. Please see here for more info: https://hexdocs.pm/mix/1.11.4/Mix.Tasks.Compile.App.html

I will submit a pull request to fix this.

Reusable embeds in `use Params.Schema, %{}`

Just as suggestion or maybe I missing something and this can be already achieved:

defmodule SomeIdentityParams do
  use Params.Schema
  import App.Params.Helpers

  @fields ~w(id code)
  schema do
    field :id, :id
    field :code, :string
    # more here
  end

  def changeset(changeset, params) do
    cast(changeset, params, @fields)
    |> at_least_one_should_be_present([:id, :code])
  end
end

# this one looks fine
defmodule SomeControllerParams do
  use Params.Schema
  @fields ~w(tarif_id some_identity)a

  schema do
    field :tarif_id, :id
    embeds_one :some_identity, SomeIdentityParams
  end

  def changeset(cs, params) do
    cast(cs, params, @fields)
    |> validate_required(@fields)
    |> cast_embed(:passenger, required: true)
  end
end

# but it can be much shorter
defmodule SomeControllerParams do
  use Params.Schema, %{
    tarif_id: :id,
    some_identity: SomeIdentityParams
  }
end

Params.to_map behaviour for missing fields.

Both to_map and data have the behaviour that it's not possible to see if nil was submitted for a field or if that field was not submitted at all.

It makes sense that data behaves that way, since it returns a struct. But with to_map, I would expect it to use the schema as whitelist and not add extra fields unless default is used.

Maybe this should be a different function than to_map?

My usecase is an API endpoint that can update a resource. Something like this:

defmodule MyAPIEndpoint do
  defmodule MyParams do
    use Params.Schema, %{
      id!: :integer,
      name: string,
      owner_id: :integer
    }
  end

  def call(params) do
    changeset = MyParams.from(params)
    if changeset.valid? do
      attributes = Params.to_map(changeset)
      MyRepo.update_attributes(attributes)
    end
  end
end

So I want to update only the submitted keys. In this case it should be possible to update owner_id to the value nil.

Again, I will be happy to help with the implementation. Just wanted your oppinion before getting started ;)

Feature Request: Put functions in the schema values

I love this library, but I almost never build my own changeset validation functions because it's too cumbersome. I usually just use with after using params validation for types.

However, I think that it would be awesome if we could put validation functions in the schema. Could we somehow detect those and build a changeset validation out of functions, as long as they have the right call signature?

defparams asdf %{
  x!: fn :x, x -> is_integer(x) and x > 0 end,
  y!: :string,
}

This would satisfy the other 20% of api validation that I need. I'm happy to take a stab at this, if you guys think that it's feasible and desirable.

String booleans do not get converted

Hi i've uncovered a bug relating to booleans.

String booleans do not get converted (although normal behaviour of Ecto.Changeset is to convert "true" into true)

# in MyController
defmodule MyController do
  defparams(
    index_params(%{
      page: [field: :integer, default: 1],
      per_page: [field: :integer, default: 15],
      has_errors: :boolean,
      is_approved: [field: :boolean, default: false]
    })
  )
end

# check params received by controller
iex> IO.inspect(params)
%{"is_approved" => "true"}

# check changeset data
iex> IO.inspect(changeset.data)
%Params.MyController.IndexParams{
  __meta__: #Ecto.Schema.Metadata<:built, "params Elixir.Params.MyController.IndexParams">,
  _id: nil,
  has_errors: nil,
  is_approved: false,     <--- this should be true
  page: 1,
  per_page: 15
}

I've checked params_test.exs and there is no test suite that coveres booleans

Becoming a maintainer

Hello @vic

I'm @k-asm from MIXI, a Japan-based company.
We run a service called MIXI M, a financial service, and heavily rely on your awesome project for our Elixir-based software.
Your work has been crucial for us, and we really appreciate it.

We saw that you're looking for maintainers, and we'd love to help out.
Our team wants to contribute in order that this project continues to thrive.

Becoming a maintainer

Hi, vic.

I'm interested in to be a maintainer of params.

I:

  • love open source.
  • love Elixir.

Know more about me at my website.

If there are any other requirements, let me know. ;)

Modules defined in `nofile`

Consider this example:

defmodule MyParams do
  use Params.Schema, %{
    nested: %{
      foo: :integer
    }
  }
end

It will define a nested schema in a module called MyParams.Nested.

However, it seems that it will be defined in a "wrong" context. It's source is set to "nofile", which I have had a few problems with:

  • Sometimes when I compile my app, it will warn about redefining the nested module
  • Code coverage tools does not handle it (errors on missing nofile file).

I have tried to fix it in Params.Def but was not able to get something working.

I think the main problem is that the macro chain is "broken" by some normal functions, which means that the module is actually created on runtime and not compiletime (because the macro expansion stopped).

I see different solutions but was not able to implement them without messing with the current code more than I wanted to:

  • Get the __CALLER__ in the original __using__ macro and pass that on to Params.Def. Then create module with something like Module.create(name, quoted_content, Macro.Env.location(caller))
  • Use macros all the way down to where the module is created, so that it will be fuilly expanded on compiletime.

Default values?

Thanks for a great library. I'm just learning Elixir, so I might miss something obvious here.

Is it possible to somehow specify default values in a schema?

I have something like this:

defparams index_params %{
  filter: %{
    foo_id: :integer,
    bar_id: :integer
  },
  page: %{
    number: :integer,
    size: :integer
  },
  sort: :string
}

filter is not required, but it should default to %{}.
page is not required, but it should default to e.g. %{number: 1, size: 20}.
sort is not required, and should default to e.g. "id"

I could of course write a function that adds the default values, but since Ecto.Schema supports this it could be nice to somehow specify it directly in the schema.

:atom not included in the types

Hi, big fan of this library for typecasting.

I notice that the atom type is not provided and raises an invalid type error, is this expected behaviour? Was hoping to cast strings to atoms automatically.

(ArgumentError) invalid or unknown type :atom for field :status

Stacktrace:
  │ (ecto 3.7.1) lib/ecto/schema.ex:2201: Ecto.Schema.check_field_type!/4
  │ (ecto 3.7.1) lib/ecto/schema.ex:1881: Ecto.Schema.__field__/4
  │ (stdlib 3.16.1) erl_eval.erl:685: :erl_eval.do_apply/6
  │ (stdlib 3.16.1) erl_eval.erl:123: :erl_eval.exprs/5
  │ (stdlib 3.16.1) erl_eval.erl:919: :erl_eval.try_clauses/8
  │ (stdlib 3.16.1) erl_eval.erl:123: :erl_eval.exprs/5Elixir

Default params with plain `use Params.Schema`

Hello, here is my issue:

defmodule MyParams do
  use Params.Schema

  schema do
    field :count, :integer, default: 1
  end
end

changeset = MyParams.from(%{})
assert %{count: 1} = Params.to_map(changeset) # but it is %{}

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.