-
Notifications
You must be signed in to change notification settings - Fork 46
Mix task for creating component templates #1059
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
Changes from all commits
e3ba777
a6ff011
4d82791
05cc19b
9c30363
b87edf5
65ed0cd
a662a1a
3406110
aef571a
dd41853
9553ea6
b3de3e5
3cf421c
541c25f
e34bcd6
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,3 +1,5 @@ | ||
| tmp/ | ||
|
|
||
| .elixir_ls | ||
| /priv/plts/* | ||
| !/priv/plts/.gitkeep | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,88 @@ | ||
| [Filter, Endpoint, Sink, Source, Bin, Pipeline] | ||
| |> Enum.map(fn component_type -> | ||
| component_name = inspect(component_type) | ||
|
|
||
| defmodule Module.concat(Mix.Tasks.Membrane.Gen, component_type) do | ||
| @shortdoc "Generates a template for a Membrane #{component_name}" | ||
| @moduledoc """ | ||
| Generates a template for a Membrane #{component_name} with the provided module name. | ||
|
|
||
| $ mix membrane.gen.#{String.downcase(component_name)} module_name [-l target_location] | ||
|
|
||
| ## Options | ||
| * `-l, --location` - If a target location is provided, the #{component_name} will be created there, relative to the `lib` directory. | ||
| The filename must also be present and have an `.ex` extension. If location is not provided, then it will be | ||
| inferred from the provided module name - it will be converted to lowercase and `.` separators will be interpreted | ||
| as directory separators. For example, a #{component_name} with module name `Foo.Bar` will be created at `lib/foo/bar.ex`. | ||
| """ | ||
| use Mix.Task | ||
|
|
||
| @switches [location: :string] | ||
| @aliases [l: :location] | ||
|
|
||
| @impl true | ||
| def run(argv) do | ||
| do_run("lib", argv) | ||
| end | ||
|
|
||
| @spec do_run(binary(), [binary()]) :: any() | ||
|
FelonEkonom marked this conversation as resolved.
|
||
| def do_run(base_dir, argv) do | ||
| {option, argv} = OptionParser.parse!(argv, aliases: @aliases, strict: @switches) | ||
|
|
||
| module_name = | ||
| case argv do | ||
| [] -> | ||
| Mix.raise(""" | ||
| Module name not provided. | ||
|
|
||
| This task expects a module name for the newly created #{unquote(component_type)}: | ||
|
|
||
| $ mix membrane.gen.#{String.downcase(unquote(component_name))} My#{unquote(component_name)} | ||
|
|
||
| """) | ||
|
|
||
| [module_name | _rest] -> | ||
| module_name | ||
| end | ||
|
|
||
| if not valid_module_name?(module_name) do | ||
| Mix.raise(""" | ||
| Invalid module name, please provide a valid one. | ||
| (no other special characters than dots are allowed and the module name as well as dot-separated segments in it must start with uppercase letters). | ||
| """) | ||
| end | ||
|
|
||
| component_path = | ||
| case option do | ||
| [] -> Macro.underscore(module_name) <> ".ex" | ||
| [{:location, path} | _rest] -> path | ||
| end | ||
| |> then(&Path.join(base_dir, &1)) | ||
|
|
||
| component_path |> Path.dirname() |> File.mkdir_p!() | ||
|
|
||
| File.write!(component_path, get_component(module_name)) | ||
| end | ||
|
|
||
| defp get_component(module_name) do | ||
| template = | ||
| "../../../templates" | ||
| |> Path.expand(__DIR__) | ||
| |> Path.join(Macro.underscore(unquote(component_type)) <> ".ex") | ||
| |> File.read!() | ||
|
|
||
| template | ||
| |> String.split("\n") | ||
| |> List.replace_at(0, "defmodule #{module_name} do") | ||
| |> Enum.join("\n") | ||
| end | ||
|
|
||
| @spec valid_module_name?(String.t()) :: boolean() | ||
| defp valid_module_name?(name) do | ||
| case Code.string_to_quoted(name) do | ||
| {:ok, {:__aliases__, _metadata, _name}} -> true | ||
| _other -> false | ||
| end | ||
| end | ||
| end | ||
| end) | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,10 @@ | ||
| defmodule Mix.Tasks.Membrane.Gen do | ||
| @shortdoc "Lists all available Membrane component generators" | ||
| @moduledoc @shortdoc | ||
| use Mix.Task | ||
|
|
||
| @impl true | ||
| def run(_argv) do | ||
| Mix.Tasks.Help.run(["--search", "membrane.gen."]) | ||
| end | ||
| end |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,154 @@ | ||
| defmodule Membrane.TemplateBin do | ||
| @moduledoc """ | ||
| This is a generated template for a Membrane.Bin. Uncomment the snippets as necessary. | ||
| """ | ||
| use Membrane.Bin | ||
|
|
||
| require Membrane.Logger | ||
|
|
||
| # def_input_pad :input, | ||
| # accepted_format: _any | ||
| # availability: :on_request | :always, # default - :always | ||
| # max_instances: pos_integer() | :infinity, # relevant only for pads with `availability: :on_request`, default - :infinity | ||
| # options: [ | ||
| # Same structure as in def_options/1 | ||
| # ] | ||
|
|
||
| # def_output_pad :output, | ||
| # accepted_format: _any | ||
| # availability: :on_request | :always, # default - :always | ||
| # max_instances: pos_integer() | :infinity, # relevant only for pads with `availability: :on_request`, default - :infinity | ||
| # options: [ | ||
| # Same structure as in def_options/1 | ||
| # ] | ||
|
|
||
| # This macro also defines a struct of this module, which is then used for providing options | ||
| # when instantiating this component. | ||
| # def_options some_option: [ | ||
| # spec: typespec of the option. | ||
| # default: default value - if not set, providing this option will be mandatory. | ||
| # inspector: function converting fields' value to a string for documentation purposes. If not set, &inspect/1 will be used. | ||
| # description: """ | ||
| # Desription of the option. | ||
| # """ | ||
| # ] | ||
|
|
||
| defmodule State do | ||
| # Using this struct is not strictly necessary, but it's considered a good practice | ||
| # and is strongly encouraged. Having a state with static fields with defined | ||
| # typespecs adds robustness to the codebase and can prevent many bugs. | ||
| @moduledoc false | ||
|
|
||
| # When you add new fields to the struct remember to add them to this spec too. | ||
| @type t :: %__MODULE__{} | ||
|
|
||
| defstruct [] | ||
| end | ||
|
|
||
| # ----------------- | ||
| # --- CALLBACKS --- | ||
| # ----------------- | ||
| # These callbacks have been ordered as they are usually being executed in the lifecycle | ||
| # of a typical Membrane component - see https://hexdocs.pm/membrane_core/components_lifecycle.html. | ||
| # Most of them are optional and have default implementations, which are present here as commented out code. | ||
| # Any exceptions to this rule are mentioned above the relevant callbacks. | ||
|
|
||
| # By default this callback will return with state set to an empty map %{}, | ||
| # however we recommend using a dedicated State struct. | ||
| @impl true | ||
| def handle_init(_ctx, _opts) do | ||
| {[], %State{}} | ||
| end | ||
|
|
||
| # This callback will be executed only for pads with `availability: :on_request`. | ||
| # @impl true | ||
| # def handle_pad_added(_pad, _context, state) do | ||
| # {[], state} | ||
| # end | ||
|
|
||
| # This callback will be executed only for pads with `availability: :on_request`. | ||
| # @impl true | ||
| # def handle_pad_removed(_pad, _context, state) do | ||
| # {[], state} | ||
| # end | ||
|
Comment on lines
+64
to
+73
Member
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. Let's mention it will be executed only for dynamic pads
Member
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. I know that I mentioned |
||
|
|
||
| # @impl true | ||
| # def handle_setup(_context, state) do | ||
| # {[], state} | ||
| # end | ||
|
|
||
| # @impl true | ||
| # def handle_playing(_context, state) do | ||
| # {[], state} | ||
| # end | ||
|
|
||
| # @impl true | ||
| # def handle_info(message, _context, state) do | ||
| # Membrane.Logger.warning(""" | ||
| # Received message but no handle_info callback has been specified. Ignoring. | ||
| # Message: #{inspect(message)}\ | ||
| # """) | ||
|
Comment on lines
+87
to
+90
Contributor
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. One would need to add |
||
| # | ||
| # {[], state} | ||
| # end | ||
|
|
||
| # @impl true | ||
| # def handle_child_setup_completed(_child, _ctx, state) do | ||
| # {[], state} | ||
| # end | ||
|
|
||
| # @impl true | ||
| # def handle_child_playing(_child, _ctx, state) do | ||
| # {[], state} | ||
| # end | ||
|
|
||
| # @impl true | ||
| # def handle_element_start_of_stream(_element, _pad, _ctx, state) do | ||
| # {[], state} | ||
| # end | ||
|
|
||
| # @impl true | ||
| # def handle_element_end_of_stream(_element, _pad, _ctx, state) do | ||
| # {[], state} | ||
| # end | ||
|
|
||
| # @impl true | ||
| # def handle_child_notification(_notification, _element, _ctx, state) do | ||
| # {[], state} | ||
| # end | ||
|
|
||
| # @impl true | ||
| # def handle_parent_notification(_notification, _ctx, state) do | ||
| # {[], state} | ||
| # end | ||
|
|
||
| # @impl true | ||
| # def handle_crash_group_down(_group_name, _ctx, state) do | ||
| # {[], state} | ||
| # end | ||
|
|
||
| # @impl true | ||
| # def handle_terminate_request(_ctx, state) do | ||
| # {[terminate: :normal], state} | ||
| # end | ||
|
|
||
| # @impl true | ||
| # def handle_child_terminated(_child, _ctx, state) do | ||
| # {[], state} | ||
| # end | ||
|
|
||
| # This callback doesn't have a default implementation, but will be called only | ||
| # if a `:start_timer` action has been executed. For more information and examples | ||
| # of timer usage see https://hexdocs.pm/membrane_core/timer.html. | ||
| # @impl true | ||
| # def handle_tick(timer_id, context, state) do | ||
| # ... | ||
| # end | ||
|
|
||
| # This callback doesn't have a default implementation and will be called only if | ||
| # a child removes it's own dynamic pad. If not implemented, the bin will crash. | ||
| # @impl true | ||
| # handle_child_pad_removed(element, pad, ctx, state) do | ||
| # ... | ||
| # end | ||
| 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.
I am suprised that you can use
component_namewithoutunquoteThere 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.
that's because it's being resolved on the level of module compilation