Current section

Files

Jump to
oaskit lib oaskit internal spec_builder.ex
Raw

lib/oaskit/internal/spec_builder.ex

defmodule Oaskit.Internal.SpecBuilder do
alias JSV.Builder
alias JSV.Cast
alias JSV.Key
alias JSV.Ref
alias JSV.RNS
alias Oaskit.Spec.Components
alias Oaskit.Spec.Header
alias Oaskit.Spec.Operation
alias Oaskit.Spec.Parameter
alias Oaskit.Spec.PathItem
alias Oaskit.Spec.Reference
alias Oaskit.Spec.RequestBody
alias Oaskit.Spec.Response
@moduledoc false
def build_operations(normal_spec, opts) when is_map(opts) do
spec = Oaskit.SpecValidator.validate!(normal_spec)
to_build = stream_operations(spec)
jsv_ctx = JSV.build_init!(opts.jsv_opts)
{_root_ns, _, jsv_ctx} = JSV.build_add!(jsv_ctx, normal_spec)
global_security = spec.security
{validations_by_op_id, jsv_ctx} =
Enum.map_reduce(to_build, jsv_ctx, fn {rev_path, op_id, op, pathitem_parameters}, jsv_ctx ->
{validation, jsv_ctx} =
build_op_validation(rev_path, op, pathitem_parameters, spec, jsv_ctx, opts)
security = build_op_security(rev_path, op, global_security)
extensions = build_op_extensions(rev_path, op, opts)
{{op_id, %{security: security, validation: validation, extensions: extensions}}, jsv_ctx}
end)
jsv_root = JSV.to_root!(jsv_ctx, :root)
{to_ops_map(validations_by_op_id), jsv_root}
end
defp stream_operations(spec) do
Stream.flat_map(spec.paths, fn {path, path_item} ->
path_item
|> deref(PathItem, [path, "paths"], spec)
|> then(fn {path_item, rev_path} ->
pathitem_parameters = pathitem_parameters(path_item, rev_path, spec)
buildable_operations(path_item, rev_path, pathitem_parameters)
end)
end)
end
defp buildable_operations(path_item, rev_path, pathitem_parameters) do
Stream.map(path_item, fn {verb, operation} ->
%Operation{operationId: operation_id} = operation
{[verb | rev_path], operation_id, operation, pathitem_parameters}
end)
end
defp deref(%Reference{"$ref": "#/" <> bin_path = full_path}, expected, _rev_path, spec) do
{object, rev_path} = resolve_ref(spec, String.split(bin_path, "/"))
case object do
%^expected{} = found ->
{found, rev_path}
other ->
raise "could not dereference #{inspect(full_path)} (using #{inspect(:lists.reverse(rev_path))}), " <>
"expected struct #{inspect(expected)}, found #{inspect(other)}"
end
rescue
_ in KeyError ->
reraise ArgumentError, "could not find reference #{full_path}", __STACKTRACE__
end
defp deref(%mod{} = object, mod, rev_path, _spec) do
{object, rev_path}
end
defp deref(nil, _, rev_path, _spec) do
{nil, rev_path}
end
defp resolve_ref(data, path, rev_path_acc \\ [])
defp resolve_ref(%Components{} = object, [h | t], acc) do
h = String.to_existing_atom(h)
subs =
case Map.fetch!(object, h) do
nil -> %{}
%{} = map -> map
end
resolve_ref(subs, t, [h | acc])
end
defp resolve_ref(%_struct{} = object, [h | t], acc) do
# When resolving a reference into the spec, if the spec is a struct then we
# cast the path segment to an atom. Otherwise we keep it as string.
h = String.to_existing_atom(h)
resolve_ref(Map.fetch!(object, h), t, [h | acc])
end
defp resolve_ref(%{} = object, [h | t], acc) do
resolve_ref(Map.fetch!(object, h), t, [h | acc])
end
defp resolve_ref(%{} = maybe_struct, [], acc) do
{maybe_struct, acc}
end
defp pathitem_parameters(path_item, rev_path, spec) do
case path_item.parameters do
nil ->
[]
[] ->
[]
ps ->
ps
|> Enum.with_index()
|> Enum.map(fn {p, index} ->
{_parameter, _rev_path} = deref(p, Parameter, [{:index, index} | rev_path], spec)
end)
end
end
defp to_ops_map(ops_list) do
Enum.reduce(ops_list, %{}, fn
{op_id, _}, acc when is_map_key(acc, op_id) ->
raise ArgumentError, "duplicate operation id #{inspect(op_id)}"
{op_id, op_spec}, acc ->
Map.put(acc, op_id, op_spec)
end)
end
# -- Validation -------------------------------------------------------------
defp build_op_validation(rev_path, op, pathitem_parameters, spec, jsv_ctx, opts) do
validations = []
# Body
{validations, jsv_ctx} =
case build_body_validation(op.requestBody, ["requestBody" | rev_path], spec, jsv_ctx) do
{:no_validation, jsv_ctx} ->
{validations, jsv_ctx}
{required?, body_validations, jsv_ctx} ->
{[{:body, required?, body_validations} | validations], jsv_ctx}
end
# Parameters
{validations, jsv_ctx} =
case build_parameters_validation(
op.parameters,
pathitem_parameters,
rev_path,
spec,
jsv_ctx
) do
{[], jsv_ctx} ->
{validations, jsv_ctx}
{parameters_by_location, jsv_ctx} ->
{[{:parameters, parameters_by_location} | validations], jsv_ctx}
end
# Responses
{validations, jsv_ctx} =
if opts.responses do
{resp_validations, jsv_ctx} =
build_responses_validations(op.responses, rev_path, spec, jsv_ctx)
{[{:responses, resp_validations} | validations], jsv_ctx}
else
{validations, jsv_ctx}
end
{validations, jsv_ctx}
end
# -- Body Validation --------------------------------------------------------
defp build_body_validation(%RequestBody{} = req_body, rev_path, _spec, jsv_ctx)
when is_map(req_body) do
{matchers, jsv_ctx} =
req_body.content
|> sorted_media_type_clauses()
|> Enum.map_reduce(jsv_ctx, fn
{original_media_type, media_matcher, media_spec}, jsv_ctx ->
case media_spec do
%{schema: true} ->
{{media_matcher, :no_validation}, jsv_ctx}
%{schema: nil} ->
{{media_matcher, :no_validation}, jsv_ctx}
%{schema: _schema} ->
{schema, jsv_ctx} =
build_schema_key(
["schema", original_media_type, "content" | rev_path],
jsv_ctx
)
{{media_matcher, schema}, jsv_ctx}
_ ->
{{media_matcher, :no_validation}, jsv_ctx}
end
end)
{req_body.required, matchers, jsv_ctx}
end
defp build_body_validation(%Reference{} = ref, rev_path, spec, jsv_ctx) do
{request_body, body_rev_path} = deref(ref, RequestBody, rev_path, spec)
build_body_validation(request_body, body_rev_path, spec, jsv_ctx)
end
defp build_body_validation(nil, _rev_path, _spec, jsv_ctx) do
{:no_validation, jsv_ctx}
end
defp sorted_media_type_clauses(content_map) do
content_map
|> Enum.map(fn {media_type, media_spec} ->
matcher = media_type_to_matcher(media_type)
{media_type, matcher, media_spec}
end)
|> sort_media_type_clauses()
end
defp media_type_to_matcher(media_type) when is_binary(media_type) do
case Plug.Conn.Utils.media_type(media_type) do
:error -> {media_type, ""}
{:ok, primary, secondary, _} -> {primary, secondary}
end
end
defp sort_media_type_clauses(list) do
Enum.sort_by(list, fn {_, {primary, secondary}, _} ->
{media_priority(primary), media_priority(secondary), primary, secondary}
end)
end
defp media_priority(type) do
case type do
"*" -> 2
"" -> 1
t when is_binary(t) -> 0
end
end
# -- Parameters Validation --------------------------------------------------
defp build_parameters_validation([], [], _rev_path, _spec, jsv_ctx) do
{[], jsv_ctx}
end
defp build_parameters_validation(nil, [], _rev_path, _spec, jsv_ctx) do
{[], jsv_ctx}
end
# _wrev means "with rev path"
defp build_parameters_validation(parameters, pathitem_parameters_wrev, rev_path, spec, jsv_ctx) do
parameters_wrev =
parameters
|> Enum.with_index()
|> Enum.map(fn {p_or_ref, index} ->
deref(p_or_ref, Parameter, [{:index, index}, "parameters" | rev_path], spec)
end)
# We need to keep only pathitem parameters that are not overriden by the
# operation.
defined_by_op =
Map.new(parameters_wrev, fn {%{name: name, in: loc}, _} -> {{name, loc}, true} end)
pathitem_parameters_wrev =
Enum.filter(pathitem_parameters_wrev, fn {%{name: name, in: loc}, _} ->
not Map.has_key?(defined_by_op, {name, loc})
end)
all_parameters_wrev = pathitem_parameters_wrev ++ parameters_wrev
{built_params, jsv_ctx} =
Enum.flat_map_reduce(
all_parameters_wrev,
jsv_ctx,
fn
{%Parameter{in: p_in} = parameter, rev_path}, jsv_ctx
when p_in in [:path, :query, :header] ->
{built_param, jsv_ctx} =
build_parameter_validation(parameter, rev_path, jsv_ctx)
{[built_param], jsv_ctx}
{%Parameter{}, _}, jsv_ctx ->
{[], jsv_ctx}
end
)
built_params =
built_params
|> Enum.group_by(& &1.in)
|> Enum.into(%{path: [], query: [], header: []})
{built_params, jsv_ctx}
end
# sobelow_skip ["DOS.StringToAtom"]
defp build_parameter_validation(parameter, rev_path, jsv_ctx) do
parameter = Parameter.with_defaults(parameter)
# get a simplified version of the schema (we are interested in the type for
# casting the path/query-string parameters that are always a string, and the
# array type so we must handle the explode option).
#
# We are not building the schema in JSV as it will automatically be resolved
# from the current path (the JSV context has the full normalized spec). But
# we still carry the context: since we will resolve some references, when
# calling the JSV built it will benefit from its cache of references.
{schema_summary, jsv_ctx} = summarize_parameter_schema(parameter.schema, :root, jsv_ctx)
precast = build_parameter_precast(parameter, schema_summary)
# We will strip the brackets from the name if present, as phoenix casts
# `?a[]=1&a[]=2` as `%{"a"=>[1,2]}` and not `%{"a[]"=>[1,2]}`.
#
# We should do that for parameters that are arrays with explode:true but we
# cannot make sure that we expect an array. The schema could be a reference
# to an anyOf of references to an if/else, etc. Too many cases. So we will
# always strip the brackets.
bin_key = strip_brackets_suffix(parameter.name)
# On the other hand we will report errors with the brackets enforced only if
# we detected an array type.
ext_key = ensure_brackets_suffix(parameter, schema_summary)
key = String.to_atom(bin_key)
{schema_key, jsv_ctx} =
case parameter do
%{schema: true} -> {:no_validation, jsv_ctx}
%{schema: nil} -> {:no_validation, jsv_ctx}
%{schema: _schema} -> build_schema_key(["schema" | rev_path], jsv_ctx)
end
built = %{
bin_key: bin_key,
ext_key: ext_key,
key: key,
required: parameter_required(parameter),
precast: precast,
schema_key: schema_key,
in: parameter.in
}
{built, jsv_ctx}
end
defp strip_brackets_suffix(name) do
case String.ends_with?(name, "[]") do
true -> String.slice(name, 0..-3//1)
false -> name
end
end
defp ensure_brackets_suffix(%{explode: true, name: name}, {:array, _}) do
case String.ends_with?(name, "[]") do
true -> name
false -> name <> "[]"
end
end
defp ensure_brackets_suffix(%{name: name}, _) do
name
end
defp parameter_required(%Parameter{required: req?}) when is_boolean(req?) do
req?
end
defp parameter_required(%Parameter{in: p_in}) do
case p_in do
:path -> true
:query -> false
end
end
# credo:disable-for-next-line Credo.Check.Refactor.CyclomaticComplexity
defp summarize_parameter_schema(schema, ns, jsv_ctx) do
case schema do
true ->
{:noprecast_schema_type, jsv_ctx}
false ->
{:noprecast_schema_type, jsv_ctx}
nil ->
{:noprecast_schema_type, jsv_ctx}
%{"type" => "integer"} ->
{:integer, jsv_ctx}
%{"type" => "boolean"} ->
{:boolean, jsv_ctx}
%{"type" => "number"} ->
{:number, jsv_ctx}
%{"type" => "string"} ->
{:string, jsv_ctx}
%{"type" => "array", "items" => %{"type" => "integer"}} ->
{{:array, :integer}, jsv_ctx}
%{"type" => "array", "items" => %{"type" => "boolean"}} ->
{{:array, :boolean}, jsv_ctx}
%{"type" => "array", "items" => %{"type" => "number"}} ->
{{:array, :number}, jsv_ctx}
%{"type" => "array", "items" => %{"type" => "string"}} ->
{{:array, :string}, jsv_ctx}
%{"type" => "array", "items" => %{"$ref" => _} = item_ref} ->
case summarize_parameter_schema_ref(item_ref, ns, jsv_ctx) do
{type, jsv_ctx} when type in [:integer, :boolean, :number, :string] ->
{{:array, type}, jsv_ctx}
_ ->
{:noprecast_schema_type, jsv_ctx}
end
%{"type" => "object", "properties" => props} when is_map(props) ->
{prop_casters, jsv_ctx} = object_property_casters(props, ns, jsv_ctx)
{{:object, prop_casters}, jsv_ctx}
%{"type" => "object"} ->
# An object without declared scalar properties: we can still parse the
# raw value into a map, there is just nothing to cast.
{{:object, %{}}, jsv_ctx}
%{"$ref" => _} ->
summarize_parameter_schema_ref(schema, ns, jsv_ctx)
_too_hard_for_best_effort ->
{:noprecast_schema_type, jsv_ctx}
end
end
defp object_property_casters(properties, ns, jsv_ctx) do
Enum.reduce(properties, {%{}, jsv_ctx}, fn {k, v}, {acc, jsv_ctx} ->
{sub_summary, jsv_ctx} = summarize_parameter_schema(v, ns, jsv_ctx)
case sub_summary do
type when type in [:integer, :number, :boolean] ->
{Map.put(acc, to_string(k), scalar_caster(type)), jsv_ctx}
# String properties need no cast. Arrays/objects/unknown cannot be
# expressed by these object serializations, so they are left as-is.
_other ->
{acc, jsv_ctx}
end
end)
end
# If we want to support fetching parameters from refs we need to do it the
# right way, _i.e_ JSV having support for fetching a ref recursively with NS
# management, until we find a %{"type" => _} or no "$ref" anymore.
#
# The code below works but it does belong to JSV. For now this library does
# not support precasting complex parameters and the users should expect a
# string and do complex stuff on their side.
#
# I'm leaving the code as-is but instead of raising or warning we will just
# silently ignore pre casting.
#
# Also JSV needs to expose the build context as a struct, currently we are
# digging into a private record.
defp summarize_parameter_schema_ref(%{"$ref" => ref} = subschema, ns, jsv_ctx) do
{:ok, sub_ns} =
case subschema do
%{"$id" => id} -> RNS.derive(ns, id)
_ -> {:ok, ns}
end
ref = Ref.parse!(ref, sub_ns)
{:build, builder, jsv_validators} = jsv_ctx
new_builder = Builder.ensure_resolved!(builder, ref)
key = Key.of(ref)
resolved = Builder.fetch_resolved!(builder, key)
{resolved.raw, sub_ns, {:build, new_builder, jsv_validators}}
rescue
# bail and let users handle more complex spec structure
_ -> {:noprecast_schema_type, jsv_ctx}
else
{new_schema, new_ns, new_jsv_ctx} ->
# Recursion here
summarize_parameter_schema(new_schema, new_ns, new_jsv_ctx)
end
# Builds the list of precast steps applied to a raw parameter value before JSON
# Schema validation. Each clause matches a single (parameter, summary)
# combination - or a group of them - and returns the full list of steps
# directly. Returns nil when nothing applies: the raw value is then passed
# as-is and the schema decides whether to accept it.
# -- Scalars. The value is a single string regardless of location/style/explode.
defp build_parameter_precast(_parameter, :integer) do
[&Cast.string_to_integer/1]
end
defp build_parameter_precast(_parameter, :number) do
[&Cast.string_to_number/1]
end
defp build_parameter_precast(_parameter, :boolean) do
[&Cast.string_to_boolean/1]
end
defp build_parameter_precast(_parameter, :string) do
[]
end
# -- Path and header. Only the `simple` style is valid here. For arrays
# `explode` does not change the serialization (always comma separated); for
# objects it does: "r,100,g,200" (false) vs "r=100,g=200" (true).
defp build_parameter_precast(%{in: loc, style: :simple}, {:array, type})
when loc in [:path, :header] do
[{:split, ","} | array_element_casts(type)]
end
defp build_parameter_precast(%{in: loc, style: :simple, explode: false}, {:object, props})
when loc in [:path, :header] do
[{:object_flat, ","}, {:object_cast, props}]
end
defp build_parameter_precast(%{in: loc, style: :simple, explode: true}, {:object, props})
when loc in [:path, :header] do
[{:object_kv, ","}, {:object_cast, props}]
end
# -- Query, exploded. Phoenix already split repeated/bracketed params into
# lists (arrays) and nested maps (deepObject), so there is no string to split.
defp build_parameter_precast(%{in: :query, style: :deepObject}, {:object, props}) do
[{:object_cast, props}]
end
defp build_parameter_precast(%{in: :query, explode: true}, {:array, type}) do
array_element_casts(type)
end
# -- Query, non-exploded. The value is a single delimited string.
defp build_parameter_precast(%{in: :query, explode: false, style: :form}, {:array, type}) do
[{:split, ","} | array_element_casts(type)]
end
defp build_parameter_precast(%{in: :query, explode: false, style: :form}, {:object, props}) do
[{:object_flat, ","}, {:object_cast, props}]
end
defp build_parameter_precast(
%{in: :query, explode: false, style: :spaceDelimited},
{:array, type}
) do
[{:split, " "} | array_element_casts(type)]
end
defp build_parameter_precast(
%{in: :query, explode: false, style: :pipeDelimited},
{:array, type}
) do
[{:split, "|"} | array_element_casts(type)]
end
# -- Everything else: unsupported serialization (e.g. exploded form objects),
# :noprecast_schema_type, cookie, ... Let the user handle the raw value.
defp build_parameter_precast(parameter, _summary) do
# Uncomment to be alerted, when developing, about parameter combinations that
# are not pre-cast. This is expected for some valid combinations, so we
# return nil and let the user handle the raw value.
_ = parameter
# raise_unsupported_parameter(parameter)
nil
end
@doc false
@spec raise_unsupported_parameter(term) :: no_return
def raise_unsupported_parameter(parameter) do
debug_opts =
parameter
|> Map.take([:in, :explode, :style])
|> Map.to_list()
|> inspect()
raise ArgumentError,
"unsupported combination of parameter options for parameter " <>
"#{inspect(parameter.name)}, got: #{debug_opts} " <>
"(using default values)"
end
defp array_element_casts(summary) do
case summary do
:integer -> [{:array, &Cast.string_to_integer/1}]
:number -> [{:array, &Cast.string_to_number/1}]
:boolean -> [{:array, &Cast.string_to_boolean/1}]
:string -> []
end
end
defp scalar_caster(summary) do
case summary do
:integer -> &Cast.string_to_integer/1
:number -> &Cast.string_to_number/1
:boolean -> &Cast.string_to_boolean/1
end
end
# -- Responses Validation ---------------------------------------------------
defp build_responses_validations(responses, rev_path, spec, jsv_ctx) do
{validations, jsv_ctx} =
Enum.map_reduce(responses, jsv_ctx, fn {code_str, resp_or_ref}, jsv_ctx ->
{response, rev_path} =
deref(resp_or_ref, Response, [code_str, "responses" | rev_path], spec)
code = cast_response_code(code_str)
{resp_validation, jsv_ctx} = build_response_validation(response, rev_path, spec, jsv_ctx)
{{code, resp_validation}, jsv_ctx}
end)
{Map.new(validations), jsv_ctx}
end
defp cast_response_code("default") do
:default
end
defp cast_response_code(code) do
String.to_integer(code)
end
# A response validation carries both the body content matchers and the header
# validations. Either part can be empty/:no_validation independently.
defp build_response_validation(response, rev_path, spec, jsv_ctx) do
{body, jsv_ctx} = build_response_body_validation(response, rev_path, jsv_ctx)
{headers, jsv_ctx} = build_response_headers_validations(response, rev_path, spec, jsv_ctx)
{%{body: body, headers: headers}, jsv_ctx}
end
defp build_response_body_validation(%{content: nil}, _rev_path, jsv_ctx) do
{:no_validation, jsv_ctx}
end
defp build_response_body_validation(response, rev_path, jsv_ctx) do
{matchers, jsv_ctx} =
response.content
|> sorted_media_type_clauses()
|> Enum.map_reduce(jsv_ctx, fn
{original_media_type, media_matcher, media_spec}, jsv_ctx ->
case media_spec do
%{schema: true} ->
{{media_matcher, :no_validation}, jsv_ctx}
%{schema: nil} ->
{{media_matcher, :no_validation}, jsv_ctx}
%{schema: _schema} ->
{schema, jsv_ctx} =
build_schema_key(
["schema", original_media_type, "content" | rev_path],
jsv_ctx
)
{{media_matcher, schema}, jsv_ctx}
_ ->
{{media_matcher, :no_validation}, jsv_ctx}
end
end)
{matchers, jsv_ctx}
end
defp build_response_headers_validations(%{headers: headers}, rev_path, spec, jsv_ctx)
when is_map(headers) do
headers
# The "Content-Type" response header is described by the response content,
# not by a header definition. OpenAPI mandates that such an entry be ignored.
|> Enum.reject(fn {name, _} -> String.downcase(name) == "content-type" end)
|> Enum.map_reduce(jsv_ctx, fn {name, header_or_ref}, jsv_ctx ->
{header, rev_path} = deref(header_or_ref, Header, [name, "headers" | rev_path], spec)
build_response_header_validation(name, header, rev_path, jsv_ctx)
end)
end
defp build_response_headers_validations(_response, _rev_path, _spec, jsv_ctx) do
{[], jsv_ctx}
end
defp build_response_header_validation(name, header, rev_path, jsv_ctx) do
# Response headers only support the `simple` style. We reuse the parameter
# precast machinery by presenting the header as a simple-style header
# parameter.
synthetic = %{
in: :header,
style: header.style || :simple,
explode: header.explode || false,
name: name
}
{summary, jsv_ctx} = summarize_parameter_schema(header.schema, :root, jsv_ctx)
precast = build_parameter_precast(synthetic, summary)
{schema_key, jsv_ctx} =
case header do
%{schema: true} -> {:no_validation, jsv_ctx}
%{schema: nil} -> {:no_validation, jsv_ctx}
%{schema: _schema} -> build_schema_key(["schema" | rev_path], jsv_ctx)
end
built = %{
# conn.resp_headers keys are lowercase, so we match on the downcased name.
name: String.downcase(name),
ext_name: name,
required: header_required(header),
precast: precast,
schema_key: schema_key
}
{built, jsv_ctx}
end
defp header_required(%{required: req}) when is_boolean(req) do
req
end
defp header_required(_header) do
false
end
# -- Security ---------------------------------------------------------------
defp build_op_security(_rev_path, op, global_security) do
case op.security do
nil -> global_security
defined -> defined
end
end
# -- Extensions -------------------------------------------------------------
defp build_op_extensions(_rev_path, op, opts) when map_size(op.extensions) > 0 do
mod = opts[:spec_module]
op.extensions
|> Enum.flat_map(fn pair ->
case mod.load_extension(pair) do
{k, v} -> [{k, v}]
nil -> []
other -> exit({:bad_return_value, other})
end
end)
|> Map.new()
end
defp build_op_extensions(_rev_path, _op, _) do
%{}
end
# -- Helpers ----------------------------------------------------------------
defp build_schema_key(rev_path, jsv_ctx) do
ref = rev_path_to_ref(rev_path, [])
{_schema_key, _jsv_ctx} = JSV.build_key!(jsv_ctx, ref)
end
# Reverse the reversed path and ensure all keys are strings. This is
# especially useful when dealing with references to the responses, as they are
# under a numeric string key (like "200", "404", etc.).
#
# Parameters are true indexes, we have a special tuple for those.
defp rev_path_to_ref([{:index, i} | t], acc) when is_integer(i) do
rev_path_to_ref(t, [i | acc])
end
defp rev_path_to_ref([h | t], acc) when is_binary(h) do
rev_path_to_ref(t, [h | acc])
end
defp rev_path_to_ref([h | t], acc) when is_atom(h) do
rev_path_to_ref(t, [Atom.to_string(h) | acc])
end
defp rev_path_to_ref([], acc) do
Ref.pointer!(acc, :root)
end
end