Skip to content

Commit

Permalink
Inital commit
Browse files Browse the repository at this point in the history
  • Loading branch information
jerel committed Jun 27, 2016
0 parents commit 1031467
Show file tree
Hide file tree
Showing 12 changed files with 332 additions and 0 deletions.
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
/_build
/cover
/deps
/doc
erl_crash.dump
*.ez
23 changes: 23 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# EctoFields

Provides commonly used fields for Ecto projects.

## Installation

To install EctoFields:

1. Add ecto_fields to your list of dependencies in `mix.exs`:

def deps do
[{:ecto_fields, "~> 0.0.1"}]
end

2. Use the fields in your Ecto schema:

schema "user" do
field :name, :string
field :email, EctoFields.Email
field :website, EctoFields.URL
field :ip_address, EctoFields.IP
end

30 changes: 30 additions & 0 deletions config/config.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# This file is responsible for configuring your application
# and its dependencies with the aid of the Mix.Config module.
use Mix.Config

# This configuration is loaded before any dependency and is restricted
# to this project. If another project depends on this project, this
# file won't be loaded nor affect the parent project. For this reason,
# if you want to provide default values for your application for
# 3rd-party users, it should be done in your "mix.exs" file.

# You can configure for your application as:
#
# config :ecto_fields, key: :value
#
# And access this configuration in your application as:
#
# Application.get_env(:ecto_fields, :key)
#
# Or configure a 3rd-party app:
#
# config :logger, level: :info
#

# It is also possible to import configuration files, relative to this
# directory. For example, you can emulate configuration per environment
# by uncommenting the line below and defining dev.exs, test.exs and such.
# Configuration from the imported file will override the ones defined
# here (which is why it is important to import them last).
#
# import_config "#{Mix.env}.exs"
2 changes: 2 additions & 0 deletions lib/ecto_fields.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
defmodule EctoFields do
end
75 changes: 75 additions & 0 deletions lib/fields/email.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
defmodule EctoFields.Email do
@behaviour Ecto.Type
def type, do: :string

@doc """
Validate that the given value is a valid email
## Examples
iex> EctoFields.Email.cast("[email protected]/")
:error
iex> EctoFields.Email.cast("[email protected].")
:error
iex> EctoFields.Email.cast("[email protected]<script src='x.js'>")
:error
iex> EctoFields.Email.cast("[email protected]")
"[email protected]"
iex> EctoFields.Email.cast("foo.bar+baz/@long.example.photography.uk")
"foo.bar+baz/@long.example.photography.uk"
iex> EctoFields.Email.cast("test@localhost")
"test@localhost"
iex> EctoFields.Email.cast("[email protected]")
"[email protected]"
iex> EctoFields.Email.cast("test@2001:1620:28:1:b6f:8bca:93:a116")
"test@2001:1620:28:1:b6f:8bca:93:a116"
"""
# max_length=254 to be compliant with RFCs 3696 and 5321
def cast(email) when is_binary(email) and byte_size(email) > 0 and byte_size(email) < 255 do
# Thanks to the Django Project for the regex inspiration.
# https://tools.ietf.org/html/rfc2822#section-3.2.4
user_regex = Regex.compile!("(^[-!#$%&'*+/=?^_`{}|~0-9A-Z]+(\.[-!#$%&'*+/=?^_`{}|~0-9A-Z]+)*\\z)", [:caseless, :multiline])

# limited to label length of 63 per RFC 1034
domain_regex = Regex.compile!("((?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\.)+)(?:[A-Z0-9-]{2,63}(?<!-))\\z", [:caseless, :multiline])

result = with [user, domain] <- String.split(email, "@"),
true <- Regex.match?(user_regex, user),
true <- Regex.match?(domain_regex, domain) || domain == "localhost" || is_valid_ip?(domain) do
true
end

# workaround missing with/else for Elixir 1.2
case result do
true -> email
false -> :error
end
end

def cast(_) do
:error
end

# converts a string to our ecto type
def load(email), do: {:ok, email}

# converts our ecto type to a string
def dump(email), do: {:ok, email}


defp is_valid_ip?(domain) do
case domain |> String.to_char_list |> :inet_parse.address do
{:ok, _} -> true
{:error, _} -> false
end
end

end
33 changes: 33 additions & 0 deletions lib/fields/ip.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
defmodule EctoFields.IP do
@behaviour Ecto.Type
def type, do: :string

@doc """
Validate that the given value is a valid ip
## Examples
iex> EctoFields.IP.cast("192.168.10.1")
"192.168.10.1"
iex> EctoFields.IP.cast("http://example.com")
:error
iex> EctoFields.IP.cast("2001:1620:28:1:b6f:8bca:93:a116")
"2001:1620:28:1:b6f:8bca:93:a116"
"""
def cast(ip) when is_binary(ip) and byte_size(ip) > 0 do
case ip |> String.to_char_list |> :inet_parse.address do
{:ok, _} -> ip
{:error, _} -> :error
end
end

def cast(_), do: :error

# converts a string to our ecto type
def load(ip), do: {:ok, ip}

# converts our ecto type to a string
def dump(ip), do: {:ok, ip}
end
32 changes: 32 additions & 0 deletions lib/fields/slug.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
defmodule EctoFields.Slug do
@behaviour Ecto.Type
def type, do: :string

@doc """
Coerce a regular string into a slug
## Examples
iex> EctoFields.Slug.cast(" My latest blog post-")
"my-latest-blog-post"
iex> EctoFields.Slug.cast("From the ЉЊАБЖЗ Naughty ЁЂЃЄ Strings цчшщъыьэюя list")
"from-the-naughty-strings-list"
"""
def cast(title) when is_binary(title) and byte_size(title) > 0 do
title
|> String.normalize(:nfd)
|> String.downcase
|> String.replace(~r/[^a-z\s]/u, "")
|> String.replace(~r/\s+/, "-")
|> String.replace(~r/^\-*(.*?)\-*$/, "\\1")
end

def cast(_), do: :error

# converts a string to our ecto type
def load(slug), do: {:ok, slug}

# converts our ecto type to a string
def dump(slug), do: {:ok, slug}
end
66 changes: 66 additions & 0 deletions lib/fields/url.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
defmodule EctoFields.URL do
@behaviour Ecto.Type
def type, do: :string

@doc """
Validate that the given value is a valid fully qualified url
## Examples
iex> EctoFields.URL.cast("http://example.com")
"http://example.com"
iex> EctoFields.URL.cast("https://example.com")
"https://example.com"
iex> EctoFields.URL.cast("http://example.com/test/foo.html?search=1&page=two#header")
"http://example.com/test/foo.html?search=1&page=two#header"
iex> EctoFields.URL.cast("myblog.html")
:error
iex> EctoFields.URL.cast("http://example.com\blog\first")
:error
"""
def cast(url) when is_binary(url) and byte_size(url) > 0 do
url
|> validate_protocol
|> validate_host
|> validate_uri
end

def cast(_), do: :error

# converts a string to our ecto type
def load(url), do: {:ok, url}

# converts our ecto type to a string
def dump(url), do: {:ok, url}

defp validate_protocol("http://" <> rest = url) do
{url, rest}
end
defp validate_protocol("https://" <> rest = url) do
{url, rest}
end
defp validate_protocol(_), do: :error

defp validate_host(:error), do: :error
defp validate_host({url, rest}) do
[domain | uri] = String.split(rest, "/")
case String.to_char_list(domain) |> :inet_parse.domain do
true -> {url, Enum.join(uri, "/")}
_ -> :error
end
end

defp validate_uri(:error), do: :error
defp validate_uri({url, uri}) do
if uri == URI.encode(uri) |> URI.decode do
url
else
:error
end
end

end
48 changes: 48 additions & 0 deletions mix.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
defmodule EctoFields.Mixfile do
use Mix.Project

def project do
[app: :ecto_fields,
version: "0.0.1",
elixir: "~> 1.2",
build_embedded: Mix.env == :prod,
start_permanent: Mix.env == :prod,
deps: deps,
package: package(),
description: description()]
end

# Configuration for the OTP application
#
# Type "mix help compile.app" for more information
def application do
[applications: [:logger]]
end

def package do
[licenses: ["MIT"],
maintainers: ["jerel"],
links: %{"GitHub" => "https://github.com/jerel/ecto_fields"}]
end

def description do
"""
Provides commonly used fields for Ecto projects.
"""
end

# Dependencies can be Hex packages:
#
# {:mydep, "~> 0.3.0"}
#
# Or git/path repositories:
#
# {:mydep, git: "https://github.com/elixir-lang/mydep.git", tag: "0.1.0"}
#
# Type "mix help deps" for more examples and options
defp deps do
[{:ecto, "~> 1.0"},
{:ex_doc, ">= 0.0.0", only: :dev},
{:mix_test_watch, "~> 0.2", only: :dev}]
end
end
7 changes: 7 additions & 0 deletions mix.lock
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
%{"decimal": {:hex, :decimal, "1.1.2", "79a769d4657b2d537b51ef3c02d29ab7141d2b486b516c109642d453ee08e00c", [:mix], []},
"earmark": {:hex, :earmark, "0.2.1", "ba6d26ceb16106d069b289df66751734802777a3cbb6787026dd800ffeb850f3", [:mix], []},
"ecto": {:hex, :ecto, "1.1.8", "0f0348e678fa5a450c266d69816808f97fbd82ade32cf88d4b09bbe8f8c27545", [:mix], [{:sbroker, "~> 0.7", [hex: :sbroker, optional: true]}, {:postgrex, "~> 0.11.0", [hex: :postgrex, optional: true]}, {:poolboy, "~> 1.4", [hex: :poolboy, optional: false]}, {:poison, "~> 1.0 or ~> 2.0", [hex: :poison, optional: true]}, {:mariaex, "~> 0.5.0 or ~> 0.6.0", [hex: :mariaex, optional: true]}, {:decimal, "~> 1.0", [hex: :decimal, optional: false]}]},
"ex_doc": {:hex, :ex_doc, "0.12.0", "b774aabfede4af31c0301aece12371cbd25995a21bb3d71d66f5c2fe074c603f", [:mix], [{:earmark, "~> 0.2", [hex: :earmark, optional: false]}]},
"fs": {:hex, :fs, "0.9.2", "ed17036c26c3f70ac49781ed9220a50c36775c6ca2cf8182d123b6566e49ec59", [:rebar], []},
"mix_test_watch": {:hex, :mix_test_watch, "0.2.6", "9fcc2b1b89d1594c4a8300959c19d50da2f0ff13642c8f681692a6e507f92cab", [:mix], [{:fs, "~> 0.9.1", [hex: :fs, optional: false]}]},
"poolboy": {:hex, :poolboy, "1.5.1", "6b46163901cfd0a1b43d692657ed9d7e599853b3b21b95ae5ae0a777cf9b6ca8", [:rebar], []}}
9 changes: 9 additions & 0 deletions test/ecto_fields_test.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
defmodule EctoFieldsTest do
use ExUnit.Case
doctest EctoFields

doctest EctoFields.Email
doctest EctoFields.IP
doctest EctoFields.Slug
doctest EctoFields.URL
end
1 change: 1 addition & 0 deletions test/test_helper.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
ExUnit.start()

0 comments on commit 1031467

Please sign in to comment.