Repository: elixir-ecto/ecto (elixir)

Issue:
UUID.cast to allow binaries
### Environment

* Elixir version: 1.4.2
* Database and version: Postgres 9.6.1
* Ecto version: 2.1.4
* Database adapter and version: Postgrex 0.13.0
* Operating system: OSX 10.12.4

### Current behavior

I have a use case for manually generating binary UUIDs. Currently Ecto.UUID doesn't accept raw binaries even if they're valid 16 bytes. Is there any reason it shouldn't?

```elixir
defmodule File do
  use Ecto.Schema

  schema "files" do
    field :hash, Ecto.UUID, autogenerate: false
  end
end
```

```
> Repo.insert(%File{hash: :erlang.md5("file content")})
> ** (Ecto.ChangeError) value `<<209, 11, 76, 63, 241, 35, 178, 109, 192, 104, 212, 58, 139, 239, 45, 35>>` for `File.hash` in `insert` does not match type Ecto.UUID
      (ecto) lib/ecto/repo/schema.ex:699: Ecto.Repo.Schema.dump_field!/6
      (ecto) lib/ecto/repo/schema.ex:708: anonymous fn/6 in Ecto.Repo.Schema.dump_fields!/5
      (stdlib) lists.erl:1263: :lists.foldl/3
      (ecto) lib/ecto/repo/schema.ex:706: Ecto.Repo.Schema.dump_fields!/5
      (ecto) lib/ecto/repo/schema.ex:655: Ecto.Repo.Schema.dump_changes!/6
      (ecto) lib/ecto/repo/schema.ex:200: anonymous fn/13 in Ecto.Repo.Schema.do_insert/4
```

Digging into the UUID generate functions also yields some unexpected results:

```
> Ecto.UUID.generate() |> Ecto.UUID.cast()
{:ok, "ff7f95df-c6df-4743-a630-74069d82ac63"}
> Ecto.UUID.bingenerate() |> Ecto.UUID.cast()
:error
```

### Expected behavior

I'd expect Ecto.UUID to accept any valid 16 byte binary.

Pull request:
Allow casting binary UUIDs
Currently, only dash-separated strings are allowed in `UUID.cast/1`. This adds the ability to pass 16 byte binaries. The same validation rules apply to both binaries and dash-separated strings.

Closes #2045

Patch:
diff --git a/lib/ecto/uuid.ex b/lib/ecto/uuid.ex
index 49fb795b..196578d5 100644
--- a/lib/ecto/uuid.ex
+++ b/lib/ecto/uuid.ex
@@ -29,9 +29,9 @@ defmodule Ecto.UUID do
   catch
     :error -> :error
   else
-    casted ->
-      {:ok, casted}
+    casted -> {:ok, casted}
   end
+  def cast(<< _::128 >> = binary), do: encode(binary)
   def cast(_), do: :error
 
   @doc """
@@ -145,14 +145,14 @@ defmodule Ecto.UUID do
   Converts a binary UUID into a string.
   """
   def load(<<_::128>> = uuid) do
-   {:ok, encode(uuid)}
+    encode(uuid)
   end
   def load(<<_::64, ?-, _::32, ?-, _::32, ?-, _::32, ?-, _::96>> = string) do
     raise "trying to load string UUID as Ecto.UUID: #{inspect string}. " <>
           "Maybe you wanted to declare :uuid as your database field?"
   end
   def load(%Ecto.Query.Tagged{type: :uuid, value: uuid}) do
-    {:ok, encode(uuid)}
+    encode(uuid)
   end
   def load(_), do: :error
 
@@ -160,7 +160,8 @@ defmodule Ecto.UUID do
   Generates a version 4 (random) UUID.
   """
   def generate do
-    bingenerate() |> encode
+    {:ok, uuid} = encode(bingenerate())
+    uuid
   end
 
   @doc """
@@ -185,11 +186,32 @@ defmodule Ecto.UUID do
                  e9::4, e10::4, e11::4, e12::4 >>) do
     << e(a1), e(a2), e(a3), e(a4), e(a5), e(a6), e(a7), e(a8), ?-,
        e(b1), e(b2), e(b3), e(b4), ?-,
-       e(c1), e(c2), e(c3), e(c4), ?-,
-       e(d1), e(d2), e(d3), e(d4), ?-,
+       encode_version(c1), e(c2), e(c3), e(c4), ?-,
+       encode_variant(d1), e(d2), e(d3), e(d4), ?-,
        e(e1), e(e2), e(e3), e(e4), e(e5), e(e6), e(e7), e(e8), e(e9), e(e10), e(e11), e(e12) >>
+  catch
+    :error -> :error
+  else
+    encoded -> {:ok, encoded}
   end
 
+  @compile {:inline, encode_version: 1}
+
+  defp encode_version(1), do: ?1
+  defp encode_version(2), do: ?2
+  defp encode_version(3), do: ?3
+  defp encode_version(4), do: ?4
+  defp encode_version(5), do: ?5
+  defp encode_version(_), do: throw(:error)
+
+  @compile {:inline, encode_variant: 1}
+
+  defp encode_variant(8), do: ?8
+  defp encode_variant(9), do: ?9
+  defp encode_variant(10), do: ?a
+  defp encode_variant(11), do: ?b
+  defp encode_variant(_), do: throw(:error)
+
   @compile {:inline, e: 1}
 
   defp e(0),  do: ?0


Test patch:
diff --git a/test/ecto/uuid_test.exs b/test/ecto/uuid_test.exs
index 594de932..b0b50acc 100644
--- a/test/ecto/uuid_test.exs
+++ b/test/ecto/uuid_test.exs
@@ -10,18 +10,15 @@ defmodule Ecto.UUIDTest do
 
   test "cast" do
     assert Ecto.UUID.cast(@test_uuid) == {:ok, @test_uuid}
+    assert Ecto.UUID.cast(@test_uuid_binary) == {:ok, @test_uuid}
     assert Ecto.UUID.cast(@test_uuid_upper_case) == {:ok, String.downcase(@test_uuid_upper_case)}
     assert Ecto.UUID.cast(@test_uuid_invalid_characters) == :error
     assert Ecto.UUID.cast(@test_uuid_invalid_shape) == :error
-    assert Ecto.UUID.cast(@test_uuid_binary) == :error
     assert Ecto.UUID.cast(nil) == :error
   end
 
   test "cast!" do
     assert Ecto.UUID.cast!(@test_uuid) == @test_uuid
-    assert_raise Ecto.CastError, "cannot cast <<96, 29, 116, 228, 168, 211, 75, 110, 131, 101, 237, 219, 76, 137, 51, 39>> to Ecto.UUID", fn ->
-      assert Ecto.UUID.cast!(@test_uuid_binary)
-    end
     assert_raise Ecto.CastError, "cannot cast nil to Ecto.UUID", fn ->
       assert Ecto.UUID.cast!(nil)
     end