Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
tmp/

.elixir_ls
/priv/plts/*
!/priv/plts/.gitkeep
Expand Down
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
* Deprecate `:links` option for `:unsafely_name_processes_for_observer` in favour of `:report_links_to_observer` configuration entry
* [Set label](https://hexdocs.pm/elixir/Process.html#set_label/1) for all the Membrane processes (i.e. for both Membrane components like pipelines, elements and bins and for utility components like subprocess supervisors)
* Update required Elixir version to `~> 1.17`
* Add Mix tasks for easier initialization of components [#1059](https://github.com/membraneframework/membrane_core/pull/1059)

## 1.2.6
* Add tutorials [#1007](https://github.com/membraneframework/membrane_core/pull/1007), plugins [#1012](https://github.com/membraneframework/membrane_core/pull/1012) and demos [#1013](https://github.com/membraneframework/membrane_core/pull/1013) to the docs.
Expand Down
5 changes: 3 additions & 2 deletions lib/membrane/bin.ex
Original file line number Diff line number Diff line change
Expand Up @@ -99,8 +99,9 @@ defmodule Membrane.Bin do

The callback won't be invoked, when you have initiated the pad removal,
eg. when you have returned `t:Membrane.Bin.Action.remove_link()` action
which made one of your children's pads be removed.
By default, it does nothing.
which made one of your children's pads be removed. Not having the
callback implemented when a child removes its pad will result in the
bin crashing.
"""
@callback handle_child_pad_removed(
child :: Child.name(),
Expand Down
29 changes: 22 additions & 7 deletions lib/membrane/core/options_specs.ex
Original file line number Diff line number Diff line change
Expand Up @@ -7,17 +7,17 @@ defmodule Membrane.Core.OptionsSpecs do
alias Membrane.Pad

@spec options_doc() :: String.t()
def options_doc do
def options_doc() do
"""
Options are defined by a keyword list, where each key is an option name and
is described by another keyword list with following fields:

* `spec:` typespec for value in struct
* `default:` default value for option. If not present, value for this option
will have to be provided each time options struct is created
* `inspector:` function converting fields' value to a string. Used when
creating documentation instead of `inspect/1`, eg. `inspector: &Membrane.Time.inspect/1`
* `description:` string describing an option. It will be used for generating the docs
* `spec:` Typespec of the values this option can assume. Will be used in t() type.
* `default:` Default value for option. If not present, value for this option
will have to be provided each time options struct is created.
* `inspector:` Function converting fields' value to a string. Used when
creating documentation instead of `inspect/1`, eg. `inspector: &Membrane.Time.inspect/1`.
* `description:` String describing the option. It will be used for generating the docs.
"""
end

Expand Down Expand Up @@ -55,6 +55,21 @@ defmodule Membrane.Core.OptionsSpecs do
end
end

@spec pad_options_doc() :: String.t()
def pad_options_doc() do
"""
Pad options are defined by a keyword list, where each key is an option name and
is described by another keyword list with following fields:

* `spec:` Typespec of the values this option can assume. Will be used in the type definition of pad options for a given pad.
* `default:` Default value for option. If not present, value for this option
will have to be provided each time options the pad is created.
* `inspector:` Function converting fields' value to a string. Used when
creating documentation instead of `inspect/1`, eg. `inspector: &Membrane.Time.inspect/1`.
* `description:` String describing the option. It will be used for generating the docs.
"""
end

@spec def_pad_options(Pad.name(), nil | Keyword.t()) :: {Macro.t(), Macro.t()}
def def_pad_options(_pad_name, []) do
no_code =
Expand Down
5 changes: 5 additions & 0 deletions lib/membrane/pad.ex
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ defmodule Membrane.Pad do
use Bunch

alias Membrane.Buffer
alias Membrane.Core.OptionsSpecs

@availability_values [:always, :on_request]

Expand Down Expand Up @@ -120,6 +121,8 @@ defmodule Membrane.Pad do

Demand unit is derived from the first element inside the bin linked to the
given input.

#{OptionsSpecs.pad_options_doc()}
"""
@type bin_spec ::
{name(),
Expand All @@ -130,6 +133,8 @@ defmodule Membrane.Pad do

@typedoc """
Describes how a pad should be declared inside an element.

#{OptionsSpecs.pad_options_doc()}
"""
@type element_spec ::
{name(),
Expand Down
5 changes: 3 additions & 2 deletions lib/membrane/pipeline.ex
Original file line number Diff line number Diff line change
Expand Up @@ -143,8 +143,9 @@ defmodule Membrane.Pipeline do

The callback won't be invoked, when you have initiated the pad removal,
eg. when you have returned `t:Membrane.Pipeline.Action.remove_link()`
action which made one of your children's pads be removed.
By default, it does nothing.
action which made one of your children's pads be removed. Not having the
callback implemented when a child removes its pad will result in the
pipeline crashing.
"""
@callback handle_child_pad_removed(
child :: Child.name(),
Expand Down
88 changes: 88 additions & 0 deletions lib/mix/tasks/membrane.gen.component.ex
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}"

Copy link
Copy Markdown
Member

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_name without unquote

Copy link
Copy Markdown
Contributor Author

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

@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()
Comment thread
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)
10 changes: 10 additions & 0 deletions lib/mix/tasks/membrane.gen.ex
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
6 changes: 4 additions & 2 deletions mix.exs
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,8 @@ defmodule Membrane.Mixfile do
defp dialyzer() do
opts = [
plt_local_path: "priv/plts",
flags: [:error_handling, :unmatched_returns]
flags: [:error_handling, :unmatched_returns],
plt_add_apps: [:mix]
]

if System.get_env("CI") == "true" do
Expand Down Expand Up @@ -238,7 +239,8 @@ defmodule Membrane.Mixfile do
"GitHub" => link(),
"Membrane Framework Homepage" => "https://membrane.stream"
},
files: ~w"lib .formatter.exs mix.exs README* LICENSE* CHANGELOG* #{@hex_packages_path}"
files:
~w"lib .formatter.exs mix.exs README* LICENSE* CHANGELOG* #{@hex_packages_path} templates"
]
end

Expand Down
154 changes: 154 additions & 0 deletions templates/bin.ex
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

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's mention it will be executed only for dynamic pads

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I know that I mentioned dynamic pads, but now I think we should avoid this term here. If target audience of this task are beginners, it will be more obvious for them if we will talk about pads with availability: :on_request, because they can see this field in the pad definition above. Please fix it here and in similar places.


# @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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One would need to add require Membrane.Logger for this to work after it get's uncommented

#
# {[], 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
Loading
Loading