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