defmodule OpenAPI.Spec do @moduledoc """ OpenAPI description as read by the read phase Modules contained in this namespace represent portions of the OpenAPI spec. While there is some occasional processing done by the reader (for example, to add additional context to a schema while it is read), most of this data is an exact replica of what is contained in the API description. All spec modules have a function `decode/2` which is not intended to be called by client library authors, but does the work of parsing the JSON or Yaml contents. """ require Logger import OpenAPI.Reader.State alias OpenAPI.Reader.State alias OpenAPI.Spec alias OpenAPI.Spec.Components alias OpenAPI.Spec.ExternalDocumentation alias OpenAPI.Spec.Info alias OpenAPI.Spec.Path.Item alias OpenAPI.Spec.Server alias OpenAPI.Spec.Tag @typedoc "Key or index of a Yaml document" @type path_segment :: String.t() | integer @typedoc "Fully-qualified absolute file and set of path segments" @type full_path :: {String.t(), [path_segment]} @typedoc "Reference to another part of the spec. Reserved for schemas in this library." @type ref :: {:ref, full_path} # # Definition # @typedoc "Open API specification" @type t :: %__MODULE__{ openapi: String.t(), info: Info.t(), servers: [Server.t()], paths: %{optional(:string) => Item.t()}, components: Components.t(), security: [term], tags: [term], external_docs: ExternalDocumentation.t() | nil } defstruct [ :openapi, :info, :servers, :paths, :components, :security, :tags, :external_docs ] # # Decoder # @doc false @spec decode(State.t(), String.t()) :: State.t() def decode(%State{} = state, filename) do yaml = state.files[filename] spec_version = Map.fetch!(yaml, "openapi") {%State{} = state, info} = decode_info(state, yaml) {%State{} = state, servers} = decode_servers(state, yaml) {%State{} = state, components} = decode_components(state, yaml) {%State{} = state, paths} = decode_paths(state, yaml) {%State{} = state, tags} = decode_tags(state, yaml) {%State{} = state, external_docs} = decode_external_docs(state, yaml) spec = %__MODULE__{ openapi: spec_version, info: info, servers: servers, paths: paths, components: components, security: [], tags: tags, external_docs: external_docs } %State{state | spec: merge(state.spec, spec)} end @spec decode_info(State.t(), State.yaml()) :: {State.t(), Info.t()} defp decode_info(state, %{"info" => info}), do: with_path(state, info, "info", &Info.decode/2) @spec decode_servers(State.t(), State.yaml()) :: {State.t(), [Server.t()]} defp decode_servers(state, %{"servers" => servers}) when is_list(servers) do with_path(state, servers, "servers", fn state, servers -> {state, servers} = servers |> Enum.with_index() |> Enum.reduce({state, []}, fn {server, index}, {state, servers} -> {state, server} = with_path(state, server, index, &Server.decode/2) {state, [server | servers]} end) {state, Enum.reverse(servers)} end) end defp decode_servers(state, _yaml), do: {state, [%Spec.Server{url: "/"}]} @spec decode_components(State.t(), State.yaml()) :: {State.t(), Components.t()} defp decode_components(state, %{"components" => components}) do with_path(state, components, "components", &Components.decode/2) end defp decode_components(state, _yaml), do: {state, Components.decode(state, %{})} @spec decode_tags(State.t(), State.yaml()) :: {State.t(), [Tag.t()]} defp decode_tags(state, %{"tags" => tags}) do with_path(state, tags, "tags", fn state, tags -> {state, tags} = tags |> Enum.with_index() |> Enum.reduce({state, []}, fn {tag, index}, {state, tags} -> {state, tag} = with_path(state, tag, index, &Tag.decode/2) {state, [tag | tags]} end) {state, Enum.reverse(tags)} end) end defp decode_tags(state, _yaml), do: {state, []} @spec decode_external_docs(State.t(), State.yaml()) :: {State.t(), ExternalDocumentation.t()} defp decode_external_docs(%State{} = state, %{"externalDocs" => docs}) do with_path(state, docs, "externalDocs", &ExternalDocumentation.decode/2) end defp decode_external_docs(state, _docs), do: {state, nil} @spec decode_paths(State.t(), State.yaml()) :: {State.t(), %{optional(String.t()) => Item.t()}} defp decode_paths(state, %{"paths" => paths}) do with_path(state, paths, "paths", fn state, paths -> Enum.reduce(paths, {state, %{}}, fn {key, path}, {state, paths} -> {state, path} = with_path(state, path, key, &Item.decode/2) {state, Map.put(paths, key, path)} end) end) end defp decode_paths(state, _yaml) do Logger.warning("Evaluating spec with no `paths` object; this will likely result in no output") {state, %{}} end @spec merge(t | nil, t) :: t defp merge(nil, spec_two), do: spec_two defp merge(spec_one, spec_two) do %__MODULE__{ openapi: openapi_one, info: info_one, servers: servers_one, paths: paths_one, components: components_one, security: security_one, tags: tags_one, external_docs: external_docs_one } = spec_one %__MODULE__{ servers: servers_two, paths: paths_two, components: components_two, security: security_two, tags: tags_two } = spec_two %__MODULE__{ openapi: openapi_one, info: info_one, servers: Enum.uniq_by(servers_two ++ servers_one, & &1.url), paths: Map.merge(paths_one, paths_two, fn _k, item_one, item_two -> Item.merge(item_one, item_two) end), components: Components.merge(components_one, components_two), security: security_one ++ security_two, tags: Enum.uniq_by(tags_two ++ tags_one, & &1.name), external_docs: external_docs_one } end end