Skip to content
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

More docs for v1.2 #923

Open
wants to merge 10 commits into
base: master
Choose a base branch
from
Open
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
4 changes: 4 additions & 0 deletions assets/images/spec_with_audio.svg
Copy link
Contributor

Choose a reason for hiding this comment

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

I hate to be the fun destroyer there, but have you considered using Mermaid diagrams? It would definitely be easier to maintain them. I think it should be possible to embed them within ex_doc, as they do it here: https://github.com/elixir-lang/elixir/blob/78f63d08313677a680868685701ae79a2459dcc1/lib/elixir/lib/supervisor.ex#L244

Copy link
Member

Choose a reason for hiding this comment

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

Copy link
Member Author

Choose a reason for hiding this comment

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

There is a problem with using Mermaid that I cannot label pads there

Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 4 additions & 0 deletions assets/images/spec_without_audio.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
51 changes: 51 additions & 0 deletions guides/components_lifecycle.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
# Lifecycle of Membrane Components

The lifecycle of Membrane Components is closely related to the execution of Membrane callbacks within these components. While there are some differences among the lifecycles of Membrane Pipelines, Bins, and Elements, they share many similarities. Let's explore the component lifecycle and identify differences depending on the type of component being addressed.

## Initialization
The first callback executed in every Membrane Component is `handle_init/2`. This function is executed synchronously and blocks the parent component, except in the case of a Membrane Pipeline, as it does not have a parent. It is advisable to avoid heavy computations within this function. `handle_init/2` is ideally used for spawning children in a pipeline or bin through the `:spec` action.
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
The first callback executed in every Membrane Component is `handle_init/2`. This function is executed synchronously and blocks the parent component, except in the case of a Membrane Pipeline, as it does not have a parent. It is advisable to avoid heavy computations within this function. `handle_init/2` is ideally used for spawning children in a pipeline or bin through the `:spec` action.
The first callback executed in every Membrane Component is `handle_init/2`. This function is executed synchronously and blocks the parent component, except in the case of a Membrane Pipeline, as it does not have a parent. It is advisable to avoid heavy computations within this function. `handle_init/2` is ideally used for spawning children in a pipeline or bin through the `:spec` action or parsing some options.

Copy link
Contributor

Choose a reason for hiding this comment

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

I would argue that a pipeline HAS a parent in the form of a process that spawns it and AFAIK that parent synchronously waits for the handle_init completion of the pipeline process.

Copy link
Contributor

Choose a reason for hiding this comment

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

I also think it would be good to describe what does it mean that a function is "executed synchronously" - it might not be obvious that the "blocking" phase happens after returning the :spec action and prevents the parent from invoking any other callbacks.


## Setup
Following `handle_init/2` is `handle_setup/2`, which is executed asynchronously. This is an optimal time to set up resources or perform other intensive operations required for the element to function properly.
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
Following `handle_init/2` is `handle_setup/2`, which is executed asynchronously. This is an optimal time to set up resources or perform other intensive operations required for the element to function properly.
Following `handle_init/2` is `handle_setup/2`, which is executed asynchronously (the parent process does not wait for its completion). This is an optimal time to set up resources or perform other intensive operations required for the component to function properly.


## Linking pads
For components with pads having `availability: :on_request`, the corresponding `handle_pad_added/3` callbacks are called between `handle_setup/2` and `handle_playing/2` if they are linked in the same spec where the component was spawned. Linking the pad in a different spec from the one spawning the element may lead to `handle_pad_added/3` being invoked after `handle_playing/2`.
Copy link
Member

Choose a reason for hiding this comment

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

As we spoke recently, I believe that linking happens only after the child setup is completed, see https://github.com/membraneframework/membrane_core/blob/master/lib/membrane/core/lifecycle_controller.ex#L14 and

- Wait until all children are initialized and all dependent specs are fully handled. Dependent specs are


## Playing
Once setup is completed, a component can enter the `:playing` state by invoking the `handle_playing/2` callback. Note that:
- Components spawned within the same `:spec` always enter the `:playing` state simultaneously. If the setup process for one component takes longer, the others will wait.
- Elements and Bins wait for their parent to enter the `playing` state before executing `handle_playing/2`.
- By default, after `handle_setup/2`, a component's setup is considered complete. This behavior can be modified by returning `setup: :incomplete` from `handle_setup/2`. The component must then mark its setup as completed by returning `setup: :complete` from another callback, like `handle_info/3`, to enter `:playing` playback.
Copy link
Contributor

Choose a reason for hiding this comment

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

Shouldn't this one be a part of the Setup description?


## Processing the data (applies only to Elements)
After `handle_playing/2`, Elements are prepared to process data flowing through their pads.

### Events
Events are one type of item that can be sent via an Element's pads and are managed in `handle_event/4`. Events can travel both upstream and downstream relative to the pad’s direction.
Copy link
Contributor

Choose a reason for hiding this comment

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

How about emphasizing that events are the only "stream elements" that can be sent upstream?


### Stream Formats
The stream format, which defines the type of data carried by `Membrane.Buffer`s, must be declared before the first data buffer and is managed in `handle_stream_format/4`.

### Start of Stream
This callback (`handle_start_of_stream`) is activated just before processing the first `Membrane.Buffer` from a specific input pad.

### Buffers
The core of multimedia processing involves handling `Membrane.Buffer`s, which contain multimedia payload and may also include metadata or timestamps, all managed within the `handle_buffer/4` callback.

### Demands
If the Element has pads with `flow_control: :manual`, entering `:playing` playback allows it to send demand using `:demand` action or to receive it in `handle_demand/5` callback.
Copy link
Contributor

Choose a reason for hiding this comment

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

How about distinguishing between manual input pads (capable of sending :demand action) and manual output pads (for which handle_demand can be called?)


## After processing the data
When an element determines that it will no longer send buffers from a specific pad, it can return `:end_of_stream` action to that pad. The linked element receives this in `handle_end_of_stream/3`. The parent component (either a Bin or Pipeline) is notified via `handle_element_end_of_stream/4`.
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
When an element determines that it will no longer send buffers from a specific pad, it can return `:end_of_stream` action to that pad. The linked element receives this in `handle_end_of_stream/3`. The parent component (either a Bin or Pipeline) is notified via `handle_element_end_of_stream/4`.
When an element determines that it will no longer send buffers from a specific pad, it can return `:end_of_stream` action to that pad. The linked element receives it in `handle_end_of_stream/3`. The parent component (either a Bin or Pipeline) is notified via `handle_element_end_of_stream/4`.


## Terminating
Typically, the last callback executed within a Membrane Component is `handle_terminate_request`. By default, it returns a `terminate: :normal` action, concluding the component's lifespan with the reason `:normal`. This behavior can be modified by overriding the default implementation, but ensure to return a `terminate: reason` elsewhere to avoid termination issues in your Pipeline.
Copy link
Contributor

Choose a reason for hiding this comment

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

I am not sure the phrase "concluding the component's lifespan" is widely recognized :D How about something simpler, just like "terminating the component"?


## Callbacks not strictly related to the lifecycle
Some callbacks are not confined to specific stages of the Membrane Component lifecycle.

### Handling parent or child notification
`handle_parent_notification/3` and `handle_child_notification/4` can occur at any point during the component's lifecycle and are tasked with managing notifications from a parent or child component, respectively.

### Handling messages from non-Membrane Erlang Processes
The `handle_info/3` callback is present in all Membrane Components and `handle_call/3` in Membrane Pipelines. These can be triggered at any time while the component is alive, functioning similarly to their substituted in `GenServer`.
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
The `handle_info/3` callback is present in all Membrane Components and `handle_call/3` in Membrane Pipelines. These can be triggered at any time while the component is alive, functioning similarly to their substituted in `GenServer`.
The `handle_info/3` callback is present in all Membrane Components and `handle_call/3` in Membrane Pipelines. These can be triggered at any time while the component is alive, functioning similarly to their counterparts in `GenServer`.

115 changes: 115 additions & 0 deletions guides/timer.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
# Timer usage examples
Exampls below illustrate how to use `:start_timer`, `:timer_interval` and `:stop_timer` actions on the example of `Membrane.Source`, but the API looks the same for all kinds of the Membrane Components
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
Exampls below illustrate how to use `:start_timer`, `:timer_interval` and `:stop_timer` actions on the example of `Membrane.Source`, but the API looks the same for all kinds of the Membrane Components
Examples below illustrate how to use `:start_timer`, `:timer_interval` and `:stop_timer` actions on the example of `Membrane.Source`, but the API looks the same for all kinds of the Membrane Components

put that through Grammarly to avoid typos


### Emit empty buffer every 100 milliseconds
```elixir
defmodule MySource do
use Membrane.Source

def_output_pad :output, accepted_format: SomeFormat

@impl true
def handle_init(_ctx, _opts), do: {[], %{}}

@impl true
def handle_playing(_ctx, state) do
# let's start a timer named :my_timer that will tick every 100 milliseconds

actions = [
start_timer: {:my_timer, Membrane.Time.milliseconds(100)}
]

{actions, state}
end

@impl true
def handle_tick(:my_timer, ctx, state) do
# in this callback we handle ticks of :my_timer
# we send a stream format if it hasn't been sent yet and a buffer

maybe_stream_format =
if ctx.pads.output.stream_format == nil,
do: [stream_format: %SomeFormat{}],
else: []
Comment on lines +30 to +33
Copy link
Contributor

Choose a reason for hiding this comment

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

How about moving stream format sending to the handle_playing callback? I think it's more common to do it this way and we won't need to tinker around context


buffer = [buffer: {:output, %Membrane.Buffer{payload: ""}}]

{maybe_stream_format ++ buffer, state}
end
end
```

### Emit empty buffer every 100 millisecond if parent hasn't stopped you
The source below accepts following notifications from the parent:
- `:pause` - after receiving it the source will pause sending buffers. The paused soure can be resumed again.
- `:resume` - resumes sending buffers from the paused source.
- `:stop` - the stopped source won't send any buffer again.

```elixir
defmodule MyComplexSource
use Membrane.Source

def_output_pad :output, accepted_format: SomeFormat

@impl true
def handle_init(_ctx, _opts) do
# after starting a timer, status will always be either :resumed, :paused
# or :pause_on_next_handle_tick
{[], %{status: nil}}
end

@impl true
def handle_playing(_ctx, state) do
# let's start a timer named :my_timer ...
start_timer_action = [
start_timer: {:my_timer, Membrane.Time.milliseconds(100)}
]

# ... and send a stream format
actions = start_timer_action ++ [stream_format: %SomeFormat{}]
{actions, %{state | status: :resumed}}
end

@impl true
def handle_parent_notification(notification, ctx, _state) when ctx.playback == :stopped do
raise "Cannot handle parent notification: #{inspect(notification)} before handle_playing"
end

@impl true
def handle_parent_notification(notification, _ctx, state) when notification in [:pause, :resume, :stop] do
case notification do
:pause when state.status == :resumed ->
# let's postopne pausing :my_timer to the upcomping handle_tick
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
# let's postopne pausing :my_timer to the upcomping handle_tick
# let's postpone pausing :my_timer to the upcoming handle_tick

{[], %{state | status: :pause_on_next_handle_tick}}

:resume when state.status == :paused ->
# resume :my_timer by returning :timer_interval action
actions = [timer_interval: {:my_timer, Membrane.Time.milliseconds(100)}]
{actions, %{state | status: :resumed}}

:resume when state.status == :pause_on_next_handle_tick ->
# case when we receive :pause and :resume notifications without a tick
# between them
{[], %{state | status: :resumed}}

:stop ->
# stop :my_timer using :stop_timer action
{[stop_timer: :my_timer], %{state | status: :stopped}}
end
end

@impl true
def handle_tick(:my_timer, _ctx, state) do
case state.status do
:resumed ->
buffer = %Membrane.Buffer{payload: ""}
{[buffer: {:output, buffer}], state}

:pause_on_next_handle_tick ->
# pause :my_timer using :timer_interval action with interval set to :no_interval
actions = [timer_interval: {:my_timer, :no_interval}]
{actions, %{state | status: :paused}}
end
end
end
```
30 changes: 29 additions & 1 deletion lib/membrane/bin/action.ex
Copy link
Contributor

Choose a reason for hiding this comment

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

Please apply the same changes as in case of pipeline/action.ex ;)

Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,35 @@ defmodule Membrane.Bin.Action do
Action that instantiates children and links them according to `Membrane.ChildrenSpec`.

Children's playback is changed to the current bin playback.
`c:Membrane.Parent.handle_spec_started/3` callback is executed once the children are spawned.
`c:Membrane.Bin.handle_spec_started/3` callback is executed once the children are spawned.

This is an example of a value that could be passed within `spec` action
```elixir
child(:file_source, %My.File.Source{path: path})
|> child(:demuxer, My.Demuxer)
|> via_out(:video)
|> child(:decoder, My.Decoder)
|> child(:ai_filter, My.AI.Filter{mode: :picasso_effect)
|> child(:encoder, My.Encoder)
|> via_in(:video)
|> child(:webrtc_sink, My.WebRTC.Sink)
```
along with it's visualisation

![](assets/images/spec_without_audio.svg)

Returning another spec
```elixir
get_child(:demuxer)
|> via_out(:audio)
|> child(:scratch_remover, My.Scratch.Remover)
|> via_in(:audio)
|> get_child(:webrtc_sink)
```

will result in having following children topology:

![](assets/images/spec_with_audio.svg)
"""
@type spec :: {:spec, ChildrenSpec.t()}

Expand Down
2 changes: 2 additions & 0 deletions lib/membrane/element/action.ex
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,8 @@ defmodule Membrane.Element.Action do
This action sets the latency for the element.

This action is permitted only in callback `c:Membrane.Element.Base.handle_init/2`.

The example of usage of this actions is [there](../../../guides/timer.md)
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
The example of usage of this actions is [there](../../../guides/timer.md)
The example of usage of these actions is [there](../../../guides/timer.md)

"""
@type latency :: {:latency, latency :: Membrane.Time.non_neg()}

Expand Down
28 changes: 28 additions & 0 deletions lib/membrane/pipeline/action.ex
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,34 @@ defmodule Membrane.Pipeline.Action do

Children's playback is changed to the current pipeline state.
`c:Membrane.Pipeline.handle_spec_started/3` callback is executed once it happens.

This is an example of a value that could be passed within `spec` action
```elixir
child(:file_source, %My.File.Source{path: path})
|> child(:demuxer, My.Demuxer)
|> via_out(:video)
|> child(:decoder, My.Decoder)
|> child(:ai_filter, My.AI.Filter{mode: :picasso_effect)
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
|> child(:ai_filter, My.AI.Filter{mode: :picasso_effect)
|> child(:ai_filter, %My.AI.Filter{mode: :picasso_effect})

|> child(:encoder, My.Encoder)
|> via_in(:video)
|> child(:webrtc_sink, My.WebRTC.Sink)
```
along with it's visualisation

![](assets/images/spec_without_audio.svg)

Returning another spec
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
Returning another spec
Returning another spec (on top of the previous one)

```elixir
get_child(:demuxer)
|> via_out(:audio)
|> child(:scratch_remover, My.Scratch.Remover)
|> via_in(:audio)
|> get_child(:webrtc_sink)
```

will result in having following children topology:
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
will result in having following children topology:
will result in the following children topology:


![](assets/images/spec_with_audio.svg)
"""
@type spec :: {:spec, ChildrenSpec.t()}

Expand Down
4 changes: 3 additions & 1 deletion mix.exs
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,8 @@ defmodule Membrane.Mixfile do
"guides/upgrading/v1.0.0-rc0.md",
"guides/upgrading/v1.0.0-rc1.md",
"guides/upgrading/v1.0.0.md",
"guides/components_lifecycle.md",
"guides/timer.md",
LICENSE: [title: "License"]
],
formatters: ["html"],
Expand Down Expand Up @@ -146,7 +148,7 @@ defmodule Membrane.Mixfile do
{:bunch, "~> 1.6"},
{:ratio, "~> 3.0 or ~> 4.0"},
# Development
{:ex_doc, "~> 0.28", only: :dev, runtime: false},
{:ex_doc, "0.34.2", only: :dev, runtime: false},
Copy link
Member

Choose a reason for hiding this comment

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

why this version?

Copy link
Member Author

Choose a reason for hiding this comment

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

I will check once again but I searched for a version that generates valid docs

{:makeup_diff, "~> 0.1", only: :dev, runtime: false},
{:dialyxir, "~> 1.1", only: :dev, runtime: false},
{:credo, "~> 1.7", only: :dev, runtime: false},
Expand Down
12 changes: 6 additions & 6 deletions mix.lock
Original file line number Diff line number Diff line change
Expand Up @@ -2,23 +2,23 @@
"bunch": {:hex, :bunch, "1.6.1", "5393d827a64d5f846092703441ea50e65bc09f37fd8e320878f13e63d410aec7", [:mix], [], "hexpm", "286cc3add551628b30605efbe2fca4e38cc1bea89bcd0a1a7226920b3364fe4a"},
"bunt": {:hex, :bunt, "1.0.0", "081c2c665f086849e6d57900292b3a161727ab40431219529f13c4ddcf3e7a44", [:mix], [], "hexpm", "dc5f86aa08a5f6fa6b8096f0735c4e76d54ae5c9fa2c143e5a1fc7c1cd9bb6b5"},
"coerce": {:hex, :coerce, "1.0.1", "211c27386315dc2894ac11bc1f413a0e38505d808153367bd5c6e75a4003d096", [:mix], [], "hexpm", "b44a691700f7a1a15b4b7e2ff1fa30bebd669929ac8aa43cffe9e2f8bf051cf1"},
"credo": {:hex, :credo, "1.7.8", "9722ba1681e973025908d542ec3d95db5f9c549251ba5b028e251ad8c24ab8c5", [:mix], [{:bunt, "~> 0.2.1 or ~> 1.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "cb9e87cc64f152f3ed1c6e325e7b894dea8f5ef2e41123bd864e3cd5ceb44968"},
"dialyxir": {:hex, :dialyxir, "1.4.4", "fb3ce8741edeaea59c9ae84d5cec75da00fa89fe401c72d6e047d11a61f65f70", [:mix], [{:erlex, ">= 0.2.7", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "cd6111e8017ccd563e65621a4d9a4a1c5cd333df30cebc7face8029cacb4eff6"},
"earmark_parser": {:hex, :earmark_parser, "1.4.41", "ab34711c9dc6212dda44fcd20ecb87ac3f3fce6f0ca2f28d4a00e4154f8cd599", [:mix], [], "hexpm", "a81a04c7e34b6617c2792e291b5a2e57ab316365c2644ddc553bb9ed863ebefa"},
"credo": {:hex, :credo, "1.7.10", "6e64fe59be8da5e30a1b96273b247b5cf1cc9e336b5fd66302a64b25749ad44d", [:mix], [{:bunt, "~> 0.2.1 or ~> 1.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "71fbc9a6b8be21d993deca85bf151df023a3097b01e09a2809d460348561d8cd"},
"dialyxir": {:hex, :dialyxir, "1.4.5", "ca1571ac18e0f88d4ab245f0b60fa31ff1b12cbae2b11bd25d207f865e8ae78a", [:mix], [{:erlex, ">= 0.2.7", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "b0fb08bb8107c750db5c0b324fa2df5ceaa0f9307690ee3c1f6ba5b9eb5d35c3"},
"earmark_parser": {:hex, :earmark_parser, "1.4.42", "f23d856f41919f17cd06a493923a722d87a2d684f143a1e663c04a2b93100682", [:mix], [], "hexpm", "6915b6ca369b5f7346636a2f41c6a6d78b5af419d61a611079189233358b8b8b"},
"erlex": {:hex, :erlex, "0.2.7", "810e8725f96ab74d17aac676e748627a07bc87eb950d2b83acd29dc047a30595", [:mix], [], "hexpm", "3ed95f79d1a844c3f6bf0cea61e0d5612a42ce56da9c03f01df538685365efb0"},
"ex_doc": {:hex, :ex_doc, "0.34.2", "13eedf3844ccdce25cfd837b99bea9ad92c4e511233199440488d217c92571e8", [:mix], [{:earmark_parser, "~> 1.4.39", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_c, ">= 0.1.0", [hex: :makeup_c, repo: "hexpm", optional: true]}, {:makeup_elixir, "~> 0.14 or ~> 1.0", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1 or ~> 1.0", [hex: :makeup_erlang, repo: "hexpm", optional: false]}, {:makeup_html, ">= 0.1.0", [hex: :makeup_html, repo: "hexpm", optional: true]}], "hexpm", "5ce5f16b41208a50106afed3de6a2ed34f4acfd65715b82a0b84b49d995f95c1"},
"excoveralls": {:hex, :excoveralls, "0.18.3", "bca47a24d69a3179951f51f1db6d3ed63bca9017f476fe520eb78602d45f7756", [:mix], [{:castore, "~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "746f404fcd09d5029f1b211739afb8fb8575d775b21f6a3908e7ce3e640724c6"},
"file_system": {:hex, :file_system, "1.0.1", "79e8ceaddb0416f8b8cd02a0127bdbababe7bf4a23d2a395b983c1f8b3f73edd", [:mix], [], "hexpm", "4414d1f38863ddf9120720cd976fce5bdde8e91d8283353f0e31850fa89feb9e"},
"jason": {:hex, :jason, "1.4.4", "b9226785a9aa77b6857ca22832cffa5d5011a667207eb2a0ad56adb5db443b8a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "c5eb0cab91f094599f94d55bc63409236a8ec69a21a67814529e8d5f6cc90b3b"},
"junit_formatter": {:hex, :junit_formatter, "3.4.0", "d0e8db6c34dab6d3c4154c3b46b21540db1109ae709d6cf99ba7e7a2ce4b1ac2", [:mix], [], "hexpm", "bb36e2ae83f1ced6ab931c4ce51dd3dbef1ef61bb4932412e173b0cfa259dacd"},
"makeup": {:hex, :makeup, "1.1.2", "9ba8837913bdf757787e71c1581c21f9d2455f4dd04cfca785c70bbfff1a76a3", [:mix], [{:nimble_parsec, "~> 1.2.2 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "cce1566b81fbcbd21eca8ffe808f33b221f9eee2cbc7a1706fc3da9ff18e6cac"},
"makeup": {:hex, :makeup, "1.2.1", "e90ac1c65589ef354378def3ba19d401e739ee7ee06fb47f94c687016e3713d1", [:mix], [{:nimble_parsec, "~> 1.4", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "d36484867b0bae0fea568d10131197a4c2e47056a6fbe84922bf6ba71c8d17ce"},
"makeup_diff": {:hex, :makeup_diff, "0.1.1", "01498f8c95970081297837eaf4686b6f3813e535795b8421f15ace17a59aea37", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "fadb0bf014bd328badb7be986eadbce1a29955dd51c27a9e401c3045cf24184e"},
"makeup_elixir": {:hex, :makeup_elixir, "0.16.2", "627e84b8e8bf22e60a2579dad15067c755531fea049ae26ef1020cad58fe9578", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "41193978704763f6bbe6cc2758b84909e62984c7752b3784bd3c218bb341706b"},
"makeup_elixir": {:hex, :makeup_elixir, "1.0.1", "e928a4f984e795e41e3abd27bfc09f51db16ab8ba1aebdba2b3a575437efafc2", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "7284900d412a3e5cfd97fdaed4f5ed389b8f2b4cb49efc0eb3bd10e2febf9507"},
"makeup_erlang": {:hex, :makeup_erlang, "1.0.1", "c7f58c120b2b5aa5fd80d540a89fdf866ed42f1f3994e4fe189abebeab610839", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "8a89a1eeccc2d798d6ea15496a6e4870b75e014d1af514b1b71fa33134f57814"},
"meck": {:hex, :meck, "0.9.2", "85ccbab053f1db86c7ca240e9fc718170ee5bda03810a6292b5306bf31bae5f5", [:rebar3], [], "hexpm", "81344f561357dc40a8344afa53767c32669153355b626ea9fcbc8da6b3045826"},
"mock": {:hex, :mock, "0.3.8", "7046a306b71db2488ef54395eeb74df0a7f335a7caca4a3d3875d1fc81c884dd", [:mix], [{:meck, "~> 0.9.2", [hex: :meck, repo: "hexpm", optional: false]}], "hexpm", "7fa82364c97617d79bb7d15571193fc0c4fe5afd0c932cef09426b3ee6fe2022"},
"mox": {:hex, :mox, "1.2.0", "a2cd96b4b80a3883e3100a221e8adc1b98e4c3a332a8fc434c39526babafd5b3", [:mix], [{:nimble_ownership, "~> 1.0", [hex: :nimble_ownership, repo: "hexpm", optional: false]}], "hexpm", "c7b92b3cc69ee24a7eeeaf944cd7be22013c52fcb580c1f33f50845ec821089a"},
"nimble_ownership": {:hex, :nimble_ownership, "1.0.0", "3f87744d42c21b2042a0aa1d48c83c77e6dd9dd357e425a038dd4b49ba8b79a1", [:mix], [], "hexpm", "7c16cc74f4e952464220a73055b557a273e8b1b7ace8489ec9d86e9ad56cb2cc"},
"nimble_ownership": {:hex, :nimble_ownership, "1.0.1", "f69fae0cdd451b1614364013544e66e4f5d25f36a2056a9698b793305c5aa3a6", [:mix], [], "hexpm", "3825e461025464f519f3f3e4a1f9b68c47dc151369611629ad08b636b73bb22d"},
"nimble_parsec": {:hex, :nimble_parsec, "1.4.0", "51f9b613ea62cfa97b25ccc2c1b4216e81df970acd8e16e8d1bdc58fef21370d", [:mix], [], "hexpm", "9c565862810fb383e9838c1dd2d7d2c437b3d13b267414ba6af33e50d2d1cf28"},
"numbers": {:hex, :numbers, "5.2.4", "f123d5bb7f6acc366f8f445e10a32bd403c8469bdbce8ce049e1f0972b607080", [:mix], [{:coerce, "~> 1.0", [hex: :coerce, repo: "hexpm", optional: false]}, {:decimal, "~> 1.9 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "eeccf5c61d5f4922198395bf87a465b6f980b8b862dd22d28198c5e6fab38582"},
"qex": {:hex, :qex, "0.5.1", "0d82c0f008551d24fffb99d97f8299afcb8ea9cf99582b770bd004ed5af63fd6", [:mix], [], "hexpm", "935a39fdaf2445834b95951456559e9dc2063d0a055742c558a99987b38d6bab"},
Expand Down