From 7c1d8ba662523a10ac1708b3428502c38a97e9d1 Mon Sep 17 00:00:00 2001 From: shahryar tavakkoli Date: Wed, 23 Mar 2022 23:06:29 +0430 Subject: [PATCH] add english version of readme --- README.md | 139 ++++++++++++++++-- .../config/config.exs | 4 + .../mishke_installer_developer/application.ex | 19 +-- .../plugin_manager/event/hook.ex | 2 +- .../event/reference/on_user_before_login.ex | 23 +++ .../event/reference/on_user_before_save.ex | 22 --- .../endpoint.ex | 2 +- .../live/live_test_page_one.ex | 5 +- .../templates/layout/root.html.heex | 1 + .../page/live_test_page_one.html.heex | 5 + .../mishka_installer_db.gen.migration.ex | 6 +- .../priv/activity_migration.exs.eex | 18 +++ .../priv/plugin_migration.exs.eex | 24 +++ 13 files changed, 223 insertions(+), 47 deletions(-) create mode 100644 developer/mishke_installer_developer/lib/mishke_installer_developer/installer_lib/plugin_manager/event/reference/on_user_before_login.ex rename developer/mishke_installer_developer/lib/{mishke_installer_developer/installer_lib/mix => }/tasks/mishka_installer_db.gen.migration.ex (95%) create mode 100644 developer/mishke_installer_developer/priv/activity_migration.exs.eex create mode 100644 developer/mishke_installer_developer/priv/plugin_migration.exs.eex diff --git a/README.md b/README.md index ab64656..a1dda5e 100644 --- a/README.md +++ b/README.md @@ -1,21 +1,142 @@ -# MishkaInstaller +# Elixir programming language plugin management system -**TODO: Add description** +## Build purpose +--- -## Installation +Imagine you are going to make an application that will have many plugins built for it in the future. But the fact that many manipulations will be made on your source code makes it difficult to maintain the application. For example, you present a content management system for your users, and now they need to activate a section for registration and SMS; the system allows you to present your desired input/output absolutely plugin oriented to your users and makes it possible for the developers to write their required applications beyond the core source code. +> We have used this library in the [Mishka content management system](https://github.com/mishka-group/mishka-cms). -If [available in Hex](https://hex.pm/docs/publish), the package can be installed -by adding `mishka_installer` to your list of dependencies in `mix.exs`: +## Plugin management system implementation theory +--- +The library categorizes your whole software design structure into many parts; and has an appropriate dependency that is optional with `Genserver`; it considers a monitoring branch for each of your plugins, which results in fewer errors and `downtime`. The considered part: + +1. Behaviors and events +2. Recalling or `Hook` with priority +3. `State` management and links to the database (`PostgreSQL` support) + +Except from the 1st item, which can be redefined based on the developer's needs in his/her personal systems, the remaining items are almost constant, and a lot of functions will be handed to the developer to manage each plugin. + +## Behaviors and events +--- +In this section, you can define a series of events for each `event`, for example: after `successful registration` or `unsuccessful purchase` from “the store”, and for each `event`, put a set of `callbacks` in one module. After completing this step, when the user wants to create his own plugin, the `@behaviour` module will call you in its action module. +This helps you have a regular and error-free system, and the library uses an almost integrated structure in all of its events. + +## `Hook` with priority +--- +In Mishka Elixir Plugin Management Library, a series of action or `hook` functions are given to the developer of the main plugin or software, which helps build plugins outside the system and convert software sections into separate `events`. Some of the functions of this module include the following: + +1. Registering a plugin outside of the system in database and ram `state` +2. Removing plugin from database and `state` +3. Restoring plugin +4. Successful pause of plugin +5. `Hook` plugin +6. Search among the `events` + +And other functions that help both the mother software become an event-driven system and the developer can build the desired plugin or extension for different parts of the software and install it on the system as a separate package. This package can also be published in `hex`. + +## State management and links to the database supporting `PostgreSQL` +--- + +The `Hook` module manages a large part of this part, and the developer of the external plugin usually does not need it much. Still, this part creates a `state` on RAM for each plugin that is activated in a specific event and a dynamic supervisor for it. This allows us in case of an error in each plugin; the other plugins in the different events face no errors, and the system will try to restart with various strategies. +It should be noted for more stability and data storage after registering a plugin in the system; This section also maintains a backup copy of the database and strategies for recall in the event in case of an error. But to speed up the calling of each plugin, the website always uses `state`. + +## Installing the library: +--- +It should be noted that this library must be installed in two parts of the plugin and the software that wants to display the plugins, and due to its small dependencies, it does not cause any problems. To install, just add this library to your "mix.exs" in the "deps" function as follows: ```elixir def deps do [ - {:mishka_installer, "~> 0.1.0"} + {:mishka_installer, "~> 0.0.1"} ] end ``` -Documentation can be generated with [ExDoc](https://github.com/elixir-lang/ex_doc) -and published on [HexDocs](https://hexdocs.pm). Once published, the docs can -be found at [https://hexdocs.pm/mishka_installer](https://hexdocs.pm/mishka_installer). +## Using the library: +--- + +After installing this library, you must first install the required database of this package on your website, for which a `mix task` has been created, which is enough to load it once in your terminal, in the project path before the start. + +```elixir +mix mishka_installer.db.gen.migration +``` + +After implementing the above sections, you must first implement events in your main software and place the `call` function from the `Hook` module there to call all the plugins activated in the event you want based on priority. And give the `state` you want, to these plugins in order, and the output you expect will eventually be generated. + +For example, you can see the mentioned description in a function controller in phoenix after a successful registration as the following: + +```elixir +def login(conn, %{"user" => %{"email" => email, "password" => password}} = _params) do + # If your conditions are passed we call an event and pass it a struct of entries + # which our developers need to create plugin with this information + state = %MishkaInstaller.Reference.OnUserAfterLogin{ + conn: conn, + endpoint: :html, + ip: user_ip, type: :email, + user_info: user_info + } + + hook = MishkaInstaller.Hook.call(event: "on_user_after_login", state: state) + + hook.conn + |> renew_session() + |> put_session(:user_id, user_info.id) + |> put_flash(:info, "You entered to our world, well played.") + |> redirect(to: "/home") +end +``` + +Now the event is ready in the part where you need to allow the developer to make his own plugins for it. And it's time to write a plugin for this section. This is very simple. Consider the following example: + +```elixir +defmodule MishkaUser.SuccessLogin do + alias MishkaInstaller.Reference.OnUserAfterLogin + use MishkaInstaller.Hook, + module: __MODULE__, + behaviour: OnUserAfterLogin, + event: :on_user_after_login, + initial: [] + + @spec initial(list()) :: {:ok, OnUserAfterLogin.ref(), list()} + def initial(args) do + event = %PluginState{name: "MishkaUser.SuccessLogin", event: Atom.to_string(@ref), priority: 1} + Hook.register(event: event) + {:ok, @ref, args} + end + + @spec call(OnUserAfterLogin.t()) :: {:reply, OnUserAfterLogin.t()} + def call(%OnUserAfterLogin{} = state) do + new_state = Your_Code_Or_Function + {:reply, new_state} + end +end +``` + +> As you can see in the above, we used `MishkaInstaller.Reference.OnUserAfterLogin` in order to activate `behavior` which has a few `callback` in it, and you can see [here](https://github.com/mishka-group/mishka_installer/blob/master/lib/plugin_manager/event/reference/on_user_after_login.ex). + +--- + +> There should be two main functions in each plugin, namely `initial` and also `call`. In the first function, we introduce our plugin, and in the second function, whenever the action function calls this special event for which the plugin is written, based on priority. This plugin is also called. But what is important is the final output of the `call` function. This output may be the input of other plugins with higher priorities. The order of the plugins is from small to large, and if several plugins are registered for a number, it is sorted by name in the second parameter. And it should be noted that in any case, if you did not want this `state` to go to other plugins and the last output is returned in the same plugin, and you can replace `{:reply, :halt, new_state}` with `{:reply, new_state}`. + +Subsequent plugins with higher priorities are not counted, and the loop ends here. +Notice that a `Genserver` will be made based on each plugin name without a supervisor, which can be used for temporary memory in the case when the ` __using__` function is used as above, which results in the following option: + +```elixir +use MishkaInstaller.Hook, + module: __MODULE__, + behaviour: OnUserAfterLogin, + event: :on_user_after_login, + initial: [] +``` + +The last step to use the plugin you have to put it in your `Application` module so that whenever the server is turned off and on, the plugin is run again and if it is not registered, a copy of its support will be provided once in the database. + +```elixir +children = [ + %{id: YOUR_PLUGIN_MODULE, start: {YOUR_PLUGIN_MODULE, :start_link, [[]]}} +] +``` + +You can see our recommendations and other colleagues in the [Proposal](https://github.com/mishka-group/Proposals) repository, and if you have a request or idea, send us the full description. +> **Please help us by submitting suggestions and reviewing the project so that [Mishka Group](https://github.com/mishka-group) can produce more products and provide them to programmers and webmasters, and online software.** diff --git a/developer/mishke_installer_developer/config/config.exs b/developer/mishke_installer_developer/config/config.exs index 5ccac4f..fc17c47 100644 --- a/developer/mishke_installer_developer/config/config.exs +++ b/developer/mishke_installer_developer/config/config.exs @@ -29,6 +29,10 @@ config :mishke_installer_developer, MishkeInstallerDeveloper.Mailer, adapter: Sw # Swoosh API client is needed for adapters other than SMTP. config :swoosh, :api_client, false +config :mishke_installer_developer, :basic, + repo: MishkeInstallerDeveloper.Repo, + pubsub: MishkeInstallerDeveloper.PubSub + # Configure esbuild (the version is required) config :esbuild, version: "0.14.0", diff --git a/developer/mishke_installer_developer/lib/mishke_installer_developer/application.ex b/developer/mishke_installer_developer/lib/mishke_installer_developer/application.ex index 1cc8b62..4c2ed68 100644 --- a/developer/mishke_installer_developer/lib/mishke_installer_developer/application.ex +++ b/developer/mishke_installer_developer/lib/mishke_installer_developer/application.ex @@ -1,12 +1,13 @@ defmodule MishkeInstallerDeveloper.Application do - # See https://hexdocs.pm/elixir/Application.html - # for more information on OTP Applications - @moduledoc false - use Application @impl true def start(_type, _args) do + plugin_runner_config = [ + strategy: :one_for_one, + name: PluginStateOtpRunner + ] + children = [ # Start the Ecto repository MishkeInstallerDeveloper.Repo, @@ -15,19 +16,19 @@ defmodule MishkeInstallerDeveloper.Application do # Start the PubSub system {Phoenix.PubSub, name: MishkeInstallerDeveloper.PubSub}, # Start the Endpoint (http/https) - MishkeInstallerDeveloperWeb.Endpoint + MishkeInstallerDeveloperWeb.Endpoint, # Start a worker by calling: MishkeInstallerDeveloper.Worker.start_link(arg) # {MishkeInstallerDeveloper.Worker, arg} + {Registry, keys: :unique, name: PluginStateRegistry}, + {DynamicSupervisor, plugin_runner_config}, + {Task.Supervisor, name: MishkaInstaller.Activity}, + %{id: MishkaSocial.Auth.Strategy, start: {MishkaSocial.Auth.Strategy, :start_link, [[]]}} ] - # See https://hexdocs.pm/elixir/Supervisor.html - # for other strategies and supported options opts = [strategy: :one_for_one, name: MishkeInstallerDeveloper.Supervisor] Supervisor.start_link(children, opts) end - # Tell Phoenix to update the endpoint configuration - # whenever the application is updated. @impl true def config_change(changed, _new, removed) do MishkeInstallerDeveloperWeb.Endpoint.config_change(changed, removed) diff --git a/developer/mishke_installer_developer/lib/mishke_installer_developer/installer_lib/plugin_manager/event/hook.ex b/developer/mishke_installer_developer/lib/mishke_installer_developer/installer_lib/plugin_manager/event/hook.ex index 5e878ef..f29edc3 100644 --- a/developer/mishke_installer_developer/lib/mishke_installer_developer/installer_lib/plugin_manager/event/hook.ex +++ b/developer/mishke_installer_developer/lib/mishke_installer_developer/installer_lib/plugin_manager/event/hook.ex @@ -294,7 +294,7 @@ defmodule MishkaInstaller.Hook do # This part helps us to wait for database and completing PubSub either def handle_info(:timeout, state) do cond do - !is_nil(MishkaInstaller.get_config(:pubsub)) && is_nil(Process.whereis(MishkaHtml.PubSub)) -> {:noreply, state, 100} + !is_nil(MishkaInstaller.get_config(:pubsub)) && is_nil(Process.whereis(MishkaInstaller.get_config(:pubsub))) -> {:noreply, state, 100} !is_nil(MishkaInstaller.get_config(:pubsub)) -> unquote(module_selected).initial(unquote(initial_entry)) {:noreply, state} diff --git a/developer/mishke_installer_developer/lib/mishke_installer_developer/installer_lib/plugin_manager/event/reference/on_user_before_login.ex b/developer/mishke_installer_developer/lib/mishke_installer_developer/installer_lib/plugin_manager/event/reference/on_user_before_login.ex new file mode 100644 index 0000000..e9637ff --- /dev/null +++ b/developer/mishke_installer_developer/lib/mishke_installer_developer/installer_lib/plugin_manager/event/reference/on_user_before_login.ex @@ -0,0 +1,23 @@ +defmodule MishkaInstaller.Reference.OnUserBeforeLogin do + + defstruct [:ip, :assigns, :output, :input] + + @type input() :: map() + @type assigns() :: Phoenix.LiveView.Socket.assigns() + @type output() :: Phoenix.LiveView.Rendered.t() | nil + @type ip() :: String.t() | tuple() # User's IP from both side endpoints connections + @type ref() :: :on_user_before_login # Name of this event + @type reason() :: map() | String.t() # output of state for this event + @type registerd_info() :: MishkaInstaller.PluginState.t() # information about this plugin on state which was saved + @type state() :: %__MODULE__{ip: ip(), assigns: assigns(), input: input(), output: output()} + @type t :: state() # help developers to keep elixir style + @type optional_callbacks :: {:ok, ref(), registerd_info()} | {:error, ref(), reason()} + + @callback initial(list()) :: {:ok, ref(), list()} | {:error, ref(), reason()} # Register hook + @callback call(state()) :: {:reply, state()} | {:reply, :halt, state()} # Developer should decide what and Hook call function + @callback stop(registerd_info()) :: optional_callbacks() # Stop of hook module + @callback start(registerd_info()) :: optional_callbacks() # Start of hook module + @callback delete(registerd_info()) :: optional_callbacks() # Delete of hook module + @callback unregister(registerd_info()) :: optional_callbacks() # Unregister of hook module + @optional_callbacks stop: 1, start: 1, delete: 1, unregister: 1 # Developer can use this callbacks if he/she needs +end diff --git a/developer/mishke_installer_developer/lib/mishke_installer_developer/installer_lib/plugin_manager/event/reference/on_user_before_save.ex b/developer/mishke_installer_developer/lib/mishke_installer_developer/installer_lib/plugin_manager/event/reference/on_user_before_save.ex index 8a696dd..10b0258 100644 --- a/developer/mishke_installer_developer/lib/mishke_installer_developer/installer_lib/plugin_manager/event/reference/on_user_before_save.ex +++ b/developer/mishke_installer_developer/lib/mishke_installer_developer/installer_lib/plugin_manager/event/reference/on_user_before_save.ex @@ -1,24 +1,2 @@ defmodule MishkaInstaller.Reference.OnUserBeforeSave do - - defstruct [:ip, :socket, :session, :output] - - @type user_info() :: map() - @type session() :: map() - @type output() :: Phoenix.LiveView.Rendered.t()| nil - @type socket() :: Phoenix.LiveView.Socket.t() - @type ip() :: String.t() | tuple() # User's IP from both side endpoints connections - @type ref() :: :on_user_before_login # Name of this event - @type reason() :: map() | String.t() # output of state for this event - @type registerd_info() :: MishkaInstaller.PluginState.t() # information about this plugin on state which was saved - @type state() :: %__MODULE__{ip: ip(), socket: socket(), session: session(), output: output()} - @type t :: state() # help developers to keep elixir style - @type optional_callbacks :: {:ok, ref(), registerd_info()} | {:error, ref(), reason()} - - @callback initial(list()) :: {:ok, ref(), list()} | {:error, ref(), reason()} # Register hook - @callback call(state()) :: {:reply, state()} | {:reply, :halt, state()} # Developer should decide what and Hook call function - @callback stop(registerd_info()) :: optional_callbacks() # Stop of hook module - @callback start(registerd_info()) :: optional_callbacks() # Start of hook module - @callback delete(registerd_info()) :: optional_callbacks() # Delete of hook module - @callback unregister(registerd_info()) :: optional_callbacks() # Unregister of hook module - @optional_callbacks stop: 1, start: 1, delete: 1, unregister: 1 # Developer can use this callbacks if he/she needs end diff --git a/developer/mishke_installer_developer/lib/mishke_installer_developer_web/endpoint.ex b/developer/mishke_installer_developer/lib/mishke_installer_developer_web/endpoint.ex index 3dd28a7..c8eedc3 100644 --- a/developer/mishke_installer_developer/lib/mishke_installer_developer_web/endpoint.ex +++ b/developer/mishke_installer_developer/lib/mishke_installer_developer_web/endpoint.ex @@ -10,7 +10,7 @@ defmodule MishkeInstallerDeveloperWeb.Endpoint do signing_salt: "eCruAW2v" ] - socket "/live", Phoenix.LiveView.Socket, websocket: [connect_info: [session: @session_options]] + socket "/live", Phoenix.LiveView.Socket, websocket: [connect_info: [:peer_data, session: @session_options]] # Serve at "/" the static files from "priv/static" directory. # diff --git a/developer/mishke_installer_developer/lib/mishke_installer_developer_web/live/live_test_page_one.ex b/developer/mishke_installer_developer/lib/mishke_installer_developer_web/live/live_test_page_one.ex index 746d49d..ca3c6ca 100644 --- a/developer/mishke_installer_developer/lib/mishke_installer_developer_web/live/live_test_page_one.ex +++ b/developer/mishke_installer_developer/lib/mishke_installer_developer_web/live/live_test_page_one.ex @@ -7,8 +7,9 @@ defmodule MishkeInstallerDeveloperWeb.LiveTestPageOne do end @impl true - def mount(_params, _session, socket) do - new_socket = assign(socket, page_title: "Live Test Page One", self_pid: self()) + def mount(params, _session, socket) do + user_id = get_connect_info(socket, :peer_data).address + new_socket = assign(socket, page_title: "Live Test Page One", self_pid: self(), user_id: user_id, input: params) {:ok, new_socket} end end diff --git a/developer/mishke_installer_developer/lib/mishke_installer_developer_web/templates/layout/root.html.heex b/developer/mishke_installer_developer/lib/mishke_installer_developer_web/templates/layout/root.html.heex index 7fee7b8..759c18c 100644 --- a/developer/mishke_installer_developer/lib/mishke_installer_developer_web/templates/layout/root.html.heex +++ b/developer/mishke_installer_developer/lib/mishke_installer_developer_web/templates/layout/root.html.heex @@ -8,6 +8,7 @@ <%= live_title_tag assigns[:page_title] || "MishkeInstallerDeveloper", suffix: " · Mishka" %> +
diff --git a/developer/mishke_installer_developer/lib/mishke_installer_developer_web/templates/page/live_test_page_one.html.heex b/developer/mishke_installer_developer/lib/mishke_installer_developer_web/templates/page/live_test_page_one.html.heex index 5747bdd..13443d1 100644 --- a/developer/mishke_installer_developer/lib/mishke_installer_developer_web/templates/page/live_test_page_one.html.heex +++ b/developer/mishke_installer_developer/lib/mishke_installer_developer_web/templates/page/live_test_page_one.html.heex @@ -5,4 +5,9 @@

<%= live_redirect "Home", to: Routes.live_path(@socket, MishkeInstallerDeveloperWeb.LiveTestPage) %> | <%= live_redirect "PageOne", to: Routes.live_path(@socket, MishkeInstallerDeveloperWeb.LiveTestPageOne) %> +

+ <%= + state = %MishkaInstaller.Reference.OnUserBeforeLogin{ip: @user_id, assigns: assigns, output: nil, input: @input} + MishkaInstaller.Hook.call(event: "on_user_before_login", state: state).output + %> \ No newline at end of file diff --git a/developer/mishke_installer_developer/lib/mishke_installer_developer/installer_lib/mix/tasks/mishka_installer_db.gen.migration.ex b/developer/mishke_installer_developer/lib/tasks/mishka_installer_db.gen.migration.ex similarity index 95% rename from developer/mishke_installer_developer/lib/mishke_installer_developer/installer_lib/mix/tasks/mishka_installer_db.gen.migration.ex rename to developer/mishke_installer_developer/lib/tasks/mishka_installer_db.gen.migration.ex index 8ac48cc..cbc7b5a 100644 --- a/developer/mishke_installer_developer/lib/mishke_installer_developer/installer_lib/mix/tasks/mishka_installer_db.gen.migration.ex +++ b/developer/mishke_installer_developer/lib/tasks/mishka_installer_db.gen.migration.ex @@ -5,7 +5,7 @@ defmodule Mix.Tasks.MishkaInstaller.Db.Gen.Migration do import Mix.Ecto import Mix.Generator - + @project_config_name :mishke_installer_developer @spec run([any]) :: :ok @doc false @@ -21,7 +21,7 @@ defmodule Mix.Tasks.MishkaInstaller.Db.Gen.Migration do ensure_repo(repo, args) path = Ecto.Migrator.migrations_path(repo) - :mishka_installer + @project_config_name |> Application.app_dir() |> Path.join("priv/*.eex") |> Path.wildcard() @@ -66,7 +66,7 @@ defmodule Mix.Tasks.MishkaInstaller.Db.Gen.Migration do defp prefix do - :mishka_installer + @project_config_name |> Application.fetch_env!(:basic) |> Keyword.get(:prefix, nil) end diff --git a/developer/mishke_installer_developer/priv/activity_migration.exs.eex b/developer/mishke_installer_developer/priv/activity_migration.exs.eex new file mode 100644 index 0000000..ea467e8 --- /dev/null +++ b/developer/mishke_installer_developer/priv/activity_migration.exs.eex @@ -0,0 +1,18 @@ +defmodule <%= module_prefix %>.Repo.Migrations.CreateMishkaInistrallerActivityTable do + use Ecto.Migration + + def change do + create table(:activities, primary_key: false<%= if not is_nil(db_prefix), do: ", prefix: \"#{db_prefix}\"" %>) do + add(:id, :uuid, primary_key: true) + add(:type, :integer, null: false) + add(:action, :integer, null: false) + add(:section, :integer, null: false, null: false) + add(:section_id, :uuid, primary_key: false, null: true) + add(:priority, :integer, null: false) + add(:status, :integer, null: false) + add(:extra, :map, null: true) + + timestamps() + end + end +end diff --git a/developer/mishke_installer_developer/priv/plugin_migration.exs.eex b/developer/mishke_installer_developer/priv/plugin_migration.exs.eex new file mode 100644 index 0000000..bc6b1e0 --- /dev/null +++ b/developer/mishke_installer_developer/priv/plugin_migration.exs.eex @@ -0,0 +1,24 @@ +defmodule <%= module_prefix %>.Repo.Migrations.CreateMishkaInistrallerPluginTable do + use Ecto.Migration + + def change do + create table(:plugins, primary_key: false<%= if not is_nil(db_prefix), do: ", prefix: \"#{db_prefix}\"" %>) do + add(:id, :uuid, primary_key: true) + add(:name, :string, size: 200, null: false) + add(:event, :string, size: 200, null: false) + add(:priority, :integer, null: false) + add(:status, :integer, null: false) + add(:depend_type, :integer, null: false) + add(:depends, {:array, :string}, null: true) + add(:extra, {:array, :map}, null: false) + + timestamps() + end + create( + index(:plugins, [:name], + name: :index_plugins_on_name, + unique: true + ) + ) + end +end