2017-07-10 2 views
0

In Elixir möchte ich Validierung einer Datenstruktur bei ihrer Erstellung erzwingen. Um dies zu erreichen, erzwinge ich den Aufruf einer Callback-Funktion, die gerade erzeugte Datenstrukturen erzeugt und entweder diese Datenstruktur zurückgibt, wenn sie gültig ist, oder ein Fehlertupel.Callback implementieren, nachdem% MyStruct {} erstellt wurde

Wie kann man es implementieren?

Wenn ich ein Verhalten wie ValidatedStruct deklariere und den Callback im Modul der Struktur implementiere - wie könnte ich es erzwingen, es bei der Strukturinitialisierung aufzurufen, bevor ich eine neue Struktur im aufrufenden Code zurückgebe?

+0

Sie wollen dieser Rückruf automatisch aufgerufen wird, wenn ein Benutzer des '% Some verwendet {foo: bar}' Syntax, um eine Struktur zu schaffen, ? – Dogbert

+0

@Dogbert Ja, genau. – iTollu

+0

Vielleicht würde es aber ausreichen, einen solchen Rückruf für den Client-Code verfügbar zu machen, falls er sich selbst überprüfen möchte. Und dann den Rückruf während der weiteren Verarbeitung aufrufen. Ich weiß immer noch nicht, was der beste Weg ist, um einen solchen Integritätscheck in Elixir zu machen. Von Scala Hintergrund kommen. – iTollu

Antwort

1

Sie können die Struktur über eine Funktion erstellen, die Sie new nennen können, und von dort aus Ihre Validierungsmethode aufrufen.

Z. B: (nicht getestet)

defmodule User do 
    defstruct name: nil, errors: %{} 

    def new(opts) do 
    __struct__(__MODULE__, opts) |> validate 
    end 

    defp validate(user) do 
    if !user.name, do: add_error(user, :name, "Name is required"), else: user 
    end 

    defp add_error(user, property, message) do 
    %User{user | errors: Map.put(user.errors, property, message } 
    end 

    def valid?(user) do 
    Enum.empty?(user.errors) 
    end 
end 
+1

_Sidenote: _ immer eine Pipe mit einem Begriff starten: '__MODULE__ |> __struct __ (opts) |> validate'. – mudasobwa

+0

Danke zum Beispiel, @ Nicolas-Garnil, ich werde es testen und einen weiteren Kommentar mit meinen Beobachtungen schreiben. – iTollu

+0

In der Funktion 'new' sollte man' struct' (Kernel.struct/2) nennen, nicht '__struct__'. Ich möchte keine Fehler in der Struktur selbst speichern. Oft kann es sich nur um einen einzelnen Wert handeln. Wie% OrderId {v: "some_order_id"}. Fehlerberichte sollten von der Struktur entkoppelt sein. Ich sehe es eher monadisch. Es könnte dann innerhalb des "mit" -Blocks verwendet werden. – iTollu

2

Sie können nicht garantieren, dass eine Struktur gültige Werte in Elixir enthält. Ein Struct ist nur eine Map mit einem __struct__ Feld, das einen Atom enthält (normalerweise ein Modulname). Sie können jede Karte nehmen, ein __struct__ Feld hinzufügen und es wird diese Struktur.

Zum Beispiel, hier ist mir eine MapSet Struktur ohne ein anderes Feld zu konstruieren. iex kann nicht einmal die Struktur drucken, da die Inspect Implementierung von MapSet angenommen, dass es eine ist map Schlüssel eine Karte in der Struktur enthalten:

iex(1)> %{__struct__: MapSet} 
%Inspect.Error{message: "got FunctionClauseError with message \"no function clause matching in MapSet.to_list/1\" while inspecting \e[39m%{\e[0m\e[33m\e[36m__struct__: \e[0m\e[33m\e[36mMapSet\e[0m\e[33m\e[39m}\e[0m\e[33m"} 

in der Regel Elixir Bibliotheken tun Was ist eine new Funktion im Modul hinzufügen, die Argumente akzeptiert und gibt {:ok, struct} bei einem gültigen Eingang und {:error, "description"} (oder nur :error) bei einem Fehler zurück. Dies verhindert nicht, dass Benutzer die Struktur mit der Syntax %ModuleName{} erstellen. Sie können eine einfache Sicherheitsmaßnahme hinzufügen, indem Sie ein valid?-Feld hinzufügen, das standardmäßig false ist, und dann prüfen, ob dieser Wert in allen Ihren Funktionen wahr ist. Ecto.Changeset verwendet eine ähnliche Technik, die Repo.insert nicht einmal versucht, die Daten in eine Datenbank einzufügen, wenn es einen Fehler in Ecto.Changeset gibt. Auch dies ist trivial zu umgehen.

Hier ist ein Beispiel:

defmodule MyStruct do 
    defstruct [:x, :y, valid?: false] 

    # We want `x` and `y` to always be integers. 
    def new(x, y) when is_integer(x) and is_integer(y) do 
    {:ok, %__MODULE__{x: x, y: y, valid?: true}} 
    end 
    def new(_, _), do: :error 

    def print(%__MODULE__{x: x, y: y, valid?: true}) do 
    IO.inspect {x, y} 
    end 
end 

defmodule Main do 
    def main do 
    IO.inspect MyStruct.new(1, 2) 
    IO.inspect MyStruct.new(1, 2.3) 
    {:ok, a} = MyStruct.new(1, 2) 
    MyStruct.print(a) 
    try do 
     # This will throw an error because `valid?` will be false. 
     MyStruct.print(%MyStruct{}) 
    rescue 
     e -> IO.inspect e 
    end 
    # This however will work and there's no way to stop it. 
    MyStruct.print(%MyStruct{x: "x", y: "y", valid?: true}) 
    end 
end 

Main.main 

Ausgang:

{:ok, %MyStruct{valid?: true, x: 1, y: 2}} 
:error 
{1, 2} 
%FunctionClauseError{arity: 1, function: :print, module: MyStruct} 
{"x", "y"} 
+0

Danke für so detaillierte Antwort! Was ich suche - macht es nicht schwer, es zu umgehen, sondern macht den Bypass-Versuch explizit für den Autor des Codes. Damit würde ich als Autor des Client-Codes leicht merken, wenn ich den Check umgehe. Scheint, ich muss in die Implementierung von Ecto Changeset eintauchen. – iTollu

Verwandte Themen