# Copyright 2018 Google LLC # # 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 # # http://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. defmodule GoogleApi.Gax.ModelBase do require Poison.Decode @moduledoc """ This module helps you quickly and concisely define API models. Example: defmodule Pet do use GoogleApi.Gax.ModelBase field(:id) field(:category, as: Category) field(:tags, as: Tag, type: :list) end """ defmacro __using__(_opts) do quote do import unquote(__MODULE__) @fields [] @before_compile unquote(__MODULE__) end end defmacro __before_compile__(_env) do quote do defstruct Keyword.keys(@fields) @doc """ Unwrap a decoded JSON object into its complex fields. """ @spec decode(struct(), keyword()) :: struct() def decode(value, _options) do Enum.reduce(@fields, value, fn {field_name, opts}, v -> if struct = Keyword.get(opts, :as) do Map.update!(v, field_name, fn current -> type = Keyword.get(opts, :type, :primitive) struct = Keyword.get(opts, :as) GoogleApi.Gax.ModelBase.decode(current, type, struct) end) else v end end) end end end defmacro field(field_name, opts \\ []) do quote do @fields [{unquote(field_name), unquote(opts)} | @fields] end end @doc """ Helper to decode model fields """ @spec decode(struct(), :list | :map | :primitive, nil | module()) :: struct() def decode(nil, _, _) do nil end def decode(value, _, nil) do value end def decode(value, :list, DateTime) do Enum.map(value, &parse_date_time/1) end def decode(value, _, DateTime) do parse_date_time(value) end def decode(value, :list, Date) do Enum.map(value, &parse_date/1) end def decode(value, _, Date) do parse_date(value) end def decode(value, :map, module) when not is_nil(value) do value |> Enum.map(fn {k, v} -> {k, poison_transform(v, %{as: struct(module)})} end) |> Enum.into(%{}) end def decode(value, :list, module) do poison_transform(value, %{as: [struct(module)]}) end def decode(value, _, module) do poison_transform(value, %{as: struct(module)}) end if function_exported?(Poison.Decode, :decode, 2) do def poison_transform(value, options) do Poison.Decode.decode(value, options) end else # Short-circuit if the value has already been transformed. # This works around a bug in poison 4 where Poison.decode does an extra # transformation pass on sub-structs. def poison_transform(%{__struct__: _} = value, %{as: _}) do value end def poison_transform([%{__struct__: _} | _] = value, %{as: [_]}) do value end def poison_transform(value, options) do Poison.Decode.transform(value, options) end end @doc """ Helper to encode model into JSON """ @spec encode(struct(), keyword()) :: String.t() def encode(value, options) do value |> Map.from_struct() |> Enum.filter(fn {_k, v} -> v != nil end) |> Enum.into(%{}) |> Poison.Encoder.encode(options) end defp parse_date(nil), do: nil defp parse_date(ymd) do case Date.from_iso8601(ymd) do {:ok, date} -> date _ -> ymd end end defp parse_date_time(nil), do: nil defp parse_date_time(%DateTime{} = date_time), do: date_time defp parse_date_time(iso8601) do case DateTime.from_iso8601(iso8601) do {:ok, datetime, _offset} -> datetime _ -> iso8601 end end end