defmodule Stripe.Entity do @moduledoc """ A behaviour implemented by modules which represent Stripe objects. Intended for internal use within the library. A Stripe Entity is just a struct, optionally containing some logic for transforming a raw result from the Stripe API into a final struct. This is achieved through the use of the `from_json/2` macro. The list of objects which are recognised by the library upon receipt are currently static and contained in `Stripe.Converter`. When a map containing the `"object"` key is received from the API (even when nested inside another map), and the value of that field (for example, `"foo_widget"`) is in the list of supported objects, the converter will expect `Stripe.FooWidget` to be present and to implement this behaviour. To implement this behaviour, simply add `use Stripe.Entity` to the top of the entity module and make sure it defines a struct. This will also enable the use of the `from_json/2` macro, which allows for changes to the data received from Stripe before it is converted to a struct. """ @doc false # Not to be directly implemented, use the `from_json/2` macro instead @callback __from_json__(data :: map) :: map @doc false defmacro __using__(_opts) do quote do require Stripe.Entity import Stripe.Entity, only: [from_json: 2] @behaviour Stripe.Entity def __from_json__(data), do: data defoverridable __from_json__: 1 end end @doc """ Specifies logic that transforms data from Stripe to our Stripe object. The Stripe API docs specify that: > JSON is returned by all API responses, including errors, although our API > libraries convert responses to appropriate language-specific objects. To this end, sometimes it is desirable to make changes to the raw data received from the Stripe API, to aid its conversion into an appropriate Elixir data struct. One example is the convention of converting `"enum"` values (for example `"status"` values of `"succeeded"` or `"failed"`) into atoms instead of keeping them as strings. This macro is used in modules implementing the `Stripe.Entity` behaviour in order to specify this extra logic. Its use is optional, and the default is no transformation; i.e. the received JSON keys are merely converted to atoms and cast to the struct defined by the module. The macro is used like this: ``` from_json data do data |> cast_to_atom([:type, :status]) |> cast_each(:fee_details, &cast_to_atom(&1, :type)) end ``` It takes a parameter name to which the data received from Stripe is bound, and a `do` block which should return the transformed data. The transformation receives the JSON response from Stripe, with all keys converted to atoms (apart from keys inside a metadata map, which remain binaries) and should return a map which is ready to be cast to the struct the module defines. The helper `cast_*` functions defined in this module are automatically imported into the scope of this macro. The helper functions are all `nil`/missing key-safe, meaning that they will not magically add fields or error on fields which are missing or unset. You should therefore write your transformation assuming all possible data is actually present. """ defmacro from_json(param, do: block) do quote do def __from_json__(unquote(param)) do import Stripe.Entity, except: [from_json: 2] unquote(block) end end end @doc """ Cast the value of the given key or keys to an atom. Provide either a single atom key or a list of atom keys whose values should be converted from binaries to atoms. Used commonly to convert `"enum"` values (values which belong to a predefined set) in Stripe responses, for example a `:status` field. If a key is not set or the value is `nil`, no transformation occurs. """ @spec cast_to_atom(map, atom | [atom]) :: map def cast_to_atom(%{} = data, keys) when is_list(keys) do Enum.reduce(keys, data, fn key, data -> cast_to_atom(data, key) end) end def cast_to_atom(%{} = data, key) do key = List.wrap(key) maybe_update_in(data, key, maybe(&String.to_atom/1)) end @doc """ Applies the given function over a list present in the data. Provide either a single atom key or a list of atom keys whose values are lists. Each element of such a list will be mapped using the function passed. For example, if there is a field `:fee_details` which is a list of maps, each containing a `:type` key whose value we want to cast to an atom, then we write: ``` data |> cast_each(:fee_details, &cast_to_atom(&1, :type)) ``` If a key is not set or the value is `nil`, no transformation occurs. """ @spec cast_each(map, atom | [atom], (any -> any)) :: map def cast_each(%{} = data, keys, fun) when is_list(keys) and is_function(fun) do Enum.reduce(keys, data, fn key, data -> cast_each(data, key, fun) end) end def cast_each(%{} = data, key, fun) when is_function(fun) do key = List.wrap(key) maybe_update_in(data, key, maybe(&Enum.map(&1, fun))) end @doc """ Applies the given function to a field accessed via the path provided. Provide a path (identical to that used by the `Access` protocol) to the field to be modified, and a function to be applied to its value. For example, if the field `:fraud_details` contains a map whose keys are `:user_report` and `:stripe_report` we wish to convert to atoms, then we write: ``` data |> cast_path([:fraud_details], &cast_to_atom(&1, [:user_report, :stripe_report])) ``` Unlike `Kernel.update_in/2`, if the path to the key does not exist, or if the value is `nil`, then no transformation occurs. """ @spec cast_path(map, [atom], (any -> any)) :: map def cast_path(%{} = data, path, fun) when is_function(fun) do maybe_update_in(data, path, fun) end defp maybe(fun) do fn nil -> nil arg -> fun.(arg) end end defp maybe_update_in(data, path, fun) do case get_in(data, path) do nil -> data val -> put_in(data, path, fun.(val)) end end end