Hey @OvermindDL1, did you looked at it?
If so can you please let me know what you think about it (something not enough clear or maybe you see something missing)?
Do you have an idea how to dispatch my events (see &ExApi.notify_event/1 private function at bottom of main lib file) across nodes? I think about next release candidate for it.
I’m also planning to create a fork of ex_doc (originally don’t plan it, but after I read original source code I can see that it’s easiest way to achieve what I’m looking for). I want to add ability to define custom modules and function groups for documentation, so for example in left sidebar you could see apis link that will work as same as modules or exceptions + each custom module could define how to group functions, so instead of: types, functions and callbacks you could see for example (on api implementation page): supported features, not supported features and not implemented features.
I think about something like:
defmodule Example do
defmacro my_generator(arg1, ...) do
# ...
quote do
# ...
defmodule MyCustomModule do
@module_type MyModuleType # i.e. I don't want a special fork only for ex_api
# ...
end
# ...
end
# ...
end
end
# and finally
defmodule MyModuleType do
def get_grouped_functions(module) do
# get functions and group them
end
def group_label(group_name), do: # returns functions group name
def label, do: "My custom modules" # return sidebar label
end
Looked over it a couple times but not made a project with it yet. I’m keeping it up in my active list to remind me to make a test project sometime to give it a try (still fairly busy at work).
Make it hookable into different things? Phoenix.PubSub would be a nice default if it exists, else just load up a named receiving process on each node to receive the events and dispatch?
I’ve not actually delved into ex_doc as of yet, though @tmbb has a lot lately so he could probably give ideas on it.
I know that it was simple question. Just don’t used Phoenix.PubSub by myself yet (only used Phoenix that uses it),. Now I will remember it. Of course I already found docs and added support to project without bigger problem. I’m going to change some things in code, so I will not push next rc today, but in next days. I will ping you for notification.
I remember his awesome work on Makeup. If he or anyone else have ideas or simple questions then I’m open to all of them and if needed I can also add additional informations to documentation.
@tmbb: When you are writing macros to generate modules then you may would like to separate them from rest “normal” (plain) modules in your documentation to easier find them, so I want to group modules like now there is separated exceptions list. Similarly functions, I don’t expected declaring types in api and implementation, but I would like to separate supported features, not supported features and not implemented features. Right now there are all grouped in functions.
I want to do small change in validation (+ update tests) and add ability to define fallback implementation - this implementation will not be registered with others and could be fetched in case there is no other implementation (unlike default implementation that is stored with other implementations).
@webdeb: I agree. I will definitely do lots of libraries/projects that will use this library, but I need to find a bigger time for it. Currently I’m focused on next release candidate.
Lol, my work on Makeup is not “awesome”. It’s literally just writing a grammar for 2 programming languages and steal the Pygments styles to render it
The real merit lies in the ExSpirit library and the people who created the Pygments’ styles. Also important were the people who’ve written the Elixir lexer, because it gave me a useful baseline on what would be the least ampunt of parsing I’d have to do to get a decent output.
@ShalokShalom: In short it’s for defining apis of all 3rd-party apps, libraries and services.
I created this library to declare unique apis of multiple sources where each implementation could not support some part(s) of that api or is just partially implemented.
Also this library could be used as an alternative when you want to use Bahaviour and Protocol, but you don’t have a data that could be passed to each function - again see REST 3rd-party api services. Sometimes you prefer to write MyImpl.get_all() instead of MyImpl.get_all(%MyEmptyStruct{}) + you can dynamically list all implementations (which is not supported in plain Behaviour).
Another helpful thing is that your implementations are identified by {group, id} (both atoms), so you can group implementations and identify them simply by atom. Imagine that you want to create protocol that declares unique code hosting api for bitbucket, github, gitlab and other services. You could pass empty struct or struct with base url, but why do you need to pass anything when you want simply fetch all issues for github implementation?
For all interested: I pushed new commit to master (not next rc yet).
This version has support for Phoenix.PubSub. I also updated validation part - they are used before calling or casting server + I added specs for all methods that used cast function. Now they returns: {:ok, server_pid} or {:error, error_struct} instead of :ok.
I’m want to add more things before next release candidate. I plan to release it tomorrow or in Monday.
Just pushed new change to master. From now each api will use GenServer. Currently there is one async function feature_for/4.
This is introduced, because now you can call each feature asynchronously (using implementation api) or synchronously (using just implementation).
For example after you compile code like it:
def_api MyApi do
@moduledoc "Example api"
@doc "Do something really long"
@spec feature_name(term) :: term
def_feature feature_name(first_argument)
def_impl :my_impl do
def_feature_impl feature_name(first_argument) do
Process.sleep(1000)
"result: " <> to_string(first_argument)
end
end
end
# config for auto load:
use Mix.Config
config :ex_api, apis: %{MyApi => [MyApi.MyImpl]}
you can call feature_name asynchronously like:
# Note: no need to implement error callback if you don't expect and/or want catch errors
MyApi.feature_for({nil, :my_impl}, :feature_name, ["sample"], fn result -> IO.puts result end)
or you can also call it synchronously like:
# Note: if not specified group name is nil by default
{:ok, impl} = ExApi.get_impl(MyApi, {nil, :my_impl})
impl.module.feature_name("sample")
# or using module name:
MyApi.MyImpl.feature_name("sample")
I want to implement two more features (fallback implementation for api if no other implementations are registered and checking feature support, so you can firstly check support and then call feature). I plan to implement them tomorrow.
A question, is it suitable for a plugin system like, say, Firestorm forum or so? Where they could setup an ‘api’ with a set of hookpoints, and they call those hooks to have plugins do things like run some code or add something to the display or so? And if so, what would that look like? Say for these use-cases:
Rendering a post so it calls the hook for that, one of the hooks wants to add a thumbs-up button to the bottom of the post, and another hook wants to take that post and change github links to contain inline content from the github link (so it needs to take the existing template and return a mutated version while all other hooks have their own chances to do the same).
Hooking into when a post is saved so it can sanitize words or throw an error and another wants to take a post when it is saved so it can scan it for indexing purposes (it needs to take the post content and probably should return a mutated version of it that).
Firstly, I need to say that I don’t wrote any forums and plug-ins for them, so maybe someone could correct me. Here is one possible solution:
# in your code
import ExApi.Api
def_api Firestorm.Hooks.Api do
@moduledoc "Hooks API for Firestorm forum."
# @doc "Hook description goes here ..."
# @spec first_hook_name(...)
def_feature first_hook_name(arg1, arg2, ...)
# other feature implementations, declare no support or simply don't implement
end
# in plug-in code:
import ExApi.Implementation
def_api_impl Firestorm.Hooks.Api, :overmind_dl1_hooks, OvermindDL1.Firestorm.HooksImplementation do
def_feature_impl first_hook_name(arg1, arg2, ...) do
# hook implementation
end
# other feature definitions
end
# main hooks module:
defmodule Firestorm.Hooks do
alias Firesotrm.Hooks.Api
def call(name, args, finish_callback, failure_callback) when is_atom(name) and is_list(args) and is_function(finish_callback) and is_function(failure_callback) do
func_call = {name, Enum.count(args} # {name, arity}
{:ok, hooks_api_impls} = ExApi.get_impls(Api)
# here `hooks_api_impls` is a map %{group_name => impls}
# you could get hooks for specified group
# or get map values and flatten result
# for example, you want to declare `def core_call(name, args)`
# and fetch impls like: `hooks_api_impls[:firestorm_core]`
choosed_hooks = # ...
for impl <- choosed_hooks, is_supported_hook(impl, func_call) do
# here you could call hook asynchronously like:
{:ok, _pid} = impl.api.feature_for(impl, name, args, &finish_callback/1, &failure_callback/1)
# or synchronously like:
case apply(impl.module, name, args) do
{:ok, result} -> apply(&finish_callback(result))
{:error, error} -> apply(&failure_callback(result))
end
end
end
defp is_supported_hook(impl, func_call) do
{:ok, result} = ExApi.get_feature_support(impl, func_call)
end
end
Of course you need to implement something like Firesotrm.Hooks.Loader.
Hmm, how about that these hooks will use drab library?
Hmm, my idea is to do something like:
defmodule Firestorm.Article do
@moduledoc "Article context"
alias Firestorm.Article.Post
alias Firestorm.{Hooks, Repo}
@doc "Validates, call hooks and saves post data into database"
def save_post(input_data) do
changeset = Post.changeset(input_data)
Hooks.call(:before_save_post, [changeset], &on_before_save_post_finish/1, &on_before_save_post_failure/1)
end
defp on_before_save_post_failure(error) do
# format error and show user, auto-submit bug, or just do nothing :-)
end
defp on_before_save_post_finish(result) do
Repo.insert(result)
end
# other context functions ...
end
I wrote this response quickly so there could be errors/mistakes/typos, but this answer firstly comes to my mind. Let me think what do you think about it.
Well that looks awesome! That is what I was most curious about, an example I could digest to see how it works (I need to dedicate some time to read your extensive thoughts, I keep getting distracted at work so cannot keep my mind on them long enough for it to soak in ^.^;).