Powered by AppSignal & Oban Pro
Would you like to see your link here? Contact us

Typed Struct

articles/typed-struct.livemd

Typed Struct

Setup

Mix.install([:ecto, :typed_struct, :typed_struct_ecto_changeset])

Pure Struct

struct 와 changset 이 연결된 PR 을 진행하다보니 struct 를 좀 더 쉽게 선언할 수 있는 방식에 관심이 가기 시작했다.

Cloud Studio 에서는 nested 된 struct 구조를 만들고 조회하는 방식으로 운영해야 하기 때문에, struct 를 효율적으로 만들고 관리할 수 있다면 아주 도움이 되기 때문.

우선 이전에 PR 에 제안했던 모델을 살펴보자.

defmodule Model do
  import Ecto.Changeset

  defstruct name: nil, flip: false, crop: false, background_frame: 1000

  @types %{name: :string, flip: :boolean, crop: :boolean, background_frame: :integer}

  def changeset(%__MODULE__{} = model, attrs \\ %{}) do
    {model, @types}
    |> cast(attrs, [:name, :flip, :crop, :background_frame])
    |> validate_required([:name])
    |> validate_length(:name, min: 3, max: 100)
  end
end
Model.changeset(%Model{name: "hello"})
Model.changeset(%Model{name: "hello"}, %{name: nil})
Model.changeset(%Model{name: "hello"}, %{name: 1000})

몇가지 변경할 부분을 정리해보자

  • boolean 타입에는 ? suffix 추가
  • changeset/2 에서 사용되는 @types

typed_struct

defmodule TypedModel do
  use TypedStruct

  typedstruct do
    field(:name, String.t(), enforce: true)
    field(:flip?, boolean(), default: false)
    field(:crop?, boolean(), default: false)
    field(:background_frame, non_neg_integer(), default: 1000)
  end
end
%TypedModel{name: "hello", crop?: true}
%TypedModel{name: 1000}
TypedModel.__struct__() |> Map.from_struct() |> Map.keys()

typed_struct & changeset

typed_struct 를 schemaless changeset 처럼 사용할 수 있도록 도와주는 typed_struct_ecto_changeset 을 추가해보자

defmodule TypedModel2 do
  use TypedStruct
  import Ecto.Changeset

  typedstruct do
    plugin(TypedStructEctoChangeset)

    field(:name, String.t(), enforce: true)
    field(:flip?, boolean(), default: false)
    field(:crop?, boolean(), default: false)
    field(:background_frame, non_neg_integer(), default: 1000)
  end

  def changeset(%__MODULE__{} = model, attrs \\ %{}) do
    model
    |> cast(attrs, [:name, :flip?, :crop?, :background_frame])
    |> validate_required([:name])
    |> validate_length(:name, min: 3, max: 100)
  end
end
TypedModel2.changeset(%TypedModel2{name: nil}, %{name: "hello", crop?: false})

Nested

nested 된 모듈도 제대로 동작할까?

defmodule Children do
  use TypedStruct
  import Ecto.Changeset

  typedstruct do
    plugin(TypedStructEctoChangeset)

    field(:name, String.t(), enforce: true)
    field(:value, non_neg_integer(), default: 1000)
  end

  def changeset(%__MODULE__{} = model, attrs \\ %{}) do
    model
    |> cast(attrs, [:name, :value])
    |> validate_required([:name])
    |> validate_length(:name, min: 3, max: 100)
  end
end
defmodule Parent do
  use TypedStruct
  import Ecto.Changeset

  typedstruct do
    plugin(TypedStructEctoChangeset)

    field(:name, String.t(), enforce: true)
    field(:children, Children.t(), default: %Children{name: "wow"})
  end

  def changeset(%__MODULE__{} = model, attrs \\ %{}) do
    model
    |> cast(attrs, [:name, :value])
    |> validate_required([:name])
    |> validate_length(:name, min: 3, max: 100)
  end
end
Parent.changeset(%Parent{name: ""}, %{})

바로 되지는 않는다. 뭔가 association 을 설정해 줘야 하는 듯하다.

Conclusion

typed_struct 는 pure struct 에서 changeset 을 활용할 때, 별도의 타입선언을 생략해주는 것 외에는 큰 장점은 없어 보인다.

struct 를 타입과 같이 깔끔하게 선언하고 싶을때 사용하면 좋을 것 같다.

hello