From 86be1d6ca312d69f4d20e0043a7681c6aa07ea31 Mon Sep 17 00:00:00 2001 From: Garth Kidd Date: Mon, 17 Jun 2019 10:04:28 +1000 Subject: [PATCH 1/2] WIP: typed arguments --- lib/notion.ex | 112 +++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 101 insertions(+), 11 deletions(-) diff --git a/lib/notion.ex b/lib/notion.ex index 479f2a5..227117c 100644 --- a/lib/notion.ex +++ b/lib/notion.ex @@ -41,7 +41,62 @@ defmodule Notion do end end - defmacro defevent(event) do + defmodule EventOptions do + @moduledoc "Event Options" + + defstruct [:t_measurements, :t_metadata, :defaults] + + @doc false + def defaults() do + [ + t_measurements: {:map, [], Elixir}, + t_metadata: {:map, [], Elixir}, + defaults: :measurements_and_metadata + ] + end + + @typedoc """ + Specifies which arguments get defaults: + + * `:measurements_and_metadata`: both arguments + * `:metadata_only`: only the last argument + * `:nil`: neither argument + """ + @type defaults_value :: :measurements_and_metadata | :metadata_only | nil + + @typedoc """ + `defevent` macro options: + + * `t_measurements`: the typespec of your measurements (default: `map`) + * `t_metadata`: the typespec of your metadata (default: `map`) + * `defaults`: which arguments get default arguments + + For Dialyzer to not complain, you [MUST]: + + * Use atoms for your measurement and metadata keys + + * Set `defaults` to `:metadata_only` if you have any `required` keys in your measurements, + and to `:nil` if you have any `required` keys in your `metadata`. + + [MUST]: https://tools.ietf.org/html/rfc2119#section-1 + """ + @type option :: + {:t_measurements, term()} + | {:t_metadata, term()} + | {:defaults, defaults_value()} + + @doc false + def from(options) when is_list(options) do + options = Keyword.merge(defaults(), options) + IO.inspect(options) + struct!(__MODULE__, options) + end + end + + @spec defevent(list(atom), list(EventOptions.options())) :: term() + defmacro defevent(event, options \\ []) do + event_options = EventOptions.from(options) + names = case event do event when is_list(event) -> event @@ -50,16 +105,51 @@ defmodule Notion do function_name = Enum.join(names, "_") - quote do - @event [@notion_name | unquote(names)] - @events @event - - @spec unquote(:"#{function_name}")(map, map) :: :ok - # credo:disable-for-next-line - def unquote(:"#{function_name}")(measurements \\ %{}, metadata \\ %{}) do - labels = labels(metadata) - :telemetry.execute(@event, measurements, labels) - end + case event_options.defaults do + :measurements_and_metadata -> + quote do + @event [@notion_name | unquote(names)] + @events @event + @spec unquote(:"#{function_name}")( + unquote(event_options.t_measurements), + unquote(event_options.t_metadata) + ) :: :ok + # credo:disable-for-next-line + def unquote(:"#{function_name}")(measurements \\ %{}, metadata \\ %{}) do + labels = labels(metadata) + :telemetry.execute(@event, measurements, labels) + end + end + + :metadata_only -> + quote do + @event [@notion_name | unquote(names)] + @events @event + @spec unquote(:"#{function_name}")( + unquote(event_options.t_measurements), + unquote(event_options.t_metadata) + ) :: :ok + # credo:disable-for-next-line + def unquote(:"#{function_name}")(measurements, metadata \\ %{}) do + labels = labels(metadata) + :telemetry.execute(@event, measurements, labels) + end + end + + nil -> + quote do + @event [@notion_name | unquote(names)] + @events @event + @spec unquote(:"#{function_name}")( + unquote(event_options.t_measurements), + unquote(event_options.t_metadata) + ) :: :ok + # credo:disable-for-next-line + def unquote(:"#{function_name}")(measurements, metadata) do + labels = labels(metadata) + :telemetry.execute(@event, measurements, labels) + end + end end end end From eafc23580da785034b3dad9508e0fb60ce35e8a7 Mon Sep 17 00:00:00 2001 From: Garth Kidd Date: Mon, 17 Jun 2019 10:38:58 +1000 Subject: [PATCH 2/2] squash! WIP: typed arguments Determining the arity of the emitted function from the presence or absence of `measurements` and `metadata` might feel like we were "forced" to use typespecs, but `map` isn't so bad... --- lib/notion.ex | 106 +++++++++++++++++++++----------------------------- 1 file changed, 44 insertions(+), 62 deletions(-) diff --git a/lib/notion.ex b/lib/notion.ex index 227117c..b87ee67 100644 --- a/lib/notion.ex +++ b/lib/notion.ex @@ -3,6 +3,9 @@ defmodule Notion do Notion is a thin wrapper around [`:telemetry`](https://github.com/beam-telemetry/telemetry) that defines functions that dispatch telemetry events, documentation, and specs for your applications events. """ + @typedoc "An event name" + @type event_name :: list(atom()) + @doc false defmacro __using__(opts) do quote bind_quoted: [opts: opts] do @@ -41,59 +44,49 @@ defmodule Notion do end end - defmodule EventOptions do - @moduledoc "Event Options" - - defstruct [:t_measurements, :t_metadata, :defaults] - - @doc false - def defaults() do - [ - t_measurements: {:map, [], Elixir}, - t_metadata: {:map, [], Elixir}, - defaults: :measurements_and_metadata - ] - end - - @typedoc """ - Specifies which arguments get defaults: - - * `:measurements_and_metadata`: both arguments - * `:metadata_only`: only the last argument - * `:nil`: neither argument - """ - @type defaults_value :: :measurements_and_metadata | :metadata_only | nil + @typedoc """ + `defevent` macro option: - @typedoc """ - `defevent` macro options: + * `measurements`: the typespec of your measurements, e.g. `map` + * `metadata`: the typespec of your metadata, e.g. `map` - * `t_measurements`: the typespec of your measurements (default: `map`) - * `t_metadata`: the typespec of your metadata (default: `map`) - * `defaults`: which arguments get default arguments + For Dialyzer to pass, you [MUST] use atoms for your measurement and metadata keys. - For Dialyzer to not complain, you [MUST]: + [MUST]: https://tools.ietf.org/html/rfc2119#section-1 + """ + @type defevent_option :: {:measurements, term()} | {:metadata, term()} - * Use atoms for your measurement and metadata keys + defmodule EventOptions do + @moduledoc false - * Set `defaults` to `:metadata_only` if you have any `required` keys in your measurements, - and to `:nil` if you have any `required` keys in your `metadata`. + defstruct [:measurements, :metadata] - [MUST]: https://tools.ietf.org/html/rfc2119#section-1 - """ - @type option :: - {:t_measurements, term()} - | {:t_metadata, term()} - | {:defaults, defaults_value()} + def defaults(), do: [measurements: nil, metadata: nil] - @doc false + @spec from(list(Notion.defevent_option())) :: %__MODULE__{} def from(options) when is_list(options) do options = Keyword.merge(defaults(), options) - IO.inspect(options) struct!(__MODULE__, options) end end - @spec defevent(list(atom), list(EventOptions.options())) :: term() + @doc """ + Define a function to send an event. + + ```elixir + defevent [:event_suffix], measurements: map, metadata: map + ``` + + The `measurements` and `metadata` options [MUST] be a valid [typespec]. (`map` is fine.) + + If you'll never send `metadata`, leave it out, and Notion will define a 1-ary function. + + If you'll never send `measurements`, leave it out, and Notion will define a 0-ary function. + + [MUST]: https://tools.ietf.org/html/rfc2119#section-1 + [typespec]: https://hexdocs.pm/elixir/typespecs.html + """ + @spec defevent(event_name(), list(EventOptions.options())) :: term() defmacro defevent(event, options \\ []) do event_options = EventOptions.from(options) @@ -105,45 +98,34 @@ defmodule Notion do function_name = Enum.join(names, "_") - case event_options.defaults do - :measurements_and_metadata -> + case event_options do + %EventOptions{measurements: nil, metadata: nil} -> quote do @event [@notion_name | unquote(names)] @events @event - @spec unquote(:"#{function_name}")( - unquote(event_options.t_measurements), - unquote(event_options.t_metadata) - ) :: :ok + @spec unquote(:"#{function_name}")() :: :ok # credo:disable-for-next-line - def unquote(:"#{function_name}")(measurements \\ %{}, metadata \\ %{}) do - labels = labels(metadata) - :telemetry.execute(@event, measurements, labels) + def unquote(:"#{function_name}")() do + :telemetry.execute(@event, %{}, labels()) end end - :metadata_only -> + %EventOptions{measurements: measurements, metadata: nil} -> quote do @event [@notion_name | unquote(names)] @events @event - @spec unquote(:"#{function_name}")( - unquote(event_options.t_measurements), - unquote(event_options.t_metadata) - ) :: :ok + @spec unquote(:"#{function_name}")(unquote(measurements)) :: :ok # credo:disable-for-next-line - def unquote(:"#{function_name}")(measurements, metadata \\ %{}) do - labels = labels(metadata) - :telemetry.execute(@event, measurements, labels) + def unquote(:"#{function_name}")(measurements) do + :telemetry.execute(@event, measurements, labels()) end end - nil -> + %EventOptions{measurements: measurements, metadata: metadata} -> quote do @event [@notion_name | unquote(names)] @events @event - @spec unquote(:"#{function_name}")( - unquote(event_options.t_measurements), - unquote(event_options.t_metadata) - ) :: :ok + @spec unquote(:"#{function_name}")(unquote(measurements), unquote(metadata)) :: :ok # credo:disable-for-next-line def unquote(:"#{function_name}")(measurements, metadata) do labels = labels(metadata)