-
Notifications
You must be signed in to change notification settings - Fork 2
Observe changes to specs on the file system #24
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
This file was deleted.
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -28,10 +28,10 @@ defmodule Mimicry.MockServerList do | |||||
|
|
||||||
| Idempotent, this will not create a duplicate for the same combination of `title` + `version`. | ||||||
| """ | ||||||
| @spec create_server(Specification.t()) :: | ||||||
| @spec create_server(Specification.t(), String.t()) :: | ||||||
| {:ok, pid()} | {:error, :invalid_specification} | {:error, :unknown} | ||||||
| def create_server(spec) do | ||||||
| case start_mock_server(spec) do | ||||||
| def create_server(spec, file \\ "") do | ||||||
| case spec |> start_mock_server(file) do | ||||||
| {:ok, pid} -> {:ok, pid} | ||||||
| {:error, {:already_started, pid}} -> {:ok, pid} | ||||||
| {:error, :invalid_specification} -> {:error, :invalid_specification} | ||||||
|
|
@@ -99,6 +99,24 @@ defmodule Mimicry.MockServerList do | |||||
| end | ||||||
| end | ||||||
|
|
||||||
| def find_server_by_specification_path(path) do | ||||||
| children() | ||||||
| |> Enum.find(fn pid -> | ||||||
| pid |> :sys.get_state() |> Keyword.get(:path) == path | ||||||
| end) | ||||||
| |> case do | ||||||
| nil -> | ||||||
| {:error, :not_found} | ||||||
|
|
||||||
| pid -> | ||||||
| {:ok, pid} | ||||||
| end | ||||||
| end | ||||||
|
|
||||||
| def update_server_specification(pid, spec = %Specification{}) do | ||||||
| pid |> MockServer.update_server_specification(spec) | ||||||
| end | ||||||
|
|
||||||
| @doc """ | ||||||
| used to seed servers given via the spec folder upon startup | ||||||
| """ | ||||||
|
|
@@ -121,20 +139,23 @@ defmodule Mimicry.MockServerList do | |||||
| DynamicSupervisor.init(strategy: :one_for_one) | ||||||
| end | ||||||
|
|
||||||
| defp start_mock_server(_spec = %Specification{supported: false}) do | ||||||
| defp start_mock_server({_, _spec = %Specification{supported: false}, _}) do | ||||||
| {:error, :unsupported_spec} | ||||||
| end | ||||||
|
|
||||||
| defp start_mock_server(spec = %Specification{}) do | ||||||
| child_spec = spec |> MockServer.create_id() |> MockServer.child_spec(spec) | ||||||
| DynamicSupervisor.start_child(__MODULE__, child_spec) | ||||||
| rescue | ||||||
| e in RuntimeError -> | ||||||
| Logger.error(e.message, spec: spec) | ||||||
| {:error, :invalid_specification} | ||||||
| defp start_mock_server({:ok, spec = %Mimicry.OpenAPI.Specification{}, file_path}) do | ||||||
| child_spec = spec |> MockServer.create_id() |> MockServer.child_spec(spec, file_path) | ||||||
|
|
||||||
| case DynamicSupervisor.start_child(__MODULE__, child_spec) do | ||||||
| {:error, _} -> | ||||||
| :error | ||||||
|
|
||||||
| value -> | ||||||
| value | ||||||
| end | ||||||
| end | ||||||
|
|
||||||
| defp start_mock_server(_), do: {:error, :invalid_specification} | ||||||
| defp start_mock_server(_, _), do: {:error, :invalid_specification} | ||||||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Only a single triple is passed.
Suggested change
|
||||||
|
|
||||||
| defp children do | ||||||
| DynamicSupervisor.which_children(__MODULE__) | ||||||
|
|
@@ -146,7 +167,7 @@ defmodule Mimicry.MockServerList do | |||||
| pid |> :sys.get_state() |> Keyword.take([:id, :entities, :spec]) |> Enum.into(%{}) | ||||||
| end | ||||||
|
|
||||||
| defp do_load_specification_on_startup(true) do | ||||||
| defp do_load_specification_on_startup(enabled?) when enabled? == true do | ||||||
| SpecFolder.load_all() | ||||||
| |> Enum.each(&start_mock_server/1) | ||||||
| end | ||||||
|
|
||||||
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -0,0 +1,91 @@ | ||||||
| defmodule Mimicry.Utils.SpecificationFileObserver do | ||||||
| @moduledoc """ | ||||||
| Listens to changes in the folder defined via `Mimicry.Utils.SpecificationFolder.base_path/0` and reloads | ||||||
| the specifications currently running in mimicry | ||||||
| """ | ||||||
|
|
||||||
| alias Mimicry.MockServerList | ||||||
| alias Mimicry.OpenAPI.Parser | ||||||
| alias Mimicry.Utils.{SpecificationFileReader, SpecificationFolder} | ||||||
|
|
||||||
| require Logger | ||||||
|
|
||||||
| use GenServer | ||||||
|
|
||||||
| def start_link(opts \\ %{}) do | ||||||
| GenServer.start_link(__MODULE__, opts, []) | ||||||
| end | ||||||
|
|
||||||
| @impl true | ||||||
| def init(_args) do | ||||||
| {:ok, watcher_pid} = FileSystem.start_link(dirs: [SpecificationFolder.base_path()]) | ||||||
| watcher_pid |> FileSystem.subscribe() | ||||||
| {:ok, %{watcher_pid: watcher_pid}} | ||||||
| end | ||||||
|
|
||||||
| @impl true | ||||||
| def handle_info({:file_event, _watcher_pid, {path, events}}, state) do | ||||||
| Logger.metadata(path: path, events: events) | ||||||
|
|
||||||
| case SpecificationFileReader.extension(path) do | ||||||
| extension when extension in [:yaml, :json] -> | ||||||
| path |> handle_file_event(events) | ||||||
| {:noreply, state} | ||||||
|
|
||||||
| :unsupported -> | ||||||
| {:noreply, state} | ||||||
| end | ||||||
| end | ||||||
|
|
||||||
| @impl true | ||||||
| def handle_info({:file_event, _watcher_pid, :stop}, state) do | ||||||
| {:noreply, state} | ||||||
| end | ||||||
|
|
||||||
| defp handle_file_event(path, [:moved_from]) do | ||||||
| case path |> Path.basename() |> MockServerList.find_server_by_specification_path() do | ||||||
| {:ok, pid} -> | ||||||
| Logger.info("Removing") | ||||||
| pid |> MockServerList.delete_server() | ||||||
|
|
||||||
| _ -> | ||||||
| nil | ||||||
| end | ||||||
| end | ||||||
|
Comment on lines
+45
to
+54
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There is another events list matching deletion: I temporarily solved it with: defp handle_file_event(path, [:moved_from]) do
maybe_delete_server(path)
end
defp handle_file_event(path, [:deleted]) do
maybe_delete_server(path)
end
# snip
defp maybe_delete_server(path) do
case path |> Path.basename() |> MockServerList.find_server_by_specification_path() do
{:ok, pid} ->
Logger.info("Removing")
pid |> MockServerList.delete_server()
_ ->
nil
end
end |
||||||
|
|
||||||
| defp handle_file_event(path, [:modified, :closed]) do | ||||||
| path | ||||||
| |> parse() | ||||||
| |> upsert(path) | ||||||
| end | ||||||
|
|
||||||
| defp handle_file_event(path, [:moved_to]) do | ||||||
| path | ||||||
| |> parse() | ||||||
| |> upsert(path) | ||||||
| end | ||||||
|
|
||||||
| defp handle_file_event(_path, _events) do | ||||||
| Logger.info("Unknown") | ||||||
| end | ||||||
|
|
||||||
| defp parse(path) do | ||||||
| {:ok, content, ext} = path |> SpecificationFileReader.read() | ||||||
| Parser.parse(content, ext) | ||||||
| end | ||||||
|
|
||||||
| defp upsert(spec, path) do | ||||||
| case path | ||||||
| |> Path.basename() | ||||||
| |> MockServerList.find_server_by_specification_path() do | ||||||
| {:ok, pid} -> | ||||||
| Logger.info("Reloading...") | ||||||
|
|
||||||
| pid |> MockServerList.update_server_specification(spec) | ||||||
|
|
||||||
| _ -> | ||||||
| Logger.info("Creating...") | ||||||
| spec |> MockServerList.create_server(path) | ||||||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This solves the creation and deletion on the fly, otherwise a server cannot be removed as paths won't match anymore:
Suggested change
Since |
||||||
| end | ||||||
| end | ||||||
| end | ||||||
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -11,8 +11,10 @@ defmodule Mimicry.Utils.SpecificationFolder do | |||||
| alias Mimicry.OpenAPI.{Parser, Specification} | ||||||
|
|
||||||
| @doc """ | ||||||
| will attempt to load all specifications in the configured folder | ||||||
| Will attempt to load all specifications in the configured folder | ||||||
| while preserving paths with the specifications | ||||||
| """ | ||||||
| @spec load_all() :: list() | ||||||
| def load_all do | ||||||
| if base_path() |> File.dir?() do | ||||||
| load_all_deduplicated() | ||||||
|
|
@@ -37,7 +39,7 @@ defmodule Mimicry.Utils.SpecificationFolder do | |||||
| |> Enum.map(&Path.basename/1) | ||||||
| |> Enum.map(&Task.async(fn -> load(&1) end)) | ||||||
| |> Task.await_many() | ||||||
| |> Enum.filter(fn val -> val != :error end) | ||||||
| |> Enum.filter(fn {status, _, _} -> status != :error end) | ||||||
| |> deduplicate() | ||||||
| end | ||||||
|
|
||||||
|
|
@@ -57,11 +59,12 @@ defmodule Mimicry.Utils.SpecificationFolder do | |||||
| defp load(path) do | ||||||
| case load_file(path) do | ||||||
| {content, ext} -> | ||||||
| Parser.parse(content, ext) | ||||||
| specification = Parser.parse(content, ext) | ||||||
| {:ok, specification, path} | ||||||
|
|
||||||
| nil -> | ||||||
| Logger.warn("Found invalid specification in Specification folder: #{path}") | ||||||
| :error | ||||||
| {:error, nil, path} | ||||||
| end | ||||||
| end | ||||||
|
|
||||||
|
|
@@ -73,7 +76,7 @@ defmodule Mimicry.Utils.SpecificationFolder do | |||||
| |> Enum.uniq_by(&duplicate_condition/1) | ||||||
| end | ||||||
|
|
||||||
| defp duplicate_condition(%Specification{title: title, version: version}) do | ||||||
| defp duplicate_condition({%Specification{title: title, version: version}, _}) do | ||||||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. First item of the triple is the status; function will not match otherwise
Suggested change
|
||||||
| "#{title}-#{version}" | ||||||
| end | ||||||
|
|
||||||
|
|
||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Caller must adhere to expected argument format (single triple, no 2 args)