# `Magpie.Metadata`
[🔗](https://github.com/alexcassol/magpie/blob/v0.5.1/lib/magpie/metadata.ex#L1)

Decodes the metadata objects of the `files` endpoints into structs.

Dropbox describes every entry in a user's Dropbox with one of three
objects, told apart by a `".tag"` key: `"file"`, `"folder"` or `"deleted"`.
Magpie turns them into `Magpie.FileMetadata`, `Magpie.FolderMetadata` and
`Magpie.DeletedMetadata`, with `DateTime` timestamps and a first-class
`content_hash`, so callers pattern match on the struct instead of
inspecting string keys:

    client
    |> Magpie.Files.ListFolder.stream("/Photos")
    |> Enum.map(fn
      %Magpie.FileMetadata{name: name, size: size} -> {name, size}
      %Magpie.FolderMetadata{name: name} -> {name, :folder}
      %Magpie.DeletedMetadata{name: name} -> {name, :deleted}
    end)

Every function in `Magpie.Files`, `Magpie.Files.ListFolder` and
`Magpie.Files.UploadSession` that returns metadata decodes it before
handing it back, so `decode/1` is only needed when you call an endpoint
yourself through `Magpie.post/3`:

    {:ok, raw} = Magpie.post(client, "/files/get_metadata", %{"path" => "/a.txt"})
    %Magpie.FileMetadata{} = Magpie.Metadata.decode(raw)

## Forward compatibility

Decoding is lenient by design. A map whose `".tag"` Magpie does not know
(should Dropbox add a fourth kind of entry) is returned untouched, and so
is anything that is not a map — a stubbed test response, or an error
payload that reached the decoder. Fields Dropbox adds later that the
structs do not carry are dropped; the raw payload is always available
through `Magpie.post/3`.

# `t`

```elixir
@type t() ::
  Magpie.FileMetadata.t()
  | Magpie.FolderMetadata.t()
  | Magpie.DeletedMetadata.t()
```

# `content_hash`

```elixir
@spec content_hash(binary() | Enumerable.t()) :: String.t()
```

Computes the Dropbox `content_hash` of `data` — a binary, or an enumerable
of binaries such as `File.stream!/2` — so a transfer can be verified
against `Magpie.FileMetadata.content_hash` without another request.

Dropbox hashes the content in 4 MiB blocks with SHA-256, concatenates the
block digests and hashes the result once more (see the
[Content hash](https://www.dropbox.com/developers/reference/content-hash)
reference).

    iex> Magpie.Metadata.content_hash("")
    "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"

    {:ok, %{body: body}} = Magpie.Files.download(client, "/report.pdf")
    {:ok, %Magpie.FileMetadata{content_hash: hash}} = Magpie.Files.get_metadata(client, "/report.pdf")
    Magpie.Metadata.content_hash(body) == hash
    # => true

# `decode`

```elixir
@spec decode(term(), :infer | :file | :folder) :: t() | term()
```

Decodes one metadata object.

The `".tag"` picks the struct. Dropbox omits the tag when the endpoint's
return type is known up front — `/files/upload` and `/files/list_revisions`
always answer with files, `/files/create_folder_v2` with a folder — and
`kind` says what to expect then: `:file`, `:folder`, or `:infer` (the
default), which treats a map with a `"rev"` as a file (every file has one,
no folder does) and any other map with a `"name"` as a folder. Unknown tags
and non-map values are returned as they are.

    iex> Magpie.Metadata.decode(%{".tag" => "folder", "name" => "Photos", "id" => "id:1"})
    %Magpie.FolderMetadata{name: "Photos", id: "id:1"}

    iex> Magpie.Metadata.decode(%{"name" => "a.txt", "rev" => "015", "size" => 3})
    %Magpie.FileMetadata{name: "a.txt", rev: "015", size: 3}

    iex> Magpie.Metadata.decode(%{"name" => "a.txt"}, :file)
    %Magpie.FileMetadata{name: "a.txt"}

    iex> Magpie.Metadata.decode(%{".tag" => "hologram", "name" => "x"})
    %{".tag" => "hologram", "name" => "x"}

# `decode_matches`

```elixir
@spec decode_matches(term()) :: term()
```

Decodes the `"metadata"` of every search match on a `/files/search_v2`
page. Dropbox wraps each match's entry in a one-variant union
(`%{".tag" => "metadata", "metadata" => ...}`); Magpie flattens it so the
match holds the struct directly.

    iex> page = %{"matches" => [%{"metadata" => %{".tag" => "metadata", "metadata" => %{".tag" => "file", "name" => "a", "rev" => "1"}}}]}
    iex> Magpie.Metadata.decode_matches(page)
    %{"matches" => [%{"metadata" => %Magpie.FileMetadata{name: "a", rev: "1"}}]}

# `decode_page`

```elixir
@spec decode_page(term(), :infer | :file | :folder) :: term()
```

Decodes every item under `"entries"` of a paginated page, leaving the rest
of the page — `"cursor"`, `"has_more"` — as it is, so the page still drives
`Magpie.Pager`. `kind` is passed on to `decode/2`.

    iex> Magpie.Metadata.decode_page(%{"entries" => [%{".tag" => "deleted", "name" => "x"}], "has_more" => false})
    %{"entries" => [%Magpie.DeletedMetadata{name: "x"}], "has_more" => false}

# `unwrap`

```elixir
@spec unwrap(term(), :infer | :file | :folder) :: t() | term()
```

Decodes a response that wraps the metadata under a `"metadata"` key —
`/files/create_folder_v2`, `/files/delete_v2`, `/files/copy_v2` and
`/files/move_v2` all answer that way — returning just the struct.

`kind` is passed on to `decode/2`. Responses of any other shape are
returned untouched.

    iex> Magpie.Metadata.unwrap(%{"metadata" => %{".tag" => "file", "name" => "a", "rev" => "1"}})
    %Magpie.FileMetadata{name: "a", rev: "1"}

---

*Consult [api-reference.md](api-reference.md) for complete listing*
