zf

zenflows testing
git clone https://s.sonu.ch/~srfsh/zf.git
Log | Files | Refs | Submodules | README | LICENSE

pipeline.ex (12842B)


      1 defmodule Absinthe.Pipeline do
      2   @moduledoc """
      3   Execute a pipeline of phases.
      4 
      5   A pipeline is merely a list of phases. This module contains functions for building,
      6   modifying, and executing pipelines of phases.
      7 
      8   Pipelines are used to build, validate and manipulate GraphQL documents or schema's.
      9 
     10   * See [`Absinthe.Plug`](https://hexdocs.pm/absinthe_plug/Absinthe.Plug.html) on adjusting the document pipeline for GraphQL over http requests.
     11   * See [`Absinthe.Phoenix`](https://hexdocs.pm/absinthe_phoenix/) on adjusting the document pipeline for GraphQL over Phoenix channels.
     12   * See `Absinthe.Schema` on adjusting the schema pipeline for schema manipulation.
     13   """
     14 
     15   alias Absinthe.Phase
     16 
     17   @type data_t :: any
     18 
     19   @type phase_config_t :: Phase.t() | {Phase.t(), Keyword.t()}
     20 
     21   @type t :: [phase_config_t | [phase_config_t]]
     22 
     23   @spec run(data_t, t) :: {:ok, data_t, [Phase.t()]} | {:error, String.t(), [Phase.t()]}
     24   def run(input, pipeline) do
     25     pipeline
     26     |> List.flatten()
     27     |> run_phase(input)
     28   end
     29 
     30   @defaults [
     31     adapter: Absinthe.Adapter.LanguageConventions,
     32     operation_name: nil,
     33     variables: %{},
     34     context: %{},
     35     root_value: %{},
     36     validation_result_phase: Phase.Document.Validation.Result,
     37     result_phase: Phase.Document.Result,
     38     jump_phases: true
     39   ]
     40 
     41   def options(overrides \\ []) do
     42     Keyword.merge(@defaults, overrides)
     43   end
     44 
     45   @spec for_document(Absinthe.Schema.t()) :: t
     46   @spec for_document(Absinthe.Schema.t(), Keyword.t()) :: t
     47   @doc """
     48   The default document pipeline
     49   """
     50   def for_document(schema, options \\ []) do
     51     options = options(Keyword.put(options, :schema, schema))
     52 
     53     [
     54       Phase.Init,
     55       {Phase.Telemetry, Keyword.put(options, :event, [:execute, :operation, :start])},
     56       # Parse Document
     57       {Phase.Parse, options},
     58       # Convert to Blueprint
     59       {Phase.Blueprint, options},
     60       # Find Current Operation (if any)
     61       {Phase.Document.Validation.ProvidedAnOperation, options},
     62       {Phase.Document.CurrentOperation, options},
     63       # Mark Fragment/Variable Usage
     64       Phase.Document.Uses,
     65       # Validate Document Structure
     66       {Phase.Document.Validation.NoFragmentCycles, options},
     67       Phase.Document.Validation.LoneAnonymousOperation,
     68       {Phase.Document.Validation.SelectedCurrentOperation, options},
     69       Phase.Document.Validation.KnownFragmentNames,
     70       Phase.Document.Validation.NoUndefinedVariables,
     71       Phase.Document.Validation.NoUnusedVariables,
     72       Phase.Document.Validation.NoUnusedFragments,
     73       Phase.Document.Validation.UniqueFragmentNames,
     74       Phase.Document.Validation.UniqueOperationNames,
     75       Phase.Document.Validation.UniqueVariableNames,
     76       # Apply Input
     77       {Phase.Document.Context, options},
     78       {Phase.Document.Variables, options},
     79       Phase.Document.Validation.ProvidedNonNullVariables,
     80       Phase.Document.Arguments.Normalize,
     81       # Map to Schema
     82       {Phase.Schema, options},
     83       # Ensure Types
     84       Phase.Validation.KnownTypeNames,
     85       Phase.Document.Arguments.VariableTypesMatch,
     86       # Process Arguments
     87       Phase.Document.Arguments.CoerceEnums,
     88       Phase.Document.Arguments.CoerceLists,
     89       {Phase.Document.Arguments.Parse, options},
     90       Phase.Document.MissingVariables,
     91       Phase.Document.MissingLiterals,
     92       Phase.Document.Arguments.FlagInvalid,
     93       # Validate Full Document
     94       Phase.Document.Validation.KnownDirectives,
     95       Phase.Document.Validation.RepeatableDirectives,
     96       Phase.Document.Validation.ScalarLeafs,
     97       Phase.Document.Validation.VariablesAreInputTypes,
     98       Phase.Document.Validation.ArgumentsOfCorrectType,
     99       Phase.Document.Validation.KnownArgumentNames,
    100       Phase.Document.Validation.ProvidedNonNullArguments,
    101       Phase.Document.Validation.UniqueArgumentNames,
    102       Phase.Document.Validation.UniqueInputFieldNames,
    103       Phase.Document.Validation.FieldsOnCorrectType,
    104       Phase.Document.Validation.OnlyOneSubscription,
    105       # Check Validation
    106       {Phase.Document.Validation.Result, options},
    107       # Prepare for Execution
    108       Phase.Document.Arguments.Data,
    109       # Apply Directives
    110       Phase.Document.Directives,
    111       # Analyse Complexity
    112       {Phase.Document.Complexity.Analysis, options},
    113       {Phase.Document.Complexity.Result, options},
    114       # Execution
    115       {Phase.Subscription.SubscribeSelf, options},
    116       {Phase.Document.Execution.Resolution, options},
    117       # Format Result
    118       Phase.Document.Result,
    119       {Phase.Telemetry, Keyword.put(options, :event, [:execute, :operation, :stop])}
    120     ]
    121   end
    122 
    123   @default_prototype_schema Absinthe.Schema.Prototype
    124 
    125   @spec for_schema(nil | Absinthe.Schema.t()) :: t
    126   @spec for_schema(nil | Absinthe.Schema.t(), Keyword.t()) :: t
    127   @doc """
    128   The default schema pipeline
    129   """
    130   def for_schema(schema, options \\ []) do
    131     options =
    132       options
    133       |> Enum.reject(fn {_, v} -> is_nil(v) end)
    134       |> Keyword.put(:schema, schema)
    135       |> Keyword.put_new(:prototype_schema, @default_prototype_schema)
    136 
    137     [
    138       Phase.Schema.TypeImports,
    139       Phase.Schema.DeprecatedDirectiveFields,
    140       Phase.Schema.ApplyDeclaration,
    141       Phase.Schema.Introspection,
    142       {Phase.Schema.Hydrate, options},
    143       Phase.Schema.Arguments.Normalize,
    144       {Phase.Schema, options},
    145       Phase.Schema.Validation.TypeNamesAreUnique,
    146       Phase.Schema.Validation.TypeReferencesExist,
    147       Phase.Schema.Validation.TypeNamesAreReserved,
    148       # This phase is run once now because a lot of other
    149       # validations aren't possible if type references are invalid.
    150       Phase.Schema.Validation.NoCircularFieldImports,
    151       {Phase.Schema.Validation.Result, pass: :initial},
    152       Phase.Schema.FieldImports,
    153       Phase.Schema.Validation.KnownDirectives,
    154       Phase.Document.Validation.KnownArgumentNames,
    155       {Phase.Schema.Arguments.Parse, options},
    156       Phase.Schema.Arguments.Data,
    157       Phase.Schema.Directives,
    158       Phase.Schema.Validation.DefaultEnumValuePresent,
    159       Phase.Schema.Validation.DirectivesMustBeValid,
    160       Phase.Schema.Validation.InputOutputTypesCorrectlyPlaced,
    161       Phase.Schema.Validation.InterfacesMustResolveTypes,
    162       Phase.Schema.Validation.ObjectInterfacesMustBeValid,
    163       Phase.Schema.Validation.ObjectMustImplementInterfaces,
    164       Phase.Schema.Validation.NoInterfaceCyles,
    165       Phase.Schema.Validation.QueryTypeMustBeObject,
    166       Phase.Schema.Validation.NamesMustBeValid,
    167       Phase.Schema.Validation.UniqueFieldNames,
    168       Phase.Schema.RegisterTriggers,
    169       Phase.Schema.MarkReferenced,
    170       Phase.Schema.ReformatDescriptions,
    171       # This phase is run again now after additional validations
    172       {Phase.Schema.Validation.Result, pass: :final},
    173       Phase.Schema.Build,
    174       Phase.Schema.InlineFunctions,
    175       {Phase.Schema.Compile, options}
    176     ]
    177   end
    178 
    179   @doc """
    180   Return the part of a pipeline before a specific phase.
    181 
    182   ## Examples
    183 
    184       iex> Pipeline.before([A, B, C], B)
    185       [A]
    186   """
    187   @spec before(t, phase_config_t) :: t
    188   def before(pipeline, phase) do
    189     result =
    190       List.flatten(pipeline)
    191       |> Enum.take_while(&(!match_phase?(phase, &1)))
    192 
    193     case result do
    194       ^pipeline ->
    195         raise RuntimeError, "Could not find phase #{phase}"
    196 
    197       _ ->
    198         result
    199     end
    200   end
    201 
    202   @doc """
    203   Return the part of a pipeline after (and including) a specific phase.
    204 
    205   ## Examples
    206 
    207       iex> Pipeline.from([A, B, C], B)
    208       [B, C]
    209   """
    210   @spec from(t, atom) :: t
    211   def from(pipeline, phase) do
    212     result =
    213       List.flatten(pipeline)
    214       |> Enum.drop_while(&(!match_phase?(phase, &1)))
    215 
    216     case result do
    217       [] ->
    218         raise RuntimeError, "Could not find phase #{phase}"
    219 
    220       _ ->
    221         result
    222     end
    223   end
    224 
    225   @doc """
    226   Replace a phase in a pipeline with another, supporting reusing the same
    227   options.
    228 
    229   ## Examples
    230 
    231   Replace a simple phase (without options):
    232 
    233       iex> Pipeline.replace([A, B, C], B, X)
    234       [A, X, C]
    235 
    236   Replace a phase with options, retaining them:
    237 
    238       iex> Pipeline.replace([A, {B, [name: "Thing"]}, C], B, X)
    239       [A, {X, [name: "Thing"]}, C]
    240 
    241   Replace a phase with options, overriding them:
    242 
    243       iex> Pipeline.replace([A, {B, [name: "Thing"]}, C], B, {X, [name: "Nope"]})
    244       [A, {X, [name: "Nope"]}, C]
    245 
    246   """
    247   @spec replace(t, Phase.t(), phase_config_t) :: t
    248   def replace(pipeline, phase, replacement) do
    249     Enum.map(pipeline, fn candidate ->
    250       case match_phase?(phase, candidate) do
    251         true ->
    252           case phase_invocation(candidate) do
    253             {_, []} ->
    254               replacement
    255 
    256             {_, opts} ->
    257               if is_atom(replacement) do
    258                 {replacement, opts}
    259               else
    260                 replacement
    261               end
    262           end
    263 
    264         false ->
    265           candidate
    266       end
    267     end)
    268   end
    269 
    270   # Whether a phase configuration is for a given phase
    271   @spec match_phase?(Phase.t(), phase_config_t) :: boolean
    272   defp match_phase?(phase, phase), do: true
    273   defp match_phase?(phase, {phase, _}) when is_atom(phase), do: true
    274   defp match_phase?(_, _), do: false
    275 
    276   @doc """
    277   Return the part of a pipeline up to and including a specific phase.
    278 
    279   ## Examples
    280 
    281       iex> Pipeline.upto([A, B, C], B)
    282       [A, B]
    283   """
    284   @spec upto(t, phase_config_t) :: t
    285   def upto(pipeline, phase) do
    286     beginning = before(pipeline, phase)
    287     item = get_in(pipeline, [Access.at(length(beginning))])
    288     beginning ++ [item]
    289   end
    290 
    291   @doc """
    292   Return the pipeline with the supplied phase removed.
    293 
    294   ## Examples
    295 
    296       iex> Pipeline.without([A, B, C], B)
    297       [A, C]
    298   """
    299   @spec without(t, Phase.t()) :: t
    300   def without(pipeline, phase) do
    301     pipeline
    302     |> Enum.filter(&(not match_phase?(phase, &1)))
    303   end
    304 
    305   @doc """
    306   Return the pipeline with the phase/list of phases inserted before
    307   the supplied phase.
    308 
    309   ## Examples
    310 
    311   Add one phase before another:
    312 
    313       iex> Pipeline.insert_before([A, C, D], C, B)
    314       [A, B, C, D]
    315 
    316   Add list of phase before another:
    317 
    318       iex> Pipeline.insert_before([A, D, E], D, [B, C])
    319       [A, B, C, D, E]
    320 
    321   """
    322   @spec insert_before(t, Phase.t(), phase_config_t | [phase_config_t]) :: t
    323   def insert_before(pipeline, phase, additional) do
    324     beginning = before(pipeline, phase)
    325     beginning ++ List.wrap(additional) ++ (pipeline -- beginning)
    326   end
    327 
    328   @doc """
    329   Return the pipeline with the phase/list of phases inserted after
    330   the supplied phase.
    331 
    332   ## Examples
    333 
    334   Add one phase after another:
    335 
    336       iex> Pipeline.insert_after([A, C, D], A, B)
    337       [A, B, C, D]
    338 
    339   Add list of phases after another:
    340 
    341       iex> Pipeline.insert_after([A, D, E], A, [B, C])
    342       [A, B, C, D, E]
    343 
    344   """
    345   @spec insert_after(t, Phase.t(), phase_config_t | [phase_config_t]) :: t
    346   def insert_after(pipeline, phase, additional) do
    347     beginning = upto(pipeline, phase)
    348     beginning ++ List.wrap(additional) ++ (pipeline -- beginning)
    349   end
    350 
    351   @doc """
    352   Return the pipeline with the phases matching the regex removed.
    353 
    354   ## Examples
    355 
    356       iex> Pipeline.reject([A, B, C], ~r/A|B/)
    357       [C]
    358   """
    359   @spec reject(t, Regex.t() | (module -> boolean)) :: t
    360   def reject(pipeline, %Regex{} = pattern) do
    361     reject(pipeline, fn phase ->
    362       Regex.match?(pattern, Atom.to_string(phase))
    363     end)
    364   end
    365 
    366   def reject(pipeline, fun) do
    367     Enum.reject(pipeline, fn
    368       {phase, _} -> fun.(phase)
    369       phase -> fun.(phase)
    370     end)
    371   end
    372 
    373   @spec run_phase(t, data_t, [Phase.t()]) ::
    374           {:ok, data_t, [Phase.t()]} | {:error, String.t(), [Phase.t()]}
    375   def run_phase(pipeline, input, done \\ [])
    376 
    377   def run_phase([], input, done) do
    378     {:ok, input, done}
    379   end
    380 
    381   def run_phase([phase_config | todo] = all_phases, input, done) do
    382     {phase, options} = phase_invocation(phase_config)
    383 
    384     case phase.run(input, options) do
    385       {:record_phases, result, fun} ->
    386         result = fun.(result, all_phases)
    387         run_phase(todo, result, [phase | done])
    388 
    389       {:ok, result} ->
    390         run_phase(todo, result, [phase | done])
    391 
    392       {:jump, result, destination_phase} when is_atom(destination_phase) ->
    393         run_phase(from(todo, destination_phase), result, [phase | done])
    394 
    395       {:insert, result, extra_pipeline} ->
    396         run_phase(List.wrap(extra_pipeline) ++ todo, result, [phase | done])
    397 
    398       {:swap, result, target, replacements} ->
    399         todo
    400         |> replace(target, replacements)
    401         |> run_phase(result, [phase | done])
    402 
    403       {:replace, result, final_pipeline} ->
    404         run_phase(List.wrap(final_pipeline), result, [phase | done])
    405 
    406       {:error, message} ->
    407         {:error, message, [phase | done]}
    408 
    409       _ ->
    410         {:error, "Last phase did not return a valid result tuple.", [phase | done]}
    411     end
    412   end
    413 
    414   @spec phase_invocation(phase_config_t) :: {Phase.t(), list}
    415   defp phase_invocation({phase, options}) when is_list(options) do
    416     {phase, options}
    417   end
    418 
    419   defp phase_invocation(phase) do
    420     {phase, []}
    421   end
    422 end