bglusman

bglusman

I’m curious how others in the community deal with first class enums… especially crossing boundaries like database, code logic, and absinthe? My team and I developed a home baked solution that I quite like, and am tempted to open source, though its not very much code, but I thought I’d check first what others are doiing and whether there are other solutions out there I’m not aware of…

What our solution mostly does that I like is, makes them first class, makes it easy to use them both in the database and in code, and expose as first class enums in absinthe/graphQL, supports optionally enforcing the limitation in database, supports deprecation, and supports versioning (though versioning support is pretty limited/manual and mostly tied to the DB enforcement to allow dropping the old constraint and adding the new one in a safe, DRY way…)

Are there other solutions supporting all of that, and/or more I’m not thinking of/haven’t had to deal with?

By the way happy to just share the few modules of code here as snippets instead of/in addition to “open sourcing” as a library/package, just curiious as the main point here is just curiosity/discussion on how others handle and whether its a pain point that wants tackling better than Ecto.Enum does at present… dont’ want to focus on our solution as I don’t think its anything amazing, but it works well enough for us…

Showing Posts 8 to 1

Eiji

Eiji

It’s much simpler than only one of many optional components I’ve added to enumex.

and

are things that another optional component do.

This is a very manual version of what my code do. For Advanced case I’m automatically modifying all tables to support a new enum (with added or removed value). What you do only drops constraint and requires manual conversion for all tables using your constraint.

Generally what you do is implement part of features from my Simple approach with a bit of Advanced approach code that is unfortunately very manual to use especially if a specific enum is used by multiple tables or when there is lots of enums used.


So in short it’s not even 1/3 of features from enumex package. Your solution lacks of automation and many helpful features to use enum on Elixir side (like automatically generated guards or specification). What you show is very poorly documented, for example I use behaviours to document functions and macros defined inside my macros.

Also I don’t believe credo would be “happy” validating your code (ABCSize check for example). In contracts all of my features are documented (mostly in components as my package does not force any conventions), have full specification, 100% test coverage, passes all credo and dialyzer checks.

bglusman

bglusman OP

Thanks @karlosmid and @Eiji ! I had not seen either of those libraries and are exactly what I was curious about, I’ll have to dig into them… we’re not likely to replace our in-house tool because we use it extensively and it works well, but, good to be aware of/make easier for others to discover from this discussion perhaps…

For what it’s worth/in case anyone is interested, here’s the code from our in homebrewed solution, including an extension for Absinthe integration and for supporting constraints in migrations via a versioning convention… for usage, you use the module defining a new Enum, and then import or require your enum module to expose the macros for usage in your code…

defmodule Optimizer.Ecto.Types.Enumerable do
  @moduledoc """
  Enumerable type for Ecto fields.  Main benefit vs Ecto.Enum being first class `values` and macro functions for each
  value so that compilation will fail if they are removed/renamed.

  NOTE:  if an enum is stored in the database, DO NOT simply drop a value,
  it should be deprecated first, then removed after a migration to remove the value from the database.
  deprecation is a new feature used via an optional keyword argument to the `use` macro, e.g.

  use Optimizer.Ecto.Types.Enumerable, values: [:foo, :bar], deprecated: [:baz]

  Based on:
  https://github.com/KamilLelonek/exnumerator/blob/2e72dd1a34e119e5198aebf18ebfb4bed77f3646/lib/exnumerable.ex
  """
  # credo:disable-for-this-file
  defmacro __using__(opts) do
    quote location: :keep,
          bind_quoted: [current_values: opts[:values], deprecated_values: opts[:deprecated] || []] do
      @behaviour Ecto.Type

      require Logger

      @impl Ecto.Type
      def type, do: :string
      def __current_values, do: unquote(current_values)
      def __values, do: unquote((current_values ++ deprecated_values) |> Enum.sort())

      # these are used in guards hence macros
      defmacro current_values, do: unquote(current_values)
      defmacro values, do: unquote((current_values ++ deprecated_values) |> Enum.sort())

      values = current_values ++ deprecated_values

      for value <- values, atom = value, string = Atom.to_string(value) do
        deprecated? = deprecated_values |> Enum.member?(value)

        downcased_atom =
          string
          |> String.downcase()
          |> String.to_atom()

        upcased_string = String.upcase(string)

        @impl Ecto.Type
        def cast(unquote(string)), do: {:ok, unquote(atom)}

        if upcased_string != string do
          def cast(unquote(upcased_string)), do: {:ok, unquote(atom)}
        end

        def cast(unquote(atom)), do: {:ok, unquote(atom)}

        @impl Ecto.Type
        if deprecated? do
          def load(unquote(string)) do
            Logger.warning(
              "Deprecated value #{unquote(string)} loaded for #{inspect(__MODULE__)}"
            )

            {:ok, unquote(atom)}
          end
        else
          def load(unquote(string)), do: {:ok, unquote(atom)}
        end

        @impl Ecto.Type
        if deprecated? do
          def dump(unquote(string)) do
            Logger.warning(
              "Deprecated value #{unquote(string)} dumped for #{inspect(__MODULE__)}"
            )

            {:ok, unquote(string)}
          end

          def dump(unquote(atom)) do
            Logger.warning("Deprecated value #{unquote(atom)} dumped for #{inspect(__MODULE__)}")
            {:ok, unquote(atom)}
          end
        else
          def dump(unquote(string)), do: {:ok, unquote(string)}
          def dump(unquote(atom)), do: {:ok, unquote(string)}
        end

        if deprecated? do
          defmacro unquote(downcased_atom)() do
            IO.warn(
              "Deprecated enum value #{unquote(downcased_atom)} used, please update code, this is only supported for legacy compatibility",
              Macro.Env.stacktrace(__ENV__)
            )

            unquote(atom)
          end
        else
          defmacro unquote(downcased_atom)(), do: unquote(atom)
        end
      end

      def cast(_), do: :error
      def load(_), do: :error
      def dump(_), do: :error

      def cast!(term) do
        case cast(term) do
          {:ok, value} ->
            value

          :error ->
            raise Ecto.CastError,
              message: "cannot cast #{inspect(term)} as #{inspect(__MODULE__)}"
        end
      end

      @impl Ecto.Type
      def embed_as(_), do: :self

      @impl Ecto.Type
      def equal?(value_1, value_2), do: value_1 === value_2
    end
  end
end

and our absinthe EnumBuilder

defmodule EnumBuilder do
  @moduledoc """
  This module is used to dynamically build an enum type for the GraphQL schema.
  """

  alias Absinthe.Blueprint
  alias Absinthe.Blueprint.Schema
  alias Absinthe.Schema.Notation
  alias Absinthe.Type.Deprecation

  @spec run(Blueprint.t(), atom(), module()) :: Blueprint.t()
  def run(blueprint = %Blueprint{}, name, enum_module) do
    %{schema_definitions: [schema]} = blueprint

    new_enum = build_dynamic_enum(enum_module, name)

    new_schema = %{schema | type_definitions: [new_enum | schema.type_definitions]}

    %{blueprint | schema_definitions: [new_schema]}
  end

  defp build_dynamic_enum(enum_module, name) do
    %Schema.EnumTypeDefinition{
      name: name |> Atom.to_string() |> Absinthe.Utils.camelize(),
      identifier: name,
      module: __MODULE__,
      __reference__: Notation.build_reference(__ENV__),
      values: values(enum_module)
    }
  end

  defp values(enum_module) do
    # require enum_module
    {current_values, values} = extract_module_values(enum_module)

    for value <- values do
      deprecated? = value not in current_values

      define_value(
        value,
        String.upcase("#{value}"),
        enum_value_description(enum_module, value),
        deprecated?
      )
    end
  end

  defp extract_module_values(enum_module) do
    {
      enum_module.__current_values(),
      enum_module.__values()
    }
  end

  defp enum_value_description(enum_module, value) do
    if function_exported?(enum_module, :to_description, 1) do
      enum_module.to_description(value)
    else
      nil
    end
  end

  defp define_value(key, name, description, deprecated?) do
    %Schema.EnumValueDefinition{
      identifier: key,
      value: key,
      name: name,
      description: description,
      deprecation:
        if deprecated? do
          %Deprecation{reason: "deprecated"}
        else
          nil
        end,
      module: __MODULE__,
      __reference__: Notation.build_reference(__ENV__)
    }
  end

  defmacro __using__(_options) do
    quote do
      Module.register_attribute(__MODULE__, :dynamic_enums, accumulate: true, persist: true)
      @before_compile unquote(__MODULE__)
      import unquote(__MODULE__)
    end
  end

  defmacro __before_compile__(_env) do
    quote do
      def __all_dynamic_enums__ do
        @dynamic_enums
      end
    end
  end

  defmacro dynamic_enum(enum_identifier, enum_module) do
    %{module: mod} = __CALLER__

    quote bind_quoted: binding() do
      Module.put_attribute(mod, :dynamic_enums, {enum_identifier, enum_module})
    end
  end
end

and middleware

defmodule DynamicEnumCreationPhase do
  @moduledoc """
  based on Absinthe phases https://hexdocs.pm/absinthe/1.5.0/Absinthe.Phase.html
  """
  alias Absinthe.Blueprint
  alias Absinthe.Pipeline
  alias Absinthe.Phase

  @behaviour Phase

  @spec get_module :: __MODULE__
  def get_module, do: __MODULE__

  @spec pipeline(Pipeline.t()) :: Pipeline.t()
  def pipeline(pipeline) do
    Pipeline.insert_after(pipeline, Phase.Schema.TypeImports, __MODULE__)
  end

  @impl Phase
  def run(blueprint = %Blueprint{}, _) do
    {:ok,
     Enums.__all_dynamic_enums__()
     |> Enum.reduce(blueprint, fn {identifier, enum_module}, acc ->
       EnumBuilder.run(acc, identifier, enum_module)
     end)}
  end
end

We also add in our repo.ex file, though this is purely optional/we use less now than we used to


  @spec create_enum_constraint(atom | binary, any, :array | :string, maybe_improper_list) :: %{
          :__struct__ => Ecto.Migration.Constraint | Ecto.Migration.Index | Ecto.Migration.Table,
          :prefix => any,
          optional(any) => any
        }
  def create_enum_constraint(table, field, field_type, enum_list) when is_list(enum_list) do
    check = build_enum_constraint_check(field, enum_list, field_type)

    Migration.create(Migration.constraint(table, :"valid_#{field}", check: check))
  end

  @spec drop_enum_constraint(atom | binary, any) :: %{:prefix => any, optional(any) => any}
  def drop_enum_constraint(table, field) do
    Migration.drop(Migration.constraint(table, :"valid_#{field}"))
  end

  defp build_enum_constraint_check(field, enum_list, :string) do
    "#{field} ~ '^(#{Enum.join(enum_list, "|")})$'"
  end

  defp build_enum_constraint_check(field, enum_list, :array) do
    array = form_array_string(enum_list, [])

    ~s(#{field} <@ ARRAY[#{array}])
  end

  defp form_array_string([head | []], acc) do
    [wrap_in_io_list(head, false) | acc]
    |> Enum.reverse()
    |> to_string()
  end

  defp form_array_string([head | tail], acc) do
    form_array_string(tail, [wrap_in_io_list(head) | acc])
  end

  defp wrap_in_io_list(val, trailing_comma? \\ true) do
    initial =
      if trailing_comma? do
        [","]
      else
        []
      end

    [[~S('), to_string(val), ~S(')] | initial]
  end

Could publish as a library, but, not a ton of code and not sure how it compares yet to above existing libraries I wasn’t aware of so just throwing out there for feedback/interest

bglusman

bglusman OP

Oh I forgot about this library, I was talking about the built in Ecto.Enum but I had seen and probably used this library years ago becore Ecto.Enum was introduced in Ecto core… being able to check if a string IS a valid value is an interesting tool/usecase, though I’m not sure when I’d use it vs just patternmatching on the actual value or membership in the full list of values? Anyway, thanks for reminding me of!

Eiji

Eiji

https://forum.elixirforum.com/t/enumex-first-stable-release/71528

You define enum module which itself does nothing except storing compile-time data from DSL and for that module you add one or more components that uses that data to generate a function at compile-time.

I support various scenarios:

  1. Simple - just storing an integer/string in the database - only Elixir-side validations - good for a single project

  2. Advanced - as same as simple stores a static list of values, but using a migration and adapter you can create an enumerated type in database - validations in both Elixir in database - good for working on same db in multiple projects - limited support for databases supporting enumerated types

  3. Custom - like Advanced, but with support for much more database like in Simple; it uses a relation instead of the enumerated type - validations in both Elixir in database - good for runtime-determined values (like user roles)

Both Simple and Advanced cases (Enumex.Static) have an absinthe component and various other components as well … I believe my solution is therefore the most feature-complete.

karlosmid

karlosmid

Hi, we are using protobuf enum definition, we put those in global protobuf folder.

enum CurrencyCode {
  XXX    = 0;  // no currency, fun money etc
  BTC    = 1;  /// Bitcoin

Then with GenEnum – GenEnum v0.4.3
we got helpers:

defmodule MyApp.CurrencyCode.Items do
  defmacro btc() do
    :BTC
  end
  
  defmacro xxx() do
    :XXX
  end
  
  # ... and so on for all currency codes
end
axelson

axelson

Scenic Core Team

GitHub - gjaldon/ecto_enum: Ecto extension to support enums in models · GitHub works pretty well for my needs. And it generates functions that allow you to use the enums elsewhere in the codebase, e.g.

iex> MyApp.SomeEnum.valid_value?("standard")
true
iex> MyApp.SomeEnum.valid_value?("bogus_value")
false
bglusman

bglusman OP

I do recall looking at typedstruct at some point and its nice it saves some typing and all, but, I’m not sure if I realized the direction it went, allowing you to use types in your struct definition as opposed to saving you tying up a type for your struct… that’s interesting… but given the direction of Elixir’s support for static typing more directly in the language, I’m not likely to start taking up a tool now that relies on the… what I’ll call deprecated type syntax? Since my understanding is there may be support for migrating that to the new type system, but the syntax and behavior will definitely be different.

But while the compile time errors there are nice, it does nothing to address use of the values elsewhere in code, as in my experience when you have enums, you need to reference specific values in multiple places, and one of the things I find inadequate about Ecto.Enum is it doesn’t help you do that in a DRY or safe way protecting from typos etc… our module (mostly) does, as long as its used “correctly”…

dogweather

dogweather

My solution feels ad hoc and I’m sure I’ll be evolving it:

  use TypedStruct
  use Domo

  @type plan_id :: :free | :starter | :pro | :scale | :enterprise

  typedstruct enforce: true do
    field :id,                 plan_id
    # ...
  end 

But I have a lot of boilerplate code that’s not DRY. I’m actually not too sure how much of it I need. I do wish this was entirely declarative, DRY, and introspectable so that I only need declare this plan_id type once.

I do get nice IDE and compile-time error messages from the above, though if I supply an invalid plan_id:

Failed to build IndexFlow.Billing.Plan struct.
Invalid value :abc for field :id of %IndexFlow.Billing.Plan{}.
Expected the value matching the :free | :starter | :pro | :scale | :enterprise type.
— All posts loaded —

Where Next? Top

Trending in Discussions Top

AstonJ
As the title says, please share what you’ve been up to with Elixir. Whether that’s been learning it, looking into it, making stuff with i...
2977 94592 917
New
cblavier
Hey there, It’s been more than a year since we started using LiveView as our main UI library and building a whole library of UI componen...
New
mudasobwa
I am happy to introduce the very α version of the new programming language compiled to BEAM. Welcome Cure. It has literally three kille...
New
heathen
Quite interesting article Google brought me. Didn’t find any mentions about it here. What do you think in general? Would you use togethe...
New
maennchen
:warning: Security advisory: Decimal DoS vulnerability A vulnerability has been published for decimal where very large exponents can cau...
New
marciol
It would be helpful to have a list of companies worldwide that hire engineers without prior experience in Elixir. Often, it can be quite ...
New
durvia
Anyone running long-lived stateful processes on BEAM? We’re building an AI agent runtime and would love to compare notes. We’re a small ...
New

Other Trending Topics Top

marciok
Hi there! We created Gust: A task orchestrator inspired by Airflow. For those who have never heard about Aiflow, it’s a Python-based wor...
New
jimsynz
Beam Bots (or just BB for short) is a framework for building fault-tolerant robotics applications in Elixir using familiar OTP patterns. ...
New
Dmk
Xamal is a deployment tool for Elixir apps that deploys native releases to bare metal servers over SSH. It’s a port of GitHub - basecamp/...
New
netoum
Corex is an accessible, unstyled UI component library for Phoenix that integrates Zag.js state machines using Vanilla JavaScript and Live...
New
webofbits
Aludel - LLM Evaluation Workbench Aludel is an embeddable Phoenix LiveView dashboard for evaluating and comparing LLM prompts across mult...
New
webofbits
With AI doing more of the implementation work, I’ve been wondering how much coding I should deliberately keep doing myself. My main conc...
#ai
New

We're in Beta

About us Mission Statement

Options

Thread Display Mode




Thread Preview

Skip Thread Previews