<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Nyoman Abiwinanda</title>
    <description>The latest articles on DEV Community by Nyoman Abiwinanda (@abiwinanda).</description>
    <link>https://dev.to/abiwinanda</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F522618%2F4b4249f7-d8fe-403b-bbdd-245427ec90b4.jpeg</url>
      <title>DEV Community: Nyoman Abiwinanda</title>
      <link>https://dev.to/abiwinanda</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/abiwinanda"/>
    <language>en</language>
    <item>
      <title>Solving Data Consistency in Elixir with Ecto's prepare_changes</title>
      <dc:creator>Nyoman Abiwinanda</dc:creator>
      <pubDate>Sun, 26 Jan 2025 16:58:39 +0000</pubDate>
      <link>https://dev.to/abiwinanda/solving-data-consistency-in-elixir-with-ectos-preparechanges-428g</link>
      <guid>https://dev.to/abiwinanda/solving-data-consistency-in-elixir-with-ectos-preparechanges-428g</guid>
      <description>&lt;p&gt;When developing an application, it's common for the state of one piece of data to depend on the state of another. This interdependence introduces complexity, as changes in one dataset must remain consistent with changes in others to avoid conflicts or inaccuracies. This challenge is often known as the data consistency problem.&lt;/p&gt;

&lt;h2&gt;
  
  
  Intro to Data Consistency Problem
&lt;/h2&gt;

&lt;p&gt;Consider the following simple example app where you have a bank and an account that belongs to a bank.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight viml"&gt;&lt;code&gt;defmodule ElixirApp&lt;span class="p"&gt;.&lt;/span&gt;Bank &lt;span class="k"&gt;do&lt;/span&gt;
   use Ecto&lt;span class="p"&gt;.&lt;/span&gt;Schema

   schema &lt;span class="s2"&gt;"banks"&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
      field&lt;span class="p"&gt;(:&lt;/span&gt;name&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="nb"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      field&lt;span class="p"&gt;(:&lt;/span&gt;is_active&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;boolean&lt;span class="p"&gt;)&lt;/span&gt;

      timestamps&lt;span class="p"&gt;()&lt;/span&gt;
  end
end

defmodule ElixirApp&lt;span class="p"&gt;.&lt;/span&gt;Account &lt;span class="k"&gt;do&lt;/span&gt;
   use Ecto&lt;span class="p"&gt;.&lt;/span&gt;Schema

   schema &lt;span class="s2"&gt;"accounts"&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
      field&lt;span class="p"&gt;(:&lt;/span&gt;name&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="nb"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      field&lt;span class="p"&gt;(:&lt;/span&gt;currency&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="nb"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      field&lt;span class="p"&gt;(:&lt;/span&gt;is_active&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;boolean&lt;span class="p"&gt;)&lt;/span&gt;

      belongs_to&lt;span class="p"&gt;(:&lt;/span&gt;bank&lt;span class="p"&gt;,&lt;/span&gt; Bank&lt;span class="p"&gt;)&lt;/span&gt;

      timestamps&lt;span class="p"&gt;()&lt;/span&gt;
   end
end
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In this app, a bank has an "active" status, and if the bank becomes inactive, all accounts associated with it must also be set to inactive. This data consistency rule can be implemented within the bank's domain or context module. For instance, you might implement an &lt;code&gt;update_bank/2&lt;/code&gt; function as shown below:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight viml"&gt;&lt;code&gt;defmodule ElixirApp&lt;span class="p"&gt;.&lt;/span&gt;Banks &lt;span class="k"&gt;do&lt;/span&gt;
   alias ElixirApp&lt;span class="p"&gt;.{&lt;/span&gt;Account&lt;span class="p"&gt;,&lt;/span&gt; Bank&lt;span class="p"&gt;}&lt;/span&gt;
   alias ElixirApp&lt;span class="p"&gt;.&lt;/span&gt;Repo

   &lt;span class="k"&gt;import&lt;/span&gt; Ecto&lt;span class="p"&gt;.&lt;/span&gt;Query&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;warn&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; false

   &lt;span class="k"&gt;def&lt;/span&gt; update_bank&lt;span class="p"&gt;(&lt;/span&gt;%Bank&lt;span class="p"&gt;{}&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; bank&lt;span class="p"&gt;,&lt;/span&gt; attrs&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
      with &lt;span class="p"&gt;{:&lt;/span&gt;ok&lt;span class="p"&gt;,&lt;/span&gt; updated_bank&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;-&lt;/span&gt; do_update_bank&lt;span class="p"&gt;(&lt;/span&gt;bank&lt;span class="p"&gt;,&lt;/span&gt; attrs&lt;span class="p"&gt;),&lt;/span&gt;
           &lt;span class="p"&gt;{:&lt;/span&gt;ok&lt;span class="p"&gt;,&lt;/span&gt; _num_of_deactivated_accounts&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;-&lt;/span&gt; maybe_deactivate_bank_accounts&lt;span class="p"&gt;(&lt;/span&gt;updated_bank&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
         &lt;span class="p"&gt;{:&lt;/span&gt;ok&lt;span class="p"&gt;,&lt;/span&gt; updated_bank&lt;span class="p"&gt;}&lt;/span&gt;
      end
   end

   defp do_update_bank&lt;span class="p"&gt;(&lt;/span&gt;%Bank&lt;span class="p"&gt;{}&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; bank&lt;span class="p"&gt;,&lt;/span&gt; attrs&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
      bank
      &lt;span class="p"&gt;|&amp;gt;&lt;/span&gt; Bank&lt;span class="p"&gt;.&lt;/span&gt;changeset&lt;span class="p"&gt;(&lt;/span&gt;attrs&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="p"&gt;|&amp;gt;&lt;/span&gt; Repo&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;update&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
   end

   defp maybe_deactivate_bank_accounts&lt;span class="p"&gt;(&lt;/span&gt;%Bank&lt;span class="p"&gt;{&lt;/span&gt;active&lt;span class="p"&gt;:&lt;/span&gt; true&lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
      &lt;span class="p"&gt;{:&lt;/span&gt;ok&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
   end

   defp maybe_deactivate_bank_accounts&lt;span class="p"&gt;(&lt;/span&gt;%Bank&lt;span class="p"&gt;{&lt;/span&gt;id&lt;span class="p"&gt;:&lt;/span&gt; bank_id&lt;span class="p"&gt;,&lt;/span&gt; active&lt;span class="p"&gt;:&lt;/span&gt; false&lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt;updated_accounts_count&lt;span class="p"&gt;,&lt;/span&gt; _&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt;
         from&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;a&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; Account&lt;span class="p"&gt;,&lt;/span&gt;
            where&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;bank_id &lt;span class="p"&gt;==&lt;/span&gt; ^bank_id&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="k"&gt;update&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="k"&gt;set&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;is_active&lt;span class="p"&gt;:&lt;/span&gt; ^false&lt;span class="p"&gt;]]&lt;/span&gt;
         &lt;span class="p"&gt;)&lt;/span&gt;
         &lt;span class="p"&gt;|&amp;gt;&lt;/span&gt; Repo&lt;span class="p"&gt;.&lt;/span&gt;update_all&lt;span class="p"&gt;([])&lt;/span&gt;

      &lt;span class="p"&gt;{:&lt;/span&gt;ok&lt;span class="p"&gt;,&lt;/span&gt; updated_accounts_count&lt;span class="p"&gt;}&lt;/span&gt;
   end 
end
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Within the &lt;code&gt;update_bank/2&lt;/code&gt; function, each time a bank's status is updated, it’s passed to the &lt;code&gt;maybe_deactivate_bank_accounts/1&lt;/code&gt; function. This function ensures that if the bank is now inactive, all associated accounts are also deactivated, maintaining consistency across related data.&lt;/p&gt;

&lt;p&gt;The previous implementation works but it has one major drawback that needs to be handled by developers consistently.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;&lt;em&gt;All functions that perform an update to a bank must be ensured to implement the same data consistency logic.&lt;/em&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This means that:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;The data consistency logic code can potentially be duplicated across different modules as the application evolves. I.e. the update operation to a bank takes place outside the &lt;code&gt;Banks&lt;/code&gt; context module itself.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The data consistency logic becomes “unknown unknown” in the long run. This manual step introduces a risk of inconsistency if it’s overlooked, potentially leading to data integrity issues.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The key takeaway is that this implementation could lead to an increase in "unknown unknowns" over time. This risk arises because developers may unintentionally overlook the need for consistent data handling, leading to hidden bugs and data inconsistencies. To mitigate this, it’s essential to encapsulate or abstract this data consistency logic in a more centralized, reliable place. By doing so, we reduce code duplication and ensure that new developers don't need to be aware of this requirement upfront when adding features. This is where Ecto's &lt;code&gt;prepare_changes/2&lt;/code&gt; function comes in, offering an ideal solution for enforcing consistency at the database layer automatically.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to Use Ecto &lt;code&gt;prepare_changes/2&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;As explained in the official documentation, the &lt;code&gt;prepare_changes/2&lt;/code&gt;function allows you to specify a function that the repository will execute upon insert, update, or delete operations. This means that any function passed to &lt;code&gt;prepare_changes/2&lt;/code&gt; will only run when the changeset is submitted to the &lt;code&gt;Repo&lt;/code&gt; module. This approach is especially beneficial for abstracting or encapsulating data consistency logic directly within the schema's changeset function, allowing us to enforce consistency while keeping the changeset function itself pure and focused on validation and data transformation.&lt;/p&gt;

&lt;p&gt;Let’s use the example app before to see how this function can be used inside the &lt;code&gt;Bank&lt;/code&gt; changeset function to maintain consistency between banks and accounts.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight viml"&gt;&lt;code&gt;defmodule ElixirApp&lt;span class="p"&gt;.&lt;/span&gt;Bank &lt;span class="k"&gt;do&lt;/span&gt;
   use Ecto&lt;span class="p"&gt;.&lt;/span&gt;Schema
   alias Ecto&lt;span class="p"&gt;.&lt;/span&gt;Changeset
   &lt;span class="k"&gt;import&lt;/span&gt; Ecto&lt;span class="p"&gt;.&lt;/span&gt;Changeset
   &lt;span class="k"&gt;import&lt;/span&gt; Ecto&lt;span class="p"&gt;.&lt;/span&gt;Query&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;warn&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; false

   schema &lt;span class="s2"&gt;"banks"&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
      field&lt;span class="p"&gt;(:&lt;/span&gt;name&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="nb"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      field&lt;span class="p"&gt;(:&lt;/span&gt;is_active&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;boolean&lt;span class="p"&gt;)&lt;/span&gt;

      timestamps&lt;span class="p"&gt;()&lt;/span&gt;
   end

   @doc false
   &lt;span class="k"&gt;def&lt;/span&gt; changeset&lt;span class="p"&gt;(&lt;/span&gt;bank&lt;span class="p"&gt;,&lt;/span&gt; attrs&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
     bank
     &lt;span class="p"&gt;|&amp;gt;&lt;/span&gt; cast&lt;span class="p"&gt;(&lt;/span&gt;attrs&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[:&lt;/span&gt;name&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;is_active&lt;span class="p"&gt;])&lt;/span&gt;
     &lt;span class="p"&gt;|&amp;gt;&lt;/span&gt; validate_required&lt;span class="p"&gt;([:&lt;/span&gt;name&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;is_active&lt;span class="p"&gt;])&lt;/span&gt;
     &lt;span class="p"&gt;|&amp;gt;&lt;/span&gt; prepare_changes&lt;span class="p"&gt;(&lt;/span&gt;&amp;amp;ensure_accounts_status/&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
   end

   defp ensure_accounts_status&lt;span class="p"&gt;(&lt;/span&gt;%Changeset&lt;span class="p"&gt;{&lt;/span&gt;action&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="k"&gt;update&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; changeset&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
      bank_id &lt;span class="p"&gt;=&lt;/span&gt; get_field&lt;span class="p"&gt;(&lt;/span&gt;changeset&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;id&lt;span class="p"&gt;)&lt;/span&gt;
      bank_active_status &lt;span class="p"&gt;=&lt;/span&gt; get_field&lt;span class="p"&gt;(&lt;/span&gt;changeset&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;is_active&lt;span class="p"&gt;)&lt;/span&gt;

      &lt;span class="k"&gt;if&lt;/span&gt; bank_active_status &lt;span class="p"&gt;==&lt;/span&gt; false &lt;span class="k"&gt;do&lt;/span&gt;
         from&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;a&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="s2"&gt;"accounts"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            where&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;bank_id &lt;span class="p"&gt;==&lt;/span&gt; ^bank_id&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="k"&gt;update&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="k"&gt;set&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;is_active&lt;span class="p"&gt;:&lt;/span&gt; ^false&lt;span class="p"&gt;]]&lt;/span&gt;
         &lt;span class="p"&gt;)&lt;/span&gt;
         &lt;span class="p"&gt;|&amp;gt;&lt;/span&gt; changeset&lt;span class="p"&gt;.&lt;/span&gt;repo&lt;span class="p"&gt;.&lt;/span&gt;update_all&lt;span class="p"&gt;([])&lt;/span&gt;
      end

      changeset
   end

   defp ensure_accounts_status&lt;span class="p"&gt;(&lt;/span&gt;%Changeset&lt;span class="p"&gt;{&lt;/span&gt;action&lt;span class="p"&gt;:&lt;/span&gt; _&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; changeset&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
      changeset
   end
end
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In the implementation above, the code inside the &lt;code&gt;ensure_accounts_status/1&lt;/code&gt; function is only executed when the bank's status is updated to "inactive." This ensures that account status changes occur only when necessary, maintaining efficiency. Additionally, it's important to note that the changeset provided by &lt;code&gt;prepare_changes/2&lt;/code&gt; includes access to the repository (&lt;code&gt;repo&lt;/code&gt;), allowing the developer to perform CRUD operations as part of the data consistency logic. This makes it possible to handle complex business rules directly within the changeset, without needing to manually invoke operations outside of it.&lt;/p&gt;

&lt;p&gt;The use of &lt;code&gt;prepare_changes/1&lt;/code&gt; has the following pros and cons&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pros:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Encapsulating the data consistency logic down to the changeset function means that any updates to banks that use the bank’s changeset function will be guaranteed to produce a consistent result with other data which in this case accounts.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Less chance of duplicating the same data consistency logic across different domain functions as the application evolves.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Using the changeset function to perform a data validation will not make any database call (at least yet until it is passed to the &lt;code&gt;Repo&lt;/code&gt; module) therefore keeping the changeset function pure.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Cons:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Performing the data consistency logic inside the changeset could increase the sense of “magic” when performing CRUD operations to the bank. It can become slightly unobvious of the source of the operation that makes accounts inactive. However, this could justified as long as it simplifies your application development in a way that reduces bugs or errors.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Logic duplication could still happen if the &lt;code&gt;Bank&lt;/code&gt; module has multiple changeset functions and each function must perform the same data consistency operation. This however can be slightly simplified by abstracting the data consistency logic inside a private function and then re-using it across different changeset functions.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;As effective as the &lt;code&gt;prepare_changes/2&lt;/code&gt; function can be for addressing data consistency problems, it’s important to know that there should be limits to its use. For example, it’s not advisable to overload &lt;code&gt;prepare_changes/2&lt;/code&gt; with complex logic or side effects, such as sending notifications, creating new records of bank, or other operations that could obscure the flow of the application. Keeping the function focused on its core responsibility—ensuring data consistency—will help maintain clarity and make your application’s behavior more predictable to other developers.&lt;/p&gt;

&lt;p&gt;Now that you have an alternative approach for tackling data consistency issues with Ecto, you can try it out in your project and assess whether it helps reduce the likelihood of data inconsistency in your application. By keeping your code clean and leveraging Ecto's powerful tools like &lt;code&gt;prepare_changes/2&lt;/code&gt;, you’ll be able to build more reliable and maintainable applications. Happy coding!&lt;/p&gt;

</description>
      <category>elixir</category>
      <category>webdev</category>
    </item>
    <item>
      <title>GitHub Action: Adding Post Steps in Composite Actions</title>
      <dc:creator>Nyoman Abiwinanda</dc:creator>
      <pubDate>Sun, 23 Jul 2023 11:48:58 +0000</pubDate>
      <link>https://dev.to/abiwinanda/github-action-adding-post-steps-in-composite-actions-5ak3</link>
      <guid>https://dev.to/abiwinanda/github-action-adding-post-steps-in-composite-actions-5ak3</guid>
      <description>&lt;p&gt;Often when we create CI/CD pipelines, we create some steps where the purpose is to setup something for the upcoming steps. For example, you might add a step to install aws cli, install commands with &lt;code&gt;apt-get&lt;/code&gt;, restoring build artifact from a cache, and etc.&lt;/p&gt;

&lt;p&gt;These setup commands tend to be copy-pasted across different pipelines making them less DRY. However, in many CI/CD tools or platforms such as GitHub Action, we could define an action, much like a function, to extract the repeated steps into a single reusable action. This process where we extract a bunch of steps to make them reusable can be implemented with a &lt;code&gt;composite&lt;/code&gt; action in the GitHub platform.&lt;/p&gt;

&lt;p&gt;To quickly demonstrate GitHub &lt;code&gt;composite&lt;/code&gt; actions, let say we have the following CI workflow under &lt;code&gt;.github/workflows&lt;/code&gt; directory that performs a unit test&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Defined in .github/workflows/ci.yml&lt;/span&gt;
&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Some CI workflow&lt;/span&gt;

&lt;span class="nn"&gt;...&lt;/span&gt;

&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;unitTest&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Run Unit Test&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v3&lt;/span&gt;

      &lt;span class="c1"&gt;# Setup step to speed-up dependencies installation &lt;/span&gt;
      &lt;span class="c1"&gt;# and app compilation&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Restore build artifacts&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;...&lt;/span&gt; &lt;span class="c1"&gt;# run commands to restore app build artifacts&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Install app dependencies&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;...&lt;/span&gt; &lt;span class="c1"&gt;# run commands to install app dependencies&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Compile app&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;...&lt;/span&gt; &lt;span class="c1"&gt;# run commands to compile app&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Run test&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;...&lt;/span&gt; &lt;span class="c1"&gt;# run commands to run unit test&lt;/span&gt;

      &lt;span class="s"&gt;...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You realize later that the step &lt;code&gt;Restore build artifacts&lt;/code&gt; can be used for other existing or future workflows but instead of copy-pasting the code to other workflows, you instead create a &lt;code&gt;composite&lt;/code&gt; action called &lt;code&gt;cache&lt;/code&gt; under &lt;code&gt;.github/actions/cache&lt;/code&gt; directory&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Defined in .github/actions/cache/action.yml&lt;/span&gt;
&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Cache&lt;/span&gt;

&lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Restore&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;build&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;artifacts"&lt;/span&gt;

&lt;span class="na"&gt;runs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;using&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;composite"&lt;/span&gt;
  &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Cache&lt;/span&gt;
      &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;...&lt;/span&gt; &lt;span class="c1"&gt;# run commands to restore app build artifacts&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;by this stage we have the following file tree&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;.&lt;/span&gt;
├── .github
│   ├── actions
│   │   └── cache
│   │       └── action.yml 
│   │   
│   └── workflows
│       └── ci.yml
└── ...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;we could reference our defined &lt;code&gt;cache&lt;/code&gt; action inside our CI as follows&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Some CI workflow&lt;/span&gt;

&lt;span class="nn"&gt;...&lt;/span&gt;

&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;unitTest&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Run Unit Test&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v3&lt;/span&gt;

      &lt;span class="c1"&gt;# Reusing the cache action&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Restore build artifacts&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;./.github/actions/cache&lt;/span&gt;

      &lt;span class="s"&gt;...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So far, &lt;code&gt;composite&lt;/code&gt; action works beautifully to make your workflows DRY-er however there is one drawback which GitHub &lt;code&gt;composite&lt;/code&gt; action don't natively support (at least by the time of writing this post) which is to run post steps.&lt;/p&gt;

&lt;p&gt;Post steps are simply steps that GitHub will run after a workflows has reach its final steps. For the cache example, we might want to run a post step to rebuild the app build artifacts for future CI. We could define this step as another action however this gives us inconvenience as we have to keep remembering to put the rebuild action at certain step in the workflows. How can we do better and let the &lt;code&gt;composite&lt;/code&gt; action handle this post steps seamlessly for us?&lt;/p&gt;

&lt;p&gt;After doing a bit of research, I came across the following &lt;a href="https://github.com/actions/runner/issues/1478" rel="noopener noreferrer"&gt;GitHub issue: Support pre and post steps in Composite Actions&lt;/a&gt; in the &lt;a href="https://github.com/actions/runner" rel="noopener noreferrer"&gt;GitHub action runner repo&lt;/a&gt;. In the discussion, I found out that we could use a &lt;code&gt;node&lt;/code&gt; action to indirectly perform post steps in a &lt;code&gt;composite&lt;/code&gt; actions.&lt;/p&gt;

&lt;p&gt;To run post steps in &lt;code&gt;composite&lt;/code&gt; actions, first we will re-use the &lt;code&gt;with-post-step&lt;/code&gt; action that is defined in &lt;a href="https://github.com/pyTooling/Actions/blob/main/with-post-step/action.yml" rel="noopener noreferrer"&gt;here&lt;/a&gt; and in this post we wouldn't look into the implementation detail of the code.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;.github/actions/with-post-step/action.yml&lt;/em&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Copied from: https://github.com/pyTooling/Actions/blob/main/with-post-step/action.yml&lt;/span&gt;
&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;With post step&lt;/span&gt;

&lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Generic&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;JS&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Action&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;to&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;execute&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;a&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;main&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;command&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;and&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;set&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;a&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;command&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;as&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;a&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;post&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;step."&lt;/span&gt;

&lt;span class="na"&gt;inputs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;main&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Main&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;command/script."&lt;/span&gt;
    &lt;span class="na"&gt;required&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="na"&gt;post&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Post&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;command/script."&lt;/span&gt;
    &lt;span class="na"&gt;required&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Name&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;of&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;the&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;state&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;variable&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;used&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;to&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;detect&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;the&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;post&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;step."&lt;/span&gt;
    &lt;span class="na"&gt;required&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
    &lt;span class="na"&gt;default&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;POST&lt;/span&gt;

&lt;span class="na"&gt;runs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;using&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;node16"&lt;/span&gt;
  &lt;span class="na"&gt;main&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;main.js"&lt;/span&gt;
  &lt;span class="na"&gt;post&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;main.js"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;em&gt;.github/actions/with-post-step/main.js&lt;/em&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Ref: https://github.com/pyTooling/Actions/blob/main/with-post-step/main.js&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;spawn&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;child_process&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;appendFileSync&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;fs&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;EOL&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;os&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;cmd&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;subprocess&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;spawn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;cmd&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;stdio&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;inherit&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;shell&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="nx"&gt;subprocess&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;exit&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;exitCode&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;exitCode&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;exitCode&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;INPUT_KEY&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toUpperCase&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;`STATE_&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="c1"&gt;// Are we in the 'post' step?&lt;/span&gt;
  &lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;INPUT_POST&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="c1"&gt;// Otherwise, this is the main step&lt;/span&gt;
  &lt;span class="nf"&gt;appendFileSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;GITHUB_STATE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;=true&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;EOL&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;INPUT_MAIN&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The file tree should now looks something like this&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;.&lt;/span&gt;
├── .github
│   ├── actions
│   │   ├── cache
│   │   |   └── action.yml 
│   │   └── with-post-step
│   │       ├── action.yml 
│   │       └── main.js
│   │   
│   └── workflows
│       └── ci.yml
└── ...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;once we have &lt;code&gt;with-post-step&lt;/code&gt; action defined, go back to &lt;code&gt;cache&lt;/code&gt; action.yml definition and use the &lt;code&gt;with-post-step&lt;/code&gt; action to define the post steps of the &lt;code&gt;cache&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Defined in .github/actions/cache/action.yml&lt;/span&gt;
&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Cache&lt;/span&gt;

&lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Restore&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;and&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;rebuild&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;build&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;artifacts"&lt;/span&gt;

&lt;span class="na"&gt;runs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;using&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;composite"&lt;/span&gt;
  &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Cache&lt;/span&gt;
      &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;./.github/actions/with-post-steps&lt;/span&gt;
      &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;main&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;...&lt;/span&gt; &lt;span class="c1"&gt;# run commands to restore app build artifacts&lt;/span&gt;
        &lt;span class="na"&gt;post&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;...&lt;/span&gt; &lt;span class="c1"&gt;# run commands to rebuild app build artifacts&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once the post steps is in place for the &lt;code&gt;cache&lt;/code&gt; action, using the action in any workflow will add an additional post steps when the workflow is running.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Farreky9otjfi0tw0jh9f.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Farreky9otjfi0tw0jh9f.png" alt="Running post steps in github action" width="698" height="742"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you are interested to try out this yourself, the code in this post can be found in my &lt;a href="https://github.com/abiwinanda/composite-post-gha" rel="noopener noreferrer"&gt;composite-post-gha repo&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reference
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/actions/runner/issues/1478" rel="noopener noreferrer"&gt;GitHub issue: Support pre and post steps in Composite Actions.&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/pyTooling/Actions/blob/main/with-post-step/action.yml" rel="noopener noreferrer"&gt;with-post-steps action source code.&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>github</category>
      <category>githubactions</category>
      <category>cicd</category>
      <category>devops</category>
    </item>
    <item>
      <title>Controller-Free Approach for File Export in Phoenix LiveView</title>
      <dc:creator>Nyoman Abiwinanda</dc:creator>
      <pubDate>Tue, 04 Jul 2023 21:46:31 +0000</pubDate>
      <link>https://dev.to/abiwinanda/controller-free-approach-for-file-export-in-phoenix-liveview-fah</link>
      <guid>https://dev.to/abiwinanda/controller-free-approach-for-file-export-in-phoenix-liveview-fah</guid>
      <description>&lt;p&gt;In a recent project involving LiveView, I was tasked with building a page to display an accounting report. Alongside this requirement, it was important to enable users to export the report as an Excel file. While the common approach involves creating a regular Phoenix controller with an API to generate the Excel file, I sought to explore a more efficient and seamless solution. In this post, I will share an alternative method that allows us to export or generate an excel (or any other) file directly from the LiveView itself, eliminating the need for a separate controller.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Traditional Approach
&lt;/h2&gt;

&lt;p&gt;Traditionally, the export feature is implemented by creating a Phoenix controller responsible for building the Excel file and sending it as a response. Here's an example of how it's typically done:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight elixir"&gt;&lt;code&gt;&lt;span class="k"&gt;defmodule&lt;/span&gt; &lt;span class="no"&gt;SomeController&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="no"&gt;AppName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:controller&lt;/span&gt;

  &lt;span class="c1"&gt;# This function is used to generate and export the report Excel file&lt;/span&gt;
  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="n"&gt;export&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="c1"&gt;# Fetch the report data from the DB...&lt;/span&gt;
    &lt;span class="n"&gt;report_data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;fetch_report_data_from_db&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# ...and then build the excel file&lt;/span&gt;
    &lt;span class="n"&gt;raw_content&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;generate_excel_file&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;report_data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;conn&lt;/span&gt;
    &lt;span class="o"&gt;|&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;put_resp_content_type&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"text/xlsx"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;|&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;put_resp_header&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"content-disposition"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"attachment; filename=report.xlsx"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;|&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;raw_content&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;While this method is widely used, there is one thing that bothers me. Since the controller independently fetches the report data from the database and generates the Excel file, it duplicates the data retrieval process already performed in the LiveView page. This redundancy raises a question: Can we export the report data to an Excel file without relying on a Phoenix controller? Is it possible to construct the export file directly within the LiveView, reusing the report data stored in the LiveView socket?&lt;/p&gt;

&lt;h2&gt;
  
  
  An Alternative Solution
&lt;/h2&gt;

&lt;p&gt;To address this, my co-workers and I tried to devised an approach that enables exporting and downloading files using Phoenix LiveView alone. In our LiveView process, which handles report rendering and houses the download report button, we made a modification. Instead of making an HTTP request to a Phoenix controller, we altered the download button to trigger a &lt;code&gt;phx-click&lt;/code&gt; event named &lt;code&gt;export_xlsx&lt;/code&gt; and we also add a &lt;code&gt;phx-hook&lt;/code&gt; attribute set to &lt;code&gt;PushFile&lt;/code&gt; to ensure that it triggers the required hook callback in the clients as you will see later.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;button&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"export-btn"&lt;/span&gt; &lt;span class="na"&gt;phx-click=&lt;/span&gt;&lt;span class="s"&gt;"export_xlsx"&lt;/span&gt; &lt;span class="na"&gt;phx-hook=&lt;/span&gt;&lt;span class="s"&gt;"PushFile"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
   Export to excel
&lt;span class="nt"&gt;&amp;lt;/button&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight elixir"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="n"&gt;handle_event&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"export_xlsx"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;socket&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
   &lt;span class="c1"&gt;# Reusing the report data fetched during the LiveView access&lt;/span&gt;
   &lt;span class="n"&gt;report_data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;socket&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;assigns&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;report_data&lt;/span&gt;

   &lt;span class="c1"&gt;# Building the Excel file&lt;/span&gt;
   &lt;span class="n"&gt;raw_content&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;generate_excel_file&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;report_data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

   &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="ss"&gt;:noreply&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;push_file&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;socket&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;raw_content&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"report.xlsx"&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="k"&gt;defp&lt;/span&gt; &lt;span class="n"&gt;push_file&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;socket&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;binary&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;filename&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
   &lt;span class="n"&gt;prefix&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"data:&lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="no"&gt;MIME&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;from_path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;filename&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;;base64,"&lt;/span&gt;

   &lt;span class="no"&gt;Phoenix&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;LiveView&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;push_event&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;socket&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"download-file"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;%{&lt;/span&gt;
      &lt;span class="ss"&gt;base64_data:&lt;/span&gt; &lt;span class="n"&gt;prefix&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;gt;&lt;/span&gt; &lt;span class="no"&gt;Base&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;encode64&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;binary&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
      &lt;span class="ss"&gt;filename:&lt;/span&gt; &lt;span class="n"&gt;filename&lt;/span&gt;
   &lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;By constructing the Excel file within the LiveView process, we can conveniently utilize the pre-existing report data stored in the LiveView socket's assigns, which is fetched during the process mount.&lt;/p&gt;

&lt;p&gt;What happens after that is where we spent the most time with. We take raw data of the excel file and then send it to client via liveview js (hook) event called &lt;code&gt;download-file&lt;/code&gt;. This is done by the function &lt;code&gt;push_file/3&lt;/code&gt; which is just a wrapper we build around the function &lt;code&gt;Phoenix.LiveView.push_event/3&lt;/code&gt;. In this case we send the file raw data in the form of base64 string.&lt;/p&gt;

&lt;p&gt;Once we sent the file raw data to client, the client has to handle the event and do something with it. To handle such event, we define a hook inside the &lt;code&gt;phoenix-live-view.js&lt;/code&gt; file as follows&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// inside phoenix-live-view.js&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;Hooks&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{};&lt;/span&gt;

&lt;span class="c1"&gt;// Create a new hook that handle the "download-file" event&lt;/span&gt;
&lt;span class="nx"&gt;Hooks&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;PushFile&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;mounted&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;handleEvent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;download-file&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;function &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;element&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createElement&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;a&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="nx"&gt;element&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setAttribute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;href&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;base64_data&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="nx"&gt;element&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setAttribute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;download&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;filename&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="nx"&gt;element&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;click&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
      &lt;span class="nx"&gt;element&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;remove&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;liveSocket&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;LiveSocket&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/live&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Socket&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;hooks&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Hooks&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;essentially what the hook does is to create an HTML link tag, click the tag (to download the file), and then removes it. The created link tag looks something like this.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="c"&gt;&amp;lt;!-- The excel file is stored using data URL --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;a&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"data:text/xlsx;base64,{base64_data}"&lt;/span&gt; &lt;span class="na"&gt;download=&lt;/span&gt;&lt;span class="s"&gt;"filename.xlsx"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/a&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once the hooks is in place, we were able to download or export the report into an excel file without the need of Phoenix controller 🎉&lt;/p&gt;

&lt;h2&gt;
  
  
  Pros &amp;amp; Cons
&lt;/h2&gt;

&lt;p&gt;By eliminating the use of a controller and integrating the Excel file creation directly within the liveview process, we could reduce the download time equivalent to the time it takes to load the report (from the database) in the liveview.&lt;/p&gt;

&lt;p&gt;While the impact of this improvement may not be substantial in certain cases, it becomes more noticeable when the report page takes some time to load. Once the report is loaded, users no longer have to wait an additional amount of time to download the report file. This enhancement contributes to a more enjoyable user experience, lending a sense of responsiveness to the download feature.&lt;/p&gt;

&lt;p&gt;However, it's important to consider that removing the controller and relying solely on liveview introduces a few drawbacks. Two immediate concerns come to mind:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Various browsers may impose restrictions on the size of files transmitted using data URLs. Consequently, if the file size exceeds these limitations, the data URL approach may not function as intended.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;In line with the previous drawback, as file sizes increase, transmitting the file using data URLs could consume additional server CPU resources due to the base64 processing involved.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Both the controller-based approach and the approach mentioned in this post have their merits. If download time and responsiveness are not critical factors, the controller approach may be preferable. Ultimately, the choice of approach is up to you, depending on the specific requirements and priorities of your project.&lt;/p&gt;

</description>
      <category>elixir</category>
      <category>phoenix</category>
      <category>liveview</category>
    </item>
    <item>
      <title>VSCode Snippet for Reducing Boilerplate Memorization in Elixir</title>
      <dc:creator>Nyoman Abiwinanda</dc:creator>
      <pubDate>Fri, 11 Nov 2022 19:51:23 +0000</pubDate>
      <link>https://dev.to/abiwinanda/vscode-snippet-for-reducing-boilerplate-memorization-in-elixir-1ip4</link>
      <guid>https://dev.to/abiwinanda/vscode-snippet-for-reducing-boilerplate-memorization-in-elixir-1ip4</guid>
      <description>&lt;p&gt;If you have been writing software in Elixir for quite some time, you might feel that Elixir code can feel like it has a lot of boilerplates or configurations code. Elixir is one of the most fun programming languages I have ever used! And I wish I could use it for all of my software projects but to think that I could just rely on Elixir to do all of my projects is kind of naive for a software developer. I have been building software in many programming languages throughout my software development career and when I found myself switching between these languages, I often forgot the boilerplates or configurations code that exist in Elixir.&lt;/p&gt;

&lt;p&gt;I have tried to anticipate this by creating sample projects so the next time I need to write code in Elixir (such as &lt;code&gt;GenServer&lt;/code&gt; or &lt;code&gt;GenStage&lt;/code&gt; code) I could just copy-paste from them. Initially, I thought this solution is enough however when I found myself needing to open other projects just to copy-paste boilerplates or configurations code that I forgot, it kind of lowered my mood to code a little bit. Have you experienced this as well?&lt;/p&gt;

&lt;p&gt;In order to solve this simple issue yet impacting my coding productivity significantly, I decided to create my own snippet to assist me to code in Elixir that could reduce the need for me to memorize these boilerplates or configurations.&lt;/p&gt;

&lt;h1&gt;
  
  
  The Snippets
&lt;/h1&gt;

&lt;p&gt;The snippets that I created are as follow:&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;code&gt;genserver&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ff7w11adbadig2wyj4xg5.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ff7w11adbadig2wyj4xg5.gif" alt="Image description" width="600" height="569"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;code&gt;genstage.producer&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fu25489gm3kbqr30bs8r3.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fu25489gm3kbqr30bs8r3.gif" alt="Image description" width="600" height="569"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;code&gt;genstage.consumer&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F25cs4sycj6ed50ou41d3.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F25cs4sycj6ed50ou41d3.gif" alt="Image description" width="600" height="446"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;code&gt;genstage.producerconsumer&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F3t57pyfe3vu2w04zpi3z.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F3t57pyfe3vu2w04zpi3z.gif" alt="Image description" width="600" height="446"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Notice that VSCode is quite clever in that you don't have to type the complete snippet for it to show the autocomplete.&lt;/p&gt;

&lt;h1&gt;
  
  
  GitHub Repo
&lt;/h1&gt;

&lt;p&gt;If you found the snippet to be useful and perhaps can help you in becoming more productive as well, feel free to find and use the snippet definition in this &lt;a href="https://github.com/abiwinanda/ex-snippet" rel="noopener noreferrer"&gt;GitHub Repo&lt;/a&gt;. You might not need all the code that is generated from the snippet so feel free to remove some of them once generated.&lt;/p&gt;

&lt;p&gt;For now, the snippets could help in generating boiler plates when creating &lt;code&gt;GenServer&lt;/code&gt; and &lt;code&gt;GenStage&lt;/code&gt; code however other elixir modules such as &lt;code&gt;Supervisor&lt;/code&gt;, &lt;code&gt;ConsumerSupervisor&lt;/code&gt;, &lt;code&gt;Broadway&lt;/code&gt;, etc have their own boiler plates as well hence I am definitely going to add more snippets in the near future. If you think you have some ideas to improve them, you are welcomed to add more snippets or modify them :).&lt;/p&gt;

</description>
      <category>elixir</category>
      <category>vscode</category>
      <category>tooling</category>
    </item>
    <item>
      <title>Limiting Concurrency in Elixir Using Registry</title>
      <dc:creator>Nyoman Abiwinanda</dc:creator>
      <pubDate>Thu, 10 Nov 2022 10:35:13 +0000</pubDate>
      <link>https://dev.to/abiwinanda/limiting-concurrency-in-elixir-using-registry-2kig</link>
      <guid>https://dev.to/abiwinanda/limiting-concurrency-in-elixir-using-registry-2kig</guid>
      <description>&lt;p&gt;When building applications with high performance in mind, we might be tempted to perform concurrent tasks as much as possible. However, performing tasks concurrently, especially in an uncontrolled manner might put your application's reliability at risk. For example, if there are one million of data that you want to process concurrently and you carelessly spawn a million processes suddenly, it might overwhelm your application resources. &lt;/p&gt;

&lt;p&gt;Elixir makes it easy for us to perform tasks concurrently however it is also important for us to control the concurrency in a controllable or sensible manner. This is often referred to as Limiting Concurrency or Controlling Back-Pressure in Elixir.&lt;/p&gt;

&lt;p&gt;There are several ways to limit concurrency in Elixir and some libraries such as &lt;code&gt;GenStage&lt;/code&gt;, &lt;code&gt;Flow&lt;/code&gt;, and &lt;code&gt;Broadway&lt;/code&gt; have an API for this however what happens if you have a GenServer process that is spawned dynamically on runtime in which you want to limit the number of running concurrent processes yourself. In this case, one way to achieve this is to use Elixir &lt;code&gt;Registry&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. What is Registry
&lt;/h3&gt;

&lt;p&gt;We won't go into too much detail about what &lt;code&gt;Registry&lt;/code&gt; is since it is better to read through the &lt;a href="https://hexdocs.pm/elixir/1.13.4/Registry.html" rel="noopener noreferrer"&gt;official documentation&lt;/a&gt; for this but in a simple way, you could think of &lt;code&gt;Registry&lt;/code&gt; as a key-value storage. It is common in elixir to index a GenServer process pids in the registry and later retrieve them in some other part of the application.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Setting Up Registry
&lt;/h3&gt;

&lt;p&gt;Setting up a registry is simple and most of the time we don't even need to write additional code. Just like any other thing in elixir, a registry run in its own process hence to start a registry process, simply go to your OTP app supervision tree (this is inside the &lt;code&gt;Application&lt;/code&gt; module) and then add the following code under the supervised children.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight elixir"&gt;&lt;code&gt;&lt;span class="n"&gt;children&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="no"&gt;Registry&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;keys:&lt;/span&gt; &lt;span class="ss"&gt;:unique&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;name:&lt;/span&gt; &lt;span class="no"&gt;YourOTPApp&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;Registry&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note that you could name your registry with any name you want, in the code above we use the &lt;code&gt;YourOTPApp.Registry&lt;/code&gt; as the process name.&lt;/p&gt;

&lt;p&gt;When creating a registry, we could specify whether we want the keys option to be &lt;code&gt;:unique&lt;/code&gt; or &lt;code&gt;:duplicate&lt;/code&gt;. If we set it to &lt;code&gt;:unique&lt;/code&gt; then any key can only be registered once. None of the options will prevent us to use the registry to limit concurrency. In this example, we will see how it is done by using the &lt;code&gt;:unique&lt;/code&gt; option key.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Register Keys to Registry
&lt;/h3&gt;

&lt;p&gt;Any processes in Elixir can be named. Assume we have a genserver process that acts as a worker. We could pass a &lt;code&gt;:name&lt;/code&gt; keyword in the &lt;code&gt;GenServer.start_link/3&lt;/code&gt; function to name the worker.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight elixir"&gt;&lt;code&gt;&lt;span class="k"&gt;defmodule&lt;/span&gt; &lt;span class="no"&gt;YourOTPApp&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;Worker&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="no"&gt;GenServer&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="n"&gt;start_link&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="no"&gt;GenServer&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;start_link&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;__MODULE__&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;name:&lt;/span&gt; &lt;span class="bp"&gt;__MODULE__&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="c1"&gt;# ...&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You could pass an atom, string, and tuple to the &lt;code&gt;:name&lt;/code&gt;. You could even pass module names such as &lt;code&gt;__MODULE__&lt;/code&gt; which is common to use in genserver processes. One drawback of using the value mentioned is that hardcoding the name to a single value will only limit the process to a single running instance. It is similar to a singleton in object-oriented programming. For example, once a process is running with the name &lt;code&gt;__MODULE__&lt;/code&gt; then no other process can run with the same name again.&lt;/p&gt;

&lt;p&gt;In this case, we don't want to hardcode to a single value, we want to be able to provide the name during runtime when we create the process. To do that we could use a registry to uniquely name the genserver process.&lt;/p&gt;

&lt;p&gt;To register the genserver process to a registry, we could use a special &lt;code&gt;:via&lt;/code&gt; tuple.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight elixir"&gt;&lt;code&gt;&lt;span class="k"&gt;defmodule&lt;/span&gt; &lt;span class="no"&gt;YourOTPApp&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;Worker&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="no"&gt;GenServer&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="n"&gt;start_link&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Keyword&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="no"&gt;GenServer&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;start_link&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;__MODULE__&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;name:&lt;/span&gt; &lt;span class="n"&gt;via&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"worker"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="c1"&gt;# ...&lt;/span&gt;

  &lt;span class="k"&gt;defp&lt;/span&gt; &lt;span class="n"&gt;via&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="ss"&gt;:via&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;Registry&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="no"&gt;YourOTPApp&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;Registry&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;}}&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;{:via, Registry, {YourOTPApp.Registry, key, value}}&lt;/code&gt; tuple will register the process to the &lt;code&gt;YourOTPApp.Registry&lt;/code&gt; with a combination of key-value pair. In this case, only the &lt;code&gt;key&lt;/code&gt; has to be unique while the &lt;code&gt;value&lt;/code&gt; can be the same. Hence, we are hardcoding the &lt;code&gt;value&lt;/code&gt; to &lt;code&gt;"worker"&lt;/code&gt; and you will see later why we do this.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Limit the Number of Concurrently Running Processes
&lt;/h3&gt;

&lt;p&gt;Once we register the worker processes to the registry, we would be able to query the worker pid from the registry. Now, we are not interested in the pid of the workers, we are just interested to know how many instances of the worker are running. To query how many workers we have, we could use the &lt;code&gt;Registry,select/2&lt;/code&gt; function to find out how many workers are running. The following code demonstrates the select query.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight elixir"&gt;&lt;code&gt;&lt;span class="n"&gt;match_all&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="ss"&gt;:"$1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:"$2"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:"$3"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="n"&gt;guards&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="ss"&gt;:==&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:"$3"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"worker"&lt;/span&gt;&lt;span class="p"&gt;}]&lt;/span&gt;
&lt;span class="n"&gt;map_result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[%{&lt;/span&gt;&lt;span class="ss"&gt;pid:&lt;/span&gt; &lt;span class="ss"&gt;:"$2"&lt;/span&gt;&lt;span class="p"&gt;}]&lt;/span&gt;

&lt;span class="no"&gt;Registry&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;select&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;YourOTPApp&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;Registry&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="n"&gt;match_all&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;guards&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;map_result&lt;/span&gt;&lt;span class="p"&gt;}])&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The select query might look a little bit cryptic so let's take a look a little bit in detail:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;match_all&lt;/code&gt; represents a &lt;code&gt;{key, pid, value}&lt;/code&gt; tuple.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;guards&lt;/code&gt; is a list of filters to query data from the registry. In this case, we only use one filter which is to select key, pid, and values registered where the value is &lt;code&gt;"worker"&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;map_result&lt;/code&gt; is a list that represents how we want to map the data retrieved from the registry. &lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;In this case, how we map the data retrieved from the registry does not matter. What matters is the number of running worker processes.&lt;/p&gt;

&lt;p&gt;Once we could count the number of running workers, we could limit the number of concurrent workers by adding a simple conditional logic. The following code demonstrates limiting the running workers to only 5 maximum.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight elixir"&gt;&lt;code&gt;&lt;span class="k"&gt;defmodule&lt;/span&gt; &lt;span class="no"&gt;YourOTPApp&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="n"&gt;start_worker&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="no"&gt;Enum&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;count&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;running_workers&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
      &lt;span class="c1"&gt;# code to start the worker&lt;/span&gt;
    &lt;span class="k"&gt;else&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="ss"&gt;:error&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:maximum_workers_reached&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="n"&gt;running_workers&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="n"&gt;match_all&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="ss"&gt;:"$1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:"$2"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:"$3"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="n"&gt;guards&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="ss"&gt;:==&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:"$3"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"import"&lt;/span&gt;&lt;span class="p"&gt;}]&lt;/span&gt;
    &lt;span class="n"&gt;map_result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[%{&lt;/span&gt;&lt;span class="ss"&gt;pid:&lt;/span&gt; &lt;span class="ss"&gt;:"$2"&lt;/span&gt;&lt;span class="p"&gt;}]&lt;/span&gt;

    &lt;span class="no"&gt;Registry&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;select&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;YourOTPApp&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;Registry&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="n"&gt;match_all&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;guards&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;map_result&lt;/span&gt;&lt;span class="p"&gt;}])&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note that registry will only returned running pids. If a worker crashed or stopped then its pid will not be returned.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Summary
&lt;/h3&gt;

&lt;p&gt;In this post, you just see how &lt;code&gt;Registry&lt;/code&gt; can be used to limit the number of concurrent processes in Elixir. Depending on your application or use cases, the way you register or query a process from the registry might differ but the concept is still the same which is to keep track of running processes using the registry and use that tracking to limit the number of them.&lt;/p&gt;

</description>
      <category>elixir</category>
      <category>beginners</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Kaffy - Admin Interface for Phoenix Applications</title>
      <dc:creator>Nyoman Abiwinanda</dc:creator>
      <pubDate>Sun, 06 Dec 2020 16:16:40 +0000</pubDate>
      <link>https://dev.to/abiwinanda/kaffy-admin-interface-for-phoenix-applications-3oon</link>
      <guid>https://dev.to/abiwinanda/kaffy-admin-interface-for-phoenix-applications-3oon</guid>
      <description>&lt;p&gt;If you are using elixir right now, chances are that this isn't your first programming language that you have used to build your apps. Probably you are coming from other programming languages such as Python, Java, C#, Golang, etc. Speaking of this, anyone coming from Python or at least has used Python as their main programming language to build your apps? I am one of these people.&lt;/p&gt;

&lt;p&gt;As most people already know, the most used web framework in Python is Django. One of the Django's feature that I love the most or I think is unique is the Django admin interface. It provides you with a web interface where you could perform CRUD operations over you applications data out of the box. This is really useful especially if you or someone are trying to test your application and need to have a way of preparing a mock or test data quickly.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2Fmm30oq2w3o3mspqxy961.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2Fmm30oq2w3o3mspqxy961.png" alt="Alt Text" width="800" height="402"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Unfortunately there isn't a similar feature in Phoenix nor a third party module that allows you to have this kind of admin interface, at least when I first use elixir. However, there is one now and I feel there are still not many people know about this tool in the elixir community. I thought this tool deserve more recognition so I decided to let people know about it through this post.&lt;/p&gt;

&lt;h1&gt;
  
  
  Kaffy
&lt;/h1&gt;

&lt;p&gt;Kaffy is a tool that is inspired by Django admin that allows you to have a simple, flexible, and customizable admin interface in Phoenix application. With some minimal configurations, it ables to understand your application schemas and then make a CRUD page out of it.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2Fgckxbgldbzg4g919tkwi.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2Fgckxbgldbzg4g919tkwi.png" alt="Alt Text" width="800" height="517"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I won't deep dive to much into how to use or set it up since it's pretty much documented in its &lt;a href="https://github.com/aesmail/kaffy" rel="noopener noreferrer"&gt;GitHub repo&lt;/a&gt; but in summary not only it makes a CRUD page for your schema but it also enables you to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Create a custom dashboard page with widgets.&lt;/li&gt;
&lt;li&gt;Create a custom static page.&lt;/li&gt;
&lt;li&gt;Register your own scheduled tasks.&lt;/li&gt;
&lt;li&gt;And more.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Not only that it provides a similar functionality just like Django admin, but I personally think that the admin design looks much more pleasant in Kaffy :D.&lt;/p&gt;

&lt;h1&gt;
  
  
  Original Creator
&lt;/h1&gt;

&lt;p&gt;First of all I didn't create this tool. I am just a guy who thought that this tool has a potential to be used by a lot of people in the future so I decided to share it. I originally found this tool around August 2020 from the following &lt;a href="https://elixirforum.com/t/kaffy-a-quick-and-flexible-admin-interface-for-phoenix-applications/31355" rel="noopener noreferrer"&gt;post in elixirforum&lt;/a&gt;. In that post you could find who is the original creator and some points that encouraged the creation of Kaffy in the first place. I encourage you to check it out since it contains a lot of information about the tool it self. Last but not least, Kaffy is pretty much new and it is still actively developed by people. If you are interested, maybe you could contribute to its &lt;a href="https://github.com/aesmail/kaffy" rel="noopener noreferrer"&gt;GitHub repo&lt;/a&gt; and help make it a better tool for the elixir community to use :). Cheers ~&lt;/p&gt;

</description>
      <category>elixir</category>
    </item>
    <item>
      <title>Speed-up Building your Elixir Unit Tests with Fictitious</title>
      <dc:creator>Nyoman Abiwinanda</dc:creator>
      <pubDate>Sun, 29 Nov 2020 12:47:58 +0000</pubDate>
      <link>https://dev.to/abiwinanda/speed-up-building-your-elixir-unit-tests-with-fictitious-5gk1</link>
      <guid>https://dev.to/abiwinanda/speed-up-building-your-elixir-unit-tests-with-fictitious-5gk1</guid>
      <description>&lt;p&gt;Imagine the following situation, you have several APIs to build or complete in a day or days. You started to work on them one by one, finish each of them in about an hour or two, feels satisfied when you finish one and that satisfaction makes you feel ready to move on to another tasks but only to realise at the end that you forgot to make the unit tests and making them could cost you about the same time or even more time than you need to finish up your APIs. Have you ever been in this kind of situation? Well... I do, sometimes it bother me so much that I decided to skip the unit test only to realised in the long run that this degrade my productivity because things just breaks unnoticedly as your applications are becoming bigger, complex, and more people are working on the same code base.&lt;/p&gt;

&lt;h1&gt;
  
  
  Why Unit Test can be Tedious?
&lt;/h1&gt;

&lt;p&gt;Before we even touch our keyboard and just think about unit test briefly in our head, unit testing a code seems to be a simple task especially in a functional world. Typically these are the steps when we test our code:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2F07m1vz0lv1u4vfpahmca.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2F07m1vz0lv1u4vfpahmca.png" alt="Alt Text" width="541" height="122"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Prepare your input to be fed to your function or system under test.&lt;/li&gt;
&lt;li&gt;Call your function or invoke what ever things that you trying to test with the test input that you generate.&lt;/li&gt;
&lt;li&gt;Check whether your function produce the expected output.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Basically these steps can be summarised with a phrase &lt;code&gt;Prepare, Test, Assert&lt;/code&gt;. Sounds pretty simple right? Sure, but, this is usually what you think about it before you touch your keyboard and start making your real test though.&lt;/p&gt;

&lt;p&gt;Sometimes the step that eats up a lot of your time isn't really the testing part but preparing the test or mock data for your system under test. Consider the following application. You have a function, lets called it &lt;code&gt;like_a_comment()&lt;/code&gt; that increment the number of like for a blog post's comment by 1&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2F7vv0zvk259bwvp1t0lpn.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2F7vv0zvk259bwvp1t0lpn.png" alt="Alt Text" width="481" height="121"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The testing part is rather simple, you pass a sample comment to the function &lt;code&gt;like_a_comment()&lt;/code&gt; and then checked at the end whether the number of likes for that comment is incremented by one. However we might not realised that in obtaining the input comment it self, it might require us to walk through several steps. For example, before we have a comment we need to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Have a post that the comment is belonged to.&lt;/li&gt;
&lt;li&gt;Even before that a user must exist as the owner of that post.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So you realised that the scope of your code is kind of more illustrated by the following diagram&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2F4znthxtw7lanzcs86rt3.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2F4znthxtw7lanzcs86rt3.png" alt="Alt Text" width="800" height="219"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You might be saying &lt;strong&gt;Hey, can we just wrapped the steps to create a comment in a function and reuse it through out other tests?&lt;/strong&gt; Sure we can, in fact that is exactly what I had been doing previously. However, it only cover that one specific case. As my application gets bigger, more complex, and new data are introduced into the system, more data (utility) functions that I need to prepare before I could start writing the actual test. Would it be nice if there is an out of the box tool where I could just say &lt;strong&gt;I want a comment with no like to be created and I don't care how the comment is supposed to created&lt;/strong&gt;? Turns out there is one in elixir.&lt;/p&gt;

&lt;h1&gt;
  
  
  Make your Unit Test Data with Fictitious
&lt;/h1&gt;

&lt;p&gt;To address the issue that we discussed previously, I created a library called &lt;strong&gt;Fictitious&lt;/strong&gt; which I open source to &lt;a href="https://hex.pm/" rel="noopener noreferrer"&gt;hex.pm&lt;/a&gt; for anyone to use. Fictitious, like its name, is a tool that enables you to create a fictitious data in elixir. It helps you to create mock data for your unit test without having the hassle of preparing the data in an convoluted order according to their associations that they have. Fictitious will ensure that whatever schema that you give as an input will create the related data for you.&lt;/p&gt;

&lt;p&gt;Consider the following two schemas &lt;code&gt;Person&lt;/code&gt; and &lt;code&gt;Country&lt;/code&gt;&lt;/p&gt;

&lt;h5&gt;
  
  
  Person Schema
&lt;/h5&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight elixir"&gt;&lt;code&gt;&lt;span class="k"&gt;defmodule&lt;/span&gt; &lt;span class="no"&gt;YourApp&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;Schema&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;Person&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="no"&gt;Ecto&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;Schema&lt;/span&gt;
  &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="no"&gt;Ecto&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;Changeset&lt;/span&gt;
  &lt;span class="n"&gt;alias&lt;/span&gt; &lt;span class="no"&gt;YourApp&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;Schema&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;Country&lt;/span&gt;

  &lt;span class="n"&gt;schema&lt;/span&gt; &lt;span class="s2"&gt;"persons"&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="n"&gt;field&lt;/span&gt; &lt;span class="ss"&gt;:name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:string&lt;/span&gt;
    &lt;span class="n"&gt;field&lt;/span&gt; &lt;span class="ss"&gt;:age&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:integer&lt;/span&gt;
    &lt;span class="n"&gt;field&lt;/span&gt; &lt;span class="ss"&gt;:gender&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:string&lt;/span&gt;
    &lt;span class="n"&gt;field&lt;/span&gt; &lt;span class="ss"&gt;:email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:string&lt;/span&gt;
    &lt;span class="n"&gt;belongs_to&lt;/span&gt; &lt;span class="ss"&gt;:nationality&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;Country&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;references:&lt;/span&gt; &lt;span class="ss"&gt;:id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;foreign_key:&lt;/span&gt; &lt;span class="ss"&gt;:country_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;type:&lt;/span&gt; &lt;span class="ss"&gt;:id&lt;/span&gt;

    &lt;span class="n"&gt;timestamps&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h5&gt;
  
  
  Country Schema
&lt;/h5&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight elixir"&gt;&lt;code&gt;&lt;span class="k"&gt;defmodule&lt;/span&gt; &lt;span class="no"&gt;YourApp&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;Schema&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;Country&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="no"&gt;Ecto&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;Schema&lt;/span&gt;
  &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="no"&gt;Ecto&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;Changeset&lt;/span&gt;
  &lt;span class="n"&gt;alias&lt;/span&gt; &lt;span class="no"&gt;YourApp&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;Schema&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;Person&lt;/span&gt;

  &lt;span class="n"&gt;schema&lt;/span&gt; &lt;span class="s2"&gt;"countries"&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="n"&gt;field&lt;/span&gt; &lt;span class="ss"&gt;:name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:string&lt;/span&gt;
    &lt;span class="n"&gt;has_many&lt;/span&gt; &lt;span class="ss"&gt;:people&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;Person&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;foreign_key:&lt;/span&gt; &lt;span class="ss"&gt;:country_id&lt;/span&gt;

    &lt;span class="n"&gt;timestamps&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;where &lt;code&gt;Person&lt;/code&gt; is belonged to a &lt;code&gt;Country&lt;/code&gt;. Let say in this case a person &lt;strong&gt;must&lt;/strong&gt; belonged to a country. This means that &lt;code&gt;country_id&lt;/code&gt; can't be null and when there is a value for it, it must references to an existing id of a country. This kind of data schemes could make data preparation in a test becomes tedious however this is one problem where fictitious could help you.&lt;/p&gt;

&lt;p&gt;For example, calling &lt;code&gt;fictionize/1&lt;/code&gt; to &lt;code&gt;Country&lt;/code&gt; will makes a fictitious country:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight elixir"&gt;&lt;code&gt;&lt;span class="n"&gt;iex&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="no"&gt;Fictitious&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;fictionize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;YourApp&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;Schema&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;Country&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="ss"&gt;:ok&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;%&lt;/span&gt;&lt;span class="no"&gt;YourApp&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;Schema&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;Country&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="ss"&gt;__meta__:&lt;/span&gt; &lt;span class="c1"&gt;#Ecto.Schema.Metadata&amp;lt;:loaded, "countries"&amp;gt;,&lt;/span&gt;
  &lt;span class="ss"&gt;id:&lt;/span&gt; &lt;span class="mi"&gt;67&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="ss"&gt;name:&lt;/span&gt; &lt;span class="s2"&gt;"B8LemwxB8ULP4NLUaFnKfwWkMmBYy8BTytkSN2PiL1UTO47yRM"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="ss"&gt;people:&lt;/span&gt; &lt;span class="c1"&gt;#Ecto.Association.NotLoaded&amp;lt;association :people is not loaded&amp;gt;,&lt;/span&gt;
  &lt;span class="ss"&gt;inserted_at:&lt;/span&gt; &lt;span class="sx"&gt;~U[2020-04-31 06:19:27Z]&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="ss"&gt;updated_at:&lt;/span&gt; &lt;span class="sx"&gt;~U[2020-04-31 06:19:27Z]&lt;/span&gt;
&lt;span class="p"&gt;}}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;however calling &lt;code&gt;fictionize/1&lt;/code&gt; to &lt;code&gt;Person&lt;/code&gt; will creates a person by creating the country as well:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight elixir"&gt;&lt;code&gt;&lt;span class="n"&gt;iex&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="ss"&gt;:ok&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;person&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Fictitious&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;fictionize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;YourApp&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;Schema&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;Person&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="ss"&gt;:ok&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;%&lt;/span&gt;&lt;span class="no"&gt;YourApp&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;Schema&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;Person&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="ss"&gt;__meta__:&lt;/span&gt; &lt;span class="c1"&gt;#Ecto.Schema.Metadata&amp;lt;:loaded, "persons"&amp;gt;,&lt;/span&gt;
  &lt;span class="ss"&gt;id:&lt;/span&gt; &lt;span class="mi"&gt;725&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="ss"&gt;name:&lt;/span&gt; &lt;span class="s2"&gt;"bElHKj9zVwnkLRpO4Y23yon9n80gm1yeAEL4PgtgkxBc0p2Y7C"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="ss"&gt;age:&lt;/span&gt; &lt;span class="mi"&gt;364&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="ss"&gt;gender:&lt;/span&gt; &lt;span class="s2"&gt;"dF1O5Eq4ombjzah"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="ss"&gt;email:&lt;/span&gt; &lt;span class="s2"&gt;"hpOXdOriGA9xaMhnwese40PqqL2Ine"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="ss"&gt;nationality:&lt;/span&gt; &lt;span class="c1"&gt;#Ecto.Association.NotLoaded&amp;lt;association :nationality is not loaded&amp;gt;,&lt;/span&gt;
  &lt;span class="ss"&gt;nationality_id:&lt;/span&gt; &lt;span class="mi"&gt;401&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="ss"&gt;inserted_at:&lt;/span&gt; &lt;span class="sx"&gt;~U[2020-04-31 06:19:27Z]&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="ss"&gt;updated_at:&lt;/span&gt; &lt;span class="sx"&gt;~U[2020-04-31 06:19:27Z]&lt;/span&gt;
&lt;span class="p"&gt;}}&lt;/span&gt;

&lt;span class="n"&gt;iex&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="no"&gt;YourApp&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;Repo&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;preload&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;person&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:nationality&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;%&lt;/span&gt;&lt;span class="no"&gt;YourApp&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;Schema&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;Person&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="ss"&gt;__meta__:&lt;/span&gt; &lt;span class="c1"&gt;#Ecto.Schema.Metadata&amp;lt;:loaded, "persons"&amp;gt;,&lt;/span&gt;
  &lt;span class="ss"&gt;id:&lt;/span&gt; &lt;span class="mi"&gt;725&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="ss"&gt;name:&lt;/span&gt; &lt;span class="s2"&gt;"bElHKj9zVwnkLRpO4Y23yon9n80gm1yeAEL4PgtgkxBc0p2Y7C"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="ss"&gt;age:&lt;/span&gt; &lt;span class="mi"&gt;364&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="ss"&gt;gender:&lt;/span&gt; &lt;span class="s2"&gt;"dF1O5Eq4ombjzah"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="ss"&gt;email:&lt;/span&gt; &lt;span class="s2"&gt;"hpOXdOriGA9xaMhnwese40PqqL2Ine"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="ss"&gt;nationality:&lt;/span&gt; &lt;span class="p"&gt;%&lt;/span&gt;&lt;span class="no"&gt;YourApp&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;Schema&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;Country&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="ss"&gt;__meta__:&lt;/span&gt; &lt;span class="c1"&gt;#Ecto.Schema.Metadata&amp;lt;:loaded, "countries"&amp;gt;,&lt;/span&gt;
    &lt;span class="ss"&gt;id:&lt;/span&gt; &lt;span class="mi"&gt;401&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="ss"&gt;name:&lt;/span&gt; &lt;span class="s2"&gt;"lcb1e86TY6RSccL6vPGjXOv43gnp1t"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="ss"&gt;people:&lt;/span&gt; &lt;span class="c1"&gt;#Ecto.Association.NotLoaded&amp;lt;association :people is not loaded&amp;gt;&lt;/span&gt;
    &lt;span class="ss"&gt;inserted_at:&lt;/span&gt; &lt;span class="sx"&gt;~U[2020-04-31 06:19:27Z]&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="ss"&gt;updated_at:&lt;/span&gt; &lt;span class="sx"&gt;~U[2020-04-31 06:19:27Z]&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="ss"&gt;nationality_id:&lt;/span&gt; &lt;span class="mi"&gt;401&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="ss"&gt;inserted_at:&lt;/span&gt; &lt;span class="sx"&gt;~U[2020-04-31 06:19:27Z]&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="ss"&gt;updated_at:&lt;/span&gt; &lt;span class="sx"&gt;~U[2020-04-31 06:19:27Z]&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In this case, having the country to be created automatically removes the trouble of having to prepare country data before a person could be created or in other word Fictitious ensures that you get the targeted or wanted entity to be created.&lt;/p&gt;

&lt;p&gt;You might be thinking, &lt;strong&gt;I don't want the data to be completely random, I want to override the data with some values. How can I do that?&lt;/strong&gt; No problem, fictitious allows you to override any field in the schema by simply providing the value at the second argument.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight elixir"&gt;&lt;code&gt;&lt;span class="n"&gt;iex&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="ss"&gt;:ok&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;country&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Fictitious&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;fictionize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;YourApp&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;Schema&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;Country&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;name:&lt;/span&gt; &lt;span class="s2"&gt;"Indonesia"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="ss"&gt;:ok&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;%&lt;/span&gt;&lt;span class="no"&gt;YourApp&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;Schema&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;Country&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="ss"&gt;__meta__:&lt;/span&gt; &lt;span class="c1"&gt;#Ecto.Schema.Metadata&amp;lt;:loaded, "countries"&amp;gt;,&lt;/span&gt;
  &lt;span class="ss"&gt;id:&lt;/span&gt; &lt;span class="mi"&gt;7914&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="ss"&gt;name:&lt;/span&gt; &lt;span class="s2"&gt;"Indonesia"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="ss"&gt;people:&lt;/span&gt; &lt;span class="c1"&gt;#Ecto.Association.NotLoaded&amp;lt;association :people is not loaded&amp;gt;,&lt;/span&gt;
  &lt;span class="ss"&gt;inserted_at:&lt;/span&gt; &lt;span class="sx"&gt;~U[2020-04-31 06:19:27Z]&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="ss"&gt;updated_at:&lt;/span&gt; &lt;span class="sx"&gt;~U[2020-04-31 06:19:27Z]&lt;/span&gt;
&lt;span class="p"&gt;}}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;it could even override the related entity by passing it directly as follows.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight elixir"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="ss"&gt;:ok&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;person&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Fictitious&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;fictionize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;YourApp&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;Schema&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;Person&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;nationality:&lt;/span&gt; &lt;span class="n"&gt;country&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="ss"&gt;:ok&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;%&lt;/span&gt;&lt;span class="no"&gt;YourApp&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;Schema&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;Person&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="ss"&gt;__meta__:&lt;/span&gt; &lt;span class="c1"&gt;#Ecto.Schema.Metadata&amp;lt;:loaded, "persons"&amp;gt;,&lt;/span&gt;
  &lt;span class="ss"&gt;id:&lt;/span&gt; &lt;span class="mi"&gt;451&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="ss"&gt;name:&lt;/span&gt; &lt;span class="s2"&gt;"ZFvtidsGOPh6OymYJk529bL2QT9KMZic2A0ietddl2RWy"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="ss"&gt;age:&lt;/span&gt; &lt;span class="mi"&gt;150940&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="ss"&gt;gender:&lt;/span&gt; &lt;span class="s2"&gt;"rHZYpbDgJQokDX2vSpSfWUmELrTb9f"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="ss"&gt;email:&lt;/span&gt; &lt;span class="s2"&gt;"xmcuHrJvotjAQz6itQnZtoMp"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="ss"&gt;nationality:&lt;/span&gt; &lt;span class="c1"&gt;#Ecto.Association.NotLoaded&amp;lt;association :nationality is not loaded&amp;gt;,&lt;/span&gt;
  &lt;span class="ss"&gt;inserted_at:&lt;/span&gt; &lt;span class="sx"&gt;~U[2020-04-31 06:19:27Z]&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="ss"&gt;updated_at:&lt;/span&gt; &lt;span class="sx"&gt;~U[2020-04-31 06:19:27Z]&lt;/span&gt;
&lt;span class="p"&gt;}}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h1&gt;
  
  
  More About Fictitious
&lt;/h1&gt;

&lt;p&gt;Now that you know what fictitious is capable of, I hope it could help you just like it helps me in making unit test faster in elixir and becoming more productive. It definitely one of my additional or alternative tools to help me in creating unit test in elixir and hopefully it can be added to yours as well. If you are interested to know more about fictitious feel free to read the official documentation &lt;a href="https://hexdocs.pm/fictitious/Fictitious.html" rel="noopener noreferrer"&gt;here&lt;/a&gt; or if you turns out to have an idea to improve it and interested to make it better feel free to contribute in the following &lt;a href="https://github.com/abiwinanda/fictitious" rel="noopener noreferrer"&gt;GitHub repository&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Happy unit testing :)&lt;/p&gt;

</description>
      <category>elixir</category>
      <category>testing</category>
      <category>productivity</category>
    </item>
  </channel>
</rss>
