<?xml version="1.0" encoding="UTF-8"?>
<rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0">
<channel>
<atom:link href="https://bernheisel.com/rss.xml" rel="self" type="application/rss+xml" />
<title><![CDATA[]]></title>
<language>en-US</language>
<description><![CDATA[A blog about development]]></description>
<pubDate>Wed, 28 Aug 2024 00:00:00 -0400</pubDate>
<link>https://bernheisel.com/</link>
<copyright>Copyright 2026 David Bernheisel</copyright>
<generator>Artisinally Crafted by Yours Truly</generator>
<item>
<title><![CDATA[Dancing with Data]]></title>
<dc:creator>David Bernheisel</dc:creator>
<description><![CDATA[Functions come in all sorts of shapes, and I outline the kinds of functions
that could be helpful for developers, starting with simple pipelines, composed
pipelines, token pipelines, genstage, flow, broadway, and Oban.
]]></description>
<link>https://bernheisel.com/blog/dancing-with-data</link>
<guid isPermaLink="true">https://bernheisel.com/blog/dancing-with-data</guid>
<pubDate>Wed, 28 Aug 2024 00:00:00 -0400</pubDate>
<content:encoded><![CDATA[<p>
I presented at ElixirConf 2024 a talk called “Dancing with Data”. You can find
the presentation on ElixirConf when it releases (and I’ll update here when it’s
released).</p>
<p>
In the meantime, here are some supporting videos and slides:</p>
<ul>
  <li>
PDF slides: <a href="https://drive.google.com/file/d/1D9OO1shPkpnrVKuJcZmGrw-AGg-tv0KK/view?usp=drive_link">https://drive.google.com/file/d/1D9OO1shPkpnrVKuJcZmGrw-AGg-tv0KK/view?usp=drive_link</a>  </li>
  <li>
PPTX slides: <a href="https://docs.google.com/presentation/d/17ZH-qTsZlWqPjqv2rrOZHxDOVnFXE3nW/edit?usp=drive_link&ouid=110353084444571868371&rtpof=true&sd=true">https://docs.google.com/presentation/d/17ZH-qTsZlWqPjqv2rrOZHxDOVnFXE3nW/edit?usp=drive_link&amp;ouid=110353084444571868371&amp;rtpof=true&amp;sd=true</a>  </li>
  <li>
music slide: <a href="https://drive.google.com/file/d/16jw30i53bTL7VuaH0RNTq2qrOe65HNMd/view?usp=drive_link">https://drive.google.com/file/d/16jw30i53bTL7VuaH0RNTq2qrOe65HNMd/view?usp=drive_link</a>  </li>
  <li>
architectural time: <a href="https://drive.google.com/file/d/11Rk3rXdWWYGSJ1FfQt7tvoSr8hCkjuvz/view?usp=drive_link">https://drive.google.com/file/d/11Rk3rXdWWYGSJ1FfQt7tvoSr8hCkjuvz/view?usp=drive_link</a>  </li>
  <li>
Code used in slides: <a href="https://github.com/dbernheisel/elixir_conf_2024_dancing_with_data">https://github.com/dbernheisel/elixir_conf_2024_dancing_with_data</a>  </li>
</ul>
<iframe width="560" height="315" src="https://www.youtube-nocookie.com/embed/gijJ2ti1DUw?si=jyE7115cq7RDxCUc" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen="allowfullscreen">
</iframe>
<br>
<iframe width="560" height="315" src="https://www.youtube-nocookie.com/embed/0J5KaMswAhs?si=I5bfurkCMzqdL8UV" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen="allowfullscreen">
</iframe>
]]></content:encoded>
</item>
<item>
<title><![CDATA[Safe Ecto Migrations]]></title>
<dc:creator>David Bernheisel</dc:creator>
<description><![CDATA[As an Elixir developer who cares about system up-time and avoiding "scheduled maintenance" windows, and more importantly avoiding "unscheduled maintenance" windows 😉, this guide dives deep into Ecto database migrations and how they can be used safely in production systems.
]]></description>
<link>https://bernheisel.com/blog/safe-ecto-migrations</link>
<guid isPermaLink="true">https://bernheisel.com/blog/safe-ecto-migrations</guid>
<pubDate>Mon, 15 Nov 2021 00:00:00 -0500</pubDate>
<content:encoded><![CDATA[<p>
I wrote an extensive guide to Safe Ecto Migrations and published it with Fly.
Check it out at <a href="https://fly.io/phoenix-files/safe-ecto-migrations/">https://fly.io/phoenix-files/safe-ecto-migrations/</a></p>
]]></content:encoded>
</item>
<item>
<title><![CDATA[Nostalgia, Fun, and Programming]]></title>
<dc:creator>David Bernheisel</dc:creator>
<description><![CDATA[When we recognize music, movies, old photos, or video games of the past, it
has a powerful effect: nostalgia. It's a pattern of the past that we remember
fondly.

When we write code, we recall patterns of the past to consider as tools to
help solve today's problem.

I'd like to explore the connection between software development and nostalgia,
and have some fun while we're exploring the theory.
]]></description>
<link>https://bernheisel.com/blog/nostalgia-programming</link>
<guid isPermaLink="true">https://bernheisel.com/blog/nostalgia-programming</guid>
<pubDate>Tue, 05 Jan 2021 00:00:00 -0500</pubDate>
<content:encoded><![CDATA[<h2>
Introduction</h2>
<!-- speaking head -->
<p>
When we recognize music, movies, old photos, or video games of the past, it
sometimes has a powerful effect: nostalgia. It’s a pattern of the past that we
remember fondly.</p>
<p>
When we write code, we also recall patterns of the past to consider as tools to
help solve today’s problem.</p>
<p>
I’d like to have some fun exploring the connection between software development
and nostalgia.</p>
<h2>
What is Nostalgia</h2>
<p>
The word “nostalgia” started as a medical condition. Doctors invented the word
with the Greek roots ‘nostos’, meaning returning home, and ‘algos’ meaning
pain. It was invented in 1688, when Swiss soldiers would miss home so much,
that they would mentally break down, and no longer be able to ‘be a soldier’;
they longed for who they used to be.</p>
<p>
The condition of ‘nostalgia’ evolved into much less than a medical condition,
and more as a temporary emotion. The word is no longer associated with the
negative effects to the person; in fact quite the opposite: it now means “fondly
remembering the past”.</p>
<p>
Inducing nostalgia can boost psychological well-being, increase feelings of
self-esteem, <strong>social belonging</strong>, and encourage psychological growth. Nostalgia
can be a restorative way of coping with negative stress.</p>
<p>
Marketing knows about nostalgia; it’s a powerful selling tool. There are 11
Star Wars films; 10 Batman movies; 16 Final Fantasy games. Remasters of
remasters, remakes on top of remakes. <a href="https://www.youtube.com/watch?v=TNYGLMQxZ64">Remixes of older
songs</a>; you get it. Generally,
folks <strong>want</strong> to be in that mental space.</p>
<h2>
Where Nostalgia Comes From</h2>
<p>
<a href="https://en.wikipedia.org/wiki/Reminiscence_bump">  <img src="/images/Lifespan_Retrieval_Curve.jpg" alt="Lifespan Retrieval Curve">
</a></p>
<p>
This is called the <a href="https://en.wikipedia.org/wiki/Reminiscence_bump">Lifespan Retrieval
Curve</a>.</p>
<ul>
  <li>
0-5yrs Childhood Amnesia  </li>
  <li>
10-30yrs Reminisence Bump (this is our nostalgic period)  </li>
  <li>
30+ Period of Recency  </li>
</ul>
<p>
Our autobiographical memory – that is, our psychological history of ourselves
– on average shows that most of our memories are formed between the ages of
10-30 years old. Younger than 10 is considered childhood amnesia; greater than
that is considered too recent. For the life of me, I can’t remember what being 6
was like. Likewise, I couldn’t tell you what I did 2 years ago that affected my
life profoundly. It’s unconsciously regarded as too recent to be formative of
“who I am.”</p>
<p>
Our young adulthood is where we develop our self-identity; it’s when we
discover who we are and want to be. We’ll come back around to that bit. For me,
the time period is the 1990s and 2000s.</p>
<!-- cut to 90s -->
<p>
Back in the 1990s, there were several things that I did:</p>
<ul>
  <li>
Listened to cassettes I recorded from the stereo on my fuzzy earphones.  </li>
  <li>
Stopped at every Magic Eye book in Books-a-million, Borders, Hastings, or
Barnes &amp; Noble to see if I could see “it”.  </li>
  <li>
Spent 30 minutes or more just trying to decide what movie to rent; maybe it
took so long because I couldn’t accept that the movie I wanted to see didn’t
have the VHS behind it.  </li>
  <li>
Got really excited when the phone rang.  </li>
  <li>
Watched Bewitched and Dick Van Dyke on Nick-at-night.  </li>
  <li>
Replayed in my head the “The Log Song” from Ren &amp; Stimpy. It was an earworm
that never left. To this day I still hum it.  </li>
  <li>
I cleaned up poop from my Tamagotchi, jumped on goombas, and a little latter
double-barrel shotgunned demons in Doom.  </li>
</ul>
<p>
Let’s stop at that last point: video games.</p>
<h2>
<a href="https://www.theoryoffun.com/">Theory of Fun</a></h2>
<!-- Super Mario gameplay -->
<p>
Super Mario. The hero plumber that would grow up to be the most iconic and
recognizable video game character of all time. Made by a company formed in
1889: Nintendo.</p>
<p>
Video games are a source of nostalgia for many people. They’re fun. They’re
a way to escape the stress of the day. Many video games, such as Final Fantasy,
are epic stories that we can get lost within; just like books, but these are
interactive! Others, like classic Mario, have very little story and just focus
on addictive game mechanics.</p>
<p>
Video games are developed to be fun; otherwise why would we play them? Most
gamers aren’t playing for pure punishment (looking at you, Mega Man 9). Turns
out, there’s a lot of social science that goes into designing games to get and
keep players!</p>
<p>
The book “Theory of Fun” outlines some basics of what it takes to create a good
game. In it, the author Raph Koster includes a quote by Chris Crawford saying:</p>
<blockquote>
  <p>
fun is the emotional response to learning  </p>
</blockquote>
<p>
And later Raph Koster:</p>
<blockquote>
  <p>
Fun in games arises out of mastery. It arises out of comprehension. It is the
act of solving puzzles that makes games fun. With games, learning is the
drug.  </p>
</blockquote>
<blockquote>
  <p>
The idea was, games are systems built to help us learn patterns, and fun is
a neurochemical reward to encourage us to keep trying.  </p>
</blockquote>
<p>
These games <em>are built to teach us patterns</em>, and then delightfully introduce
variations on them, which keeps us learning. We see new situations as the game
progresses and have new ways to apply the learned patterns to them; or perhaps
combine patterns.</p>
<p>
Let’s look at an example.</p>
<iframe width="560" height="315" src="https://www.youtube-nocookie.com/embed/JpJ_vzdMkyw?start=4" frameborder="0" allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture" allowfullscreen="true">
</iframe>
<p>
In the original Super Mario Bros, we know that Mario moves right to advance to
the goal; we can tell because he’s on the left side of the screen– you can’t
go left; you must go right.</p>
<p>
The game introduces a pattern by establishing if you touch the Goombas, you
die. Noted– don’t touch bad guys.</p>
<p>
Let’s try pushing one of the buttons on the gamepad. Turns out Mario can jump!
Let’s avoid those bad guys. Noted– I can move right and jump</p>
<p>
There are blocks now. Looks like if I jump into them, I might get something. If
I jump on top, it acts as a platform.</p>
<p>
It introduces a pattern by establishing if you touch the mushroom, you grow.
Noted– there are power-ups that make me stronger</p>
<p>
You experiment with two elements: Getting a mushroom, and then touching the
enemy. Turns out that’s a mistake and now you shrink back down. Noted– can’t
kill bad guys by only being big, and when I’m big and get hurt, I become small.</p>
<p>
You experiment with two more elements: jump on the enemy’s heads!. Noted– bad
guys can be jumped on.</p>
<p>
Now we’re at a pit. Probably not safe to go in there, but maybe the screen will
move down and reveal a new area. Let’s try. Noted– can’t go down pits.</p>
<p>
These are universal patterns now in games. We know instinctively that we just
don’t touch enemies, and we should jump over pits.</p>
<!-- video of jumping over pits -->
<h2>
Patterns are Fun</h2>
<p>
Earlier we talked about nostalgia. Remember, nostalgia is fondly
<strong>remembering</strong> the past. It’s remembering a pattern that may be long gone from
our current life.</p>
<p>
Like nostalgia, video games also has psychological benefits. “Theory of Fun”
includes <a href="https://news.ecu.edu/2011/02/16/study-casual-video-games-demonstrate-ability-to-reduce-depression-and-anxiety/">a study from 2011 released by East Carolina
University</a>,
that found that casual gamers who played in 30 minute periods showed an 87%
improvement in cognitive response time, reduced depression symptoms by 57%, and
215% increase in executive functioning. That’s incredible!</p>
<p>
I’m starting to get the idea that nostalgia and video games are good news for
our brains and well-being. This isn’t limited to video games either; although
I’m focusing on them; these taught patterns also exist in music and movies.</p>
<p>
Thinking about pop music, almost every song on the radio has a recognizable
pattern, whether you’ve picked it out or not:</p>
<ul>
  <li>
Intro  </li>
  <li>
Verse 1  </li>
  <li>
Chorus  </li>
  <li>
Verse 2  </li>
  <li>
Chorus  </li>
  <li>
Bridge  </li>
  <li>
Chorus  </li>
  <li>
Outro  </li>
</ul>
<p>
If we analyzed the musical notes, there are patterns of notes that musicians
know sound good together called scales. Notes played that are not on the given
scale will create a dissonant sound; an out-of-tune guitar, for example.</p>
<p>
Likewise, movies and books typically follow a pattern:</p>
<ul>
  <li>
Introduce characters and setting  </li>
  <li>
Rising action or inciting incident.  </li>
  <li>
Climax and Falling  </li>
  <li>
Denouement (lessons learned)  </li>
</ul>
<p>
  <img src="/images/tictactoe.png" alt="Tic Tac Toe">
</p>
<p>
Consider Tic-Tac-Toe; it’s definitely a game, but the pattern is easily
perceived and there is no continuing variation. Mastery of Tie-Tac-Toe is
quick.</p>
<p>
Consider Korean dramas; usually a love story between a jerk guy and a shy girl.
A series of events leads to the shy girl turning away from jerk guy, almost
falling in love with another fella but then jerk guy gets hit by a car <strong>BAM</strong>
now he has amnesia. This turns jerk guy into nice guy. Shy girl now likes nice
guy. They get together, and then <strong>OH NOEES</strong> nice guy remembers stuff and now
he’s a mostly-jerk-but-now-somehow-ok-guy. Seriously, every k-drama is this in a
nutshell. Predictable.</p>
<p>
Alas, there is a point where patterns become boring and no longer fun.</p>
<blockquote>
  <p>
Delight tends to wear thin very quickly. Real fun comes from challenges that
are always at the margin of our ability. When the balance is really perfect,
people often zone out.  </p>
</blockquote>
<p>
There has to be variance in the patterns to stay interesting. Games do this by
having multiple stages or areas, power-ups, faster cars, etc. Music does this
with different instruments, tempos, singers, etc. Movies does this new
characters, settings, twists, etc.</p>
<h2>
Micro-lifecycles</h2>
<iframe width="560" height="315" src="https://www.youtube-nocookie.com/embed/pDHBqK8gc_E" frameborder="0" allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture" allowfullscreen="true">
</iframe>
<!-- Tron "The Grid" -->
<p>
We’ve explored nostalgia and patterns in popular media such as video games,
movies, and music. What does this have to do with software development?</p>
<p>
Generally, software development consists of applying code patterns to problems.
Software developers, or really <em>any</em> artisans of their craft, notice and
remember patterns. They remember which patterns work well and which don’t; what
problems those solution-patterns are good fits for and terrible fits for.
Software developers are players in the game of software.</p>
<!-- Tron "I got in" -->
<p>
People are amazing pattern-matchers. They look for patterns in everything;
sometimes even finding patterns where there wasn’t supposed to be, like a cloud
formation. In software, patterns that emerge are often collected into groups. At
the macro-level, these patterns are called architectures, such as MVC
(Model-View-Controller) common in backend frameworks, or MVVM
(Model-View-ViewModel) common in frontend frameworks, or REST (Representational
state transfer) common in web communication.</p>
<p>
At the micro-level, it looks like <strong>spoilers</strong> a game. This micro-lifecycle is
how every game works. Let’s look at the event loop of a game from the
perspective of a gamer:</p>
<pre><code>+----------------------------------------------------------+
|                                                          |
|              All forms of feedback:                      |
|     Art, animation, sound, music, movement, story.       |
|                                                          |
+-------+-------------------------------------------^------+
        |                                           |
        |                 A GAME ATOM               |
        | updates                                   |
        |                                      +----+-----------+
        |                                      |                |
   +----v--------+    +---------------+        |  🔁  place     |
   |             |    |               | input  |                |
   |   problem   +----&gt;  preparation  +-------&gt;+ +------------+ |
   |             |    |               |        | |    core    | |
   +-------------+    +---------------+        | |  mechanic  | |
                                               | +------------+ |
                                               |                |
                                               +----------------+</code></pre>
<p>
Let’s change some of these words around and apply it to software development.</p>
<pre><code>+----------------------------------------------------------+
|                                                          |
|              All forms of feedback:                      |
|      code review, performance, test suite, correctness,  |
|               those dang customers                       |
+-------+-------------------------------------------^------+
        |                                           |
        |                 A SOFTWARE UNIT           |
        | updates                                   |
        |                                      +----+-----------+
        |                                      |                |
   +----v--------+    +---------------+        |  🔁  test      |
   |             |    |               | write  |                |
   |   problem   +----&gt;  preparation  +-------&gt;+ +------------+ |
   |             |    |               | code   | |   logic    | |
   +-------------+    +---------------+        | |            | |
                                               | +------------+ |
                                               |                |
                                               +----------------+</code></pre>
<p>
This looks like test-driven development!</p>
<ul>
  <li>
We’re presented a problem  </li>
  <li>
We prepare for a solution  </li>
  <li>
Have a test (place)  </li>
  <li>
Run the logic (core mechanic)  </li>
  <li>
And adjust according to the feedback we experience.  </li>
</ul>
<p>
Software developers and gamers experience the same micro-lifecycles! But let’s
call it what it is: it’s a rapid pattern.</p>
<p>
Software development revolves around languages, architectures, and frameworks.
These are all codified patterns with dissertations, white-papers, systems, and
books written about them. Coders leverage these patterns to solve their
problems faster.</p>
<p>
Ruby on Rails is considered a really productive and fast framework for the Ruby
language. It introduced the MVC architecture to a lot of young developers. Many
developers of folks find Rails enjoyable; you have to follow the patterns that
they set out to keep progress smooth. PHP Laravel is similar. There are dozens
of frameworks that are aimed to be productive.</p>
<p>
It wasn’t always like this though. Let’s talk about JavaScript.</p>
<p>
  <img src="/images/blog/nostalgia-chaos-islands.png" alt="Chaos">
</p>
<p>
Back in the early 2000s, the web looked entirely different. JavaScript, believe
it or not, wasn’t as widely used as it is today. It was a mess; different
browsers implemented different features, and sometimes the same feature but with
different interfaces to it. There weren’t many patterns. It wasn’t as popular
because it wasn’t as enjoyable to work within. Think of it as the wild wild
west; only those willing to leave the comforts of what they knew and explore the
unknown would be able to move the language and its ecosystem forward and help
define it.</p>
<p>
  <img src="/images/blog/nostalgia-jquery-islands.png" alt="jQuery Island">
</p>
<p>
2006 came around and so did a little library called jQuery, which codified how
to do basic actions in JavaScript. It standardized, unofficially, how to access
elements on the page, how to handle events, how to animate. It was an amazing
contribution to the JavaScript community. It’s still used today! jQuery
inspired browser makers and some of those behaviors were codified into the
language properly, and some of those APIs are integrated into the browsers now
and work natively without the library.</p>
<p>
  <img src="/images/blog/nostalgia-angular-islands.png" alt="Angular Island">
</p>
<p>
Several years later in 2010, Google released a framework called Angular (known
as AngularJS now). This was a full framework that applied the MVC and/or MVVM
architecture to JavaScript.</p>
<p>
  <img src="/images/blog/nostalgia-react-islands.png" alt="React Island">
</p>
<p>
Years later in 2013, another library called React was released. Together with
other libraries like React Router, Redux and more, it essentially formed a new
framework.</p>
<p>
  <img src="/images/blog/nostalgia-ember-islands.png" alt="Ember Island">
</p>
<p>
Shortly after in 2014, Vue was released, picking up some loved patterns from
both Angular and React.</p>
<p>
  <img src="/images/blog/nostalgia-vue-islands.png" alt="Vue Island">
</p>
<p>
These islands of patterns are where developers congregated. Like little
nation-states, they liked their leaders, disliked their leaders, stagnated,
experienced rapid growth, poached employees, etc.</p>
<p>
Together, the patterns that these libraries and frameworks introduced helped
make frontend development much more enjoyable. JavaScript today is one of most
prolific languages out there, no doubt in part because patterns emerged.</p>
<p>
I think these patterns determine how much fun we’re going to have programming.</p>
<h2>
Elixir</h2>
<p>
Let’s talk about a software language called Elixir.</p>
<p>
<a href="https://elixir-lang.org/">Elixir</a> is a relatively new (born ~2011) functional
language.</p>
<blockquote>
  <p>
Elixir is a dynamic, functional language designed for building scalable and
maintainable applications.  </p>
</blockquote>
<blockquote>
  <p>
Elixir leverages the Erlang VM, known for running low-latency, distributed and
fault-tolerant systems, while also being successfully used in web development,
embedded software, data ingestion, and multimedia processing domains.  </p>
</blockquote>
<p>
Elixir is beloved by developers, <a href="https://github.com/hugobarauna/elixir-ecosystem-2020-reponses-data">particularly seasoned
developers</a>,
and I want to explore why. In fact, Elixir is the entire motivation for me to
produce this content.</p>
<p>
  <img src="/images/elixir-twitter-like-1.png" alt="I&#39;m more and more impressed by the BEAM the more I learn about Elixir, Erlang, and OTP">

  <img src="/images/elixir-twitter-like-2.png" alt="So this Elixir thing...I think I&#39;m a way bigger fan than I thought I&#39;d be">

  <img src="/images/elixir-twitter-like-3.png" alt="I&#39;m reading the Elixir Phoenix documentation and one thing I can tell is it is
pure engineering poetry. What a great piece of knowledge">

  <img src="/images/elixir-twitter-like-4.png" alt="Elixir is my new go to for backend development...you will quickly see why">

  <img src="/images/elixir-twitter-like-5.png" alt="Last time I wrote Elixir for work was over 3 years ago. I&#39;m so excited to get
back to it.">

  <img src="/images/elixir-twitter-like-6.png" alt="With OTP for distributed and fault-tolerant systems, Phoenix, LiveView, and
now JIT, there&#39;s less and less that isn&#39;t a joy to write in Elixir">
</p>
<blockquote>
  <p>
The Vault team [at Heroku] doing Elixir is only three engineers. Most of their
apps are used internally, so they are generally not worried about performance.
They continue using Elixir because they <strong>feel productive and happy with it</strong>.
They have also found it is an easier language to maintain compared to their
previous experiences.  </p>
</blockquote>
<p>
– <a href="https://elixir-lang.org/blog/2020/09/24/paas-with-elixir-at-Heroku/">“PaaS with Elixir at Heroku”</a></p>
<p>
What makes Elixir so enjoyable? As we have been exploring, fun is the emotional
response to learning, and since people are amazing pattern-matchers, we
constantly look for patterns. Perhaps Elixir has good patterns.</p>
<p>
Let’s look at some common Elixir programming patterns. These patterns can be
found in many other language. Maybe these patterns are why it’s enjoyable to
develop in Elixir?</p>
<h2>
Pipelines</h2>
<p>
When we think about what an application accomplishes, we often frame it in our
minds as a ruleset and transformation of data. That’s where we start, at least.</p>
<p>
  <img src="/images/railway-io.png" alt="Railway I/O - input -&gt; transform -&gt; output">
</p>
<ul>
  <li>
Data comes in  </li>
  <li>
Data is transformed  </li>
  <li>
Data goes out  </li>
</ul>
<p>
In Elixir, you can accomplish this with the pipeline operator:</p>
<pre><code class="makeup elixir"><span class="s">&quot;My   String&quot;</span><span class="w">
</span><span class="o">|&gt;</span><span class="w"> </span><span class="nc">String</span><span class="o">.</span><span class="n">trim</span><span class="p" data-group-id="6696609932-1">(</span><span class="p" data-group-id="6696609932-1">)</span><span class="w">
</span><span class="o">|&gt;</span><span class="w"> </span><span class="nc">String</span><span class="o">.</span><span class="n">replace</span><span class="p" data-group-id="6696609932-2">(</span><span class="s">&quot;My&quot;</span><span class="p">,</span><span class="w"> </span><span class="s">&quot;Your&quot;</span><span class="p" data-group-id="6696609932-2">)</span><span class="w">
</span><span class="o">|&gt;</span><span class="w"> </span><span class="nc">String</span><span class="o">.</span><span class="n">upcase</span><span class="p" data-group-id="6696609932-3">(</span><span class="p" data-group-id="6696609932-3">)</span><span class="w">
</span><span class="c1">#=&gt; &quot;YOUR STRING&quot;</span></code></pre>
<p>
The pipeline starts with a string <code class="inline">&quot;My String&quot;</code> and passes it into the first
position of the next function <code class="inline">String.trim/1</code>. The result of that function is
then passed into the first position of the next function <code class="inline">String.replace(..., &quot;My&quot;, &quot;Your&quot;)</code> and so on until the end when we have the result <code class="inline">&quot;YOUR STRING&quot;</code>.</p>
<p>
Elixir developers reading this are probably already bored. We’ve seen this
pattern a million times. But stick with me!</p>
<p>
It doesn’t have to be all about transformation either. If at any point, we
wanted to see what the data was between any of those functions, we could also
throw in an <code class="inline">IO.inspect()</code> between the functions.</p>
<pre><code class="makeup elixir"><span class="s">&quot;My   String&quot;</span><span class="w">
</span><span class="o">|&gt;</span><span class="w"> </span><span class="nc">String</span><span class="o">.</span><span class="n">trim</span><span class="p" data-group-id="7145709525-1">(</span><span class="p" data-group-id="7145709525-1">)</span><span class="w">
</span><span class="o">|&gt;</span><span class="w"> </span><span class="nc">IO</span><span class="o">.</span><span class="n">inspect</span><span class="p" data-group-id="7145709525-2">(</span><span class="ss">label</span><span class="p">:</span><span class="w"> </span><span class="s">&quot;the string is&quot;</span><span class="p" data-group-id="7145709525-2">)</span><span class="w">
</span><span class="o">|&gt;</span><span class="w"> </span><span class="nc">String</span><span class="o">.</span><span class="n">replace</span><span class="p" data-group-id="7145709525-3">(</span><span class="s">&quot;My&quot;</span><span class="p">,</span><span class="w"> </span><span class="s">&quot;Your&quot;</span><span class="p" data-group-id="7145709525-3">)</span><span class="w">
</span><span class="o">|&gt;</span><span class="w"> </span><span class="nc">String</span><span class="o">.</span><span class="n">upcase</span><span class="p" data-group-id="7145709525-4">(</span><span class="p" data-group-id="7145709525-4">)</span><span class="w">

</span><span class="c1">#=&gt; the string is: &quot;My String&quot;</span><span class="w">
</span><span class="c1">#=&gt; &quot;YOUR STRING&quot;</span></code></pre>
<p>
Move the <code class="inline">IO.inspect</code> down a little bit…</p>
<pre><code class="makeup elixir"><span class="s">&quot;My   String&quot;</span><span class="w">
</span><span class="o">|&gt;</span><span class="w"> </span><span class="nc">String</span><span class="o">.</span><span class="n">trim</span><span class="p" data-group-id="1262867154-1">(</span><span class="p" data-group-id="1262867154-1">)</span><span class="w">
</span><span class="o">|&gt;</span><span class="w"> </span><span class="nc">String</span><span class="o">.</span><span class="n">replace</span><span class="p" data-group-id="1262867154-2">(</span><span class="s">&quot;My&quot;</span><span class="p">,</span><span class="w"> </span><span class="s">&quot;Your&quot;</span><span class="p" data-group-id="1262867154-2">)</span><span class="w">
</span><span class="o">|&gt;</span><span class="w"> </span><span class="nc">IO</span><span class="o">.</span><span class="n">inspect</span><span class="p" data-group-id="1262867154-3">(</span><span class="ss">label</span><span class="p">:</span><span class="w"> </span><span class="s">&quot;the string is&quot;</span><span class="p" data-group-id="1262867154-3">)</span><span class="w">
</span><span class="o">|&gt;</span><span class="w"> </span><span class="nc">String</span><span class="o">.</span><span class="n">upcase</span><span class="p" data-group-id="1262867154-4">(</span><span class="p" data-group-id="1262867154-4">)</span><span class="w">

</span><span class="c1">#=&gt; the string is: &quot;Your String&quot;</span><span class="w">
</span><span class="c1">#=&gt; &quot;YOUR STRING&quot;</span></code></pre>
<p>
It’s a simple and effective way to see the state of your string as it’s passed
through the pipeline.</p>
<p>
The pipeline pattern is everywhere in Elixir, and it makes it clear how the data
is being operated upon. We can see instantly that it’s being trimmed, we’re
replacing a word, and upcasing the string. There’s no need to name interstitial
stages of the string while it’s being operated upon. Opposed to this:</p>
<pre><code class="makeup elixir"><span class="n">trimmed_string</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nc">String</span><span class="o">.</span><span class="n">trim</span><span class="p" data-group-id="6386883919-1">(</span><span class="s">&quot;My String&quot;</span><span class="p" data-group-id="6386883919-1">)</span><span class="w">
</span><span class="n">replaced_string</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nc">String</span><span class="o">.</span><span class="n">replace</span><span class="p" data-group-id="6386883919-2">(</span><span class="n">trimmed_string</span><span class="p">,</span><span class="w"> </span><span class="s">&quot;My&quot;</span><span class="p">,</span><span class="w"> </span><span class="s">&quot;Your&quot;</span><span class="p" data-group-id="6386883919-2">)</span><span class="w">
</span><span class="n">upcased_string</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nc">String</span><span class="o">.</span><span class="n">upcase</span><span class="p" data-group-id="6386883919-3">(</span><span class="n">replaced_string</span><span class="p" data-group-id="6386883919-3">)</span></code></pre>
<p>
This is a simple example, but your eyes have to scan to the right to see the
beginning of the data transformation, and then search for where that variable
goes in the next function.</p>
<p>
Pipelines are everywhere with Elixir.</p>
<p>
The pipeline operator <code class="inline">|&gt;</code> is not in every language, nor does it need to be in
order to write your code towards data transformation pipelines, but because the
operator exists, it steers developers to work in such a pattern.</p>
<p>
Another form of a data pipeline is using the <code class="inline">with</code> macro. You may have noticed
in our simple example about that a string is passed in and out of all the
functions; there wasn’t a great way to check the output if it was successful;
<code class="inline">with</code> helps with that. Let’s look at that in our next pattern:</p>
<h2>
Monads</h2>
<p>
Ok, before I lose you, we’re not going to explore what monads are; it doesn’t
matter. But, their effect is powerful and I want to explore a pattern that they
enable. If you don’t know what a monad is, that’s totally fine. Here’s a simple
definition: A monad is a unit of data and metadata (for the developer) about the
data.</p>
<p>
Not to say that Elixir has complete support for academic and mathematical
monads like Haskell, but the Elixir community’s code style leans towards using
monads, perhaps without even knowing it.</p>
<p>
The best and smallest example I can think of for monads is the Result Monad:</p>
<pre><code class="makeup elixir"><span class="k">case</span><span class="w"> </span><span class="n">this_might_work</span><span class="p" data-group-id="5599423023-1">(</span><span class="p" data-group-id="5599423023-1">)</span><span class="w"> </span><span class="k" data-group-id="5599423023-2">do</span><span class="w">
  </span><span class="p" data-group-id="5599423023-3">{</span><span class="ss">:ok</span><span class="p">,</span><span class="w"> </span><span class="n">success_data</span><span class="p" data-group-id="5599423023-3">}</span><span class="w"> </span><span class="o">-&gt;</span><span class="w">
    </span><span class="n">happy_path</span><span class="p" data-group-id="5599423023-4">(</span><span class="n">success_data</span><span class="p" data-group-id="5599423023-4">)</span><span class="w">

  </span><span class="p" data-group-id="5599423023-5">{</span><span class="ss">:error</span><span class="p">,</span><span class="w"> </span><span class="n">data_about_error</span><span class="p" data-group-id="5599423023-5">}</span><span class="w"> </span><span class="o">-&gt;</span><span class="w">
    </span><span class="n">unhappy_path</span><span class="p" data-group-id="5599423023-6">(</span><span class="n">data_about_error</span><span class="p" data-group-id="5599423023-6">)</span><span class="w">
</span><span class="k" data-group-id="5599423023-2">end</span></code></pre>
<p>
In Elixir, we’re pattern-matching on the result of <code class="inline">this_might_work()</code>, and
that function returns a result monad. The monad in this case is a 2-item tuple.
The tuple begins with either <code class="inline">:ok</code> or <code class="inline">:error</code> which is metadata about the
accompanied data. <code class="inline">{METADATA, DATA}</code></p>
<p>
If the returned tuple’s first element is <code class="inline">:ok</code>, then bind the data to
<code class="inline">success_data</code> and move on. Likewise, if it’s <code class="inline">:error</code>, then bind the
unsuccessful data to <code class="inline">data_about_error</code> and move on.</p>
<p>
This is a pervasive pattern in Elixir. I bet that 100% of Elixir codebases out
there have this pattern in it. It’s extremely effective in how to route the
data in the pipeline.</p>
<p>
Here’s another one:</p>
<pre><code class="makeup elixir"><span class="k">with</span><span class="w"> </span><span class="p" data-group-id="7382999221-1">{</span><span class="ss">:ok</span><span class="p">,</span><span class="w"> </span><span class="n">burger</span><span class="p" data-group-id="7382999221-1">}</span><span class="w"> </span><span class="o">&lt;-</span><span class="w"> </span><span class="nc">McDonalds</span><span class="o">.</span><span class="n">order</span><span class="p" data-group-id="7382999221-2">(</span><span class="s">&quot;burger&quot;</span><span class="p" data-group-id="7382999221-2">)</span><span class="p">,</span><span class="w">
     </span><span class="p" data-group-id="7382999221-3">{</span><span class="ss">:ok</span><span class="p">,</span><span class="w"> </span><span class="n">fries</span><span class="p" data-group-id="7382999221-3">}</span><span class="w"> </span><span class="o">&lt;-</span><span class="w"> </span><span class="nc">McDonalds</span><span class="o">.</span><span class="n">order</span><span class="p" data-group-id="7382999221-4">(</span><span class="s">&quot;fries&quot;</span><span class="p" data-group-id="7382999221-4">)</span><span class="p">,</span><span class="w">
     </span><span class="p" data-group-id="7382999221-5">{</span><span class="ss">:ok</span><span class="p">,</span><span class="w"> </span><span class="n">milkshake</span><span class="p" data-group-id="7382999221-5">}</span><span class="w"> </span><span class="o">&lt;-</span><span class="w"> </span><span class="nc">McDonalds</span><span class="o">.</span><span class="n">order</span><span class="p" data-group-id="7382999221-6">(</span><span class="s">&quot;milkshake&quot;</span><span class="p" data-group-id="7382999221-6">)</span><span class="w"> </span><span class="k" data-group-id="7382999221-7">do</span><span class="w">
  </span><span class="p" data-group-id="7382999221-8">{</span><span class="ss">:ok</span><span class="p">,</span><span class="w"> </span><span class="p" data-group-id="7382999221-9">[</span><span class="n">burger</span><span class="p">,</span><span class="w"> </span><span class="n">fries</span><span class="p">,</span><span class="w"> </span><span class="n">milkshake</span><span class="p" data-group-id="7382999221-9">]</span><span class="p" data-group-id="7382999221-8">}</span><span class="w">
</span><span class="k" data-group-id="7382999221-7">else</span><span class="w">
  </span><span class="p" data-group-id="7382999221-10">{</span><span class="ss">:error</span><span class="p">,</span><span class="w"> </span><span class="n">error_message</span><span class="p" data-group-id="7382999221-10">}</span><span class="w"> </span><span class="o">-&gt;</span><span class="w">
    </span><span class="nc">Emotions</span><span class="o">.</span><span class="n">frustrate</span><span class="p" data-group-id="7382999221-11">(</span><span class="p" data-group-id="7382999221-11">)</span><span class="w">
    </span><span class="nc">Memory</span><span class="o">.</span><span class="n">remember</span><span class="p" data-group-id="7382999221-12">(</span><span class="n">error_message</span><span class="p" data-group-id="7382999221-12">)</span><span class="w">
    </span><span class="p" data-group-id="7382999221-13">{</span><span class="ss">:error</span><span class="p">,</span><span class="w"> </span><span class="s">&quot;did not complete order&quot;</span><span class="p" data-group-id="7382999221-13">}</span><span class="w">
</span><span class="k" data-group-id="7382999221-7">end</span></code></pre>
<p>
Look! We’re combining two patterns now: a pipeline and result monads. Here, we
are chaining several result monads together to form a pipeline with the help of
the <code class="inline">with</code> macro. Inside the <code class="inline">with</code> statement:</p>
<ul>
  <li>
order a burger. If you get a burger, then continue  </li>
  <li>
order some fries. If you get fries, then continue  </li>
  <li>
order a milkshake. If you get a milkshake, then you’re done! Grab your food
and go!  </li>
</ul>
<p>
If they don’t have one of the above items, then the pipeline will stop and
return the first encountered error. <strong>SPOILERS</strong> the ice cream machine was
broken, so we’re going to get an error when ordering a milkshake.</p>
<p>
The result monad is extremely effective in crafting how to route data in
pipelines.</p>
<h2>
Happy Path</h2>
<p>
  <img src="/images/railway-happy.png" alt="Railway Happy/Unhappy path">
</p>
<p>
The previous example includes another pattern: seeing the happy path. I want to
show you a couple of implementations of a process; one in Ruby and another in
Elixir.</p>
<pre><code class="ruby">def order(params)
  user = User.find(params[&quot;user_id&quot;])       #| Happy path
  return unless user                        #| Unhappy path

  item = Warehouse.find(params)             #| Happy path
  return unless item                        #| Unhappy path

  money = Billing.charge_user(user, item)   #| Happy path
  return unless money                       #| Unhappy path

  schedule_delivery(user, item)
end</code></pre>
<p>
In this implementation, I can see that it needs a user, an in-stock item, and
money charged before we schedule a delivery, but visually, I see a mixture of
unhappy and happy paths. It’s a little difficult to quickly pick out what the
desired outcomes are supposed to be. Again, this is a simple example;
real-world code is harder than this in many cases.</p>
<p>
Let’s try this in Elixir using the patterns we’ve identified earlier:</p>
<pre><code class="makeup elixir"><span class="kd">def</span><span class="w"> </span><span class="nf">order</span><span class="p" data-group-id="1321610139-1">(</span><span class="n">params</span><span class="p" data-group-id="1321610139-1">)</span><span class="w"> </span><span class="k" data-group-id="1321610139-2">do</span><span class="w">
  </span><span class="k">with</span><span class="w"> </span><span class="p" data-group-id="1321610139-3">{</span><span class="ss">:ok</span><span class="p">,</span><span class="w"> </span><span class="n">user</span><span class="p" data-group-id="1321610139-3">}</span><span class="w"> </span><span class="o">&lt;-</span><span class="w"> </span><span class="nc">User</span><span class="o">.</span><span class="n">find</span><span class="p" data-group-id="1321610139-4">(</span><span class="n">params</span><span class="p" data-group-id="1321610139-4">)</span><span class="p">,</span><span class="w">               </span><span class="c1">#|\</span><span class="w">
       </span><span class="p" data-group-id="1321610139-5">{</span><span class="ss">:ok</span><span class="p">,</span><span class="w"> </span><span class="n">item</span><span class="p" data-group-id="1321610139-5">}</span><span class="w"> </span><span class="o">&lt;-</span><span class="w"> </span><span class="nc">Warehouse</span><span class="o">.</span><span class="n">find</span><span class="p" data-group-id="1321610139-6">(</span><span class="n">params</span><span class="p" data-group-id="1321610139-6">)</span><span class="p">,</span><span class="w">          </span><span class="c1">#| \ Happy Path</span><span class="w">
       </span><span class="p" data-group-id="1321610139-7">{</span><span class="ss">:ok</span><span class="p">,</span><span class="w"> </span><span class="c">_money</span><span class="p" data-group-id="1321610139-7">}</span><span class="w"> </span><span class="o">&lt;-</span><span class="w"> </span><span class="nc">Billing</span><span class="o">.</span><span class="n">charge</span><span class="p" data-group-id="1321610139-8">(</span><span class="n">user</span><span class="p">,</span><span class="w"> </span><span class="n">item</span><span class="p" data-group-id="1321610139-8">)</span><span class="w"> </span><span class="k" data-group-id="1321610139-9">do</span><span class="w">  </span><span class="c1">#| /</span><span class="w">
    </span><span class="n">schedule_delivery</span><span class="p" data-group-id="1321610139-10">(</span><span class="n">user</span><span class="p">,</span><span class="w"> </span><span class="n">item</span><span class="p" data-group-id="1321610139-10">)</span><span class="w">                      </span><span class="c1">#|/</span><span class="w">
  </span><span class="k" data-group-id="1321610139-9">else</span><span class="w">                                                 </span><span class="c1">#|\</span><span class="w">
    </span><span class="p" data-group-id="1321610139-11">{</span><span class="ss">:error</span><span class="p">,</span><span class="w"> </span><span class="bp">_</span><span class="p" data-group-id="1321610139-11">}</span><span class="w"> </span><span class="o">-&gt;</span><span class="w"> </span><span class="no">nil</span><span class="w">                                 </span><span class="c1">#|_\ Unhappy Path</span><span class="w">
  </span><span class="k" data-group-id="1321610139-9">end</span><span class="w">
</span><span class="k" data-group-id="1321610139-2">end</span></code></pre>
<p>
Here I can see that the happy path is consolidated into one area. I know
exactly what is expected for the process to complete successfully. I also know
where the error cases are handled.</p>
<p>
This code focuses on the solution first, which is important because for many
many developers, the first goal is to make it work, then make it fast, then
make it beautiful. With the solution consolidated at the top of the function,
any readers know what makes it work and where to handle error cases.</p>
<p>
Elixir isn’t perfect in this in all cases; it’s possible in any language to
obfuscate a function’s happy path, but again the tool is here which helps
visually organize the happy path.</p>
<p>
Focusing on the solution is important; developers tend to think of what can go
wrong and develop for those first. This leads to “gold-plating” your code before
it often sees any real usage. More importantly, this way of thinking infects
other areas of your psyche. It leads to anxiety!</p>
<h2>
Totality</h2>
<p>
Total functions are functions that give you a valid return value for every
combination of valid arguments. Total functions are really about communication.
In Elixir, this is accomplished with a mixture of pattern matching and
typespecs.</p>
<p>
Elixir is NOT a statically-typed language, so it’s missing some tools here to
ensure complete totality, but it does have tooling to help with communicating
totality. Elixir can define types, and there’s a tool called Dialyzer that can
integrate with your editor of choice that can provide hints to the developer.
<a href="https://github.com/elixir-lsp/elixir-ls">ElixirLS</a> is one of those tools.</p>
<p>
The pattern of totality in Elixir, in a sense, is simply good documentation.
Developers annotate their code with docs and types to communicate to other
developers what the expected input and output should be, and state guarantees
on how it should work.</p>
<p>
A basic example:
<a href="https://hexdocs.pm/elixir/String.html#upcase/2"><code class="inline">String.upcase</code></a> expects an
input string and outputs a string. It will always succeed if you give it that
input, and will always return to you a string. It does not lie to you; it’s
total.</p>
<p>
Here’s another example:</p>
<pre><code class="makeup elixir"><span class="kd">defmodule</span><span class="w"> </span><span class="nc">HeroAction</span><span class="w"> </span><span class="k" data-group-id="8911486991-1">do</span><span class="w">
  </span><span class="na">@type</span><span class="w"> </span><span class="n">hero</span><span class="w"> </span><span class="o">::</span><span class="w"> </span><span class="nc">WonderWoman</span><span class="o">.</span><span class="n">t</span><span class="p" data-group-id="8911486991-2">(</span><span class="p" data-group-id="8911486991-2">)</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="nc">Batman</span><span class="o">.</span><span class="n">t</span><span class="p" data-group-id="8911486991-3">(</span><span class="p" data-group-id="8911486991-3">)</span><span class="w">
  </span><span class="na">@type</span><span class="w"> </span><span class="n">baddie</span><span class="w"> </span><span class="o">::</span><span class="w"> </span><span class="nc">GenericBaddie</span><span class="o">.</span><span class="n">t</span><span class="p" data-group-id="8911486991-4">(</span><span class="p" data-group-id="8911486991-4">)</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="nc">BigBoss</span><span class="o">.</span><span class="n">t</span><span class="p" data-group-id="8911486991-5">(</span><span class="p" data-group-id="8911486991-5">)</span><span class="w">
  </span><span class="na">@type</span><span class="w"> </span><span class="n">slogan</span><span class="w"> </span><span class="o">::</span><span class="w"> </span><span class="nc">String</span><span class="o">.</span><span class="n">t</span><span class="p" data-group-id="8911486991-6">(</span><span class="p" data-group-id="8911486991-6">)</span><span class="w">

  </span><span class="na">@spec</span><span class="w"> </span><span class="n">fight_criminal</span><span class="p" data-group-id="8911486991-7">(</span><span class="n">hero</span><span class="p" data-group-id="8911486991-8">(</span><span class="p" data-group-id="8911486991-8">)</span><span class="p">,</span><span class="w"> </span><span class="n">baddie</span><span class="p" data-group-id="8911486991-9">(</span><span class="p" data-group-id="8911486991-9">)</span><span class="p" data-group-id="8911486991-7">)</span><span class="w"> </span><span class="o">::</span><span class="w"> </span><span class="p" data-group-id="8911486991-10">{</span><span class="ss">:ok</span><span class="p">,</span><span class="w"> </span><span class="n">slogan</span><span class="p" data-group-id="8911486991-11">(</span><span class="p" data-group-id="8911486991-11">)</span><span class="p" data-group-id="8911486991-10">}</span><span class="w">
  </span><span class="kd">def</span><span class="w"> </span><span class="nf">fight_criminal</span><span class="p" data-group-id="8911486991-12">(</span><span class="p" data-group-id="8911486991-13">%</span><span class="nc" data-group-id="8911486991-13">WonderWoman</span><span class="p" data-group-id="8911486991-13">{</span><span class="p" data-group-id="8911486991-13">}</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">hero</span><span class="p">,</span><span class="w"> </span><span class="p" data-group-id="8911486991-14">%</span><span class="nc" data-group-id="8911486991-14">GenericBaddie</span><span class="p" data-group-id="8911486991-14">{</span><span class="p" data-group-id="8911486991-14">}</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">baddie</span><span class="p" data-group-id="8911486991-12">)</span><span class="w"> </span><span class="k" data-group-id="8911486991-15">do</span><span class="w">
    </span><span class="p" data-group-id="8911486991-16">%</span><span class="nc" data-group-id="8911486991-16">FightScene</span><span class="p" data-group-id="8911486991-16">{</span><span class="w">
      </span><span class="ss">protagonist</span><span class="p">:</span><span class="w"> </span><span class="n">hero</span><span class="p">,</span><span class="w">
      </span><span class="ss">antagonist</span><span class="p">:</span><span class="w"> </span><span class="n">baddie</span><span class="w">
    </span><span class="p" data-group-id="8911486991-16">}</span><span class="w">
    </span><span class="o">|&gt;</span><span class="w"> </span><span class="n">beat_up</span><span class="p" data-group-id="8911486991-17">(</span><span class="n">hero</span><span class="o">.</span><span class="n">bracelets_of_submission</span><span class="p" data-group-id="8911486991-17">)</span><span class="w">
    </span><span class="o">|&gt;</span><span class="w"> </span><span class="n">interrogate</span><span class="p" data-group-id="8911486991-18">(</span><span class="n">hero</span><span class="o">.</span><span class="n">lasso_of_truth</span><span class="p" data-group-id="8911486991-18">)</span><span class="w">
    </span><span class="o">|&gt;</span><span class="w"> </span><span class="n">hand_to_police</span><span class="p" data-group-id="8911486991-19">(</span><span class="p" data-group-id="8911486991-19">)</span><span class="w">

    </span><span class="p" data-group-id="8911486991-20">{</span><span class="ss">:ok</span><span class="p">,</span><span class="w"> </span><span class="s">&quot;I will fight for those who cannot fight for themselves&quot;</span><span class="p" data-group-id="8911486991-20">}</span><span class="w">
  </span><span class="k" data-group-id="8911486991-15">end</span><span class="w">

  </span><span class="kd">def</span><span class="w"> </span><span class="nf">fight_criminal</span><span class="p" data-group-id="8911486991-21">(</span><span class="p" data-group-id="8911486991-22">%</span><span class="nc" data-group-id="8911486991-22">Batman</span><span class="p" data-group-id="8911486991-22">{</span><span class="p" data-group-id="8911486991-22">}</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">hero</span><span class="p">,</span><span class="w"> </span><span class="p" data-group-id="8911486991-23">%</span><span class="nc" data-group-id="8911486991-23">GenericBaddie</span><span class="p" data-group-id="8911486991-23">{</span><span class="p" data-group-id="8911486991-23">}</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">baddie</span><span class="p" data-group-id="8911486991-21">)</span><span class="w"> </span><span class="k" data-group-id="8911486991-24">do</span><span class="w">
    </span><span class="p" data-group-id="8911486991-25">%</span><span class="nc" data-group-id="8911486991-25">FightScene</span><span class="p" data-group-id="8911486991-25">{</span><span class="w">
      </span><span class="ss">protagonist</span><span class="p">:</span><span class="w"> </span><span class="n">hero</span><span class="p">,</span><span class="w">
      </span><span class="ss">antagonist</span><span class="p">:</span><span class="w"> </span><span class="n">baddie</span><span class="w">
    </span><span class="p" data-group-id="8911486991-25">}</span><span class="w">
    </span><span class="o">|&gt;</span><span class="w"> </span><span class="n">beat_up</span><span class="p" data-group-id="8911486991-26">(</span><span class="n">hero</span><span class="o">.</span><span class="n">tools</span><span class="p" data-group-id="8911486991-26">)</span><span class="w">
    </span><span class="o">|&gt;</span><span class="w"> </span><span class="n">interrogate</span><span class="p" data-group-id="8911486991-27">(</span><span class="n">hero</span><span class="o">.</span><span class="n">intimindation</span><span class="p" data-group-id="8911486991-27">)</span><span class="w">
    </span><span class="o">|&gt;</span><span class="w"> </span><span class="n">hand_to_police</span><span class="p" data-group-id="8911486991-28">(</span><span class="p" data-group-id="8911486991-28">)</span><span class="w">

    </span><span class="p" data-group-id="8911486991-29">{</span><span class="ss">:ok</span><span class="p">,</span><span class="w"> </span><span class="s">&quot;I am vengence&quot;</span><span class="p" data-group-id="8911486991-29">}</span><span class="w">
  </span><span class="k" data-group-id="8911486991-24">end</span><span class="w">

  </span><span class="c1"># ...</span><span class="w">
</span><span class="k" data-group-id="8911486991-1">end</span><span class="w">

</span><span class="nc">HeroAction</span><span class="o">.</span><span class="n">fight_criminal</span><span class="p" data-group-id="8911486991-30">(</span><span class="n">batman</span><span class="p">,</span><span class="w"> </span><span class="n">criminal</span><span class="p" data-group-id="8911486991-30">)</span></code></pre>
<p>
The guarantee of this code is that you’re able to hand it anything that matches
the type of <code class="inline">hero</code> and <code class="inline">baddie</code>, and you will always get an <code class="inline">{:ok, slogan}</code> in
return. There are no other possible way this could fail; it’s a total function.
The heroes always win and they take every opportunity to say their slogan.</p>
<p>
Additionally, if you’re using an ElixirLS-enabled editor, you can get some
hints while developing:</p>
<p>
  <img src="/images/elixirls-hint.png" alt="Hint">
</p>
<p>
Communicating with each other is an important part of developing software.
Rarely is software developed by one and only one person, and even if developed
solo, you have your future and past selves to communicate with. Documenting such
typespecs assist you with creating total functions that let you compose
functions safely. That leads us to our final pattern we’ll explore today.</p>
<h2>
Composition</h2>
<p>
Here’s the clencher, all the patterns we see above are repeated everywhere.
When you have a big problem to solve with code, you approach it the same exact
way as you would a small problem: with functions. You don’t have to shift
paradigms in how to solve a problem depending on its scale.</p>
<blockquote>
  <p>
Object-oriented programming has objects in the large, and methods in the small.  </p>
  <p>
Functional programming has functions in the large, and functions in the small.  </p>
</blockquote>
<p>
– <a href="https://www.youtube.com/watch?v=srQt1NAHYC0" title="">Scott Wlaschin: Functional Programming Design Patterns</a></p>
<p>
  <img src="/images/turtles-tiling.png" alt="Turtles all the way down">
</p>
<p>
This is comforting, because when we already have a business problem we’re
trying to solve, we don’t need a language problem or tooling problem clouding
our judgment. Our brain can focus on one thing at a time and “zone out” on
solving the business problem with our familiar and patterned toolset.</p>
<p>
In gaming, it’s important to not frustrate the player with too much difficulty
too soon. You have to give the player tools to solve the presented problems
first. Once they’ve mastered the tools, then you can make them apply their
newfound knowledge to the new problems. Likewise, we want Elixir (our tool) to
not be one of the problems as we solve the main problem (the business).
Otherwise they will give up and tell all their friends “this game sux.”</p>
<h2>
Understood</h2>
<p>
These patterns are pervasive in all Elixir codebases I’ve seen, and because
those patterns are common, developers have a good way to communicate with each
other which is the ultimate goal.</p>
<p>
Nostalgia has a positive effect on the person. These patterns of our past remind
us of who we used to be or some aspect of how we identify ourselves. Most
importantly, <strong>nostalgia increases our sense of belonging and encourages
growth</strong>.</p>
<p>
The <a href="https://www.theoryoffun.com/" title="">Theory of Fun</a> explains that <strong>fun is the emotional response to learning</strong>;
the reward for continuing to learn. What people learn are patterns in a variety
of settings: school, childhood, adulthood, that one time at the bar where I
talked too much, competitions, and (like we explored) video games. <strong>We often
have fun when seeing patterns reoccur</strong>.</p>
<p>
Software development is chock-full of patterns. Developers apply patterns
constantly to problems they encounter when trying to build something. I focused
on Elixir here, but this is true for many languages and frameworks; some maybe
more true than others. <strong>Elixir has excellent patterns, and developers that
share the same patterns understand each other, which is all we really want in
the end anyway – to be understood.</strong></p>
<p>
I’ll leave you with a quote:</p>
<blockquote>
  <p>
To [find good patterns] we must rely on feelings more than intellect.  </p>
  <p>
To work our way towards a shared language once again, we must first learn how
to discover patterns which are deep, and capable of generating life.  </p>
  <p>
The specific patterns, out of which a building or a town is made, may be alive
or dead. To the extent they are alive, they let our inner forces loose, and,
set us free; but when they are dead they keep us locked in inner conflict.  </p>
</blockquote>
<p>
– Christopher Alexandar (author of <a href="https://en.wikipedia.org/wiki/A_Pattern_Language" title="">A Pattern Language</a>)</p>
<hr class="thin">
<p>
Want to learn more about this topic? Check out these inspiring talks:</p>
<ul>
  <li>
<a href="https://www.youtube.com/watch?v=UUvU8cjCIcs" title="">Garrett Smith: The Timeless Way of Building Erlang Apps</a>  </li>
  <li>
<a href="https://www.youtube.com/watch?v=9uvp4h7gXHg" title="">Cameron Price: Micropatterns</a>  </li>
</ul>
<p>
Special thanks to <a href="http://quinnwilton.com/" title="">Quinn Wilton</a> for proofreading!</p>
]]></content:encoded>
</item>
<item>
<title><![CDATA[Moving the blog to Elixir and Phoenix LiveView]]></title>
<dc:creator>David Bernheisel</dc:creator>
<description><![CDATA[I moved my blog. It's now powered by Phoenix LiveView. Let me tell you about
the transition. I outline the features I lost, I gained, and some performance
surprises along the way.
]]></description>
<link>https://bernheisel.com/blog/moving-blog</link>
<guid isPermaLink="true">https://bernheisel.com/blog/moving-blog</guid>
<pubDate>Sat, 15 Aug 2020 00:00:00 -0400</pubDate>
<content:encoded><![CDATA[<p>
I moved my blog. It’s now powered by Phoenix LiveView. Let me tell you about the
transition. I outline the features I lost, I gained, and some performance
surprises along the way.</p>
<p>
What I’m <strong>giving up</strong>:</p>
<ol>
  <li>
Offline access  </li>
  <li>
Progressive Web App capabilities.  </li>
  <li>
Free hosting (bummer.)  </li>
  <li>
Pre-compiled syntax highlighting. Gatsby has a great <a href="https://www.gatsbyjs.com/plugins/gatsby-remark-vscode/" title="">VSCode-powered
  highlighter</a>
  that runs during compilation.  </li>
  <li>
A mature asset-processing pipeline to <a href="https://web.dev/responsive-images/">optimize
images</a>,
especially for responsiveness.  </li>
  <li>
Easier scaling if traffic gets out of hand. It’s fundamentally easier to
scale static content using a CDN service. But, given Elixir’s web request
performance, I’m not too worried about this yet, and maybe I shouldn’t be for
this little ol’ blog.  </li>
</ol>
<p>
What I’m <strong>gaining</strong>:</p>
<ol>
  <li>
A tool-chain that I understand thoroughly and can contribute to; I actually
  know Elixir and Phoenix.  </li>
  <li>
The ability to show off LiveView-enabled components (stay tuned!)  </li>
  <li>
The ability to send UI updates from the backend to the frontend efficiently.
If you didn’t notice, and if there is more than 1 current reader on this
article, look underneath the title of this article. You should see “x current
readers”. That’s enabled by LiveView and reacts immediately. If there aren’t
any other readers, then open a couple of tabs on this page, and watch the
number climb.  </li>
</ol>
<p>
What I’m <strong>not losing</strong>:</p>
<ol>
  <li>
A fast load time.  </li>
  <li>
A reactive local development tool-chain. For example, when editing the post, I
  can save the file and my dev server shows the changes almost immediately.  </li>
</ol>
<p>
The “giving up” list doesn’t bother me too much, at least not on my blog. If
you’re managing a huge static site with lots of pages; perhaps look elsewhere at
the moment. The biggest reason is that Elixir lacks an asset-pipeline to
optimize images. It wouldn’t be impossible to have with a LiveView-powered blog
with optimized images, but as far as I know, you’ll have to roll-your-own.</p>
<p>
I was initially worried that moving a static site to an Elixir-powered web
application would have too many trade-offs, but I was wrong! Many static sites
today don’t offer pre-compiled syntax highlighting, but I was using one, so it’s
a loss for me. Losing the PWA and cached offline access don’t bother me.</p>
<p>
The “gaining” list is worth it to me, as an Elixir developer, because it unlocks
some potential for what I want to do. I’m considering collecting
LiveView-powered components, and how else would I be able to show them off
without a LiveView site? So, the trade-off is totally worth it to me.</p>
<p>
The “not losing” list was surprising. Let’s talk about it; I’ll show you some
informal benchmarks.</p>
<h2>
Let’s look at the dev server</h2>
<p>
Elixir and Phoenix offer live-reloading when assets change (ie, JavaScript and
CSS), and code-reloading when Elixir code changes. To make this reloading work
in the context of a blog written with Markdown, I used
<a href="https://github.com/dashbitco/nimble_publisher">NimblePublisher</a>.
NimblePublisher is a small system that sets up compiling Markdown files into
HTML at compile-time. Create a <code class="inline">/blog</code> folder, put your markdown in there, and
configure Elixir to watch it for changes.</p>
<pre><code class="makeup elixir"><span class="c1"># config/dev.exs</span><span class="w">
</span><span class="n">config</span><span class="w"> </span><span class="ss">:bern</span><span class="p">,</span><span class="w"> </span><span class="nc">BernWeb.Endpoint</span><span class="p">,</span><span class="w">
  </span><span class="ss">live_reload</span><span class="p">:</span><span class="w"> </span><span class="p" data-group-id="1779649453-1">[</span><span class="w">
    </span><span class="ss">patterns</span><span class="p">:</span><span class="w"> </span><span class="p" data-group-id="1779649453-2">[</span><span class="w">
      </span><span class="sr">~r&quot;priv/static/.*(js|css|png|jpeg|jpg|gif|svg)$&quot;</span><span class="p">,</span><span class="w">
      </span><span class="sr">~r&quot;lib/bern_web/(live|views)/.*(ex)$&quot;</span><span class="p">,</span><span class="w">
      </span><span class="sr">~r&quot;lib/bern_web/templates/.*(eex)$&quot;</span><span class="p">,</span><span class="w">
      </span><span class="sr">~r&quot;posts/*/.*(md)$&quot;</span><span class="w"> </span><span class="c1"># &lt;-- right here!</span><span class="w">
    </span><span class="p" data-group-id="1779649453-2">]</span><span class="w">
  </span><span class="p" data-group-id="1779649453-1">]</span></code></pre>
<p>
Now, <strong>a big plus for Elixir is the boot-up time</strong>. Granted if you haven’t
changed files that requires the entire Elixir project to re-compile, booting up
the dev server is <em>very quick</em>. How quick? <strong>36 times quicker</strong>. But before you
run off to social media with this stat, just remember that it’s not very
scientific, nor is it always fair. It’s not apples-to-apples.</p>
<p>
Let’s say that I have inspiration one day and I want to write a blog post. I go
to my blog folder, start the dev server, and start writing. I haven’t changed
anything; I just want to write a post.</p>
<p>
Here’s the Elixir dev server running <code class="inline">iex -S mix phx.server</code>:</p>
<p>
  <img src="/images/elixir-dev-server-blog.png" alt="Elixir Phoenix Boot Up">
</p>
<p>
Now the Gatsby dev server running <code class="inline">yarn run gatsby develop</code>:</p>
<p>
  <img src="/images/gatsby-dev-server-blog.png" alt="Gatsby Boot Up">
</p>
<p>
If I’m reading this correctly, Gatsby took about <strong>36 seconds</strong> to boot. I
hadn’t changed anything in the project! In comparison, Elixir and Phoenix booted
in <strong>1 second</strong>, and that <em>includes Webpack</em>; granted, nothing changed in the
project and some of this is thanks to
<a href="https://github.com/mzgoddard/hard-source-webpack-plugin">HardSourceWebpackPlugin</a>;
but even without that plugin, Elixir is still up and running and serving
requests and whenever Webpack finishes it will live-reload with those compiled
CSS and JS resources.</p>
<p>
Again, I’ll repeat, this is not apples-to-apples. For example, I’m giving up
compile-time syntax highlighting for non-Elixir code blocks. Gatsby is likely
recompiling everything without needing to, whereas Elixir is a compiled language
that only recompiles what it needs to.</p>
<h2>
Syntax Highlighting</h2>
<p>
NimblePublisher also handles syntax-highlighting at compile-time, but
unfortunately only when the language is Elixir or Erlang. Since I write about
other languages, including Ruby, JavaScript, Bash, Vim, and more, I need
more syntax highlighting options. NimblePublisher is using
<a href="https://github.com/elixir-makeup/makeup">Makeup</a> for highlighting; so perhaps I can
contribute by making more Makeup lexers.</p>
<p>
To cover the gap in the meantime, I’m going to syntax-highlight at runtime; in
other words, you visiting this page ran some JavaScript to highlight syntax for
me! Since this blog is now powered with LiveView, I handled it in a hook.</p>
<p>
Let’s see how it works</p>
<pre><code class="javascript">// Configure Webpack to make another bundle for vendored code like Highlight.js
// assets/webpack.config.js

module.exports = (_env, options) =&gt; {
  return {
    // ...
    entry: {
      app: [&quot;./js/app.js&quot;, &quot;./css/app.css&quot;],
      vendor: [&quot;./js/vendor.js&quot;] // &lt;-- this part
    }
  }
}

// ==================================================
// assets/js/vendor.js
import &quot;./highlighter&quot;;


// ==================================================
// assets/js/highlighter.js
import hljs from &#39;highlight.js/lib/core&#39;;
import javascript from &#39;highlight.js/lib/languages/javascript&#39;;
import shell from &#39;highlight.js/lib/languages/shell&#39;;
import bash from &#39;highlight.js/lib/languages/bash&#39;;
import erb from &#39;highlight.js/lib/languages/erb&#39;;
import ruby from &#39;highlight.js/lib/languages/ruby&#39;;
import vim from &#39;highlight.js/lib/languages/vim&#39;;
import yaml from &#39;highlight.js/lib/languages/yaml&#39;;
import json from &#39;highlight.js/lib/languages/json&#39;;
import diff from &#39;highlight.js/lib/languages/diff&#39;;
import xml from &#39;highlight.js/lib/languages/xml&#39;;

// Yeah, this isn&#39;t very sexy, but I&#39;m trying to keep the bundle small
// by only opting into languages I use in blog posts.
hljs.registerLanguage(&#39;javascript&#39;, javascript);
hljs.registerLanguage(&#39;shell&#39;, shell);
hljs.registerLanguage(&#39;bash&#39;, bash);
hljs.registerLanguage(&#39;eex&#39;, erb);
hljs.registerLanguage(&#39;ruby&#39;, ruby);
hljs.registerLanguage(&#39;vim&#39;, vim);
hljs.registerLanguage(&#39;yaml&#39;, yaml);
hljs.registerLanguage(&#39;json&#39;, json);
hljs.registerLanguage(&#39;diff&#39;, diff);
hljs.registerLanguage(&#39;html&#39;, xml);

window.highlightAll = function(where = document) {
  where.querySelectorAll(&#39;pre code&#39;).forEach((block) =&gt; {
    const lang = block.getAttribute(&quot;class&quot;)
    // Since Makeup handles Elixir code at compile-time, I don&#39;t need
    // highlight.js to care about this language.
    if (lang !== &quot;makeup elixir&quot;) {
      const { value: value } = hljs.highlight(lang, block.innerText);
      block.innerHTML = value;
    }
  });
}

window.highlightAll()  // this covers on page load

// ==================================================
// assets/js/hooks.js
let hooks = {}

hooks.Highlight = {
  mounted() {
    window.highlightAll(this.el) // this covers LiveView patches
  }
}

export default hooks

// ==================================================
// assets/js/app.js
import hooks from &quot;./hooks&quot;;

// the normal phoenix LiveView initialization, but passing in the hooks:
window.liveSocket = new LiveSocket(&quot;/live&quot;, Socket, {
  hooks,
});

window.liveSocket.connect();</code></pre>
<pre><code class="html">&lt;!-- lib/bern_web/templates/layout/root.html.leex --&gt;
&lt;script defer phx-track-static type=&quot;text/javascript&quot;
  src=&quot;&lt;%= Routes.static_path(@conn, &quot;/js/vendor.js&quot;) %&gt;&quot;&gt;&lt;/script&gt;


&lt;!-- lib/bern_web/live/blog_show.html.leex --&gt;
&lt;div id=&quot;post-content-&lt;%= @post.id %&gt;&quot; phx-hook=&quot;Highlight&quot; phx-update=&quot;ignore&quot;&gt;
  &lt;%= raw(@post.body) %&gt;
&lt;/div&gt;</code></pre>
<p>
Cool. That’s not terrible, but still I would much-prefer ALL syntax highlighting
happen at compile-time. It’s not like this stuff changes in runtime so there’s
no reason for every visitor’s browser to do syntax highlighting for me. Kudos to
the Gatsby tool-chain for solving this problem via <a href="https://www.gatsbyjs.com/plugins/gatsby-remark-vscode/" title="">gatsby-remark-vscode</a>.</p>
<h2>
What about performance?</h2>
<p>
I’m glad you asked.</p>
<p>
There’s no practical difference in my opinion, which is <strong>an upset to common
perception of JavaScript-powered static sites being <em>much</em> slimmer and faster</strong>.</p>
<p>
Let’s look.</p>
<p>
Here’s the Blog Show page powered by Gatsby:</p>
<p>
  <img src="/images/network-gatsby-show.png" alt="Gatsby Network Show">
</p>
<p>
Here’s the Blog Show page powered by Elixir Phoenix LiveView:</p>
<p>
  <img src="/images/network-phoenix-show.png" alt="Elixir Phoenix Network Show">
</p>
<p>
<strong>REMEMBER</strong> the LiveView-powered blog now requires browser-based syntax
highlighing, so there’s an extra 79.5kb of JavaScript that needs to download and
parse and run. Also notice that there’s an extra font on the Gatsby site since
it needed it on this page.</p>
<p>
All-in-all this is very comparable judging by the <code class="inline">DomContentLoaded</code> and <code class="inline">Load</code>
times, but notice that Elixir and Phoenix are barely beating Gatsby in this
scenario.</p>
<p>
Let’s look at the Blog Index page powered by Gatsby:</p>
<p>
  <img src="/images/network-gatsby-index.png" alt="Gatsby Network Index">
</p>
<p>
Here’s the Blog Index page powered by Elixir Phoenix LiveView:</p>
<p>
  <img src="/images/network-phoenix-index.png" alt="Elixir Phoenix Network Index">
</p>
<p>
Elixir is slower in this case for <code class="inline">DOMContentLoaded</code> and <code class="inline">Load</code> times by tens of
milliseconds. Hopefully imperceptible to most visitors. Also, <strong>that’s not bad
considering this is running on a $5/mo server</strong> in the US East Coast, opposed to
CDN-cached static content at the edge by Cloudflare and GitHub Pages. Also
considering that LiveView-powered index page is <strong>literally loading every single
post it their entirety, but not rendering the bodies of the posts</strong>; so there
is room for optimization here.</p>
<p>
Lastly, just for the giggles, here’s the Lighthouse scores:</p>
<p>
Gatsby:</p>
<p>
  <img src="/images/lighthouse-gatsby.png" alt="Gatsby Network Index">
</p>
<p>
Elixir Phoenix LiveView:</p>
<p>
  <img src="/images/lighthouse-phoenix.png" alt="Elixir Phoenix Network Index">
</p>
<p>
The SEO hit on the Phoenix app is because of a <code class="inline">canonical</code> URL not being
correct. Once the LiveView blog replaces the Gatsby blog, it’ll be 100 again.
Also, the Accessibility score took a hit because of different styling where the
contrast isn’t high enough in one spot. Really, the only difference is Gatsby’s
score of 95 on performance vs LiveView’s score of 99 on performance, and the
lack of PWA on LiveView. I don’t really think these scores mean anything between
the two sites; but take it as you will.</p>
<p>
Now, of course, if you check the score now, you’ll see it’s different because
I’ve since added some web font preloading which <em>kills the score</em>. This only
happens on the first load though, thankfully.</p>
<h2>
Migrating to Phoenix LiveView from Gatsby</h2>
<p>
Here are the big changes. You’ll find several more details on your own if you
consider migrating to NimblePublisher depending on your setup.</p>
<ol>
  <li>
Update all the front-matter to a new format.  </li>
  <li>
Rename all your markdown files. In my case, the structure was
<code class="inline">./blog/blog-id/index.md</code> with images beside the post, and it moved to
<code class="inline">./posts/DATE-blog-id.md</code> with all images moving to <code class="inline">./assets/static/images</code>.  </li>
  <li>
Change all references to images in Markdown posts. <strong>Bummer</strong> point here is
that it does not use the digested version of images referenced in your
Markdown posts. This would require NimblePublisher or Earmark (the markdown
parser) to be aware of resources like images and relative URLs, and replace
them with Phoenix-generated paths. Currently it is not, and not sure how it
could be since it’s happening at compile-time, and asset digests occur after
compilation.  </li>
  <li>
Convert any React components into Phoenix templates.  </li>
  <li>
Make navbar LiveView-aware.  </li>
</ol>
<p>
For example, the front-matter went from this:</p>
<pre><code class="yaml">---
title: &quot;Phoenix LiveView and Views&quot;
tags: [&quot;elixir&quot;, &quot;phoenix&quot;]
date: 2020-06-29
excerpt: |
  Everytime I build a LiveView application, I learn something new and find a new
  pattern, and some concept finally _clicks_. Today, that concept that cemented
  in my mind is how Phoenix and Phoenix LiveView renders templates.

  I want to show you a couple different View-rendering strategies. This should
  help you decide which strategy to use.
---</code></pre>
<p>
To this:</p>
<pre><code class="makeup elixir"><span class="p" data-group-id="2326252033-1">%{</span><span class="w">
  </span><span class="ss">title</span><span class="p">:</span><span class="w"> </span><span class="s">&quot;Phoenix LiveView and Views&quot;</span><span class="p">,</span><span class="w">
  </span><span class="ss">tags</span><span class="p">:</span><span class="w"> </span><span class="p" data-group-id="2326252033-2">[</span><span class="s">&quot;elixir&quot;</span><span class="p">,</span><span class="w"> </span><span class="s">&quot;phoenix&quot;</span><span class="p" data-group-id="2326252033-2">]</span><span class="p">,</span><span class="w">
  </span><span class="ss">description</span><span class="p">:</span><span class="w"> </span><span class="s">&quot;&quot;&quot;
  Every time I build a LiveView application, I learn something new and find a
  new pattern, and some concept finally _clicks_. Today, that concept that
  cemented in my mind is how Phoenix and Phoenix LiveView renders templates.

  I want to show you a couple different View-rendering strategies. This should
  help you decide which strategy to use.
  &quot;&quot;&quot;</span><span class="w">
</span><span class="p" data-group-id="2326252033-1">}</span><span class="w">
</span><span class="o">--</span><span class="o">-</span></code></pre>
<p>
The date moved out of the front-matter and relies only on the filename now on
disk. It was a little tedious, but since my blog only has a handful of articles,
it wasn’t worth it to script something.</p>
<p>
For the most part, the HTML structure of the page is the same as it was on
Gatsby. I literally copy-pasted the HTML from my Gatsby React components into
the Phoenix templates and modified them to not rely on React.</p>
<p>
Getting the navbar links to know what page they were on was another challenge.
I’m using AlpineJS on this blog, so I leverage some event dispatching in
AlpineJS when clicking on navbar links, which is caught and assigns the
active/inactive classes on the links. It’s optimistic UI and shadows the
LiveView click events, but it should be fine.</p>
<p>
For example:</p>
<pre><code class="html">&lt;!-- lib/bern_web/templates/layout/nav.html.eex --&gt;
&lt;!-- loading the route from the conn will take care of the initial page load --&gt;
&lt;!-- further LiveView-powered navigation needs some help from AlpineJS --&gt;

&lt;% [route | _] = @conn.path_info %&gt;
&lt;nav @navigate=&quot;open = false; currentRoute = $event.detail&quot; x-data=&quot;{currentRoute: &#39;&lt;%= route %&gt;&#39;, open: false}&quot;&gt;
  &lt;%= live_redirect &quot;Blog&quot;, to: Routes.blog_path(@conn, :index),
    class: &quot;all my classes&quot;,
    &quot;@click&quot;: &quot;$dispatch(&#39;navigate&#39;, &#39;blog&#39;)&quot;,
    &quot;:class&quot;: &quot;{
      &#39;all my active classes&#39;: currentRoute === &#39;blog&#39;,
      &#39;all my inactive classes&#39;: currentRoute !== &#39;blog&#39;
    }&quot; %&gt;
  &lt;!-- ... --&gt;
&lt;/nav&gt;</code></pre>
<h2>
Conclusion</h2>
<p>
I enjoyed the process. I’m making the right decision for myself to evolve a
static-site blog into a web application that also serves static content. If
you’re <em>only</em> using it for static content, then it’s probably not the right
choice to move to LiveView if all you care about is efficiency.</p>
<p>
It turns out that argument for JavaScript-powered static site being <em>much
better</em> than server-generated content is simply not true. Elixir-powered static
sites could also use some help in the areas of syntax highlighting and asset
pipelines; so perhaps that’s where I can turn some of my attention to next!</p>
<p>
NimblePublisher isn’t the only option out there for Phoenix! You could have <a href="https://github.com/boydm/phoenix_markdown" title="">a
full-fledged Markdown engine</a> in Phoenix that renders the
markdown in runtime and interpolates dynamic content during the request. This
would solve the issue of asset versioning.</p>
<p>
If I got anything wrong or missed something obvious (totally likely) then please
tell me! <a href="https://twitter.com/bernheisel">Hit me up on twitter @bernheisel</a></p>
<h2>
Oh yeah, it’s open source</h2>
<p>
<a href="https://github.com/dbernheisel/bernheisel.com">Check out the source</a>. Lastly,
this is hosted on <a href="https://www.linode.com/?r=11f896e75a7bee1316e6a087df9fd77af1a71553">Linode</a>.</p>
]]></content:encoded>
</item>
<item>
<title><![CDATA[Phoenix LiveView and Views]]></title>
<dc:creator>David Bernheisel</dc:creator>
<description><![CDATA[Every time I build a LiveView application, I learn something new and find a
new pattern, and some concept finally _clicks_. Today, that concept that
cemented in my mind is how Phoenix and Phoenix LiveView renders templates.

I want to show you a couple different View-rendering strategies. This should
help you decide which strategy to use.
]]></description>
<link>https://bernheisel.com/blog/phoenix-liveview-and-views</link>
<guid isPermaLink="true">https://bernheisel.com/blog/phoenix-liveview-and-views</guid>
<pubDate>Mon, 29 Jun 2020 00:00:00 -0400</pubDate>
<content:encoded><![CDATA[<p>
I’ve written a couple LiveView applications now,</p>
<ol>
  <li>
<a href="https://utils.zest.dev/regex" title="">Elixir Regex Tester</a>  </li>
  <li>
A request logger, much like <a href="https://github.com/phoenixframework/phoenix_live_dashboard" title="">Phoenix Live Dashboard</a>‘s  </li>
  <li>
An internal webmail server, for me to receive and send email through
SendGrid. I hope to open-source this soon when it’s ready.  </li>
  <li>
Another private work-related project.  </li>
</ol>
<p>
Everytime I build one, I learn something new and find a new pattern, and some
concept finally <em>clicks</em>. Today, that concept that cemented in my mind is how
Phoenix and Phoenix LiveView renders templates.</p>
<p>
I want to show you a couple different View-rendering strategies. This should
help you decide which strategy to use.</p>
<p>
All of these strategies work, this is purely about opinionated code organizing,
<em>but who doesn’t love reading opinions</em>? Plus we’ll learn how the views are
rendered.</p>
<p>
This is written while using Phoenix LiveView 0.13.3.</p>
<h2>
TL;DR</h2>
<p>
Glossary of examples:</p>
<ol>
  <li>
<code class="inline">MyLive</code> = The LiveView module  </li>
  <li>
<code class="inline">MyView</code> = The standard Phoenix View module, not a LiveView.  </li>
  <li>
<code class="inline">my_live.html.leex</code> = The template rendered by <code class="inline">MyLive</code> or <code class="inline">MyView</code>  </li>
</ol>
<p>
<strong>If you have a simple LiveView</strong>, then you can implement <code class="inline">render(assigns)</code>
and inline your html with the <code class="inline">~L</code> sigil. No <code class="inline">my_live.html.leex</code> file needed.</p>
<p>
<strong>If you have a LiveView with lots of HTML</strong>, then you should use the standard
LiveView placement, and put your <code class="inline">my_live.ex</code> and <code class="inline">my_live.html.leex</code> next to
each other under <code class="inline">lib/my_app_web/live</code>. You don’t need to define <code class="inline">render/1</code>
because the default will work. Omit it.</p>
<p>
<strong>If you have a LiveView with lots of HTML helper functions</strong> that you want to
separate from business logic in the LiveView:</p>
<ol>
  <li>
Add your own standard Phoenix view <code class="inline">MyView</code> (or a better name).  </li>
  <li>
Move your <code class="inline">my_live.html.leex</code> file to the standard Phoenix locations (ie,
<code class="inline">lib/my_app_web/templates/my</code>).  </li>
  <li>
Implement your own <code class="inline">render(assigns)</code> that calls
<code class="inline">MyAppWeb.MyView.render(&quot;my_live.html&quot;, assigns)</code>. Phoenix LiveView will
still work; just remember to keep the html file named with an <code class="inline">.html.leex</code>
extension so the LiveView rendering engine kicks in.  </li>
</ol>
<p>
<strong>Remember that you can create shared Views</strong>. Alternatively, if your
helpers are used across multiple views and are generic, you can create a plain
module that encapsulates your HTML helpers. I usually call mine <code class="inline">ComponentView</code>
and use it inside any of my templates, for example:
<code class="inline">Component.primary_button(&quot;My Link&quot;, to: &quot;yadayada&quot;)</code>.</p>
<p>
<strong>If you want to use a regular View, but co-locate the template to the LiveView module</strong>,
as in you don’t want to go back to the vanilla Phoenix file structure but still
need a separate <code class="inline">MyView</code> for your HTML helpers, you can specify the root
folder and path to look in when creating your <code class="inline">MyView</code> by supplying an option:
<code class="inline">use Phoenix.View, root: &quot;lib/my_app_web/live&quot;, path: &quot;&quot;</code>. This is <a href="https://hexdocs.pm/phoenix/1.5.13/Phoenix.View.html#__using__/1-options">explained in the
<code class="inline">Phoenix.View</code>
docs</a>. This
can be wrapped up into a convenience macro though. Read on for more info.</p>
<p>
<strong>This totally ignores LiveComponent</strong> as an option. If your LiveView can be
broken up into interactive components, then breaking out into a LiveComponent is
a good option to look into and works just like a LiveView. For the purpose of
this post and exploring how rendering works, we’re going to treat LiveComponents
the same as a LiveView.</p>
<h2>
ToC</h2>
<ul>
  <li>
<a href="#default-phoenix">Phoenix Controller/View/Template</a>  </li>
  <li>
<a href="#default-liveview">Phoenix LiveView with a template Part 1</a>  </li>
  <li>
<a href="#pluggy-controllers">Pluggy Controllers</a>  </li>
  <li>
<a href="#default-liveview">Phoenix LiveView with a template Part 2</a>  </li>
  <li>
<a href="#liveview-inline">Phoenix LiveView with an inline template</a>  </li>
  <li>
<a href="#liveview-external">Phoenix LiveView with an external template</a>  </li>
</ul>
<a name="default-phoenix"></a><h2>
Default Phoenix Controller/View/Template</h2>
<p>
First, to remember where we came from, I want to show a standard Phoenix
Controller/View/Template pattern. There are several modules involved that the
<code class="inline">Plug.Conn</code> travels through in order to turn into a response for the end-user.</p>
<ol>
  <li>
Incoming request via <code class="inline">:cowboy</code>  </li>
  <li>
Endpoint  </li>
  <li>
Router  </li>
  <li>
Controller  </li>
  <li>
View  </li>
  <li>
Template (not a module)  </li>
  <li>
Outgoing response via <code class="inline">:cowboy</code>  </li>
</ol>
<pre><code class="makeup elixir"><span class="c1">### Router - lib/my_app_web/router.ex</span><span class="w">
</span><span class="kd">defmodule</span><span class="w"> </span><span class="nc">MyAppWeb.Router</span><span class="w"> </span><span class="k" data-group-id="9416959268-1">do</span><span class="w">
  </span><span class="kn">use</span><span class="w"> </span><span class="nc">MyAppWeb</span><span class="p">,</span><span class="w"> </span><span class="ss">:router</span><span class="w">

  </span><span class="c1"># ...snip...</span><span class="w">
  </span><span class="n">scope</span><span class="w"> </span><span class="s">&quot;/&quot;</span><span class="p">,</span><span class="w"> </span><span class="nc">MyAppWeb</span><span class="w"> </span><span class="k" data-group-id="9416959268-2">do</span><span class="w">
    </span><span class="n">pipe_through</span><span class="w"> </span><span class="ss">:browser</span><span class="w">

    </span><span class="n">get</span><span class="w"> </span><span class="s">&quot;/&quot;</span><span class="p">,</span><span class="w"> </span><span class="nc">PageController</span><span class="p">,</span><span class="w"> </span><span class="ss">:home</span><span class="w">
  </span><span class="k" data-group-id="9416959268-2">end</span><span class="w">
  </span><span class="c1"># ...snip...</span><span class="w">
</span><span class="k" data-group-id="9416959268-1">end</span><span class="w">


</span><span class="c1">### Controller - lib/my_app_web/controllers/page_controller.ex</span><span class="w">
</span><span class="kd">defmodule</span><span class="w"> </span><span class="nc">MyAppWeb.PageController</span><span class="w"> </span><span class="k" data-group-id="9416959268-3">do</span><span class="w">
  </span><span class="kn">use</span><span class="w"> </span><span class="nc">MyAppWeb</span><span class="p">,</span><span class="w"> </span><span class="ss">:controller</span><span class="w">  </span><span class="c1">#&lt;-- injects some logic to handle receiving</span><span class="w">
  </span><span class="c1"># the conn and passing the conn on to cowboy</span><span class="w">

  </span><span class="kd">def</span><span class="w"> </span><span class="nf">home</span><span class="p" data-group-id="9416959268-4">(</span><span class="n">conn</span><span class="p">,</span><span class="w"> </span><span class="c">_params</span><span class="p" data-group-id="9416959268-4">)</span><span class="w"> </span><span class="k" data-group-id="9416959268-5">do</span><span class="w">
    </span><span class="n">render</span><span class="p" data-group-id="9416959268-6">(</span><span class="n">conn</span><span class="p">,</span><span class="w"> </span><span class="s">&quot;home.html&quot;</span><span class="p" data-group-id="9416959268-6">)</span><span class="w">
  </span><span class="k" data-group-id="9416959268-5">end</span><span class="w">
</span><span class="k" data-group-id="9416959268-3">end</span><span class="w">


</span><span class="c1">### View - lib/my_app_web/views/page_view.ex</span><span class="w">
</span><span class="kd">defmodule</span><span class="w"> </span><span class="nc">MyAppWeb.PageView</span><span class="w"> </span><span class="k" data-group-id="9416959268-7">do</span><span class="w">
  </span><span class="kn">use</span><span class="w"> </span><span class="nc">MyAppWeb</span><span class="p">,</span><span class="w"> </span><span class="ss">:view</span><span class="w">  </span><span class="c1">#&lt;-- injects some logic to handle evaluating</span><span class="w">
  </span><span class="c1"># the embedded elixir in your templates</span><span class="w">
</span><span class="k" data-group-id="9416959268-7">end</span></code></pre>
<pre><code class="html">&lt;!-- Template - lib/my_app_web/templates/page/home.html.eex
We&#39;re going to ignore the layout stuff for now. Just know that it&#39;s
also evaluated and this template is a part of it --&gt;
&lt;p&gt;Yo! You&#39;re rendering the home page&lt;/p&gt;</code></pre>
<p>
In my mind, the template is the end of the show, though that’s not technically
correct; the real <strong>end of the line is the controller</strong>. The controller is using
the view module to evaluate the HTML and puts the result into the Plug.Conn’s
<code class="inline">resp_body</code>. The controller terminates the flow and the
once-a-request-and-now-a-response <code class="inline">Plug.Conn</code> is returned to the to the
underlying web server, which delivers the payload to the end-user through the
HTTP connection.</p>
<a name="default-liveview"></a><h2>
Default Phoenix LiveView without <code class="inline">render/1</code></h2>
<p>
We’re here to learn about LiveView though, so let’s see an example of a LiveView
without a <code class="inline">render/1</code> function.</p>
<pre><code class="makeup elixir"><span class="c1">### lib/my_app_web/live/my_live.ex</span><span class="w">
</span><span class="kd">defmodule</span><span class="w"> </span><span class="nc">MyAppWeb.MyLive</span><span class="w"> </span><span class="k" data-group-id="4270818997-1">do</span><span class="w">
  </span><span class="kn">use</span><span class="w"> </span><span class="nc">MyAppWeb</span><span class="p">,</span><span class="w"> </span><span class="ss">:live_view</span><span class="w">

  </span><span class="na">@impl</span><span class="w"> </span><span class="nc">Phoenix.LiveView</span><span class="w">
  </span><span class="kd">def</span><span class="w"> </span><span class="nf">mount</span><span class="p" data-group-id="4270818997-2">(</span><span class="c">_params</span><span class="p">,</span><span class="w"> </span><span class="c">_session</span><span class="p">,</span><span class="w"> </span><span class="n">socket</span><span class="p" data-group-id="4270818997-2">)</span><span class="w"> </span><span class="k" data-group-id="4270818997-3">do</span><span class="w">
    </span><span class="c1"># do stuff</span><span class="w">

    </span><span class="p" data-group-id="4270818997-4">{</span><span class="ss">:ok</span><span class="p">,</span><span class="w"> </span><span class="n">socket</span><span class="p" data-group-id="4270818997-4">}</span><span class="w">
  </span><span class="k" data-group-id="4270818997-3">end</span><span class="w">
</span><span class="k" data-group-id="4270818997-1">end</span></code></pre>
<pre><code class="html">&lt;!-- lib/my_app_web/live/my_live.html.leex --&gt;
&lt;p&gt;Yo! I&#39;m rendered by a LiveView&lt;/p&gt;</code></pre>
<p>
Ok, without a controller, how does a given Phoenix LiveView handle the request?
Here’s a secret: <strong>a LiveView is also an ordinary controller</strong>.</p>
<p>
Now… we may not use it like an ordinary Phoenix controller, but the request is
firstly handled like an ordinary web request; one with a Plug.Conn and a full
HTML response back to the user. The LiveView spices are garnished <em>after</em> the
HTML is delivered to the user and a new websocket is initiated to the server to
the page updates to the page.</p>
<p>
As said in the LiveView docs:</p>
<blockquote>
  <p>
A LiveView begins as a regular HTTP request and HTML response,
and then upgrades to a stateful view on client connect,
guaranteeing a regular HTML page even if JavaScript is disabled.
Any time a stateful view changes or updates its socket assigns, it is
automatically re-rendered and the updates are pushed to the client.  </p>
</blockquote>
<blockquote>
  <p>
Prove it!  </p>
  <p>
 – you  </p>
</blockquote>
<p>
ok ok.. I’ll prove it. To prove that it’s a regular controller, we’ll need to
look at some of Phoenix LiveView’s source code. Let’s look at the code that
makes <code class="inline">live(&quot;/my-route&quot;, MyLive)</code> work in the router.</p>
<pre><code class="makeup elixir"><span class="kd">defmacro</span><span class="w"> </span><span class="nf">live</span><span class="p" data-group-id="1753395438-1">(</span><span class="n">path</span><span class="p">,</span><span class="w"> </span><span class="n">live_view</span><span class="p">,</span><span class="w"> </span><span class="n">action</span><span class="w"> </span><span class="o">\\</span><span class="w"> </span><span class="no">nil</span><span class="p">,</span><span class="w"> </span><span class="n">opts</span><span class="w"> </span><span class="o">\\</span><span class="w"> </span><span class="p" data-group-id="1753395438-2">[</span><span class="p" data-group-id="1753395438-2">]</span><span class="p" data-group-id="1753395438-1">)</span><span class="w"> </span><span class="k" data-group-id="1753395438-3">do</span><span class="w">
  </span><span class="k">quote</span><span class="w"> </span><span class="ss">bind_quoted</span><span class="p">:</span><span class="w"> </span><span class="n">binding</span><span class="p" data-group-id="1753395438-4">(</span><span class="p" data-group-id="1753395438-4">)</span><span class="w"> </span><span class="k" data-group-id="1753395438-5">do</span><span class="w">
    </span><span class="p" data-group-id="1753395438-6">{</span><span class="n">action</span><span class="p">,</span><span class="w"> </span><span class="n">router_options</span><span class="p" data-group-id="1753395438-6">}</span><span class="w"> </span><span class="o">=</span><span class="w">
      </span><span class="nc">Phoenix.LiveView.Router</span><span class="o">.</span><span class="c">__live__</span><span class="p" data-group-id="1753395438-7">(</span><span class="bp">__MODULE__</span><span class="p">,</span><span class="w"> </span><span class="n">live_view</span><span class="p">,</span><span class="w"> </span><span class="n">action</span><span class="p">,</span><span class="w"> </span><span class="n">opts</span><span class="p" data-group-id="1753395438-7">)</span><span class="w">

    </span><span class="c1"># vvvvv THIS PART</span><span class="w">
    </span><span class="nc">Phoenix.Router</span><span class="o">.</span><span class="n">get</span><span class="p" data-group-id="1753395438-8">(</span><span class="n">path</span><span class="p">,</span><span class="w"> </span><span class="nc">Phoenix.LiveView.Plug</span><span class="p">,</span><span class="w"> </span><span class="n">action</span><span class="p">,</span><span class="w"> </span><span class="n">router_options</span><span class="p" data-group-id="1753395438-8">)</span><span class="w">
    </span><span class="c1"># ^^^^^ THIS PART</span><span class="w">
  </span><span class="k" data-group-id="1753395438-5">end</span><span class="w">
</span><span class="k" data-group-id="1753395438-3">end</span></code></pre>
<p>
You see it?! <code class="inline">live()</code> is calling this function:</p>
<pre><code class="makeup elixir"><span class="nc">Phoenix.Router</span><span class="o">.</span><span class="n">get</span><span class="p" data-group-id="0682931351-1">(</span><span class="s">&quot;/my-route&quot;</span><span class="p">,</span><span class="w"> </span><span class="nc">Phoenix.LiveView.Plug</span><span class="p">,</span><span class="w"> </span><span class="c">_action</span><span class="p">,</span><span class="w"> </span><span class="c">_options</span><span class="p" data-group-id="0682931351-1">)</span></code></pre>
<p>
You may recognize this as:</p>
<pre><code class="makeup elixir"><span class="n">get</span><span class="w"> </span><span class="s">&quot;/my-route&quot;</span><span class="p">,</span><span class="w"> </span><span class="nc">MyController</span><span class="p">,</span><span class="w"> </span><span class="ss">:show</span></code></pre>
<p>
in your own router. We’re going to ignore the action and options for this post,
but the important part is that the <code class="inline">live()</code> macro is adding a GET route and
calls the <code class="inline">Phoenix.LiveView.Plug</code></p>
<blockquote>
  <p>
But, that plug isn’t a controller…  </p>
  <p>
– you  </p>
</blockquote>
<p>
Ah, but it is! A Phoenix Controller, even the ones you make, are indeed all just
plugs underneath. <a href="https://hexdocs.pm/phoenix/plug.html"><strong>All Phoenix controllers are
plugs</strong></a>.</p>
<a name="pluggy-controllers"></a><h2>
Pluggy Controllers</h2>
<p>
When your controllers call <code class="inline">use MyAppWeb, :controller</code>, it’s <em>injecting code</em>
into your controller at compile-time. Let’s explore how that works.</p>
<p>
First at step 0 we need to understand that when Elixir code calls <code class="inline">use MyUsingModule</code> it’s actually calling <code class="inline">MyUsingModule.__using__(opts)</code> at
compile-time, and that resulting code is put into the module that called it.
Knowing that, let’s follow the <code class="inline">use</code> trail.</p>
<p>
Starting at the top in our own code:</p>
<pre><code class="makeup elixir"><span class="c1">######################################</span><span class="w">
</span><span class="c1">### Inside MyAppWeb.PageController ###</span><span class="w">
</span><span class="c1">######################################</span><span class="w">

</span><span class="kd">defmodule</span><span class="w"> </span><span class="nc">MyAppWeb.PageController</span><span class="w"> </span><span class="k" data-group-id="6995534225-1">do</span><span class="w">
  </span><span class="kn">use</span><span class="w"> </span><span class="nc">MyAppWeb</span><span class="p">,</span><span class="w"> </span><span class="ss">:controller</span><span class="w">

  </span><span class="kd">def</span><span class="w"> </span><span class="nf">home</span><span class="p" data-group-id="6995534225-2">(</span><span class="n">conn</span><span class="p">,</span><span class="w"> </span><span class="bp">_</span><span class="p" data-group-id="6995534225-2">)</span><span class="w"> </span><span class="k" data-group-id="6995534225-3">do</span><span class="w">
    </span><span class="n">render</span><span class="p" data-group-id="6995534225-4">(</span><span class="n">conn</span><span class="p">,</span><span class="w"> </span><span class="s">&quot;home.html&quot;</span><span class="p" data-group-id="6995534225-4">)</span><span class="w">
  </span><span class="k" data-group-id="6995534225-3">end</span><span class="w">
</span><span class="k" data-group-id="6995534225-1">end</span><span class="w">

</span><span class="c1">#######################</span><span class="w">
</span><span class="c1">### Inside MyAppWeb ###</span><span class="w">
</span><span class="c1">#######################</span><span class="w">

</span><span class="kd">defmacro</span><span class="w"> </span><span class="nf">__using__</span><span class="p" data-group-id="6995534225-5">(</span><span class="n">which</span><span class="p" data-group-id="6995534225-5">)</span><span class="w"> </span><span class="ow">when</span><span class="w"> </span><span class="n">is_atom</span><span class="p" data-group-id="6995534225-6">(</span><span class="n">which</span><span class="p" data-group-id="6995534225-6">)</span><span class="w"> </span><span class="k" data-group-id="6995534225-7">do</span><span class="w">
  </span><span class="n">apply</span><span class="p" data-group-id="6995534225-8">(</span><span class="bp">__MODULE__</span><span class="p">,</span><span class="w"> </span><span class="n">which</span><span class="p">,</span><span class="w"> </span><span class="p" data-group-id="6995534225-9">[</span><span class="p" data-group-id="6995534225-9">]</span><span class="p" data-group-id="6995534225-8">)</span><span class="w">
</span><span class="k" data-group-id="6995534225-7">end</span><span class="w">

</span><span class="kd">def</span><span class="w"> </span><span class="nf">controller</span><span class="w"> </span><span class="k" data-group-id="6995534225-10">do</span><span class="w">
  </span><span class="k">quote</span><span class="w"> </span><span class="k" data-group-id="6995534225-11">do</span><span class="w">
    </span><span class="c1"># vvv let&#39;s look in here vvv</span><span class="w">
    </span><span class="kn">use</span><span class="w"> </span><span class="nc">Phoenix.Controller</span><span class="p">,</span><span class="w"> </span><span class="ss">namespace</span><span class="p">:</span><span class="w"> </span><span class="nc">MyAppWeb</span><span class="w">
    </span><span class="c1"># ^^^ let&#39;s look in here ^^^</span><span class="w">
  </span><span class="k" data-group-id="6995534225-11">end</span><span class="w">
</span><span class="k" data-group-id="6995534225-10">end</span><span class="w">


</span><span class="c1">#################################</span><span class="w">
</span><span class="c1">### Inside Phoenix.Controller ###</span><span class="w">
</span><span class="c1">#################################</span><span class="w">

</span><span class="kd">defmacro</span><span class="w"> </span><span class="nf">__using__</span><span class="p" data-group-id="6995534225-12">(</span><span class="n">opts</span><span class="p" data-group-id="6995534225-12">)</span><span class="w"> </span><span class="k" data-group-id="6995534225-13">do</span><span class="w">
  </span><span class="k">quote</span><span class="w"> </span><span class="ss">bind_quoted</span><span class="p">:</span><span class="w"> </span><span class="p" data-group-id="6995534225-14">[</span><span class="ss">opts</span><span class="p">:</span><span class="w"> </span><span class="n">opts</span><span class="p" data-group-id="6995534225-14">]</span><span class="w"> </span><span class="k" data-group-id="6995534225-15">do</span><span class="w">
    </span><span class="kn">import</span><span class="w"> </span><span class="nc">Phoenix.Controller</span><span class="w">

    </span><span class="c1"># vvv let&#39;s look in here vvv</span><span class="w">
    </span><span class="kn">use</span><span class="w"> </span><span class="nc">Phoenix.Controller.Pipeline</span><span class="p">,</span><span class="w"> </span><span class="n">opts</span><span class="w">
    </span><span class="c1"># ^^^ let&#39;s look in here ^^^</span><span class="w">

    </span><span class="k">if</span><span class="w"> </span><span class="nc">Keyword</span><span class="o">.</span><span class="n">get</span><span class="p" data-group-id="6995534225-16">(</span><span class="n">opts</span><span class="p">,</span><span class="w"> </span><span class="ss">:put_default_views</span><span class="p">,</span><span class="w"> </span><span class="no">true</span><span class="p" data-group-id="6995534225-16">)</span><span class="w"> </span><span class="k" data-group-id="6995534225-17">do</span><span class="w">
      </span><span class="n">plug</span><span class="w"> </span><span class="ss">:put_new_layout</span><span class="p">,</span><span class="w"> </span><span class="p" data-group-id="6995534225-18">{</span><span class="nc">Phoenix.Controller</span><span class="o">.</span><span class="c">__layout__</span><span class="p" data-group-id="6995534225-19">(</span><span class="bp">__MODULE__</span><span class="p">,</span><span class="w"> </span><span class="n">opts</span><span class="p" data-group-id="6995534225-19">)</span><span class="p">,</span><span class="w"> </span><span class="ss">:app</span><span class="p" data-group-id="6995534225-18">}</span><span class="w">
      </span><span class="n">plug</span><span class="w"> </span><span class="ss">:put_new_view</span><span class="p">,</span><span class="w"> </span><span class="nc">Phoenix.Controller</span><span class="o">.</span><span class="c">__view__</span><span class="p" data-group-id="6995534225-20">(</span><span class="bp">__MODULE__</span><span class="p" data-group-id="6995534225-20">)</span><span class="w">
    </span><span class="k" data-group-id="6995534225-17">end</span><span class="w">
  </span><span class="k" data-group-id="6995534225-15">end</span><span class="w">
</span><span class="k" data-group-id="6995534225-13">end</span><span class="w">


</span><span class="c1">##########################################</span><span class="w">
</span><span class="c1">### Inside Phoenix.Controller.Pipeline ###</span><span class="w">
</span><span class="c1">##########################################</span><span class="w">

</span><span class="kd">defmacro</span><span class="w"> </span><span class="nf">__using__</span><span class="p" data-group-id="6995534225-21">(</span><span class="n">opts</span><span class="p" data-group-id="6995534225-21">)</span><span class="w"> </span><span class="k" data-group-id="6995534225-22">do</span><span class="w">
  </span><span class="k">quote</span><span class="w"> </span><span class="ss">bind_quoted</span><span class="p">:</span><span class="w"> </span><span class="p" data-group-id="6995534225-23">[</span><span class="ss">opts</span><span class="p">:</span><span class="w"> </span><span class="n">opts</span><span class="p" data-group-id="6995534225-23">]</span><span class="w"> </span><span class="k" data-group-id="6995534225-24">do</span><span class="w">

    </span><span class="na">@behaviour</span><span class="w"> </span><span class="nc">Plug</span><span class="w">
    </span><span class="c1">## AHA! HERE&#39;S YOUR CONTROLLER PLUG BEHAVIOUR</span><span class="w">

    </span><span class="kn">require</span><span class="w"> </span><span class="nc">Phoenix.Endpoint</span><span class="w">
    </span><span class="kn">import</span><span class="w"> </span><span class="nc">Phoenix.Controller.Pipeline</span><span class="w">

    </span><span class="nc">Module</span><span class="o">.</span><span class="n">register_attribute</span><span class="p" data-group-id="6995534225-25">(</span><span class="bp">__MODULE__</span><span class="p">,</span><span class="w"> </span><span class="ss">:plugs</span><span class="p">,</span><span class="w"> </span><span class="ss">accumulate</span><span class="p">:</span><span class="w"> </span><span class="no">true</span><span class="p" data-group-id="6995534225-25">)</span><span class="w">
    </span><span class="na">@before_compile</span><span class="w"> </span><span class="nc">Phoenix.Controller.Pipeline</span><span class="w">
    </span><span class="na">@phoenix_log_level</span><span class="w"> </span><span class="nc">Keyword</span><span class="o">.</span><span class="n">get</span><span class="p" data-group-id="6995534225-26">(</span><span class="n">opts</span><span class="p">,</span><span class="w"> </span><span class="ss">:log</span><span class="p">,</span><span class="w"> </span><span class="ss">:debug</span><span class="p" data-group-id="6995534225-26">)</span><span class="w">
    </span><span class="na">@phoenix_fallback</span><span class="w"> </span><span class="ss">:unregistered</span><span class="w">

    </span><span class="na">@doc</span><span class="w"> </span><span class="no">false</span><span class="w">
    </span><span class="kd">def</span><span class="w"> </span><span class="nf">init</span><span class="p" data-group-id="6995534225-27">(</span><span class="n">opts</span><span class="p" data-group-id="6995534225-27">)</span><span class="p">,</span><span class="w"> </span><span class="ss">do</span><span class="p">:</span><span class="w"> </span><span class="n">opts</span><span class="w">

    </span><span class="na">@doc</span><span class="w"> </span><span class="no">false</span><span class="w">
    </span><span class="kd">def</span><span class="w"> </span><span class="nf">call</span><span class="p" data-group-id="6995534225-28">(</span><span class="n">conn</span><span class="p">,</span><span class="w"> </span><span class="n">action</span><span class="p" data-group-id="6995534225-28">)</span><span class="w"> </span><span class="ow">when</span><span class="w"> </span><span class="n">is_atom</span><span class="p" data-group-id="6995534225-29">(</span><span class="n">action</span><span class="p" data-group-id="6995534225-29">)</span><span class="w"> </span><span class="k" data-group-id="6995534225-30">do</span><span class="w">
      </span><span class="n">conn</span><span class="w">
      </span><span class="o">|&gt;</span><span class="w"> </span><span class="n">merge_private</span><span class="p" data-group-id="6995534225-31">(</span><span class="w">
        </span><span class="ss">phoenix_controller</span><span class="p">:</span><span class="w"> </span><span class="bp">__MODULE__</span><span class="p">,</span><span class="w">
        </span><span class="ss">phoenix_action</span><span class="p">:</span><span class="w"> </span><span class="n">action</span><span class="w">
      </span><span class="p" data-group-id="6995534225-31">)</span><span class="w">
      </span><span class="c1"># fun fact, this function below was introduced</span><span class="w">
      </span><span class="c1"># ~6 years ago in Phoenix 0.5.0 and utilizes unhygienic functions.</span><span class="w">
      </span><span class="c1"># (as in you&#39;re in deep macro-land and your normal rules don&#39;t apply)</span><span class="w">
      </span><span class="o">|&gt;</span><span class="w"> </span><span class="n">phoenix_controller_pipeline</span><span class="p" data-group-id="6995534225-32">(</span><span class="n">action</span><span class="p" data-group-id="6995534225-32">)</span><span class="w">
    </span><span class="k" data-group-id="6995534225-30">end</span><span class="w">

    </span><span class="na">@doc</span><span class="w"> </span><span class="no">false</span><span class="w">
    </span><span class="kd">def</span><span class="w"> </span><span class="nf">action</span><span class="p" data-group-id="6995534225-33">(</span><span class="p" data-group-id="6995534225-34">%</span><span class="nc" data-group-id="6995534225-34">Plug.Conn</span><span class="p" data-group-id="6995534225-34">{</span><span class="ss">private</span><span class="p">:</span><span class="w"> </span><span class="p" data-group-id="6995534225-35">%{</span><span class="ss">phoenix_action</span><span class="p">:</span><span class="w"> </span><span class="n">action</span><span class="p" data-group-id="6995534225-35">}</span><span class="p" data-group-id="6995534225-34">}</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">conn</span><span class="p">,</span><span class="w"> </span><span class="c">_options</span><span class="p" data-group-id="6995534225-33">)</span><span class="w"> </span><span class="k" data-group-id="6995534225-36">do</span><span class="w">
      </span><span class="n">apply</span><span class="p" data-group-id="6995534225-37">(</span><span class="bp">__MODULE__</span><span class="p">,</span><span class="w"> </span><span class="n">action</span><span class="p">,</span><span class="w"> </span><span class="p" data-group-id="6995534225-38">[</span><span class="n">conn</span><span class="p">,</span><span class="w"> </span><span class="n">conn</span><span class="o">.</span><span class="n">params</span><span class="p" data-group-id="6995534225-38">]</span><span class="p" data-group-id="6995534225-37">)</span><span class="w">
    </span><span class="k" data-group-id="6995534225-36">end</span><span class="w">

    </span><span class="n">defoverridable</span><span class="w"> </span><span class="ss">init</span><span class="p">:</span><span class="w"> </span><span class="mi">1</span><span class="p">,</span><span class="w"> </span><span class="ss">call</span><span class="p">:</span><span class="w"> </span><span class="mi">2</span><span class="p">,</span><span class="w"> </span><span class="ss">action</span><span class="p">:</span><span class="w"> </span><span class="mi">2</span><span class="w">
  </span><span class="k" data-group-id="6995534225-24">end</span><span class="w">
</span><span class="k" data-group-id="6995534225-22">end</span></code></pre>
<p>
Wow! Wild. All this means our slim controllers actually have a lot more code in
it than it appears, and that’s ok because it makes working in Phoenix much more
convenient.</p>
<p>
<a href="https://hexdocs.pm/plug/Plug.html">All plugs must implement <code class="inline">call/2</code> which accepts a conn and returns a
conn</a>. In our case, we’re looking for a conn
that has some rendered HTML.</p>
<a name="liveview-default-2"></a><h2>
Back to Default Phoenix LiveView without <code class="inline">render/1</code></h2>
<p>
Now that we know that LiveViews are a <code class="inline">GET</code> request using a standard
controller/plug underneath, let’s look at the <code class="inline">Phoenix.LiveView.Plug</code>. We’re
still looking for how a LiveView gets to the template.</p>
<p>
LiveView has a similar <code class="inline">__using__</code> code-path. Let’s look at LiveView’s plug:</p>
<pre><code class="makeup elixir"><span class="kd">def</span><span class="w"> </span><span class="nf">call</span><span class="p" data-group-id="4003555221-1">(</span><span class="p" data-group-id="4003555221-2">%{</span><span class="ss">private</span><span class="p">:</span><span class="w"> </span><span class="p" data-group-id="4003555221-3">%{</span><span class="ss">phoenix_live_view</span><span class="p">:</span><span class="w"> </span><span class="p" data-group-id="4003555221-4">{</span><span class="n">view</span><span class="p">,</span><span class="w"> </span><span class="n">opts</span><span class="p" data-group-id="4003555221-4">}</span><span class="p" data-group-id="4003555221-3">}</span><span class="p" data-group-id="4003555221-2">}</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">conn</span><span class="p">,</span><span class="w"> </span><span class="bp">_</span><span class="p" data-group-id="4003555221-1">)</span><span class="w"> </span><span class="k" data-group-id="4003555221-5">do</span><span class="w">
  </span><span class="n">opts</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">maybe_dispatch_session</span><span class="p" data-group-id="4003555221-6">(</span><span class="n">conn</span><span class="p">,</span><span class="w"> </span><span class="n">opts</span><span class="p" data-group-id="4003555221-6">)</span><span class="w">

  </span><span class="c1"># ...snip... there&#39;s a lot of code here we&#39;re going to skip</span><span class="w">
  </span><span class="n">conn</span><span class="w">
  </span><span class="o">|&gt;</span><span class="w"> </span><span class="nc">Phoenix.Controller</span><span class="o">.</span><span class="n">put_layout</span><span class="p" data-group-id="4003555221-7">(</span><span class="no">false</span><span class="p" data-group-id="4003555221-7">)</span><span class="w">
  </span><span class="o">|&gt;</span><span class="w"> </span><span class="n">put_root_layout_from_router</span><span class="p" data-group-id="4003555221-8">(</span><span class="n">opts</span><span class="p" data-group-id="4003555221-8">)</span><span class="w">
  </span><span class="c1"># this actually is piped into `Controller.live_render(view, opts)`</span><span class="w">
  </span><span class="c1"># but I&#39;m going to cut/paste what that ends up doing</span><span class="w">
  </span><span class="o">|&gt;</span><span class="w"> </span><span class="nc">LiveView.Static</span><span class="o">.</span><span class="n">render</span><span class="p" data-group-id="4003555221-9">(</span><span class="n">conn</span><span class="p">,</span><span class="w"> </span><span class="n">view</span><span class="p">,</span><span class="w"> </span><span class="n">opts</span><span class="p" data-group-id="4003555221-9">)</span><span class="w">
  </span><span class="c1"># ... more snipping...</span><span class="w">
  </span><span class="o">|&gt;</span><span class="w"> </span><span class="n">to_rendered_content_tag</span><span class="p" data-group-id="4003555221-10">(</span><span class="n">socket</span><span class="p">,</span><span class="w"> </span><span class="n">tag</span><span class="p">,</span><span class="w"> </span><span class="n">view</span><span class="p">,</span><span class="w"> </span><span class="n">attrs</span><span class="p" data-group-id="4003555221-10">)</span><span class="w">
  </span><span class="c1"># ... more snipping...</span><span class="w">
  </span><span class="o">|&gt;</span><span class="w"> </span><span class="n">view</span><span class="o">.</span><span class="n">render</span><span class="p" data-group-id="4003555221-11">(</span><span class="p" data-group-id="4003555221-11">)</span><span class="w">  </span><span class="c1">#&lt;-- here here here!</span><span class="w">
</span><span class="k" data-group-id="4003555221-5">end</span></code></pre>
<p>
Cool; this isn’t anything new so far. This is just confirming that Phoenix
LiveView starts off as a regular HTTP request with a full HTML response. <em>How
does it render?</em></p>
<p>
We see that it’s calling <code class="inline">view.render()</code> where <code class="inline">view</code> is our own LiveView, but
we didn’t define <code class="inline">render/1</code> yet! Where’s it coming from?</p>
<p>
When we called <code class="inline">use MyAppWeb, :live_view</code> it kicked off a series of <code class="inline">__using__</code>,
which includes <code class="inline">use Phoenix.LiveView</code>. Inside <code class="inline">Phoenix.LiveView</code> it included a
<code class="inline">@before_compile Phoenix.LiveView.Renderer</code> hook. Let’s check that out.</p>
<pre><code class="makeup elixir"><span class="n">render?</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nc">Module</span><span class="o">.</span><span class="n">defines?</span><span class="p" data-group-id="5752048336-1">(</span><span class="n">env</span><span class="o">.</span><span class="n">module</span><span class="p">,</span><span class="w"> </span><span class="p" data-group-id="5752048336-2">{</span><span class="ss">:render</span><span class="p">,</span><span class="w"> </span><span class="mi">1</span><span class="p" data-group-id="5752048336-2">}</span><span class="p" data-group-id="5752048336-1">)</span><span class="w">
</span><span class="n">root</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nc">Path</span><span class="o">.</span><span class="n">dirname</span><span class="p" data-group-id="5752048336-3">(</span><span class="n">env</span><span class="o">.</span><span class="n">file</span><span class="p" data-group-id="5752048336-3">)</span><span class="w">
</span><span class="n">filename</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">template_filename</span><span class="p" data-group-id="5752048336-4">(</span><span class="n">env</span><span class="p" data-group-id="5752048336-4">)</span><span class="w">
</span><span class="n">templates</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nc">Phoenix.Template</span><span class="o">.</span><span class="n">find_all</span><span class="p" data-group-id="5752048336-5">(</span><span class="n">root</span><span class="p">,</span><span class="w"> </span><span class="n">filename</span><span class="p" data-group-id="5752048336-5">)</span><span class="w">

</span><span class="k">case</span><span class="w"> </span><span class="p" data-group-id="5752048336-6">{</span><span class="n">render?</span><span class="p">,</span><span class="w"> </span><span class="n">templates</span><span class="p" data-group-id="5752048336-6">}</span><span class="w"> </span><span class="k" data-group-id="5752048336-7">do</span><span class="w">
  </span><span class="p" data-group-id="5752048336-8">{</span><span class="no">false</span><span class="p">,</span><span class="w"> </span><span class="p" data-group-id="5752048336-9">[</span><span class="n">template</span><span class="p" data-group-id="5752048336-9">]</span><span class="p" data-group-id="5752048336-8">}</span><span class="w"> </span><span class="o">-&gt;</span><span class="w">
    </span><span class="n">ext</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">template</span><span class="w"> </span><span class="o">|&gt;</span><span class="w"> </span><span class="nc">Path</span><span class="o">.</span><span class="n">extname</span><span class="p" data-group-id="5752048336-10">(</span><span class="p" data-group-id="5752048336-10">)</span><span class="w"> </span><span class="o">|&gt;</span><span class="w"> </span><span class="nc">String</span><span class="o">.</span><span class="n">trim_leading</span><span class="p" data-group-id="5752048336-11">(</span><span class="s">&quot;.&quot;</span><span class="p" data-group-id="5752048336-11">)</span><span class="w"> </span><span class="o">|&gt;</span><span class="w"> </span><span class="nc">String</span><span class="o">.</span><span class="n">to_atom</span><span class="p" data-group-id="5752048336-12">(</span><span class="p" data-group-id="5752048336-12">)</span><span class="w">
    </span><span class="n">engine</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nc">Map</span><span class="o">.</span><span class="n">fetch!</span><span class="p" data-group-id="5752048336-13">(</span><span class="nc">Phoenix.Template</span><span class="o">.</span><span class="n">engines</span><span class="p" data-group-id="5752048336-14">(</span><span class="p" data-group-id="5752048336-14">)</span><span class="p">,</span><span class="w"> </span><span class="n">ext</span><span class="p" data-group-id="5752048336-13">)</span><span class="w">
    </span><span class="n">ast</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">engine</span><span class="o">.</span><span class="n">compile</span><span class="p" data-group-id="5752048336-15">(</span><span class="n">template</span><span class="p">,</span><span class="w"> </span><span class="n">filename</span><span class="p" data-group-id="5752048336-15">)</span><span class="w">

    </span><span class="k">quote</span><span class="w"> </span><span class="k" data-group-id="5752048336-16">do</span><span class="w">
      </span><span class="na">@file</span><span class="w"> </span><span class="k">unquote</span><span class="p" data-group-id="5752048336-17">(</span><span class="n">template</span><span class="p" data-group-id="5752048336-17">)</span><span class="w">
      </span><span class="na">@external_resource</span><span class="w"> </span><span class="k">unquote</span><span class="p" data-group-id="5752048336-18">(</span><span class="n">template</span><span class="p" data-group-id="5752048336-18">)</span><span class="w">
      </span><span class="kd">def</span><span class="w"> </span><span class="nf">render</span><span class="p" data-group-id="5752048336-19">(</span><span class="n">var!</span><span class="p" data-group-id="5752048336-20">(</span><span class="n">assigns</span><span class="p" data-group-id="5752048336-20">)</span><span class="p" data-group-id="5752048336-19">)</span><span class="w"> </span><span class="k" data-group-id="5752048336-21">do</span><span class="w">
        </span><span class="k">unquote</span><span class="p" data-group-id="5752048336-22">(</span><span class="n">ast</span><span class="p" data-group-id="5752048336-22">)</span><span class="w">
      </span><span class="k" data-group-id="5752048336-21">end</span><span class="w">
    </span><span class="k" data-group-id="5752048336-16">end</span><span class="w">
  </span><span class="c1"># ... other clauses</span><span class="w">
</span><span class="k" data-group-id="5752048336-7">end</span></code></pre>
<p>
Finally! This is where the default <code class="inline">render/1</code> function comes from. Before our
LiveView compiles, it checks to see if a <code class="inline">render/1</code> is defined, and if not, it
will drop one in for us. The default location for LiveView templates is right
next to the LiveView file itself. We see this from the <code class="inline">root = Path.dirname(env.file)</code>.</p>
<a name="liveview-inline"></a><h2>
Phoenix LiveView with inline <code class="inline">render/1</code></h2>
<p>
Another option is to implement <code class="inline">render/1</code> ourselves. <a href="https://hexdocs.pm/phoenix_live_view/Phoenix.LiveView.html#module-life-cycle">The docs make this pretty
clear how to do that</a>.</p>
<pre><code class="makeup elixir"><span class="c1">### lib/my_app_web/live/my_live.ex</span><span class="w">
</span><span class="kd">defmodule</span><span class="w"> </span><span class="nc">MyAppWeb.MyLive</span><span class="w"> </span><span class="k" data-group-id="8474747028-1">do</span><span class="w">
  </span><span class="kn">use</span><span class="w"> </span><span class="nc">MyAppWeb</span><span class="p">,</span><span class="w"> </span><span class="ss">:live_view</span><span class="w">

  </span><span class="na">@impl</span><span class="w"> </span><span class="nc">Phoenix.LiveView</span><span class="w">
  </span><span class="kd">def</span><span class="w"> </span><span class="nf">mount</span><span class="p" data-group-id="8474747028-2">(</span><span class="c">_params</span><span class="p">,</span><span class="w"> </span><span class="c">_session</span><span class="p">,</span><span class="w"> </span><span class="n">socket</span><span class="p" data-group-id="8474747028-2">)</span><span class="w"> </span><span class="k" data-group-id="8474747028-3">do</span><span class="w">
    </span><span class="c1"># do stuff</span><span class="w">

    </span><span class="p" data-group-id="8474747028-4">{</span><span class="ss">:ok</span><span class="p">,</span><span class="w"> </span><span class="n">socket</span><span class="p" data-group-id="8474747028-4">}</span><span class="w">
  </span><span class="k" data-group-id="8474747028-3">end</span><span class="w">

  </span><span class="na">@impl</span><span class="w"> </span><span class="nc">Phoenix.LiveView</span><span class="w">
  </span><span class="kd">def</span><span class="w"> </span><span class="nf">render</span><span class="p" data-group-id="8474747028-5">(</span><span class="n">assigns</span><span class="p" data-group-id="8474747028-5">)</span><span class="w"> </span><span class="k" data-group-id="8474747028-6">do</span><span class="w">
    </span><span class="sx">~L&quot;&quot;&quot;
    &lt;p&gt;Yo! I&#39;m rendered from a &lt;%= my_helper(&quot;vanilla&quot;) %&gt; view&lt;/p&gt;
    &quot;&quot;&quot;</span><span class="w">
  </span><span class="k" data-group-id="8474747028-6">end</span><span class="w">

  </span><span class="kd">def</span><span class="w"> </span><span class="nf">my_helper</span><span class="p" data-group-id="8474747028-7">(</span><span class="s">&quot;vanilla&quot;</span><span class="p" data-group-id="8474747028-7">)</span><span class="p">,</span><span class="w"> </span><span class="ss">do</span><span class="p">:</span><span class="w"> </span><span class="s">&quot;whoops no this is actually live&quot;</span><span class="w">
</span><span class="k" data-group-id="8474747028-1">end</span></code></pre>
<p>
This feels the most similar to frontend frameworks such as Vue with
single-file-components (SFCs), or React.</p>
<p>
This is a great option in case your LiveView doesn’t have a lot of HTML. Perhaps
you’re implementing a small widget. At some point, however, it becomes a little
crowded if you have a lot of business logic handling changes in the LiveView as
well as hundreds of lines of HTML and functions to conditionally render some
HTML or apply CSS classes; so you might consider separating the HTML out into
its own file.</p>
<a name="liveview-external"></a><h2>
Phoenix LiveView with external <code class="inline">render/1</code></h2>
<pre><code class="makeup elixir"><span class="c1">### lib/my_app_web/live/my_live.ex</span><span class="w">
</span><span class="kd">defmodule</span><span class="w"> </span><span class="nc">MyAppWeb.MyLive</span><span class="w"> </span><span class="k" data-group-id="4464168078-1">do</span><span class="w">
  </span><span class="kn">use</span><span class="w"> </span><span class="nc">MyAppWeb</span><span class="p">,</span><span class="w"> </span><span class="ss">:live_view</span><span class="w">

  </span><span class="na">@impl</span><span class="w"> </span><span class="nc">Phoenix.LiveView</span><span class="w">
  </span><span class="kd">def</span><span class="w"> </span><span class="nf">mount</span><span class="p" data-group-id="4464168078-2">(</span><span class="c">_params</span><span class="p">,</span><span class="w"> </span><span class="c">_session</span><span class="p">,</span><span class="w"> </span><span class="n">socket</span><span class="p" data-group-id="4464168078-2">)</span><span class="w"> </span><span class="k" data-group-id="4464168078-3">do</span><span class="w">
    </span><span class="c1"># do stuff</span><span class="w">

    </span><span class="p" data-group-id="4464168078-4">{</span><span class="ss">:ok</span><span class="p">,</span><span class="w"> </span><span class="n">socket</span><span class="p" data-group-id="4464168078-4">}</span><span class="w">
  </span><span class="k" data-group-id="4464168078-3">end</span><span class="w">

  </span><span class="na">@impl</span><span class="w"> </span><span class="nc">Phoenix.LiveView</span><span class="w">
  </span><span class="kd">def</span><span class="w"> </span><span class="nf">render</span><span class="p" data-group-id="4464168078-5">(</span><span class="n">assigns</span><span class="p" data-group-id="4464168078-5">)</span><span class="w"> </span><span class="k" data-group-id="4464168078-6">do</span><span class="w">
    </span><span class="nc">MyAppWeb.MyView</span><span class="o">.</span><span class="n">render</span><span class="p" data-group-id="4464168078-7">(</span><span class="s">&quot;my_live.html&quot;</span><span class="p">,</span><span class="w"> </span><span class="n">assigns</span><span class="p" data-group-id="4464168078-7">)</span><span class="w">
  </span><span class="k" data-group-id="4464168078-6">end</span><span class="w">
</span><span class="k" data-group-id="4464168078-1">end</span><span class="w">


</span><span class="c1">### lib/my_app_web/views/my_view.ex</span><span class="w">
</span><span class="kd">defmodule</span><span class="w"> </span><span class="nc">MyAppWeb.MyView</span><span class="w"> </span><span class="k" data-group-id="4464168078-8">do</span><span class="w">
  </span><span class="kn">use</span><span class="w"> </span><span class="nc">MyAppWeb</span><span class="p">,</span><span class="w"> </span><span class="ss">:view</span><span class="w">

  </span><span class="kd">def</span><span class="w"> </span><span class="nf">my_helper</span><span class="p" data-group-id="4464168078-9">(</span><span class="s">&quot;vanilla&quot;</span><span class="p" data-group-id="4464168078-9">)</span><span class="p">,</span><span class="w"> </span><span class="ss">do</span><span class="p">:</span><span class="w"> </span><span class="s">&quot;whoops no this is actually live&quot;</span><span class="w">
</span><span class="k" data-group-id="4464168078-8">end</span></code></pre>
<pre><code class="eex">&lt;!-- lib/my_app_web/templates/my/my_live.html.leex --&gt;
&lt;p&gt;Yo! I&#39;m rendered from a &lt;%= my_helper(&quot;vanilla&quot;) %&gt; view&lt;/p&gt;
&lt;!-- this renders &quot;whoops no this is actually live&quot; instead of &quot;vanilla&quot; --&gt;</code></pre>
<p>
If I have a lot of HTML helpers, then I tend to prefer separating that into a
View module. It’s a little tedious to setup and separate the files, and then
jump between them when developing, but it’s clear where functions should go.</p>
<p>
This bugged me though, I have HTML floating in <code class="inline">./templates</code> and sometimes in
<code class="inline">./live</code> and sometimes inline. Can we consolidate?</p>
<p>
Sure we can! <code class="inline">Phoenix.View</code> <a href="https://hexdocs.pm/phoenix/1.5.13/Phoenix.View.html?#__using__/1">provides an option to look for templates in a
different folder</a>.
Let’s try it out. We need to supply <code class="inline">root</code> and <code class="inline">path</code> with <code class="inline">use Phoenix.View</code>:</p>
<pre><code class="makeup elixir"><span class="c1">### lib/my_app_web/views/my_view.ex</span><span class="w">
</span><span class="kd">defmodule</span><span class="w"> </span><span class="nc">MyAppWeb.MyView</span><span class="w"> </span><span class="k" data-group-id="3732435713-1">do</span><span class="w">
  </span><span class="kn">use</span><span class="w"> </span><span class="nc">Phoenix.View</span><span class="p">,</span><span class="w">
    </span><span class="ss">root</span><span class="p">:</span><span class="w"> </span><span class="s">&quot;lib/my_app_web/live&quot;</span><span class="p">,</span><span class="w">
    </span><span class="ss">path</span><span class="p">:</span><span class="w"> </span><span class="s">&quot;&quot;</span><span class="p">,</span><span class="w">
    </span><span class="ss">namespace</span><span class="p">:</span><span class="w"> </span><span class="nc">MyAppWeb</span><span class="w">
  </span><span class="c1"># and all the other imports that come with `use MyAppWeb, :view`</span><span class="w">

  </span><span class="kd">def</span><span class="w"> </span><span class="nf">my_helper</span><span class="p" data-group-id="3732435713-2">(</span><span class="s">&quot;vanilla&quot;</span><span class="p" data-group-id="3732435713-2">)</span><span class="p">,</span><span class="w"> </span><span class="ss">do</span><span class="p">:</span><span class="w"> </span><span class="s">&quot;whoops no this is actually live&quot;</span><span class="w">
</span><span class="k" data-group-id="3732435713-1">end</span></code></pre>
<pre><code class="eex">&lt;!-- lib/my_app_web/live/my_live.html.leex --&gt;
&lt;p&gt;Yo! I&#39;m rendered from a &lt;%= my_helper(&quot;vanilla&quot;) %&gt; view&lt;/p&gt;
&lt;!-- this renders &quot;whoops no this is actually live&quot; instead of &quot;vanilla&quot; --&gt;</code></pre>
<p>
It’s exactly the same, except where the HTML is on disk and that we can’t use
our <code class="inline">use MyAppWeb, :view</code> as-is anymore without some further adjustment. To
prove the concept though, copy out all the additional imports you find for views
in <code class="inline">my_app_web.ex</code> and place it here for now. If it works out, then you can add
another clause in <code class="inline">my_app_web.ex</code> to handle these kinds of views. Maybe
something like this.</p>
<pre><code class="makeup elixir"><span class="c1">### lib/my_app_web.ex</span><span class="w">

</span><span class="kd">def</span><span class="w"> </span><span class="nf">view_for_live</span><span class="w"> </span><span class="k" data-group-id="4466751490-1">do</span><span class="w">
  </span><span class="k">quote</span><span class="w"> </span><span class="k" data-group-id="4466751490-2">do</span><span class="w">
    </span><span class="kn">use</span><span class="w"> </span><span class="nc">Phoenix.View</span><span class="p">,</span><span class="w">
      </span><span class="ss">root</span><span class="p">:</span><span class="w"> </span><span class="s">&quot;lib/my_app_web/live&quot;</span><span class="p">,</span><span class="w">
      </span><span class="ss">path</span><span class="p">:</span><span class="w"> </span><span class="s">&quot;&quot;</span><span class="p">,</span><span class="w">
      </span><span class="ss">namespace</span><span class="p">:</span><span class="w"> </span><span class="nc">MyAppWeb</span><span class="w">

    </span><span class="c1"># Import convenience functions from controllers</span><span class="w">
    </span><span class="kn">import</span><span class="w"> </span><span class="nc">Phoenix.Controller</span><span class="p">,</span><span class="w"> </span><span class="ss">only</span><span class="p">:</span><span class="w"> </span><span class="p" data-group-id="4466751490-3">[</span><span class="ss">get_flash</span><span class="p">:</span><span class="w"> </span><span class="mi">1</span><span class="p">,</span><span class="w"> </span><span class="ss">get_flash</span><span class="p">:</span><span class="w"> </span><span class="mi">2</span><span class="p">,</span><span class="w"> </span><span class="ss">view_module</span><span class="p">:</span><span class="w"> </span><span class="mi">1</span><span class="p" data-group-id="4466751490-3">]</span><span class="w">

    </span><span class="c1"># Use all HTML functionality (forms, tags, etc)</span><span class="w">
    </span><span class="kn">use</span><span class="w"> </span><span class="nc">Phoenix.HTML</span><span class="w">

    </span><span class="kn">import</span><span class="w"> </span><span class="nc">MyAppWeb.ErrorHelpers</span><span class="w">
    </span><span class="kn">import</span><span class="w"> </span><span class="nc">MyAppWeb.Gettext</span><span class="w">
    </span><span class="kn">import</span><span class="w"> </span><span class="nc">Phoenix.LiveView.Helpers</span><span class="w">
    </span><span class="kn">import</span><span class="w"> </span><span class="nc">MyAppWeb.LiveHelpers</span><span class="w">
    </span><span class="kn">alias</span><span class="w"> </span><span class="nc">MyAppWeb.Router.Helpers</span><span class="p">,</span><span class="w"> </span><span class="ss">as</span><span class="p">:</span><span class="w"> </span><span class="nc">Routes</span><span class="w">
  </span><span class="k" data-group-id="4466751490-2">end</span><span class="w">
</span><span class="k" data-group-id="4466751490-1">end</span><span class="w">

</span><span class="c1"># and then use this instead for your LiveView-centric Views</span><span class="w">
</span><span class="c1">### lib/my_app_web/views/my_view.ex</span><span class="w">
</span><span class="kd">defmodule</span><span class="w"> </span><span class="nc">MyAppWeb.MyView</span><span class="w"> </span><span class="k" data-group-id="4466751490-4">do</span><span class="w">
  </span><span class="kn">use</span><span class="w"> </span><span class="nc">MyAppWeb</span><span class="p">,</span><span class="w"> </span><span class="ss">:view_for_live</span><span class="w">

  </span><span class="kd">def</span><span class="w"> </span><span class="nf">my_helper</span><span class="p" data-group-id="4466751490-5">(</span><span class="s">&quot;vanilla&quot;</span><span class="p" data-group-id="4466751490-5">)</span><span class="p">,</span><span class="w"> </span><span class="ss">do</span><span class="p">:</span><span class="w"> </span><span class="s">&quot;whoops no this is actually live&quot;</span><span class="w">
</span><span class="k" data-group-id="4466751490-4">end</span></code></pre>
<h2>
What about LiveComponents?</h2>
<p>
LiveComponents are totally ignored in this article. They’re another great option
for organizing interactive partials from your LiveViews. Their rendering
strategy is very similar to LiveViews though, and most of this applies to them
as well.</p>
<p>
Hope these tips help you out! If you have any more tips, tweet at me
<a href="https://twitter.com/bernheisel">@bernheisel</a></p>
<p>
Thank you <a href="https://zachporter.dev/">zporter</a> for helping me with the post by
proof-reading!</p>
]]></content:encoded>
</item>
<item>
<title><![CDATA[Starting a New Podcast with Thinking Elixir]]></title>
<dc:creator>David Bernheisel</dc:creator>
<description><![CDATA[I joined the Thinking Elixir Podcast! I would really love it if you shared
it with shared with your friends who know or are discovering Elixir.
]]></description>
<link>https://bernheisel.com/blog/starting-a-new-podcast</link>
<guid isPermaLink="true">https://bernheisel.com/blog/starting-a-new-podcast</guid>
<pubDate>Wed, 17 Jun 2020 00:00:00 -0400</pubDate>
<content:encoded><![CDATA[<p>
I joined the <a href="https://podcast.thinkingelixir.com" title="">Thinking Elixir</a> Podcast! I’m really happy to be joining
friends like <a href="https://twitter.com/brainlid" title="">@brainlid</a> and Cade to talk about my favorite programming
language.</p>
<p>
I would really love it if you tweeted about the new podcast and shared with your
friends who know or are discovering Elixir. We’re wanting it to be a good and
succinct source to catch up on Elixir news and happenings, with some interviews
with other Elixir folks!</p>
<p>
Check it out at <a href="https://podcast.thinkingelixir.com">https://podcast.thinkingelixir.com</a> or your favorite source
for podcasts (Google, Apple, etc.).</p>
<p>
In the first episode, we’re introducing ourselves and getting to know each
other.</p>
]]></content:encoded>
</item>
<item>
<title><![CDATA[Phoenix LiveView: Multi-step forms]]></title>
<dc:creator>David Bernheisel</dc:creator>
<description><![CDATA[Big forms are a pain to manage-- even harder to manage when you need to change
values based on previous input and compute data based on that new selection.
Phoenix LiveView can make this easier. Check out some techniques I used to
help organize these forms.
]]></description>
<link>https://bernheisel.com/blog/liveview-multi-step-form</link>
<guid isPermaLink="true">https://bernheisel.com/blog/liveview-multi-step-form</guid>
<pubDate>Wed, 13 May 2020 00:00:00 -0400</pubDate>
<content:encoded><![CDATA[<p>
<a href="https://github.com/phoenixframework/phoenix_live_view" title="">Phoenix LiveView</a> has been a dream to work with so far. I <em>really</em> recommend
looking at it for your next web application. Building Tailwind, Elixir, and
Phoenix LiveView with some Vue sprinklings has been the most enjoyable tech
stack I’ve used in a long while.</p>
<p>
One of the benefits I love about LiveView is that it enables me to consolidate
some of common front-end logic into the backend, where the source of truth
belongs. A great example is a form, especially long-running or multi-step forms.</p>
<p>
Let me show you what I mean.</p>
<iframe width="334" height="720" src="https://www.youtube-nocookie.com/embed/D6tTsvCwlY0" frameborder="0" allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture" allowfullscreen="true">
</iframe>
<p>
This is accomplished without any AJAX calls, no SPAs, and no page reloads.</p>
<p>
I coded this form twice. Let me share with you my journey and some techniques I
used to help organize code.</p>
<h2>
The Ugly Way (First pass)</h2>
<p>
I coded it all with a single LiveView.</p>
<p>
It become quite ugly.</p>
<p>
I was still trying to figure out what I wanted on the form and still learning
LiveView generally. Eventually, this LiveView became an ugly 1000+-line horror
show that managed state in multiple places.</p>
<p>
It was a single <code class="inline">&lt;form&gt;</code> that handled all the fields for the database-backed
record, and each step was hidden until you hit “next”, so every change in the
form sends the entire form values.</p>
<p>
The EEX was something like this:</p>
<pre><code class="eex">&lt;div class=&quot;container&quot;&gt;
  &lt;%= f = form_for(@changeset, phx_validate: :validate, phx_save: :save %&gt;

  &lt;div class=&quot;&lt;%= unless @progress.name == &quot;who&quot;, do: &quot;hidden&quot; %&gt;&quot;&gt;
    &lt;%# my Who-related form inputs %&gt;
  &lt;/div&gt;

  &lt;div class=&quot;&lt;%= unless @progress.name == &quot;what&quot;, do: &quot;hidden&quot; %&gt;&quot;&gt;
    &lt;%# my What-related form inputs %&gt;
  &lt;/div&gt;

  &lt;div class=&quot;&lt;%= unless @progress.name == &quot;when&quot;, do: &quot;hidden&quot; %&gt;&quot;&gt;
    &lt;%# my When-related form inputs %&gt;
  &lt;/div&gt;

  &lt;/form&gt;
&lt;/div&gt;</code></pre>
<p>
I found that this approach has several drawbacks:</p>
<ul>
  <li>
When the user hits “enter”, the form will try to submit. If you’re on the
  first step, you probably don’t want that to submitted yet until they’re
  on the last step. You can override this with some JavaScript, but this
  non-standard behavior made things more complicated than it should be. I’ll
  need the JavaScript to know which step is last, and track which step it’s
  currently on. Ugh… I did this and it wasn’t great. I wanted to delete
  myself.  </li>
  <li>
When the user is on a different step, you still need to manage all the “state”
  of other steps. This is a lot of “weight” to worry about and ensure
  <em>doesn’t</em> change.  </li>
  <li>
As soon as the user interacts with the form on the first step, validations
  will occur for the entire form, <strong>even for those inputs on hidden steps</strong>.
  This means errors will already be populated before the user even interacted
  with them.  </li>
  <li>
Testing the big form was difficult. The tools were great– I just
  bad-developered and didn’t break it down well.  </li>
</ul>
<p>
Generally, I found it harder to “reason about”, especially when I have computed
fields and help text based on user input.</p>
<p>
For example, I need to persist two DateTimes with timezones, but I don’t want to
present that to the user as <code class="inline">datetime_select</code>s and have them select a timezone
from a drop-down.</p>
<p>
Instead I want a date picker, and then separately collect the times and merge it
with the user’s detected timezone (this will later be improved to allow them to
select a timezone and prefer a user’s set timezone while registering). Something
like this:</p>
<pre><code class="eex">&lt;%= date_select f, :date %&gt;
&lt;%= time_input f, :start_time %&gt;
&lt;%= time_input f, :end_time %&gt;
Your duration is &lt;%= @duration %&gt;</code></pre>
<p>
so in my params, I would receive something like this:</p>
<pre><code class="makeup elixir"><span class="kd">def</span><span class="w"> </span><span class="nf">handle_event</span><span class="p" data-group-id="4489943028-1">(</span><span class="s">&quot;validate&quot;</span><span class="p">,</span><span class="w"> </span><span class="p" data-group-id="4489943028-2">%{</span><span class="s">&quot;myform&quot;</span><span class="w"> </span><span class="p">=&gt;</span><span class="w"> </span><span class="n">params</span><span class="p" data-group-id="4489943028-2">}</span><span class="p">,</span><span class="w"> </span><span class="n">socket</span><span class="p" data-group-id="4489943028-1">)</span><span class="w"> </span><span class="k" data-group-id="4489943028-3">do</span><span class="w">
  </span><span class="nc">IO</span><span class="o">.</span><span class="n">inspect</span><span class="w"> </span><span class="n">params</span><span class="p">,</span><span class="w"> </span><span class="ss">label</span><span class="p">:</span><span class="w"> </span><span class="s">&quot;PARAMS&quot;</span><span class="w">
  </span><span class="p" data-group-id="4489943028-4">{</span><span class="ss">:noreply</span><span class="p">,</span><span class="w"> </span><span class="n">socket</span><span class="p" data-group-id="4489943028-4">}</span><span class="w">
</span><span class="k" data-group-id="4489943028-3">end</span><span class="w">

</span><span class="c1">#=&gt; PARAMS: %{</span><span class="w">
</span><span class="c1">#  &quot;date&quot; =&gt; %{&quot;year&quot; =&gt; 2020, &quot;month&quot; =&gt; 1, &quot;day&quot; =&gt; 1},</span><span class="w">
</span><span class="c1">#  &quot;start_time&quot; =&gt; &quot;08:00&quot;,</span><span class="w">
</span><span class="c1"># &quot; end_time&quot; =&gt; &quot;10:00&quot;</span><span class="w">
</span><span class="c1"># }</span></code></pre>
<p>
There’s a complicated mechanism in the time pickers that made it harder. I
needed to detect what changed:</p>
<ol>
  <li>
Was it the <code class="inline">end_time</code>? Then let’s extend the duration as well and accept the
new <code class="inline">end_time</code>.  </li>
  <li>
Was it the <code class="inline">start_time</code>? Then let’s back the <code class="inline">end_time</code> up to the same
duration away from the <code class="inline">start_time</code>.  </li>
  <li>
At some point, if we accept user input for <code class="inline">duration</code>, then we we’d want to
extend the <code class="inline">end_time</code> with the new duration.  </li>
</ol>
<p>
Now I have some fields, I need to compute them into my event struct somehow.
This is how it needs to end up:</p>
<pre><code class="makeup elixir"><span class="n">record</span><span class="o">.</span><span class="n">start_at_tz</span><span class="w"> </span><span class="c1">#=&gt; &quot;America/New_York&quot;</span><span class="w">
</span><span class="n">record</span><span class="o">.</span><span class="n">start_at_wall</span><span class="w"> </span><span class="c1">#=&gt; ~N[2020-01-01T08:00:00]</span><span class="w">
</span><span class="n">record</span><span class="o">.</span><span class="n">start_at_utc</span><span class="w"> </span><span class="c1">#=&gt; ~U[2020-01-01T13:00:00Z]</span><span class="w">

</span><span class="n">record</span><span class="o">.</span><span class="n">end_at_tz</span><span class="w"> </span><span class="c1">#=&gt; &quot;America/New_York&quot;</span><span class="w">
</span><span class="n">record</span><span class="o">.</span><span class="n">end_at_wall</span><span class="w"> </span><span class="c1">#=&gt; ~N[2020-01-01T10:00:00]</span><span class="w">
</span><span class="n">record</span><span class="o">.</span><span class="n">end_at_utc</span><span class="w"> </span><span class="c1">#=&gt; ~U[2020-01-01T15:00:00Z]</span><span class="w">

</span><span class="n">record</span><span class="o">.</span><span class="n">duration</span><span class="w"> </span><span class="c1">#=&gt; 7200 # seconds which is 2 hours</span></code></pre>
<p>
This is going to be a lot of work!</p>
<p>
Let’s not have the giant form all be in one template, or even partials; let’s
split the form up into components. These components will let me manage these
computed fields easier, as well as solve some other UX issues mentioned above.</p>
<h2>
Let’s break it down:</h2>
<ul>
  <li>
<a href="#formprogress">Manage form progress in the parent LiveView.</a>  </li>
  <li>
<a href="#extract">Split the multi-step form into LiveComponents. At least one for each visible step.</a>  </li>
  <li>
<a href="#clientside">Send input supplied client-side via <code class="inline">phx-hook</code>.</a>  </li>
  <li>
<a href="#clientinput">Handle input changes from the users from the component</a>  </li>
  <li>
<a href="#subformsubmission">Handle stepped-form submission</a>  </li>
  <li>
<a href="#formsubmission">Handle final form submission.</a>  </li>
</ul>
<a aria-hidden="true" name="formprogress"></a><h2>
Managing the form progress</h2>
<p>
I managed the form step progress by defining a <code class="inline">%Step{}</code> and then writing the
order out in the liveview as a module attribute.</p>
<pre><code class="makeup elixir"><span class="kd">defmodule</span><span class="w"> </span><span class="nc">MyAppWeb.EventLive.Step</span><span class="w"> </span><span class="k" data-group-id="0230665675-1">do</span><span class="w">
  </span><span class="na">@moduledoc</span><span class="w"> </span><span class="s">&quot;Describe a step in the multi-step form and where it can go.&quot;</span><span class="w">
  </span><span class="kd">defstruct</span><span class="w"> </span><span class="p" data-group-id="0230665675-2">[</span><span class="ss">:name</span><span class="p">,</span><span class="w"> </span><span class="ss">:prev</span><span class="p">,</span><span class="w"> </span><span class="ss">:next</span><span class="p" data-group-id="0230665675-2">]</span><span class="w">
</span><span class="k" data-group-id="0230665675-1">end</span><span class="w">

</span><span class="c1"># in the liveview</span><span class="w">

</span><span class="kd">defmodule</span><span class="w"> </span><span class="nc">MyAppWeb.EventLive.New</span><span class="w"> </span><span class="k" data-group-id="0230665675-3">do</span><span class="w">
  </span><span class="c1"># ...snip...</span><span class="w">

  </span><span class="na">@steps</span><span class="w"> </span><span class="p" data-group-id="0230665675-4">[</span><span class="w">
    </span><span class="p" data-group-id="0230665675-5">%</span><span class="nc" data-group-id="0230665675-5">Step</span><span class="p" data-group-id="0230665675-5">{</span><span class="ss">name</span><span class="p">:</span><span class="w"> </span><span class="s">&quot;who&quot;</span><span class="p">,</span><span class="w"> </span><span class="ss">prev</span><span class="p">:</span><span class="w"> </span><span class="no">nil</span><span class="p">,</span><span class="w"> </span><span class="ss">next</span><span class="p">:</span><span class="w"> </span><span class="s">&quot;what&quot;</span><span class="p" data-group-id="0230665675-5">}</span><span class="p">,</span><span class="w">
    </span><span class="p" data-group-id="0230665675-6">%</span><span class="nc" data-group-id="0230665675-6">Step</span><span class="p" data-group-id="0230665675-6">{</span><span class="ss">name</span><span class="p">:</span><span class="w"> </span><span class="s">&quot;what&quot;</span><span class="p">,</span><span class="w"> </span><span class="ss">prev</span><span class="p">:</span><span class="w"> </span><span class="s">&quot;who&quot;</span><span class="p">,</span><span class="w"> </span><span class="ss">next</span><span class="p">:</span><span class="w"> </span><span class="s">&quot;when&quot;</span><span class="p" data-group-id="0230665675-6">}</span><span class="p">,</span><span class="w">
    </span><span class="p" data-group-id="0230665675-7">%</span><span class="nc" data-group-id="0230665675-7">Step</span><span class="p" data-group-id="0230665675-7">{</span><span class="ss">name</span><span class="p">:</span><span class="w"> </span><span class="s">&quot;when&quot;</span><span class="p">,</span><span class="w"> </span><span class="ss">prev</span><span class="p">:</span><span class="w"> </span><span class="s">&quot;what&quot;</span><span class="p">,</span><span class="w"> </span><span class="ss">next</span><span class="p">:</span><span class="w"> </span><span class="s">&quot;where&quot;</span><span class="p" data-group-id="0230665675-7">}</span><span class="p">,</span><span class="w">
    </span><span class="p" data-group-id="0230665675-8">%</span><span class="nc" data-group-id="0230665675-8">Step</span><span class="p" data-group-id="0230665675-8">{</span><span class="ss">name</span><span class="p">:</span><span class="w"> </span><span class="s">&quot;where&quot;</span><span class="p">,</span><span class="w"> </span><span class="ss">prev</span><span class="p">:</span><span class="w"> </span><span class="s">&quot;when&quot;</span><span class="p">,</span><span class="w"> </span><span class="ss">next</span><span class="p">:</span><span class="w"> </span><span class="no">nil</span><span class="p" data-group-id="0230665675-8">}</span><span class="p">,</span><span class="w">
  </span><span class="p" data-group-id="0230665675-4">]</span><span class="w">

  </span><span class="kd">def</span><span class="w"> </span><span class="nf">mount</span><span class="p" data-group-id="0230665675-9">(</span><span class="c">_params</span><span class="p">,</span><span class="w"> </span><span class="n">session</span><span class="p">,</span><span class="w"> </span><span class="n">socket</span><span class="p" data-group-id="0230665675-9">)</span><span class="w"> </span><span class="k" data-group-id="0230665675-10">do</span><span class="w">
    </span><span class="n">socket</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">authenticate</span><span class="p" data-group-id="0230665675-11">(</span><span class="n">socket</span><span class="p">,</span><span class="w"> </span><span class="n">session</span><span class="p">,</span><span class="w"> </span><span class="p" data-group-id="0230665675-12">[</span><span class="ss">:with_organizations</span><span class="p">,</span><span class="w"> </span><span class="ss">:with_profile</span><span class="p" data-group-id="0230665675-12">]</span><span class="p" data-group-id="0230665675-11">)</span><span class="w">
    </span><span class="n">first_step</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nc">List</span><span class="o">.</span><span class="n">first</span><span class="p" data-group-id="0230665675-13">(</span><span class="na">@steps</span><span class="p" data-group-id="0230665675-13">)</span><span class="w">
    </span><span class="n">event</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p" data-group-id="0230665675-14">%</span><span class="nc" data-group-id="0230665675-14">Event</span><span class="p" data-group-id="0230665675-14">{</span><span class="p" data-group-id="0230665675-14">}</span><span class="w">
    </span><span class="n">params</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p" data-group-id="0230665675-15">%{</span><span class="ss">creator_id</span><span class="p">:</span><span class="w"> </span><span class="n">socket</span><span class="o">.</span><span class="n">assigns</span><span class="o">.</span><span class="n">current_user</span><span class="o">.</span><span class="n">id</span><span class="p" data-group-id="0230665675-15">}</span><span class="w">

    </span><span class="p" data-group-id="0230665675-16">{</span><span class="ss">:ok</span><span class="p">,</span><span class="w">
      </span><span class="n">socket</span><span class="w">
      </span><span class="o">|&gt;</span><span class="w"> </span><span class="n">assign</span><span class="p" data-group-id="0230665675-17">(</span><span class="ss">:event</span><span class="p">,</span><span class="w"> </span><span class="n">event</span><span class="p" data-group-id="0230665675-17">)</span><span class="w">
      </span><span class="o">|&gt;</span><span class="w"> </span><span class="n">assign</span><span class="p" data-group-id="0230665675-18">(</span><span class="ss">:params</span><span class="p">,</span><span class="w"> </span><span class="n">params</span><span class="p" data-group-id="0230665675-18">)</span><span class="w">
      </span><span class="o">|&gt;</span><span class="w"> </span><span class="n">assign</span><span class="p" data-group-id="0230665675-19">(</span><span class="ss">:changeset</span><span class="p">,</span><span class="w"> </span><span class="nc">Event</span><span class="o">.</span><span class="n">changeset</span><span class="p" data-group-id="0230665675-20">(</span><span class="n">event</span><span class="p">,</span><span class="w"> </span><span class="n">params</span><span class="p" data-group-id="0230665675-20">)</span><span class="p" data-group-id="0230665675-19">)</span><span class="w">
      </span><span class="o">|&gt;</span><span class="w"> </span><span class="n">assign</span><span class="p" data-group-id="0230665675-21">(</span><span class="ss">:progress</span><span class="p">,</span><span class="w"> </span><span class="n">first_step</span><span class="p" data-group-id="0230665675-21">)</span><span class="p" data-group-id="0230665675-16">}</span><span class="w">
  </span><span class="k" data-group-id="0230665675-10">end</span><span class="w">

  </span><span class="c1"># ...snip...</span><span class="w">
</span><span class="k" data-group-id="0230665675-3">end</span></code></pre>
<p>
When the underlying live components are finished, they’ll send a message to the
parent LiveView which will re-assign <code class="inline">:progress</code>; the conditionals in the
template will apply/remove the “hidden” class for the next appropriate step, or
previous step. You’ll see that as you read on.</p>
<p>
Let’s chop up the form.</p>
<a aria-hidden="true" name="extract"></a><h2>
Extract to LiveComponents</h2>
<p>
All this ugly-but-necessary logic should live in “form objects”. In Ecto-land
these can be managed with embedded schemas. These form objects are responsible
for the state of their own fields, and compute their own values without
affecting other steps’ values. The domain becomes much clearer.</p>
<p>
When the form is submitted, it will trigger the “save” event from the
LiveComponent. The LiveComponent can then pass the completed params up to its
parent LiveView if the changeset is valid. The parent LiveView can track these
params separately, sitting on it until final save, persisted as a draft, or
whatever you need.</p>
<p>
This has some benefits:</p>
<ul>
  <li>
form submission (hitting enter) no longer needs to override default behavior.  </li>
  <li>
isolates testing to it’s own form and LiveComponent.  </li>
  <li>
your form’s “domain” has clearer boundaries.  </li>
  <li>
user interaction and form validation makes more sense; only the visible form
  is “tainted” when the user changes it (opposed to it being tainted before
  the user even sees it).  </li>
</ul>
<p>
The multi-step form now looks like this:</p>
<pre><code class="eex">&lt;div class=&quot;container&quot;&gt;
  &lt;div class=&quot;&lt;%= unless @progress.name == &quot;who&quot;, do: &quot;hidden&quot; %&gt;&quot;&gt;
    &lt;%= live_component @socket, WhoComponent,
      id: &quot;who&quot;,
      event: @event,
      current_user: @current_user
    %&gt;
  &lt;/div&gt;

  &lt;div class=&quot;&lt;%= unless @progress.name == &quot;what&quot;, do: &quot;hidden&quot; %&gt;&quot;&gt;
    &lt;%= live_component @socket, WhatComponent, id: &quot;what&quot;, event: @event %&gt;
  &lt;/div&gt;

  &lt;div class=&quot;&lt;%= unless @progress.name == &quot;when&quot;, do: &quot;hidden&quot; %&gt;&quot;&gt;
    &lt;%= live_component @socket, WhenComponent,
      id: &quot;when&quot;,
      event: @event,
      current_user: @current_user
    %&gt;
  &lt;/div&gt;

  &lt;div class=&quot;&lt;%= unless @progress.name == &quot;where&quot;, do: &quot;hidden&quot; %&gt;&quot;&gt;
    &lt;%= live_component @socket, WhereComponent,
      id: &quot;where&quot;,
      submit_text: t(&quot;Create&quot;),
      event: @event
    %&gt;
  &lt;/div&gt;
&lt;/div&gt;</code></pre>
<p>
Let’s focus on the <code class="inline">WhenComponent</code>.</p>
<p>
Here’s the big idea:</p>
<ul>
  <li>
    <p>
Inside of the WhenComponent, we need our <code class="inline">embedded_schema</code> to represent and
  store the fields we care about on the step.    </p>
  </li>
  <li>
    <p>
When loading/updating the component itself, we’re going to initialize the
  changeset with the fields from the record.    </p>
  </li>
  <li>
    <p>
When handling validation events, we’re going to throw the params into the
  changeset and assign the new changeset back.    </p>
  </li>
  <li>
    <p>
The computed values will be updated from the changeset and/or pulled out of
  the changeset and assigned into the socket.    </p>
  </li>
  <li>
    <p>
When handling the save event, we’re going to ensure the changeset is valid,
  and if so, tell the parent LiveView that we’re good to proceed. We’ll send
  the struct up to the parent LiveView. This struct will contain the computed
  fields so it should be easier for the parent to stitch these steps’ params
  together into the final changeset that’s actually persisted.    </p>
  </li>
</ul>
<p>
Again, the flow should look like this:</p>
<ol>
  <li>
On mounting, take the Event and pluck the relevant fields out of it to create
a WhenComponent form backed by an <code class="inline">embedded_schema</code>.  </li>
  <li>
When the user is on the step, take the changes as they come and let the user
iterate on the form until it’s valid.  </li>
  <li>
When the changeset is valid and the user tries to submit it, pass the final
struct up to the parent LiveView. The parent LiveView can then switch to the
next step.  </li>
</ol>
<p>
Here is the component code:</p>
<pre><code class="makeup elixir"><span class="kd">defmodule</span><span class="w"> </span><span class="nc">MyAppWeb.EventLive.WhenComponent</span><span class="w"> </span><span class="k" data-group-id="1240246157-1">do</span><span class="w">
  </span><span class="kn">use</span><span class="w"> </span><span class="nc">MyAppWeb</span><span class="p">,</span><span class="w"> </span><span class="ss">:live_component</span><span class="w">
  </span><span class="kn">alias</span><span class="w"> </span><span class="nc">Ecto.Changeset</span><span class="w">

  </span><span class="na">@primary_key</span><span class="w"> </span><span class="no">false</span><span class="w">
  </span><span class="n">embedded_schema</span><span class="w"> </span><span class="k" data-group-id="1240246157-2">do</span><span class="w">
    </span><span class="n">field</span><span class="w"> </span><span class="ss">:timezone</span><span class="p">,</span><span class="w"> </span><span class="ss">:string</span><span class="w">
    </span><span class="n">field</span><span class="w"> </span><span class="ss">:start_at_date</span><span class="p">,</span><span class="w"> </span><span class="ss">:date</span><span class="w">
    </span><span class="n">field</span><span class="w"> </span><span class="ss">:start_at_time</span><span class="p">,</span><span class="w"> </span><span class="ss">:time</span><span class="w">
    </span><span class="n">field</span><span class="w"> </span><span class="ss">:start_at</span><span class="p">,</span><span class="w"> </span><span class="ss">:utc_datetime</span><span class="w">  </span><span class="c1"># Not on the form. This is computed</span><span class="w">

    </span><span class="n">field</span><span class="w"> </span><span class="ss">:end_at_date</span><span class="p">,</span><span class="w"> </span><span class="ss">:date</span><span class="w">
    </span><span class="n">field</span><span class="w"> </span><span class="ss">:end_at_time</span><span class="p">,</span><span class="w"> </span><span class="ss">:time</span><span class="w">
    </span><span class="n">field</span><span class="w"> </span><span class="ss">:end_at</span><span class="p">,</span><span class="w"> </span><span class="ss">:utc_datetime</span><span class="w">  </span><span class="c1"># Not on the form. This is computed</span><span class="w">

    </span><span class="n">field</span><span class="w"> </span><span class="ss">:duration</span><span class="p">,</span><span class="w"> </span><span class="ss">:integer</span><span class="w">  </span><span class="c1"># Not on the form. This is computed and displayed</span><span class="w">
  </span><span class="k" data-group-id="1240246157-2">end</span><span class="w">

  </span><span class="na">@impl</span><span class="w"> </span><span class="nc">Phoenix.LiveComponent</span><span class="w">
  </span><span class="kd">def</span><span class="w"> </span><span class="nf">update</span><span class="p" data-group-id="1240246157-3">(</span><span class="n">assigns</span><span class="p">,</span><span class="w"> </span><span class="n">socket</span><span class="p" data-group-id="1240246157-3">)</span><span class="w"> </span><span class="k" data-group-id="1240246157-4">do</span><span class="w">
    </span><span class="n">whenevent</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">from_event</span><span class="p" data-group-id="1240246157-5">(</span><span class="n">assigns</span><span class="o">.</span><span class="n">event</span><span class="p">,</span><span class="w"> </span><span class="n">assigns</span><span class="o">.</span><span class="n">current_user</span><span class="o">.</span><span class="n">profile</span><span class="o">.</span><span class="n">timezone</span><span class="p" data-group-id="1240246157-5">)</span><span class="w">
    </span><span class="n">params</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p" data-group-id="1240246157-6">%{</span><span class="p" data-group-id="1240246157-6">}</span><span class="w">
    </span><span class="n">changeset</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">changeset</span><span class="p" data-group-id="1240246157-7">(</span><span class="n">whenevent</span><span class="p">,</span><span class="w"> </span><span class="n">params</span><span class="p" data-group-id="1240246157-7">)</span><span class="w">

    </span><span class="p" data-group-id="1240246157-8">{</span><span class="ss">:ok</span><span class="p">,</span><span class="w">
      </span><span class="n">socket</span><span class="w">
      </span><span class="o">|&gt;</span><span class="w"> </span><span class="n">assign</span><span class="p" data-group-id="1240246157-9">(</span><span class="n">assigns</span><span class="p" data-group-id="1240246157-9">)</span><span class="w">
      </span><span class="o">|&gt;</span><span class="w"> </span><span class="n">assign</span><span class="p" data-group-id="1240246157-10">(</span><span class="ss">:when</span><span class="p">,</span><span class="w"> </span><span class="n">whenevent</span><span class="p" data-group-id="1240246157-10">)</span><span class="w">
      </span><span class="o">|&gt;</span><span class="w"> </span><span class="n">assign</span><span class="p" data-group-id="1240246157-11">(</span><span class="ss">:when_changeset</span><span class="p">,</span><span class="w"> </span><span class="n">changeset</span><span class="p" data-group-id="1240246157-11">)</span><span class="w">
      </span><span class="o">|&gt;</span><span class="w"> </span><span class="n">assign_computed</span><span class="p" data-group-id="1240246157-12">(</span><span class="n">changeset</span><span class="p" data-group-id="1240246157-12">)</span><span class="w">
    </span><span class="p" data-group-id="1240246157-8">}</span><span class="w">
  </span><span class="k" data-group-id="1240246157-4">end</span><span class="w">

  </span><span class="na">@impl</span><span class="w"> </span><span class="nc">Phoenix.LiveComponent</span><span class="w">
  </span><span class="kd">def</span><span class="w"> </span><span class="nf">handle_event</span><span class="p" data-group-id="1240246157-13">(</span><span class="s">&quot;validate&quot;</span><span class="p">,</span><span class="w"> </span><span class="p" data-group-id="1240246157-14">%{</span><span class="s">&quot;when_component&quot;</span><span class="w"> </span><span class="p">=&gt;</span><span class="w"> </span><span class="n">params</span><span class="p" data-group-id="1240246157-14">}</span><span class="p">,</span><span class="w"> </span><span class="n">socket</span><span class="p" data-group-id="1240246157-13">)</span><span class="w"> </span><span class="k" data-group-id="1240246157-15">do</span><span class="w">
    </span><span class="n">adjusted_params</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">adjust_time_params</span><span class="p" data-group-id="1240246157-16">(</span><span class="n">params</span><span class="p">,</span><span class="w"> </span><span class="n">socket</span><span class="o">.</span><span class="n">assigns</span><span class="o">.</span><span class="n">when_changeset</span><span class="p" data-group-id="1240246157-16">)</span><span class="w">
    </span><span class="n">changeset</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">changeset</span><span class="p" data-group-id="1240246157-17">(</span><span class="n">socket</span><span class="o">.</span><span class="n">assigns</span><span class="o">.</span><span class="ow">when</span><span class="p">,</span><span class="w"> </span><span class="n">adjusted_params</span><span class="p" data-group-id="1240246157-17">)</span><span class="w">

    </span><span class="p" data-group-id="1240246157-18">{</span><span class="ss">:noreply</span><span class="p">,</span><span class="w">
      </span><span class="n">socket</span><span class="w">
      </span><span class="o">|&gt;</span><span class="w"> </span><span class="n">assign</span><span class="p" data-group-id="1240246157-19">(</span><span class="ss">:when_changeset</span><span class="p">,</span><span class="w"> </span><span class="n">changeset</span><span class="p" data-group-id="1240246157-19">)</span><span class="w">
      </span><span class="o">|&gt;</span><span class="w"> </span><span class="n">assign_computed</span><span class="p" data-group-id="1240246157-20">(</span><span class="n">changeset</span><span class="p" data-group-id="1240246157-20">)</span><span class="w">
    </span><span class="p" data-group-id="1240246157-18">}</span><span class="w">
  </span><span class="k" data-group-id="1240246157-15">end</span><span class="w">

  </span><span class="na">@impl</span><span class="w"> </span><span class="nc">Phoenix.LiveComponent</span><span class="w">
  </span><span class="kd">def</span><span class="w"> </span><span class="nf">handle_event</span><span class="p" data-group-id="1240246157-21">(</span><span class="s">&quot;save&quot;</span><span class="p">,</span><span class="w"> </span><span class="p" data-group-id="1240246157-22">%{</span><span class="s">&quot;when_component&quot;</span><span class="w"> </span><span class="p">=&gt;</span><span class="w"> </span><span class="n">params</span><span class="p" data-group-id="1240246157-22">}</span><span class="p">,</span><span class="w"> </span><span class="n">socket</span><span class="p" data-group-id="1240246157-21">)</span><span class="w"> </span><span class="k" data-group-id="1240246157-23">do</span><span class="w">
    </span><span class="n">socket</span><span class="o">.</span><span class="n">assigns</span><span class="o">.</span><span class="ow">when</span><span class="w">
    </span><span class="o">|&gt;</span><span class="w"> </span><span class="n">changeset</span><span class="p" data-group-id="1240246157-24">(</span><span class="n">params</span><span class="p" data-group-id="1240246157-24">)</span><span class="w">
    </span><span class="o">|&gt;</span><span class="w"> </span><span class="nc">Changeset</span><span class="o">.</span><span class="n">apply_action</span><span class="p" data-group-id="1240246157-25">(</span><span class="ss">:insert</span><span class="p" data-group-id="1240246157-25">)</span><span class="w">
    </span><span class="o">|&gt;</span><span class="w"> </span><span class="k">case</span><span class="w"> </span><span class="k" data-group-id="1240246157-26">do</span><span class="w">
      </span><span class="p" data-group-id="1240246157-27">{</span><span class="ss">:ok</span><span class="p">,</span><span class="w"> </span><span class="n">record</span><span class="p" data-group-id="1240246157-27">}</span><span class="w"> </span><span class="o">-&gt;</span><span class="w">
        </span><span class="n">send</span><span class="p" data-group-id="1240246157-28">(</span><span class="n">self</span><span class="p" data-group-id="1240246157-29">(</span><span class="p" data-group-id="1240246157-29">)</span><span class="p">,</span><span class="w"> </span><span class="p" data-group-id="1240246157-30">{</span><span class="ss">:proceed</span><span class="p">,</span><span class="w"> </span><span class="n">record</span><span class="p" data-group-id="1240246157-30">}</span><span class="p" data-group-id="1240246157-28">)</span><span class="w">
        </span><span class="p" data-group-id="1240246157-31">{</span><span class="ss">:noreply</span><span class="p">,</span><span class="w"> </span><span class="n">socket</span><span class="p" data-group-id="1240246157-31">}</span><span class="w">
      </span><span class="p" data-group-id="1240246157-32">{</span><span class="ss">:error</span><span class="p">,</span><span class="w"> </span><span class="n">changeset</span><span class="p" data-group-id="1240246157-32">}</span><span class="w"> </span><span class="o">-&gt;</span><span class="w">
        </span><span class="p" data-group-id="1240246157-33">{</span><span class="ss">:noreply</span><span class="p">,</span><span class="w">
          </span><span class="n">socket</span><span class="w">
          </span><span class="o">|&gt;</span><span class="w"> </span><span class="n">assign</span><span class="p" data-group-id="1240246157-34">(</span><span class="ss">:when_changeset</span><span class="p">,</span><span class="w"> </span><span class="n">changeset</span><span class="p" data-group-id="1240246157-34">)</span><span class="w">
          </span><span class="o">|&gt;</span><span class="w"> </span><span class="n">assign_computed</span><span class="p" data-group-id="1240246157-35">(</span><span class="n">changeset</span><span class="p" data-group-id="1240246157-35">)</span><span class="w">
        </span><span class="p" data-group-id="1240246157-33">}</span><span class="w">
    </span><span class="k" data-group-id="1240246157-26">end</span><span class="w">
  </span><span class="k" data-group-id="1240246157-23">end</span><span class="w">

  </span><span class="na">@impl</span><span class="w"> </span><span class="nc">Phoenix.LiveComponent</span><span class="w">
  </span><span class="kd">def</span><span class="w"> </span><span class="nf">handle_event</span><span class="p" data-group-id="1240246157-36">(</span><span class="s">&quot;timezone&quot;</span><span class="p">,</span><span class="w"> </span><span class="n">detected_timezone</span><span class="p">,</span><span class="w"> </span><span class="n">socket</span><span class="p" data-group-id="1240246157-36">)</span><span class="w"> </span><span class="k" data-group-id="1240246157-37">do</span><span class="w">
    </span><span class="n">params</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nc">Map</span><span class="o">.</span><span class="n">put</span><span class="p" data-group-id="1240246157-38">(</span><span class="n">socket</span><span class="o">.</span><span class="n">assigns</span><span class="o">.</span><span class="n">when_changeset</span><span class="o">.</span><span class="n">params</span><span class="p">,</span><span class="w"> </span><span class="s">&quot;timezone&quot;</span><span class="p">,</span><span class="w"> </span><span class="n">detected_timezone</span><span class="p" data-group-id="1240246157-38">)</span><span class="w">
    </span><span class="n">changeset</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">changeset</span><span class="p" data-group-id="1240246157-39">(</span><span class="n">socket</span><span class="o">.</span><span class="n">assigns</span><span class="o">.</span><span class="n">when_changeset</span><span class="p">,</span><span class="w"> </span><span class="n">params</span><span class="p" data-group-id="1240246157-39">)</span><span class="w">
    </span><span class="p" data-group-id="1240246157-40">{</span><span class="ss">:noreply</span><span class="p">,</span><span class="w">
      </span><span class="n">socket</span><span class="w">
      </span><span class="o">|&gt;</span><span class="w"> </span><span class="n">assign</span><span class="p" data-group-id="1240246157-41">(</span><span class="ss">:when_changeset</span><span class="p">,</span><span class="w"> </span><span class="n">changeset</span><span class="p" data-group-id="1240246157-41">)</span><span class="w">
      </span><span class="o">|&gt;</span><span class="w"> </span><span class="n">assign_computed</span><span class="p" data-group-id="1240246157-42">(</span><span class="n">changeset</span><span class="p" data-group-id="1240246157-42">)</span><span class="w">
    </span><span class="p" data-group-id="1240246157-40">}</span><span class="w">
  </span><span class="k" data-group-id="1240246157-37">end</span><span class="w">

  </span><span class="na">@fields</span><span class="w"> </span><span class="sx">~w[timezone start_at_date start_at_time end_at_date end_at_time duration]a</span><span class="w">
  </span><span class="kd">def</span><span class="w"> </span><span class="nf">changeset</span><span class="p" data-group-id="1240246157-43">(</span><span class="n">whenevent</span><span class="p">,</span><span class="w"> </span><span class="n">params</span><span class="p" data-group-id="1240246157-43">)</span><span class="w"> </span><span class="k" data-group-id="1240246157-44">do</span><span class="w">
    </span><span class="n">whenevent</span><span class="w">
    </span><span class="o">|&gt;</span><span class="w"> </span><span class="nc">Changeset</span><span class="o">.</span><span class="n">cast</span><span class="p" data-group-id="1240246157-45">(</span><span class="n">params</span><span class="p">,</span><span class="w"> </span><span class="na">@fields</span><span class="p" data-group-id="1240246157-45">)</span><span class="w">
    </span><span class="o">|&gt;</span><span class="w"> </span><span class="n">put_stitched_datetime</span><span class="p" data-group-id="1240246157-46">(</span><span class="ss">:start_at</span><span class="p" data-group-id="1240246157-46">)</span><span class="w">
    </span><span class="o">|&gt;</span><span class="w"> </span><span class="n">put_stitched_datetime</span><span class="p" data-group-id="1240246157-47">(</span><span class="ss">:end_at</span><span class="p" data-group-id="1240246157-47">)</span><span class="w">
    </span><span class="o">|&gt;</span><span class="w"> </span><span class="n">ensure_duration</span><span class="p" data-group-id="1240246157-48">(</span><span class="p" data-group-id="1240246157-48">)</span><span class="w">
  </span><span class="k" data-group-id="1240246157-44">end</span><span class="w">

  </span><span class="kd">defp</span><span class="w"> </span><span class="nf">put_stitched_datetime</span><span class="p" data-group-id="1240246157-49">(</span><span class="n">changeset</span><span class="p">,</span><span class="w"> </span><span class="n">field</span><span class="p" data-group-id="1240246157-49">)</span><span class="w"> </span><span class="k" data-group-id="1240246157-50">do</span><span class="w">
    </span><span class="n">timezone</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nc">Changeset</span><span class="o">.</span><span class="n">get_field</span><span class="p" data-group-id="1240246157-51">(</span><span class="n">changeset</span><span class="p">,</span><span class="w"> </span><span class="ss">:timezone</span><span class="p" data-group-id="1240246157-51">)</span><span class="w">
    </span><span class="n">date</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nc">Changeset</span><span class="o">.</span><span class="n">get_field</span><span class="p" data-group-id="1240246157-52">(</span><span class="n">changeset</span><span class="p">,</span><span class="w"> </span><span class="ss">:&quot;</span><span class="si" data-group-id="1240246157-53">#{</span><span class="n">field</span><span class="si" data-group-id="1240246157-53">}</span><span class="ss">_date&quot;</span><span class="p" data-group-id="1240246157-52">)</span><span class="w">
    </span><span class="n">time</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nc">Changeset</span><span class="o">.</span><span class="n">get_field</span><span class="p" data-group-id="1240246157-54">(</span><span class="n">changeset</span><span class="p">,</span><span class="w"> </span><span class="ss">:&quot;</span><span class="si" data-group-id="1240246157-55">#{</span><span class="n">field</span><span class="si" data-group-id="1240246157-55">}</span><span class="ss">_time&quot;</span><span class="p">,</span><span class="w"> </span><span class="ld">~T[00:00:00]</span><span class="p" data-group-id="1240246157-54">)</span><span class="w">
    </span><span class="p" data-group-id="1240246157-56">{</span><span class="ss">:ok</span><span class="p">,</span><span class="w"> </span><span class="n">ndt</span><span class="p" data-group-id="1240246157-56">}</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nc">NaiveDateTime</span><span class="o">.</span><span class="n">new</span><span class="p" data-group-id="1240246157-57">(</span><span class="n">date</span><span class="p">,</span><span class="w"> </span><span class="n">time</span><span class="p" data-group-id="1240246157-57">)</span><span class="w">

    </span><span class="nc">Changeset</span><span class="o">.</span><span class="n">put_change</span><span class="p" data-group-id="1240246157-58">(</span><span class="n">changeset</span><span class="p">,</span><span class="w"> </span><span class="n">field</span><span class="p">,</span><span class="w"> </span><span class="nc">DateTime</span><span class="o">.</span><span class="n">from_naive!</span><span class="p" data-group-id="1240246157-59">(</span><span class="n">ndt</span><span class="p">,</span><span class="w"> </span><span class="n">timezone</span><span class="p" data-group-id="1240246157-59">)</span><span class="p" data-group-id="1240246157-58">)</span><span class="w">
  </span><span class="k" data-group-id="1240246157-50">end</span><span class="w">

  </span><span class="kd">def</span><span class="w"> </span><span class="nf">from_event</span><span class="p" data-group-id="1240246157-60">(</span><span class="n">event</span><span class="p">,</span><span class="w"> </span><span class="n">profile_timezone</span><span class="p" data-group-id="1240246157-60">)</span><span class="w"> </span><span class="k" data-group-id="1240246157-61">do</span><span class="w">
    </span><span class="p">%</span><span class="bp">__MODULE__</span><span class="p" data-group-id="1240246157-62">{</span><span class="ss">timezone</span><span class="p">:</span><span class="w"> </span><span class="n">event</span><span class="o">.</span><span class="n">start_at_tz</span><span class="w"> </span><span class="o">||</span><span class="w"> </span><span class="n">profile_timezone</span><span class="w"> </span><span class="o">||</span><span class="w"> </span><span class="s">&quot;Etc/UTC&quot;</span><span class="p" data-group-id="1240246157-62">}</span><span class="w">
    </span><span class="o">|&gt;</span><span class="w"> </span><span class="n">put_start_at_date</span><span class="p" data-group-id="1240246157-63">(</span><span class="n">to_date</span><span class="p" data-group-id="1240246157-64">(</span><span class="n">event</span><span class="o">.</span><span class="n">start_at</span><span class="p" data-group-id="1240246157-64">)</span><span class="p" data-group-id="1240246157-63">)</span><span class="w">
    </span><span class="o">|&gt;</span><span class="w"> </span><span class="n">put_start_at_time</span><span class="p" data-group-id="1240246157-65">(</span><span class="n">to_time</span><span class="p" data-group-id="1240246157-66">(</span><span class="n">event</span><span class="o">.</span><span class="n">start_at</span><span class="p" data-group-id="1240246157-66">)</span><span class="p" data-group-id="1240246157-65">)</span><span class="w">
    </span><span class="o">|&gt;</span><span class="w"> </span><span class="n">put_end_at_date</span><span class="p" data-group-id="1240246157-67">(</span><span class="n">to_date</span><span class="p" data-group-id="1240246157-68">(</span><span class="n">event</span><span class="o">.</span><span class="n">end_at</span><span class="p" data-group-id="1240246157-68">)</span><span class="p" data-group-id="1240246157-67">)</span><span class="w">
    </span><span class="o">|&gt;</span><span class="w"> </span><span class="n">put_end_at_time</span><span class="p" data-group-id="1240246157-69">(</span><span class="n">to_time</span><span class="p" data-group-id="1240246157-70">(</span><span class="n">event</span><span class="o">.</span><span class="n">end_at</span><span class="p" data-group-id="1240246157-70">)</span><span class="p" data-group-id="1240246157-69">)</span><span class="w">
    </span><span class="o">|&gt;</span><span class="w"> </span><span class="n">put_end_at</span><span class="p" data-group-id="1240246157-71">(</span><span class="n">event</span><span class="o">.</span><span class="n">end_at</span><span class="p" data-group-id="1240246157-71">)</span><span class="w">
    </span><span class="o">|&gt;</span><span class="w"> </span><span class="n">put_start_at</span><span class="p" data-group-id="1240246157-72">(</span><span class="n">event</span><span class="o">.</span><span class="n">start_at</span><span class="p" data-group-id="1240246157-72">)</span><span class="w">
    </span><span class="o">|&gt;</span><span class="w"> </span><span class="n">put_duration</span><span class="p" data-group-id="1240246157-73">(</span><span class="n">calc_duration</span><span class="p" data-group-id="1240246157-74">(</span><span class="n">event</span><span class="o">.</span><span class="n">start_at</span><span class="p">,</span><span class="w"> </span><span class="n">event</span><span class="o">.</span><span class="n">end_at</span><span class="p" data-group-id="1240246157-74">)</span><span class="p" data-group-id="1240246157-73">)</span><span class="w">
  </span><span class="k" data-group-id="1240246157-61">end</span><span class="w">

  </span><span class="kd">defp</span><span class="w"> </span><span class="nf">ensure_duration</span><span class="p" data-group-id="1240246157-75">(</span><span class="n">changeset</span><span class="p" data-group-id="1240246157-75">)</span><span class="w"> </span><span class="k" data-group-id="1240246157-76">do</span><span class="w">
    </span><span class="k">if</span><span class="w"> </span><span class="nc">Changeset</span><span class="o">.</span><span class="n">get_field</span><span class="p" data-group-id="1240246157-77">(</span><span class="n">changeset</span><span class="p">,</span><span class="w"> </span><span class="ss">:duration</span><span class="p" data-group-id="1240246157-77">)</span><span class="w"> </span><span class="k" data-group-id="1240246157-78">do</span><span class="w">
      </span><span class="n">changeset</span><span class="w">
    </span><span class="k" data-group-id="1240246157-78">else</span><span class="w">
      </span><span class="n">start_at</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nc">Changeset</span><span class="o">.</span><span class="n">get_field</span><span class="p" data-group-id="1240246157-79">(</span><span class="n">changeset</span><span class="p">,</span><span class="w"> </span><span class="ss">:start_at</span><span class="p" data-group-id="1240246157-79">)</span><span class="w">
      </span><span class="n">end_at</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nc">Changeset</span><span class="o">.</span><span class="n">get_field</span><span class="p" data-group-id="1240246157-80">(</span><span class="n">changeset</span><span class="p">,</span><span class="w"> </span><span class="ss">:end_at</span><span class="p" data-group-id="1240246157-80">)</span><span class="w">
      </span><span class="nc">Changeset</span><span class="o">.</span><span class="n">put_change</span><span class="p" data-group-id="1240246157-81">(</span><span class="n">changeset</span><span class="p">,</span><span class="w"> </span><span class="ss">:duration</span><span class="p">,</span><span class="w"> </span><span class="n">calc_duration</span><span class="p" data-group-id="1240246157-82">(</span><span class="n">start_at</span><span class="p">,</span><span class="w"> </span><span class="n">end_at</span><span class="p" data-group-id="1240246157-82">)</span><span class="p" data-group-id="1240246157-81">)</span><span class="w">
    </span><span class="k" data-group-id="1240246157-78">end</span><span class="w">
  </span><span class="k" data-group-id="1240246157-76">end</span><span class="w">

  </span><span class="kd">defp</span><span class="w"> </span><span class="nf">assign_computed</span><span class="p" data-group-id="1240246157-83">(</span><span class="n">socket</span><span class="p">,</span><span class="w"> </span><span class="n">changeset</span><span class="p" data-group-id="1240246157-83">)</span><span class="w"> </span><span class="k" data-group-id="1240246157-84">do</span><span class="w">
    </span><span class="c1"># I need the start_at for the DatePicker component that I render from this</span><span class="w">
    </span><span class="c1"># component. I render the timezone and duration on the form. Lastly I</span><span class="w">
    </span><span class="c1"># compute and render some autocomplete suggestions from the values in the</span><span class="w">
    </span><span class="c1"># changeset.</span><span class="w">
    </span><span class="n">socket</span><span class="w">
    </span><span class="o">|&gt;</span><span class="w"> </span><span class="n">assign</span><span class="p" data-group-id="1240246157-85">(</span><span class="ss">:timezone</span><span class="p">,</span><span class="w"> </span><span class="nc">Changeset</span><span class="o">.</span><span class="n">get_field</span><span class="p" data-group-id="1240246157-86">(</span><span class="n">changeset</span><span class="p">,</span><span class="w"> </span><span class="ss">:timezone</span><span class="p" data-group-id="1240246157-86">)</span><span class="p" data-group-id="1240246157-85">)</span><span class="w">
    </span><span class="o">|&gt;</span><span class="w"> </span><span class="n">assign</span><span class="p" data-group-id="1240246157-87">(</span><span class="ss">:start_at</span><span class="p">,</span><span class="w"> </span><span class="nc">Changeset</span><span class="o">.</span><span class="n">get_field</span><span class="p" data-group-id="1240246157-88">(</span><span class="n">changeset</span><span class="p">,</span><span class="w"> </span><span class="ss">:start_at</span><span class="p" data-group-id="1240246157-88">)</span><span class="p" data-group-id="1240246157-87">)</span><span class="w">
    </span><span class="o">|&gt;</span><span class="w"> </span><span class="n">assign</span><span class="p" data-group-id="1240246157-89">(</span><span class="ss">:duration</span><span class="p">,</span><span class="w"> </span><span class="nc">Changeset</span><span class="o">.</span><span class="n">get_field</span><span class="p" data-group-id="1240246157-90">(</span><span class="n">changeset</span><span class="p">,</span><span class="w"> </span><span class="ss">:duration</span><span class="p" data-group-id="1240246157-90">)</span><span class="p" data-group-id="1240246157-89">)</span><span class="w">
    </span><span class="o">|&gt;</span><span class="w"> </span><span class="n">assign</span><span class="p" data-group-id="1240246157-91">(</span><span class="ss">:start_time_autocomplete</span><span class="p">,</span><span class="w"> </span><span class="n">start_autocompletes</span><span class="p" data-group-id="1240246157-92">(</span><span class="n">changeset</span><span class="p" data-group-id="1240246157-92">)</span><span class="p" data-group-id="1240246157-91">)</span><span class="w">
    </span><span class="o">|&gt;</span><span class="w"> </span><span class="n">assign</span><span class="p" data-group-id="1240246157-93">(</span><span class="ss">:end_time_autocomplete</span><span class="p">,</span><span class="w"> </span><span class="n">end_autocompletes</span><span class="p" data-group-id="1240246157-94">(</span><span class="n">changeset</span><span class="p" data-group-id="1240246157-94">)</span><span class="p" data-group-id="1240246157-93">)</span><span class="w">
  </span><span class="k" data-group-id="1240246157-84">end</span><span class="w">

  </span><span class="c1"># I&#39;ll leave some of these helper functions out, but they&#39;re essentially</span><span class="w">
  </span><span class="c1"># providing nil-safety, applying defaults, and calculating from other fields</span><span class="w">
</span><span class="k" data-group-id="1240246157-1">end</span></code></pre>
<a aria-hidden="true" name="clientside"></a><h2>
Getting the user’s timezone with <code class="inline">phx-hook</code></h2>
<p>
We can estimate what the user’s timezone is by asking the browser. <strong>NOTE</strong> <em>I
don’t recommend you use this as your only source of user timezone.</em> Use this as
an example for how to get JavaScript-sourced input</p>
<p>
Let’s get the timezone. We’ll need some JavaScript.</p>
<pre><code class="javascript">window.userTimezone = Intl.DateTimeFormat().resolvedOptions().timeZone

// initialize the Phoenix LiveView socket, and pass this in as a hook:

hooks.UserTimeZone = {
  mounted() {
    const phoenix = this;
    const target = this.el.dataset.phoenixTarget;
    const els = phoenix.el.querySelectorAll(&quot;input&quot;)
    for (let el of els) { el.value = window.userTimezone; }
    phoenix.pushEventTo(target, &quot;timezone&quot;, window.userTimezone)
  }
}</code></pre>
<pre><code class="eex">&lt;%# Timezone in the WhenComponent form %&gt;
&lt;div phx-hook=&quot;UserTimeZone&quot; data-phoenix-target=&quot;#&lt;%= @id %&gt;&quot; id=&quot;user-time-zone&quot;&gt;
  &lt;div phx-update=&quot;ignore&quot;&gt;
    &lt;%= hidden_input f, :timezone %&gt;
  &lt;/div&gt;
&lt;/div&gt;</code></pre>
<p>
When the page is rendered, I’ll get a <code class="inline">hidden_input</code> populated with the detected
timezone. This will be included in further form changes and params sent to the
LiveView process. Remember to wrap it with a <code class="inline">phx-update=&quot;ignore&quot;</code> so the
JavaScript-mutated value isn’t overwritten by LiveView.</p>
<p>
You’ll notice that I’m also using <code class="inline">pushEventTo</code> after mounting. This is needed
because the user may not have interacted with the form yet to trigger a change,
so until then, I won’t have user’s timezone! I want it pushed immediately so I
can update the form’s changeset. Also, <code class="inline">pushEventTo</code> is used instead of
<code class="inline">pushEvent</code> because this is a LiveComponent, so I want the event pushed to the
LiveComponent and not the parent LiveView. I pass the target in via a data
attribute so I don’t confuse it with Phoenix’s own <code class="inline">phx-target</code>.</p>
<p>
When handling the event, we’ll merge the timezone with the existing params of
the changeset, and then re-apply the changeset and re-compute fields.</p>
<pre><code class="makeup elixir"><span class="na">@impl</span><span class="w"> </span><span class="nc">Phoenix.LiveComponent</span><span class="w">
</span><span class="kd">def</span><span class="w"> </span><span class="nf">handle_event</span><span class="p" data-group-id="1563682574-1">(</span><span class="s">&quot;timezone&quot;</span><span class="p">,</span><span class="w"> </span><span class="n">detected_timezone</span><span class="p">,</span><span class="w"> </span><span class="n">socket</span><span class="p" data-group-id="1563682574-1">)</span><span class="w"> </span><span class="k" data-group-id="1563682574-2">do</span><span class="w">
  </span><span class="n">params</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nc">Map</span><span class="o">.</span><span class="n">put</span><span class="p" data-group-id="1563682574-3">(</span><span class="n">socket</span><span class="o">.</span><span class="n">assigns</span><span class="o">.</span><span class="n">when_changeset</span><span class="o">.</span><span class="n">params</span><span class="p">,</span><span class="w"> </span><span class="s">&quot;timezone&quot;</span><span class="p">,</span><span class="w"> </span><span class="n">detected_timezone</span><span class="p" data-group-id="1563682574-3">)</span><span class="w">
  </span><span class="n">changeset</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">changeset</span><span class="p" data-group-id="1563682574-4">(</span><span class="n">socket</span><span class="o">.</span><span class="n">assigns</span><span class="o">.</span><span class="n">when_changeset</span><span class="p">,</span><span class="w"> </span><span class="n">params</span><span class="p" data-group-id="1563682574-4">)</span><span class="w">
  </span><span class="p" data-group-id="1563682574-5">{</span><span class="ss">:noreply</span><span class="p">,</span><span class="w">
    </span><span class="n">socket</span><span class="w">
    </span><span class="o">|&gt;</span><span class="w"> </span><span class="n">assign</span><span class="p" data-group-id="1563682574-6">(</span><span class="ss">:when_changeset</span><span class="p">,</span><span class="w"> </span><span class="n">changeset</span><span class="p" data-group-id="1563682574-6">)</span><span class="w">
    </span><span class="o">|&gt;</span><span class="w"> </span><span class="n">assign_computed</span><span class="p" data-group-id="1563682574-7">(</span><span class="n">changeset</span><span class="p" data-group-id="1563682574-7">)</span><span class="w">
  </span><span class="p" data-group-id="1563682574-5">}</span><span class="w">
</span><span class="k" data-group-id="1563682574-2">end</span></code></pre>
<a aria-hidden="true" name="clientinput"></a><h2>
Handling sub-form change events</h2>
<p>
Handling form change events doesn’t change with this <code class="inline">embedded_schema</code> and
component-ized approach. It’s standard Phoenix and Ecto changeset forms, so it’s
not very interesting to look at. But remember that you’ll need to use
<code class="inline">phx-target</code> to send  changes to the LiveComponent, otherwise they may bubble up
to your parent LiveView.</p>
<p>
In my case, I also need to adjust the parameters that come in, so we’ll look at
that! I need to check to see what field is changing and apply new parameters
based on what is changing.</p>
<pre><code class="makeup elixir"><span class="na">@impl</span><span class="w"> </span><span class="nc">Phoenix.LiveComponent</span><span class="w">
</span><span class="kd">def</span><span class="w"> </span><span class="nf">handle_event</span><span class="p" data-group-id="5154356243-1">(</span><span class="s">&quot;validate&quot;</span><span class="p">,</span><span class="w"> </span><span class="p" data-group-id="5154356243-2">%{</span><span class="s">&quot;when_component&quot;</span><span class="w"> </span><span class="p">=&gt;</span><span class="w"> </span><span class="n">params</span><span class="p" data-group-id="5154356243-2">}</span><span class="p">,</span><span class="w"> </span><span class="n">socket</span><span class="p" data-group-id="5154356243-1">)</span><span class="w"> </span><span class="k" data-group-id="5154356243-3">do</span><span class="w">
  </span><span class="n">adjusted_params</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">adjust_time_params</span><span class="p" data-group-id="5154356243-4">(</span><span class="n">params</span><span class="p">,</span><span class="w"> </span><span class="n">socket</span><span class="o">.</span><span class="n">assigns</span><span class="o">.</span><span class="n">when_changeset</span><span class="p" data-group-id="5154356243-4">)</span><span class="w">
  </span><span class="n">changeset</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">changeset</span><span class="p" data-group-id="5154356243-5">(</span><span class="n">socket</span><span class="o">.</span><span class="n">assigns</span><span class="o">.</span><span class="ow">when</span><span class="p">,</span><span class="w"> </span><span class="n">adjusted_params</span><span class="p" data-group-id="5154356243-5">)</span><span class="w">

  </span><span class="p" data-group-id="5154356243-6">{</span><span class="ss">:noreply</span><span class="p">,</span><span class="w">
    </span><span class="n">socket</span><span class="w">
    </span><span class="o">|&gt;</span><span class="w"> </span><span class="n">assign</span><span class="p" data-group-id="5154356243-7">(</span><span class="ss">:when_changeset</span><span class="p">,</span><span class="w"> </span><span class="n">changeset</span><span class="p" data-group-id="5154356243-7">)</span><span class="w">
    </span><span class="o">|&gt;</span><span class="w"> </span><span class="n">assign_computed</span><span class="p" data-group-id="5154356243-8">(</span><span class="n">changeset</span><span class="p" data-group-id="5154356243-8">)</span><span class="w">
  </span><span class="p" data-group-id="5154356243-6">}</span><span class="w">
</span><span class="k" data-group-id="5154356243-3">end</span><span class="w">

</span><span class="kd">defp</span><span class="w"> </span><span class="nf">assign_computed</span><span class="p" data-group-id="5154356243-9">(</span><span class="n">socket</span><span class="p">,</span><span class="w"> </span><span class="n">changeset</span><span class="p" data-group-id="5154356243-9">)</span><span class="w"> </span><span class="k" data-group-id="5154356243-10">do</span><span class="w">
  </span><span class="c1"># I need the start_at for the DatePicker component that I render from this</span><span class="w">
  </span><span class="c1"># component. I render the timezone and duration on the form. Lastly I</span><span class="w">
  </span><span class="c1"># compute and render some autocomplete suggestions from the values in the</span><span class="w">
  </span><span class="c1"># changeset</span><span class="w">
  </span><span class="n">socket</span><span class="w">
  </span><span class="o">|&gt;</span><span class="w"> </span><span class="n">assign</span><span class="p" data-group-id="5154356243-11">(</span><span class="ss">:timezone</span><span class="p">,</span><span class="w"> </span><span class="nc">Changeset</span><span class="o">.</span><span class="n">get_field</span><span class="p" data-group-id="5154356243-12">(</span><span class="n">changeset</span><span class="p">,</span><span class="w"> </span><span class="ss">:timezone</span><span class="p" data-group-id="5154356243-12">)</span><span class="p" data-group-id="5154356243-11">)</span><span class="w">
  </span><span class="o">|&gt;</span><span class="w"> </span><span class="n">assign</span><span class="p" data-group-id="5154356243-13">(</span><span class="ss">:start_at</span><span class="p">,</span><span class="w"> </span><span class="nc">Changeset</span><span class="o">.</span><span class="n">get_field</span><span class="p" data-group-id="5154356243-14">(</span><span class="n">changeset</span><span class="p">,</span><span class="w"> </span><span class="ss">:start_at</span><span class="p" data-group-id="5154356243-14">)</span><span class="p" data-group-id="5154356243-13">)</span><span class="w">
  </span><span class="o">|&gt;</span><span class="w"> </span><span class="n">assign</span><span class="p" data-group-id="5154356243-15">(</span><span class="ss">:duration</span><span class="p">,</span><span class="w"> </span><span class="nc">Changeset</span><span class="o">.</span><span class="n">get_field</span><span class="p" data-group-id="5154356243-16">(</span><span class="n">changeset</span><span class="p">,</span><span class="w"> </span><span class="ss">:duration</span><span class="p" data-group-id="5154356243-16">)</span><span class="p" data-group-id="5154356243-15">)</span><span class="w">
  </span><span class="o">|&gt;</span><span class="w"> </span><span class="n">assign</span><span class="p" data-group-id="5154356243-17">(</span><span class="ss">:start_time_autocomplete</span><span class="p">,</span><span class="w"> </span><span class="n">start_autocompletes</span><span class="p" data-group-id="5154356243-18">(</span><span class="n">changeset</span><span class="p" data-group-id="5154356243-18">)</span><span class="p" data-group-id="5154356243-17">)</span><span class="w">
  </span><span class="o">|&gt;</span><span class="w"> </span><span class="n">assign</span><span class="p" data-group-id="5154356243-19">(</span><span class="ss">:end_time_autocomplete</span><span class="p">,</span><span class="w"> </span><span class="n">end_autocompletes</span><span class="p" data-group-id="5154356243-20">(</span><span class="n">changeset</span><span class="p" data-group-id="5154356243-20">)</span><span class="p" data-group-id="5154356243-19">)</span><span class="w">
</span><span class="k" data-group-id="5154356243-10">end</span><span class="w">

</span><span class="kd">defp</span><span class="w"> </span><span class="nf">adjust_time_params</span><span class="p" data-group-id="5154356243-21">(</span><span class="n">params</span><span class="p">,</span><span class="w"> </span><span class="n">changeset</span><span class="p" data-group-id="5154356243-21">)</span><span class="w"> </span><span class="k" data-group-id="5154356243-22">do</span><span class="w">
  </span><span class="n">start_at_time</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">params_to_time</span><span class="p" data-group-id="5154356243-23">(</span><span class="n">params</span><span class="p" data-group-id="5154356243-24">[</span><span class="s">&quot;start_at_time&quot;</span><span class="p" data-group-id="5154356243-24">]</span><span class="p" data-group-id="5154356243-23">)</span><span class="w">
  </span><span class="n">end_at_time</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">params_to_time</span><span class="p" data-group-id="5154356243-25">(</span><span class="n">params</span><span class="p" data-group-id="5154356243-26">[</span><span class="s">&quot;end_at_time&quot;</span><span class="p" data-group-id="5154356243-26">]</span><span class="p" data-group-id="5154356243-25">)</span><span class="w">

  </span><span class="k">cond</span><span class="w"> </span><span class="k" data-group-id="5154356243-27">do</span><span class="w">
    </span><span class="n">end_at_time</span><span class="w"> </span><span class="o">!=</span><span class="w"> </span><span class="nc">Changeset</span><span class="o">.</span><span class="n">get_field</span><span class="p" data-group-id="5154356243-28">(</span><span class="n">changeset</span><span class="p">,</span><span class="w"> </span><span class="ss">:end_at_time</span><span class="p" data-group-id="5154356243-28">)</span><span class="w"> </span><span class="o">-&gt;</span><span class="w">
      </span><span class="n">params_from_new_end_time</span><span class="p" data-group-id="5154356243-29">(</span><span class="n">start_at_time</span><span class="p">,</span><span class="w"> </span><span class="n">end_at_time</span><span class="p">,</span><span class="w"> </span><span class="n">params</span><span class="p" data-group-id="5154356243-29">)</span><span class="w">

    </span><span class="n">start_at_time</span><span class="w"> </span><span class="o">!=</span><span class="w"> </span><span class="nc">Changeset</span><span class="o">.</span><span class="n">get_field</span><span class="p" data-group-id="5154356243-30">(</span><span class="n">changeset</span><span class="p">,</span><span class="w"> </span><span class="ss">:start_at_time</span><span class="p" data-group-id="5154356243-30">)</span><span class="w"> </span><span class="o">-&gt;</span><span class="w">
      </span><span class="n">duration</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nc">Changeset</span><span class="o">.</span><span class="n">get_field</span><span class="p" data-group-id="5154356243-31">(</span><span class="n">changeset</span><span class="p">,</span><span class="w"> </span><span class="ss">:duration</span><span class="p" data-group-id="5154356243-31">)</span><span class="w">
      </span><span class="n">params_from_new_start_time</span><span class="p" data-group-id="5154356243-32">(</span><span class="n">start_at_time</span><span class="p">,</span><span class="w"> </span><span class="n">duration</span><span class="p">,</span><span class="w"> </span><span class="n">params</span><span class="p" data-group-id="5154356243-32">)</span><span class="w">

    </span><span class="no">true</span><span class="w"> </span><span class="o">-&gt;</span><span class="w">
      </span><span class="n">params</span><span class="w">
  </span><span class="k" data-group-id="5154356243-27">end</span><span class="w">
</span><span class="k" data-group-id="5154356243-22">end</span></code></pre>
<p>
Remember, we’re in a LiveComponent so we want to target the changes to itself
and not the parent LiveView. This is accomplished with <code class="inline">phx-target</code> on the form.</p>
<pre><code class="eex">&lt;%= f = form_for @when_changeset, &quot;#&quot;,
  phx_change: :validate,
  phx_target: &quot;##{@id}&quot;,
  phx_submit: :save,
  id: @id %&gt;

  &lt;%# ... timezone input mentioned above ... %&gt;

  &lt;%= date_select f, :date %&gt;
  &lt;%= time_input f, :start_time %&gt;
  &lt;%= time_input f, :end_time %&gt;
  Your duration is &lt;%= @duration %&gt;

&lt;/form&gt;</code></pre>
<a aria-hidden="true" name="subformsubmission"></a><h2>
Handling the sub-form submission</h2>
<p>
When the user tries to submit the form, either by hitting “enter” or clicking on
the submit button, I need to validate the form once again, and if it’s good tell
the parent LiveView that it’s ok to proceed and supply all the
helpfully-computed values.</p>
<p>
This time we’ll check if the changeset is valid with
<a href="https://hexdocs.pm/ecto/Ecto.Changeset.html#apply_action/2" title=""><code class="inline">Ecto.Changeset.apply_action/2</code></a>. Based on that result, we’ll let the
LiveComponent send a message to <del>itself</del>. Actually, a LiveComponent doesn’t
run in its own process, instead it’s running inside the parent LiveView’s
process. So <code class="inline">self()</code> is actually the LiveView and not the LiveComponent. This is
how we can send the parent LiveView the result!</p>
<p>
You can <a href="https://hexdocs.pm/phoenix_live_view/Phoenix.LiveComponent.html#module-liveview-as-the-source-of-truth">read more about LiveComponent and sources of truth in the docs</a>.</p>
<pre><code class="makeup elixir"><span class="na">@impl</span><span class="w"> </span><span class="nc">Phoenix.LiveComponent</span><span class="w">
</span><span class="kd">def</span><span class="w"> </span><span class="nf">handle_event</span><span class="p" data-group-id="4940781487-1">(</span><span class="s">&quot;save&quot;</span><span class="p">,</span><span class="w"> </span><span class="p" data-group-id="4940781487-2">%{</span><span class="s">&quot;when_component&quot;</span><span class="w"> </span><span class="p">=&gt;</span><span class="w"> </span><span class="n">params</span><span class="p" data-group-id="4940781487-2">}</span><span class="p">,</span><span class="w"> </span><span class="n">socket</span><span class="p" data-group-id="4940781487-1">)</span><span class="w"> </span><span class="k" data-group-id="4940781487-3">do</span><span class="w">
  </span><span class="n">socket</span><span class="o">.</span><span class="n">assigns</span><span class="o">.</span><span class="ow">when</span><span class="w">
  </span><span class="o">|&gt;</span><span class="w"> </span><span class="n">changeset</span><span class="p" data-group-id="4940781487-4">(</span><span class="n">params</span><span class="p" data-group-id="4940781487-4">)</span><span class="w">
  </span><span class="o">|&gt;</span><span class="w"> </span><span class="nc">Changeset</span><span class="o">.</span><span class="n">apply_action</span><span class="p" data-group-id="4940781487-5">(</span><span class="ss">:insert</span><span class="p" data-group-id="4940781487-5">)</span><span class="w">
  </span><span class="o">|&gt;</span><span class="w"> </span><span class="k">case</span><span class="w"> </span><span class="k" data-group-id="4940781487-6">do</span><span class="w">
    </span><span class="p" data-group-id="4940781487-7">{</span><span class="ss">:ok</span><span class="p">,</span><span class="w"> </span><span class="n">record</span><span class="p" data-group-id="4940781487-7">}</span><span class="w"> </span><span class="o">-&gt;</span><span class="w">
      </span><span class="n">send</span><span class="p" data-group-id="4940781487-8">(</span><span class="n">self</span><span class="p" data-group-id="4940781487-9">(</span><span class="p" data-group-id="4940781487-9">)</span><span class="p">,</span><span class="w"> </span><span class="p" data-group-id="4940781487-10">{</span><span class="ss">:proceed</span><span class="p">,</span><span class="w"> </span><span class="n">record</span><span class="p" data-group-id="4940781487-10">}</span><span class="p" data-group-id="4940781487-8">)</span><span class="w">
      </span><span class="p" data-group-id="4940781487-11">{</span><span class="ss">:noreply</span><span class="p">,</span><span class="w"> </span><span class="n">socket</span><span class="p" data-group-id="4940781487-11">}</span><span class="w">
    </span><span class="p" data-group-id="4940781487-12">{</span><span class="ss">:error</span><span class="p">,</span><span class="w"> </span><span class="n">changeset</span><span class="p" data-group-id="4940781487-12">}</span><span class="w"> </span><span class="o">-&gt;</span><span class="w">
      </span><span class="p" data-group-id="4940781487-13">{</span><span class="ss">:noreply</span><span class="p">,</span><span class="w">
        </span><span class="n">socket</span><span class="w">
        </span><span class="o">|&gt;</span><span class="w"> </span><span class="n">assign</span><span class="p" data-group-id="4940781487-14">(</span><span class="ss">:when_changeset</span><span class="p">,</span><span class="w"> </span><span class="n">changeset</span><span class="p" data-group-id="4940781487-14">)</span><span class="w">
        </span><span class="o">|&gt;</span><span class="w"> </span><span class="n">assign_computed</span><span class="p" data-group-id="4940781487-15">(</span><span class="n">changeset</span><span class="p" data-group-id="4940781487-15">)</span><span class="w">
      </span><span class="p" data-group-id="4940781487-13">}</span><span class="w">
  </span><span class="k" data-group-id="4940781487-6">end</span><span class="w">
</span><span class="k" data-group-id="4940781487-3">end</span><span class="w">

</span><span class="c1"># and caught in the parent LiveView:</span><span class="w">

</span><span class="na">@impl</span><span class="w"> </span><span class="nc">Phoenix.LiveView</span><span class="w">
</span><span class="kd">def</span><span class="w"> </span><span class="nf">handle_info</span><span class="p" data-group-id="4940781487-16">(</span><span class="p" data-group-id="4940781487-17">{</span><span class="ss">:proceed</span><span class="p">,</span><span class="w"> </span><span class="p" data-group-id="4940781487-18">%</span><span class="nc" data-group-id="4940781487-18">MyAppWeb.EventLive.WhenComponent</span><span class="p" data-group-id="4940781487-18">{</span><span class="p" data-group-id="4940781487-18">}</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">form</span><span class="p" data-group-id="4940781487-17">}</span><span class="p">,</span><span class="w"> </span><span class="n">socket</span><span class="p" data-group-id="4940781487-16">)</span><span class="w"> </span><span class="k" data-group-id="4940781487-19">do</span><span class="w">
  </span><span class="p" data-group-id="4940781487-20">{</span><span class="ss">:ok</span><span class="p">,</span><span class="w"> </span><span class="n">start_at_wall</span><span class="p" data-group-id="4940781487-20">}</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nc">NaiveDateTime</span><span class="o">.</span><span class="n">new</span><span class="p" data-group-id="4940781487-21">(</span><span class="n">form</span><span class="o">.</span><span class="n">start_at_date</span><span class="p">,</span><span class="w"> </span><span class="n">form</span><span class="o">.</span><span class="n">start_at_time</span><span class="p" data-group-id="4940781487-21">)</span><span class="w">
  </span><span class="p" data-group-id="4940781487-22">{</span><span class="ss">:ok</span><span class="p">,</span><span class="w"> </span><span class="n">end_at_wall</span><span class="p" data-group-id="4940781487-22">}</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nc">NaiveDateTime</span><span class="o">.</span><span class="n">new</span><span class="p" data-group-id="4940781487-23">(</span><span class="n">form</span><span class="o">.</span><span class="n">end_at_date</span><span class="p">,</span><span class="w"> </span><span class="n">form</span><span class="o">.</span><span class="n">end_at_time</span><span class="p" data-group-id="4940781487-23">)</span><span class="w">

  </span><span class="n">params</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p" data-group-id="4940781487-24">%{</span><span class="w">
    </span><span class="ss">start_at_utc</span><span class="p">:</span><span class="w"> </span><span class="n">form</span><span class="o">.</span><span class="n">start_at</span><span class="p">,</span><span class="w">
    </span><span class="ss">start_at_wall</span><span class="p">:</span><span class="w"> </span><span class="n">start_at_wall</span><span class="p">,</span><span class="w">
    </span><span class="ss">start_at_tz</span><span class="p">:</span><span class="w"> </span><span class="n">form</span><span class="o">.</span><span class="n">timezone</span><span class="p">,</span><span class="w">
    </span><span class="ss">end_at_utc</span><span class="p">:</span><span class="w"> </span><span class="n">form</span><span class="o">.</span><span class="n">end_at</span><span class="p">,</span><span class="w">
    </span><span class="ss">end_at_wall</span><span class="p">:</span><span class="w"> </span><span class="n">end_at_wall</span><span class="p">,</span><span class="w">
    </span><span class="ss">end_at_tz</span><span class="p">:</span><span class="w"> </span><span class="n">form</span><span class="o">.</span><span class="n">timezone</span><span class="w">
  </span><span class="p" data-group-id="4940781487-24">}</span><span class="w">

  </span><span class="p" data-group-id="4940781487-25">{</span><span class="ss">:noreply</span><span class="p">,</span><span class="w">
    </span><span class="n">socket</span><span class="w">
    </span><span class="o">|&gt;</span><span class="w"> </span><span class="n">assign</span><span class="p" data-group-id="4940781487-26">(</span><span class="ss">:params</span><span class="p">,</span><span class="w"> </span><span class="nc">Map</span><span class="o">.</span><span class="n">merge</span><span class="p" data-group-id="4940781487-27">(</span><span class="n">socket</span><span class="o">.</span><span class="n">assigns</span><span class="o">.</span><span class="n">params</span><span class="p">,</span><span class="w"> </span><span class="n">params</span><span class="p" data-group-id="4940781487-27">)</span><span class="p" data-group-id="4940781487-26">)</span><span class="w">
    </span><span class="o">|&gt;</span><span class="w"> </span><span class="n">assign_step</span><span class="p" data-group-id="4940781487-28">(</span><span class="ss">:next</span><span class="p" data-group-id="4940781487-28">)</span><span class="w">
  </span><span class="p" data-group-id="4940781487-25">}</span><span class="w">
</span><span class="k" data-group-id="4940781487-19">end</span></code></pre>
<a aria-hidden="true" name="formsubmission"></a><h2>
Handling the overall form submission</h2>
<p>
You’ll notice that I have a function <code class="inline">assign_step</code> above. Let’s go to the parent
LiveView and figure out how to change steps, except on the last step we want to
persist. We’ll look for the steps in the <code class="inline">@steps</code> module attribute, assign it,
and that should swap-out the form for the next one!</p>
<p>
If there isn’t a next step, then that must mean that we’re finished, so we
should try to save.</p>
<pre><code class="makeup elixir"><span class="kd">defmodule</span><span class="w"> </span><span class="nc">MyAppWeb.EventLive.Step</span><span class="w"> </span><span class="k" data-group-id="9753250956-1">do</span><span class="w">
  </span><span class="na">@moduledoc</span><span class="w"> </span><span class="s">&quot;Describe a step in the multi-step form and where it can go.&quot;</span><span class="w">
  </span><span class="kd">defstruct</span><span class="w"> </span><span class="p" data-group-id="9753250956-2">[</span><span class="ss">:name</span><span class="p">,</span><span class="w"> </span><span class="ss">:prev</span><span class="p">,</span><span class="w"> </span><span class="ss">:next</span><span class="p" data-group-id="9753250956-2">]</span><span class="w">
</span><span class="k" data-group-id="9753250956-1">end</span><span class="w">

</span><span class="kd">defmodule</span><span class="w"> </span><span class="nc">MyAppWeb.EventLive.New</span><span class="w"> </span><span class="k" data-group-id="9753250956-3">do</span><span class="w">
  </span><span class="kn">use</span><span class="w"> </span><span class="nc">MyAppWeb</span><span class="p">,</span><span class="w"> </span><span class="ss">:live_view</span><span class="w">
  </span><span class="kn">alias</span><span class="w"> </span><span class="nc">Ecto.Changeset</span><span class="w">
  </span><span class="c1"># ...snip...</span><span class="w">

  </span><span class="kd">defp</span><span class="w"> </span><span class="nf">assign_step</span><span class="p" data-group-id="9753250956-4">(</span><span class="n">socket</span><span class="p">,</span><span class="w"> </span><span class="n">step</span><span class="p" data-group-id="9753250956-4">)</span><span class="w"> </span><span class="k" data-group-id="9753250956-5">do</span><span class="w">
    </span><span class="k">if</span><span class="w"> </span><span class="n">new_step</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nc">Enum</span><span class="o">.</span><span class="n">find</span><span class="p" data-group-id="9753250956-6">(</span><span class="na">@steps</span><span class="p">,</span><span class="w"> </span><span class="o">&amp;</span><span class="w"> </span><span class="ni">&amp;1</span><span class="o">.</span><span class="n">name</span><span class="w"> </span><span class="o">==</span><span class="w"> </span><span class="nc">Map</span><span class="o">.</span><span class="n">get</span><span class="p" data-group-id="9753250956-7">(</span><span class="n">socket</span><span class="o">.</span><span class="n">assigns</span><span class="o">.</span><span class="n">progress</span><span class="p">,</span><span class="w"> </span><span class="n">step</span><span class="p" data-group-id="9753250956-7">)</span><span class="p" data-group-id="9753250956-6">)</span><span class="w"> </span><span class="k" data-group-id="9753250956-8">do</span><span class="w">
      </span><span class="n">assign</span><span class="p" data-group-id="9753250956-9">(</span><span class="n">socket</span><span class="p">,</span><span class="w"> </span><span class="ss">:progress</span><span class="p">,</span><span class="w"> </span><span class="n">new_step</span><span class="p" data-group-id="9753250956-9">)</span><span class="w">
    </span><span class="k" data-group-id="9753250956-8">else</span><span class="w">
      </span><span class="n">save</span><span class="p" data-group-id="9753250956-10">(</span><span class="n">socket</span><span class="p" data-group-id="9753250956-10">)</span><span class="w">
    </span><span class="k" data-group-id="9753250956-8">end</span><span class="w">
  </span><span class="k" data-group-id="9753250956-5">end</span><span class="w">

  </span><span class="kd">def</span><span class="w"> </span><span class="nf">save</span><span class="p" data-group-id="9753250956-11">(</span><span class="n">socket</span><span class="p" data-group-id="9753250956-11">)</span><span class="w"> </span><span class="k" data-group-id="9753250956-12">do</span><span class="w">
    </span><span class="c1"># remember we&#39;ve merged all params together, so this should be the complete</span><span class="w">
    </span><span class="c1"># picture.</span><span class="w">
    </span><span class="k">case</span><span class="w"> </span><span class="nc">Schedule</span><span class="o">.</span><span class="n">create_event</span><span class="p" data-group-id="9753250956-13">(</span><span class="n">socket</span><span class="o">.</span><span class="n">assigns</span><span class="o">.</span><span class="n">params</span><span class="p" data-group-id="9753250956-13">)</span><span class="w"> </span><span class="k" data-group-id="9753250956-14">do</span><span class="w">
      </span><span class="p" data-group-id="9753250956-15">{</span><span class="ss">:ok</span><span class="p">,</span><span class="w"> </span><span class="n">event</span><span class="p" data-group-id="9753250956-15">}</span><span class="w"> </span><span class="o">-&gt;</span><span class="w">
          </span><span class="n">socket</span><span class="w">
          </span><span class="o">|&gt;</span><span class="w"> </span><span class="n">put_flash</span><span class="p" data-group-id="9753250956-16">(</span><span class="ss">:info</span><span class="p">,</span><span class="w"> </span><span class="n">t</span><span class="p" data-group-id="9753250956-17">(</span><span class="s">&quot;Event Created&quot;</span><span class="p" data-group-id="9753250956-17">)</span><span class="p" data-group-id="9753250956-16">)</span><span class="w">
          </span><span class="o">|&gt;</span><span class="w"> </span><span class="n">push_redirect</span><span class="p" data-group-id="9753250956-18">(</span><span class="ss">to</span><span class="p">:</span><span class="w"> </span><span class="nc">Routes</span><span class="o">.</span><span class="n">live_path</span><span class="p" data-group-id="9753250956-19">(</span><span class="n">socket</span><span class="p">,</span><span class="w"> </span><span class="nc">EventLive.Show</span><span class="p">,</span><span class="w"> </span><span class="n">event</span><span class="o">.</span><span class="n">id</span><span class="p" data-group-id="9753250956-19">)</span><span class="p" data-group-id="9753250956-18">)</span><span class="w">

      </span><span class="p" data-group-id="9753250956-20">{</span><span class="ss">:error</span><span class="p">,</span><span class="w"> </span><span class="p" data-group-id="9753250956-21">%</span><span class="nc" data-group-id="9753250956-21">Changeset</span><span class="p" data-group-id="9753250956-21">{</span><span class="p" data-group-id="9753250956-21">}</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">changeset</span><span class="p" data-group-id="9753250956-20">}</span><span class="w"> </span><span class="o">-&gt;</span><span class="w">
         </span><span class="n">socket</span><span class="w">
         </span><span class="o">|&gt;</span><span class="w"> </span><span class="n">assign</span><span class="p" data-group-id="9753250956-22">(</span><span class="ss">:changeset</span><span class="p">,</span><span class="w"> </span><span class="n">changeset</span><span class="p" data-group-id="9753250956-22">)</span><span class="w">
         </span><span class="o">|&gt;</span><span class="w"> </span><span class="n">put_flash</span><span class="p" data-group-id="9753250956-23">(</span><span class="ss">:error</span><span class="p">,</span><span class="w"> </span><span class="s">&quot;There is an issue with what you filled in&quot;</span><span class="p" data-group-id="9753250956-23">)</span><span class="w">
    </span><span class="k" data-group-id="9753250956-14">end</span><span class="w">
  </span><span class="k" data-group-id="9753250956-12">end</span><span class="w">

  </span><span class="na">@impl</span><span class="w"> </span><span class="nc">Phoenix.LiveView</span><span class="w">
  </span><span class="kd">def</span><span class="w"> </span><span class="nf">handle_event</span><span class="p" data-group-id="9753250956-24">(</span><span class="s">&quot;prev-step&quot;</span><span class="p">,</span><span class="w"> </span><span class="c">_content</span><span class="p">,</span><span class="w"> </span><span class="n">socket</span><span class="p" data-group-id="9753250956-24">)</span><span class="w"> </span><span class="k" data-group-id="9753250956-25">do</span><span class="w">
    </span><span class="p" data-group-id="9753250956-26">{</span><span class="ss">:noreply</span><span class="p">,</span><span class="w"> </span><span class="n">assign_step</span><span class="p" data-group-id="9753250956-27">(</span><span class="n">socket</span><span class="p">,</span><span class="w"> </span><span class="ss">:prev</span><span class="p" data-group-id="9753250956-27">)</span><span class="p" data-group-id="9753250956-26">}</span><span class="w">
  </span><span class="k" data-group-id="9753250956-25">end</span><span class="w">
</span><span class="k" data-group-id="9753250956-3">end</span></code></pre>
<pre><code class="eex">&lt;%= MyAppWeb.Components.secondary_button(t(&quot;Back&quot;), phx_click: &quot;prev-step&quot;) %&gt;
&lt;%= MyAppWeb.Components.primary_button(t(&quot;Next&quot;), phx_disable_with: &quot;...&quot;, submit: true) %&gt;</code></pre>
<p>
Back buttons are easy too. Add <code class="inline">phx-click=&quot;prev-step&quot;</code> and handle the event in
the same way, except using <code class="inline">:prev</code>. Make sure there’s no back button on the
first step! (otherwise you’ll mistakenly try to save).</p>
<h2>
Conclusion</h2>
<p>
I hope this helps you out in your endeavors to tackle long and complicated
forms. Tweet me <a href="https://twitter.com/bernheisel" title="">@bernheisel</a> if you have suggestions or enjoyed this post!</p>
<h2>
Update</h2>
<p>
You might want to persist a draft record in-between steps. This is a great idea!
If you do this, then you can leverage LiveView’s <code class="inline">handle_params</code> to navigate to
the appropriate step depending on the draft’s progress.</p>
<p>
Also, some of my code examples aren’t very good for managing existing resources.
Keep that in mind when developing your own multi-step form. This was written for
the context of <em>creating a new event</em>, and not editing an existing event.
Subscribe to my RSS feed to check for a new post that revisits this problem.</p>
]]></content:encoded>
</item>
<item>
<title><![CDATA[Ecto Changesets on Elixir Mix podcast]]></title>
<dc:creator>David Bernheisel</dc:creator>
<description><![CDATA[I was invited to talk on the Elixir Mix podcast. We talked about the Ecto
Changesets and modeling change well.
]]></description>
<link>https://bernheisel.com/blog/ecto-changesets-on-elixir-mix-podcast</link>
<guid isPermaLink="true">https://bernheisel.com/blog/ecto-changesets-on-elixir-mix-podcast</guid>
<pubDate>Wed, 08 Apr 2020 00:00:00 -0400</pubDate>
<content:encoded><![CDATA[<p>
Check it out here:</p>
<p>
<a href="https://topenddevs.com/podcasts/elixir-mix/episodes/emx-091-managing-change-with-ecto-with-david-bernheisel">https://topenddevs.com/podcasts/elixir-mix/episodes/emx-091-managing-change-with-ecto-with-david-bernheisel</a></p>
<p>
Coming from ActiveRecord, Ecto and Changesets were a wonderful alternative! They
cover my <a href="https://bernheisel.com/blog/ecto_changeset_tips/">other blog post</a>
where I shared some tips and tricks for working with Changesets. We also cover
Ecto.Multi, how to compose Changesets, using “embedded” schemas, and much more!</p>
<iframe width="560" height="315" src="https://www.youtube-nocookie.com/embed/Zp7CpJqQ6I0" frameborder="0" allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture" allowfullscreen="true">
</iframe>
]]></content:encoded>
</item>
<item>
<title><![CDATA[NewTab Notes Extension]]></title>
<dc:creator>David Bernheisel</dc:creator>
<description><![CDATA[Today I published a new Chrome Extension in the Chrome Web Store. This
extension replaces the New Tab page with a rendered markdown page that you can
edit. It's customizable too!
]]></description>
<link>https://bernheisel.com/blog/newtab-notes-chrome-extension</link>
<guid isPermaLink="true">https://bernheisel.com/blog/newtab-notes-chrome-extension</guid>
<pubDate>Tue, 31 Mar 2020 00:00:00 -0400</pubDate>
<content:encoded><![CDATA[<p>
Today I <a href="https://chrome.google.com/webstore/detail/newtab-notes/kfbhbipgippofpifimbcnbafehjndccn" title="">published a new Chrome Extension in the Chrome Web Store</a>.
This extension replaces the New Tab page with a rendered markdown page that you
can edit. It’s customizable too!</p>
<p>
<a href="https://github.com/dbernheisel/MarkdownTab">I made it open-source as well.</a></p>
<p>
  <img src="/images/newtab-notes-screenshot.png" alt="screenshot">
</p>
<p>
I’ve been trying to get in the habit of taking more notes and writing notes
down before I forget about them. I also use the Chrome browser. The way I look
for information in Chrome is by opening a new tab and start typing away for the
question. What if the new tab had some information on it that I could stow away?</p>
<p>
There’s plenty of existing extensions out there and I tried about 5 or 6 of them
but they didn’t suite my taste or needs. If it didn’t sync between Chrome
devices, then my notes would be scattered across machines – no go. Often times,
the markdown syntax it provided was vanilla Markdown, but I’m used to newer
features and extensions offered by GitHub Flavored Markdown, such as task lists,
tables, and autolinking. If it didn’t support GFM – no go.</p>
<p>
I’m working on a project using Elixir, Vue, and Tailwind CSS, and I wanted to
practice using those frameworks so I can understand them more. It’s all about
the repetition for learning.</p>
<p>
Since I know nothing about Chrome Extension development, I <a href="https://github.com/intrvertmichael/markdown-tab">found an existing
extension</a> that was pretty close
to what I wanted. I forked it and started customizing it for my own wants.
Fiddling around with the code made me understand more of what makes it
Chrome-specific, which isn’t too much, and the rest was just
plain-old-javascript. This led me down the path of using Vue and TailwindCSS,
and packaging it with Webpack– just like I do with my Elixir project.</p>
<ul>
  <li>
Chrome needs a <a href="https://developer.chrome.com/docs/extensions/mv2/manifest/" title=""><code class="inline">manifest.json</code></a> to specify the icon, description, and
  permissions it needs to operate within Chrome.  </li>
  <li>
In the manifest, you specify the entry point HTML. In my case, I needed
  the new tab page.  </li>
  <li>
There are some Chrome APIs you can use. The only one that I care about is
  <a href="https://developer.chrome.com/docs/extensions/reference/storage/" title=""><code class="inline">chrome.storage.sync</code></a>. This handles storing small data related to your
  extension and handles offline and online capabilities. If you’re offline,
  then it’s ok and will store the data in local storage until you’re online
  again, which then it will sync through the net and update your other
  devices.  </li>
</ul>
<p>
Maybe one day I can make it more like <a href="https://github.com/vimwiki/vimwiki" title="">vimwiki</a> which offers multi-page
linking, but for now it’s fine.</p>
]]></content:encoded>
</item>
<item>
<title><![CDATA[Managing ElixirLS updates in Neovim with asdf and vim-plug]]></title>
<dc:creator>David Bernheisel</dc:creator>
<description><![CDATA[How I manage ElixirLS, neovim, and coc.nvim with vim-plug.]]></description>
<link>https://bernheisel.com/blog/vim-elixir-ls-plug</link>
<guid isPermaLink="true">https://bernheisel.com/blog/vim-elixir-ls-plug</guid>
<pubDate>Tue, 03 Mar 2020 00:00:00 -0500</pubDate>
<content:encoded><![CDATA[<p>
<strong>Update 2020-08-20</strong></p>
<p>
Since <a href="https://github.com/elixir-lsp/elixir-ls/pull/351">asdf removed their .tool-versions
file</a> I needed to adjust the
steps below. They renamed <code class="inline">.tool-versions</code> to <code class="inline">.release-tool-versions</code>, so we
need to account for that in our vimscript.</p>
<p>
Also, if you’re running Linux, then you might need to ensure you have <a href="https://github.com/asdf-vm/asdf-erlang/wiki">GCC
version 9 installed</a>. The Erlang
release that ElixirLS is using needs 9, and I found that my system had 10
installed.</p>
<hr class="thin">
<p>
<a href="https://kassioborges.com/2019/03/21/elixir-ls-on-coc.nvim.html" title="">Kassio’s Post</a> was inspirational, and I adapted from his setup. My setup is a
little different from his:</p>
<ul>
  <li>
I use <a href="https://asdf-vm.com" title="">asdf</a> to manage my installed Elixir and Erlang versions. The ElixirLS
  project has a tested Elixir version it was developed with; and I’d like to
  use that same version. I also don’t want to have to worry about not having
  the same installed versions as them.  </li>
  <li>
I use <a href="https://github.com/junegunn/vim-plug" title="">vim-plug</a>. It has a neat feature where you can clone any repository
  whether or not it’s built for vim or not. In this case, I’m going to use it
  to grab a copy of ElixirLS, and have it run a post-update hook. ElixirLS
  doesn’t have any vim code that gets loaded, so it’s benign.  </li>
  <li>
I wanted to let the compilation happen asynchronously. I don’t want
  compilation to lock up the UI.  </li>
</ul>
<h2>
The Proof</h2>
<p>
Here’s <a href="https://github.com/elixir-lsp/elixir-ls" title="">ElixirLS</a> in action inside vim with <a href="https://github.com/neoclide/coc.nvim" title="">coc.nvim</a>:</p>
<p>
  <img src="/images/elixir-ls-in-action.gif" alt="ElixirLS in action">
</p>
<p>
Here’s me manually calling to update ElixirLS. I have a terminal on the right
that is watching the filesystem so we can see it’s actually doing something:</p>
<p>
  <img src="/images/ManuallyCalling.gif" alt="Elixir Manual Update">
</p>
<p>
Here’s me using vim-plug to update ElixirLS. I have a terminal on the right
that is watching the filesystem so we can see it’s actually doing something:</p>
<p>
  <img src="/images/PlugUpdate.gif" alt="Elixir Manual Update">
</p>
<h2>
The Vimscript</h2>
<p>
Here’s the vim setup:</p>
<pre><code class="vim">call plug#begin(&#39;~/.config/nvim/plugged&#39;)
  Plug &#39;elixir-lsp/elixir-ls&#39;, { &#39;do&#39;: { -&gt; g:ElixirLS.compile() } }
  Plug &#39;neoclide/coc.nvim&#39;, {&#39;branch&#39;: &#39;release&#39;}
call plug#end()

let g:coc_global_extensions = [&#39;coc-elixir&#39;, &#39;coc-diagnostic&#39;]

let g:ElixirLS = {}
let ElixirLS.path = stdpath(&#39;config&#39;).&#39;/plugged/elixir-ls&#39;
let ElixirLS.lsp = ElixirLS.path.&#39;/release/language_server.sh&#39;
let ElixirLS.cmd = join([
        \ &#39;cp .release-tool-versions .tool-versions &amp;&amp;&#39;,
        \ &#39;asdf install &amp;&amp;&#39;,
        \ &#39;mix do&#39;,
        \ &#39;local.hex --force --if-missing,&#39;,
        \ &#39;local.rebar --force,&#39;,
        \ &#39;deps.get,&#39;,
        \ &#39;compile,&#39;,
        \ &#39;elixir_ls.release &amp;&amp;&#39;,
        \ &#39;rm .tool-versions&#39;
        \ ], &#39; &#39;)

function ElixirLS.on_stdout(_job_id, data, _event)
  let self.output[-1] .= a:data[0]
  call extend(self.output, a:data[1:])
endfunction

let ElixirLS.on_stderr = function(ElixirLS.on_stdout)

function ElixirLS.on_exit(_job_id, exitcode, _event)
  if a:exitcode[0] == 0
    echom &#39;&gt;&gt;&gt; ElixirLS compiled&#39;
  else
    echoerr join(self.output, &#39; &#39;)
    echoerr &#39;&gt;&gt;&gt; ElixirLS compilation failed&#39;
  endif
endfunction

function ElixirLS.compile()
  let me = copy(g:ElixirLS)
  let me.output = [&#39;&#39;]
  echom &#39;&gt;&gt;&gt; compiling ElixirLS&#39;
  let me.id = jobstart(&#39;cd &#39; . me.path . &#39; &amp;&amp; git pull &amp;&amp; &#39; . me.cmd, me)
endfunction

&quot; If you want to wait on the compilation only when running :PlugUpdate
&quot; then have the post-update hook use this function instead:

&quot; function ElixirLS.compile_sync()
&quot;   echom &#39;&gt;&gt;&gt; compiling ElixirLS&#39;
&quot;   silent call system(g:ElixirLS.cmd)
&quot;   echom &#39;&gt;&gt;&gt; ElixirLS compiled&#39;
&quot; endfunction


&quot; Then, update the Elixir language server
call coc#config(&#39;elixir&#39;, {
  \ &#39;command&#39;: g:ElixirLS.lsp,
  \ &#39;filetypes&#39;: [&#39;elixir&#39;, &#39;eelixir&#39;]
  \})
call coc#config(&#39;elixir.pathToElixirLS&#39;, g:ElixirLS.lsp)</code></pre>
<p>
And this is in my <code class="inline">:CocConfig</code> (<code class="inline">~/.config/nvim/coc-settings.json</code>):</p>
<pre><code class="json">{
  &quot;codeLens.enable&quot;: true,
  &quot;diagnostic-languageserver.filetypes&quot;: {
    &quot;elixir&quot;: [&quot;mix_credo&quot;, &quot;mix_credo_compile&quot;],
    &quot;eelixir&quot;: [&quot;mix_credo&quot;, &quot;mix_credo_compile&quot;]
  }
}</code></pre>
<h2>
Include coc.nvim, ElixirLS in plug</h2>
<p>
Starting from the top:</p>
<pre><code class="vim">call plug#begin(&#39;~/.config/nvim/plugged&#39;)
  Plug &#39;elixir-lsp/elixir-ls&#39;, { &#39;do&#39;: { -&gt; g:ElixirLS.compile() } }
  Plug &#39;neoclide/coc.nvim&#39;, {&#39;branch&#39;: &#39;release&#39;}
call plug#end()

let g:coc_global_extensions = [&#39;coc-elixir&#39;, &#39;coc-diagnostic&#39;]</code></pre>
<p>
I’m using vim-plug to grab some plugins. All we care about for now is coc.nvim
and elixir-ls. I’m providing options for elixir-ls to perform an after-update
action. In this case, a lambda which immediately evaluates <code class="inline">{ &#39;do&#39;: { -&gt; g:ElixirLS.compile() } }</code></p>
<p>
To learn more about the vim lambda, check out <code class="inline">:h expr-lambda</code>. We’re going to
look at the <code class="inline">ElixirLS.compile()</code> function later.</p>
<p>
The coc.nvim setup is straight from their readme. I’m also adding some
extensions that coc.nvim will install on its own after startup. In this case I
want <a href="https://github.com/elixir-lsp/coc-elixir" title="">coc-elixir</a> and <a href="https://github.com/iamcco/coc-diagnostic" title="">coc-diagnostic</a>.</p>
<p>
coc-elixir provides coc.nvim the settings to know how to work with Elixir
projects and the language server. It also will build ElixirLS on its own, but
we’re going to circumvent that in a moment.</p>
<p>
coc-diagnostic is a generic bridge for many non-Language-Server tools like
shellcheck and credo. In this case, I’m adding it for credo. I don’t need
coc-diagnostic to provide a formatter, since the main Elixir language server
will provide that already.</p>
<h2>
Define your vim ElixirLS dictionary</h2>
<p>
Next we’re going to create a dictionary with a couple of functions. This dict is
going to manage several things for us:</p>
<ul>
  <li>
The path to the language server executable and directory.  </li>
  <li>
The commands to run when needing to compile  </li>
  <li>
Job hook functions so Neovim can run this task asynchronously  </li>
</ul>
<p>
Check out <code class="inline">:h dictionary-function</code> in vim for more info on how to be a bit more
object-oriented in your vim scripts. If you go down this rabbit hole, I really
encourage you to look into <code class="inline">:h lua</code> as well which is better-suited for serious
vim programming.</p>
<pre><code class="vim">let g:ElixirLS = {}
let ElixirLS.path = stdpath(&#39;config&#39;).&#39;/plugged/elixir-ls&#39;
let ElixirLS.lsp = ElixirLS.path.&#39;/release/language_server.sh&#39;
let ElixirLS.cmd = join([
        \ &#39;cp .release-tool-versions .tool-versions &amp;&amp;&#39;,
        \ &#39;asdf install &amp;&amp;&#39;,
        \ &#39;mix do&#39;,
        \ &#39;local.hex --force --if-missing,&#39;,
        \ &#39;local.rebar --force,&#39;,
        \ &#39;deps.get,&#39;,
        \ &#39;compile,&#39;,
        \ &#39;elixir_ls.release &amp;&amp;&#39;,
        \ &#39;rm .tool-versions&#39;
        \ ], &#39; &#39;)</code></pre>
<p>
So far it’s pretty standard stuff. We initialize an empty global dictionary
first, then start stuffing some values in there. We’re using the function
<code class="inline">stdpath</code> so we avoid hard-coding any paths.</p>
<p>
The <code class="inline">join([...], &#39; &#39;)</code> is only a way to organize the commands in a visual way.
It’s not necessary; you can totally just concat some strings together. The end
result of this join is:</p>
<pre><code class="shell">$ cp .release-tool-versions .tool-versions &amp;&amp; \
    asdf install &amp;&amp; \
    mix do local.hex --force --if-missing, local.rebar --force, deps.get, compile, elixir_ls.release &amp;&amp; \
    rm .tool-versions</code></pre>
<p>
Since I’m using <a href="https://asdf-vm.com" title="">asdf</a> and <a href="https://github.com/elixir-lsp/elixir-ls/blob/master/.release-tool-versions">so are the ElixirLS
developers</a>
I want to make sure I’m using the ElixirLS developers’ tools so I know for sure
I won’t run into trouble while developing; I want my ElixirLS to be stable since
it’s such an important tool for me.</p>
<p>
We’re going to leverage <a href="https://hexdocs.pm/mix/Mix.Tasks.Do.html" title="">mix do</a> so we’re not starting Elixir fresh for each
command. This should speed some things up.</p>
<h2>
Run it in the background</h2>
<pre><code class="vim">function ElixirLS.on_stdout(_job_id, data, _event)
  let self.output[-1] .= a:data[0]
  call extend(self.output, a:data[1:])
endfunction

let ElixirLS.on_stderr = function(ElixirLS.on_stdout)

function ElixirLS.on_exit(_job_id, exitcode, _event)
  if a:exitcode[0] == 0
    echom &#39;&gt;&gt;&gt; ElixirLS compiled&#39;
  else
    echoerr join(self.output, &#39; &#39;)
    echoerr &#39;&gt;&gt;&gt; ElixirLS compilation failed&#39;
  endif
endfunction

function ElixirLS.compile()
  let me = copy(g:ElixirLS)
  let me.output = [&#39;&#39;]
  echom &#39;&gt;&gt;&gt; compiling ElixirLS&#39;
  let me.id = jobstart(&#39;cd &#39; . me.path . &#39; &amp;&amp; git pull &amp;&amp; &#39; . me.cmd, me)
endfunction</code></pre>
<p>
These functions are adding keys to the ElixirLS dictionary. If I echo out the
dictionary, you’ll see a normal dictionary with some funcrefs.</p>
<pre><code class="vim">:echo ElixirLS
{
  &#39;cmd&#39;: &#39;asdf install &amp;&amp; mix do local.hex --force --if-missing, local.rebar --force, deps.get, compile, elixir_ls.release&#39;,
  &#39;path&#39;: &#39;/home/me/.config/nvim/plugged/elixir-ls&#39;,
  &#39;on_exit&#39;: function(&#39;2&#39;),
  &#39;on_stdout&#39;: function(&#39;1&#39;),
  &#39;lsp&#39;: &#39;/home/me/.config/nvim/plugged/elixir-ls/release/language_server.sh&#39;,
  &#39;on_stderr&#39;: function(&#39;1&#39;, {...@0}),
  &#39;compile&#39;: function(&#39;3&#39;)
}</code></pre>
<p>
One of the great things about Neovim (and Vim8+) is that it really pushed
asynchronous work forward. Neovim introduced some functions to manage background
jobs. The one we end up using is <code class="inline">jobstart({cmd}[, {opts}])</code> (check out <code class="inline">:h jobstart</code>).
<strong>Heads up</strong> this is for Neovim; Vim8 has a different API for asynchronous
work. It’s still <code class="inline">jobstart</code> but the options are different, so be sure to check
out <code class="inline">:h job-options</code>.</p>
<pre><code class="vim">function ElixirLS.compile()
  let me = copy(g:ElixirLS)
  let me.output = [&#39;&#39;]
  echom &#39;&gt;&gt;&gt; compiling ElixirLS&#39;
  let me.id = jobstart(&#39;cd &#39; . me.path . &#39; &amp;&amp; git pull &amp;&amp; &#39; . me.cmd, me)
endfunction</code></pre>
<p>
First we’re going to make a copy of the dictionary since this can be
asynchronous; we’ll call it <code class="inline">me</code>. Then we’ll initialize a new key <code class="inline">output</code> so we
can store all the background job’s output into it. Lastly, we’ll start the job.
The first argument (if a string) will shell out and execute the command you fed
it.</p>
<p>
Here’s the complete command that ends up being sent:</p>
<pre><code class="shell">$ cd {the-path} &amp;&amp; \
    git pull &amp;&amp; \
    cp .release-tool-versions .tool-versions &amp;&amp; \
    asdf install &amp;&amp; \
    mix do local.hex --force --if-missing, local.rebar --force, deps.get, compile, elixir_ls.release &amp;&amp; \
    rm .tool-versions</code></pre>
<p>
<strong>If you’re only using this via vim-plug</strong>, then vim-plug will take care of the
<code class="inline">cd {the-path} &amp;&amp; git pull</code> on its own, so we don’t need to include that.
Totally skip it and only include <code class="inline">me.cmd</code>. In my case, I wanted to be able to
run <code class="inline">:call ElixirLS.compile()</code> myself as well which will need to perform those
tasks. It doesn’t hurt to keep those commands but they’re redundant.</p>
<p>
The last argument <code class="inline">me</code> is a dictionary that contains the keys that point to
functions that will accept a certain signature; the three functions it cares
about are:</p>
<ul>
  <li>
<code class="inline">on_stdout(job_id, data, event)</code>  </li>
  <li>
<code class="inline">on_stderr(job_id, data, event)</code>  </li>
  <li>
<code class="inline">on_exit(job_id, exitcode, _event)</code>  </li>
</ul>
<p>
The values that are passed into these functions are a bit odd, but remember it’s
focused on a stream of data, and not all the data at once. This means that the
data you get will be an array of values from the background job’s output (either
stdout or stderr).</p>
<p>
Let’s look at one of the functions that receives the hook:</p>
<pre><code class="vim">function ElixirLS.on_stdout(_job_id, data, _event)
  let self.output[-1] .= a:data[0]
  call extend(self.output, a:data[1:])
endfunction</code></pre>
<p>
<code class="inline">self</code> refers to the copy of the ElixirLS dictionary that started this job.
(check out <code class="inline">:h self</code>). Before we started the job, we initialized the dictionary
to have an <code class="inline">output</code> key that had a list with one empty string <code class="inline">[&#39;&#39;]</code>. We’re
going to use this list and append all the incoming output into it. At the very
end, <code class="inline">self.output</code> have something like <code class="inline">vim•[&#39;hey&#39;, &#39;hi\nthere&#39;, &quot;I&#39;m d&quot;, &#39;one now&#39;]</code>. Since the data isn’t necessarily split at newlines, we’re going to
combine the last stored output’s with the first incoming element, and then add
the rest of the incoming data to the stored output.</p>
<p>
<code class="inline">vim•let self.output[-1] .= a:data[0]</code>. Take the last stored element and concat the
first incoming data’s element, and then assign it back to <code class="inline">vim•self.output[-1]</code>.
Then add the two lists together. <code class="inline">extend()</code> will mutate the first element.</p>
<p>
Since we want to treat <code class="inline">stderr</code> and <code class="inline">stdout</code> as the same kind of output, we’re
going to have the <code class="inline">on_stderr</code> callback forward the call to the <code class="inline">on_stdout</code>
function. This avoids duplicating the code.</p>
<p>
Finally, let’s look at the <code class="inline">on_exit</code> callback:</p>
<pre><code class="vim">function ElixirLS.on_exit(_job_id, exitcode, _event)
  if a:exitcode[0] == 0
    echom &#39;&gt;&gt;&gt; ElixirLS compiled&#39;
  else
    echoerr join(self.output, &#39; &#39;)
    echoerr &#39;&gt;&gt;&gt; ElixirLS compilation failed&#39;
  endif
endfunction</code></pre>
<p>
The exitcode is passed into this function, but it’s still the funky buffer-like
array but it should always just be the one element with the exit code. If it’s
0, then it exited ok without error so let’s echo a message indicating we’re
done.</p>
<p>
Otherwise, let’s echo out the entire output as an error that I can find with
<code class="inline">:messages</code> and investigate what went wrong.</p>
<p>
All this means now is that we can run <code class="inline">:PlugUpdate</code> and ElixirLS will now update
itself, ensuring it’s running on the best version of Elixir for itself,
everything’s updated, downloaded, and recompiled without issue. I can also run
<code class="inline">ElixirLS.compile()</code> at any time if I suspect I need to update ElixirLS.</p>
<p>
With coc.nvim I can also check <code class="inline">:CocInfo</code> to see if the language servers are
running ok.</p>
<h2>
Use the fruits of the labor</h2>
<pre><code class="vim">call coc#config(&#39;elixir&#39;, {
  \ &#39;command&#39;: g:ElixirLS.lsp,
  \ &#39;filetypes&#39;: [&#39;elixir&#39;, &#39;eelixir&#39;]
  \})
call coc#config(&#39;elixir.pathToElixirLS&#39;, g:ElixirLS.lsp)</code></pre>
<p>
Almost done!!</p>
<p>
We have a somewhat dynamic path for the newly-compiled ElixirLS. On my Mac, the
path could be <code class="inline">/Users/me/.config/...</code>, but on my Linux computer it would be
<code class="inline">/home/me/.config/...</code>. CocConfig is a JSON file that can’t evaluate any
environment variables, so I need to resort to calling it from within vim. This
really works out though.</p>
<p>
The first <code class="inline">coc#config</code> is telling coc.nvim in general that there is an available
language server for the <code class="inline">elixir</code> and <code class="inline">eexlixir</code> filetypes. Lastly, we’re going
to tell <code class="inline">coc-elixir</code> to use our own compiled ElixirLS so it doesn’t need to go
off on its own and try to manage the installation and compilation of ElixirLS.</p>
<hr class="thin">
<p>
Have any vim and Elixir tips of your own? TWEEEEEEEET at me <a href="https://twitter.com/bernheisel" title="">@bernheisel</a></p>
]]></content:encoded>
</item>
<item>
<title><![CDATA[Ecto Tips: UUID Boilerplate, Docs, and Composing Changesets]]></title>
<dc:creator>David Bernheisel</dc:creator>
<description><![CDATA[There are some helpful techniques you can employ to help you and your
coworkers when writing long changeset functions. It's hard to remember what's
required, optional, and defaulted. DB-generated UUIDs. And composing
changesets.
]]></description>
<link>https://bernheisel.com/blog/ecto_changeset_tips</link>
<guid isPermaLink="true">https://bernheisel.com/blog/ecto_changeset_tips</guid>
<pubDate>Sun, 19 Jan 2020 00:00:00 -0500</pubDate>
<content:encoded><![CDATA[<p>
I use <a href="https://hexdocs.pm/ecto/Ecto.Changeset.html" title="">Ecto Changesets</a> a lot– a TON! and I love them. Since I’ve
been using them for a couple years now, I’ve noticed some patterns and now I
have a couple tips to share.</p>
<ul>
  <li>
<a href="#extract-boilerplate">Extract Boilerplate</a>  </li>
  <li>
<a href="#db-generated-uuids">DB-generated UUIDs</a>  </li>
  <li>
<a href="#interpolate-your-docs">Interpolate Your Docs</a>  </li>
  <li>
<a href="#compose-changesets">Compose Changesets</a>  </li>
</ul>
<a name="extract-boilerplate"></a><h2>
Extract Boilerplate</h2>
<p>
I use <a href="https://en.wikipedia.org/wiki/Universally_unique_identifier" title="">UUID</a>s for all my IDs, specifically v4.</p>
<p>
Long ago I managed some legacy codebases that chose (defaulted) integer-based
IDs, and two times now I’ve had to migrate them to tables with a BigSerial as
the ID because we reached our limit of IDs for the table. It’s easy to forget.</p>
<p>
I’d be fine with BigSerial, but since I started using Ecto, I found myself using
UUIDs instead, and now it’s become a habit.</p>
<p>
<strong>What about showing UUIDs in URLs in basic CRUD endpoints?</strong></p>
<p>
Yea, it’s really ugly to show UUIDs in the browser URL bar, especially for
nested routes. But, should you show internal database IDs to users? I don’t
think so, so when using UUIDs I am constantly reminded to design towards
generating unique slugs for UX. If you see a UUID in the browser URL, I feel
like I should replace it with a human-readable unique slug instead. IDs are for
machines, slugs are for humans. URLs are for humans too, though I understand
some may not agree with that.</p>
<h3>
Let’s default to UUID in Ecto</h3>
<p>
There’s some configuration for Ecto to default to UUIDs:</p>
<ol>
  <li>
<a href="#configure-generators">Configure Generators</a>  </li>
  <li>
<a href="#configure-migrations">Configure Migrations</a>  </li>
  <li>
<a href="#configure-schema">Configure Schema</a>  </li>
</ol>
<p>
Then lastly, we’ll wrap that up and make it easier by <a href="#pull-into-macro">pulling it into a
macro</a></p>
<a name="configure-generators"></a><h3>
Configure Generators</h3>
<p>
If you’re using Phoenix and use its generators, you might care about this
section. When you run <code class="inline">mix phx.gen.schema ARGS</code>, Phoenix and Ecto will throw in
some boilerplate into your schemas and migrations. Even if you don’t, it’s
harmless to configure it just in case things change later, or other developers
on your project choose to use the generators.</p>
<pre><code class="makeup elixir"><span class="w">  </span><span class="c1">#./config/config.exs</span><span class="w">
  </span><span class="n">config</span><span class="w"> </span><span class="ss">:my_app</span><span class="p">,</span><span class="w">
    </span><span class="ss">ecto_repos</span><span class="p">:</span><span class="w"> </span><span class="p" data-group-id="7250100387-1">[</span><span class="nc">MyApp.Repo</span><span class="p" data-group-id="7250100387-1">]</span><span class="p">,</span><span class="w">
    </span><span class="ss">generators</span><span class="p">:</span><span class="w"> </span><span class="p" data-group-id="7250100387-2">[</span><span class="ss">binary_id</span><span class="p">:</span><span class="w"> </span><span class="no">true</span><span class="p" data-group-id="7250100387-2">]</span><span class="w">

  </span><span class="c1"># This will tell your Schema that the primary_key is a binary UUID:</span><span class="w">

  </span><span class="c1">#./lib/my_app/random_schema.ex</span><span class="w">
  </span><span class="na">@primary_key</span><span class="w"> </span><span class="p" data-group-id="7250100387-3">{</span><span class="ss">:id</span><span class="p">,</span><span class="w"> </span><span class="ss">:binary_id</span><span class="p">,</span><span class="w"> </span><span class="ss">autogenerate</span><span class="p">:</span><span class="w"> </span><span class="no">true</span><span class="p" data-group-id="7250100387-3">}</span><span class="w">
  </span><span class="na">@foreign_key_type</span><span class="w"> </span><span class="ss">:binary_id</span><span class="w">
  </span><span class="n">schema</span><span class="w"> </span><span class="s">&quot;my_table&quot;</span><span class="w"> </span><span class="k" data-group-id="7250100387-4">do</span><span class="w">
    </span><span class="c1"># ...</span><span class="w">
  </span><span class="k" data-group-id="7250100387-4">end</span></code></pre>
<a name="configure-migrations"></a><h3>
Configure Migrations</h3>
<p>
When creating tables through a migration, you’ll need to specify that it should
not create an ID column that generates it’s own IDs (more on this later).
Instead, we’ll supply our own primary key column called <code class="inline">id</code>.</p>
<pre><code class="makeup elixir"><span class="kd">defmodule</span><span class="w"> </span><span class="nc">MyApp.Repo.Migrations.CreateUsers</span><span class="w"> </span><span class="k" data-group-id="6030297351-1">do</span><span class="w">
  </span><span class="na">@moduledoc</span><span class="w"> </span><span class="s">&quot;Creating Users in the database&quot;</span><span class="w">
  </span><span class="kn">use</span><span class="w"> </span><span class="nc">Ecto.Migration</span><span class="w">

  </span><span class="kd">def</span><span class="w"> </span><span class="nf">change</span><span class="w"> </span><span class="k" data-group-id="6030297351-2">do</span><span class="w">
    </span><span class="n">create</span><span class="w"> </span><span class="n">table</span><span class="p" data-group-id="6030297351-3">(</span><span class="ss">:users</span><span class="p">,</span><span class="w"> </span><span class="ss">primary_key</span><span class="p">:</span><span class="w"> </span><span class="no">false</span><span class="p" data-group-id="6030297351-3">)</span><span class="w"> </span><span class="k" data-group-id="6030297351-4">do</span><span class="w">
      </span><span class="n">add</span><span class="w"> </span><span class="ss">:id</span><span class="p">,</span><span class="w"> </span><span class="ss">:binary_id</span><span class="p">,</span><span class="w"> </span><span class="ss">primary_key</span><span class="p">:</span><span class="w"> </span><span class="no">true</span><span class="w">

      </span><span class="n">timestamps</span><span class="p" data-group-id="6030297351-5">(</span><span class="p" data-group-id="6030297351-5">)</span><span class="w">
    </span><span class="k" data-group-id="6030297351-4">end</span><span class="w">
  </span><span class="k" data-group-id="6030297351-2">end</span><span class="w">
</span><span class="k" data-group-id="6030297351-1">end</span></code></pre>
<p>
In Ecto 3.x, you can also <a href="https://hexdocs.pm/ecto_sql/Ecto.Migration.html#module-repo-configuration">reconfigure default ID column
settings</a>
with <code class="inline">:migration_primary_key</code> so you wouldn’t even have to <code class="inline">add :id</code> yourself,
but unfortunately it does not play well with Ecto macros that I will end up
using since it’s via Mix config.</p>
<a name="configure-schema"></a><h3>
Configure Schema</h3>
<p>
If you’re using the generators above, then they should insert these options in
for you, but if not, you’ll need to make sure they’re present.</p>
<pre><code class="makeup elixir"><span class="kd">defmodule</span><span class="w"> </span><span class="nc">MyApp.RandomSchema</span><span class="w"> </span><span class="k" data-group-id="6879844818-1">do</span><span class="w">
  </span><span class="kn">use</span><span class="w"> </span><span class="nc">Ecto.Schema</span><span class="w">
  </span><span class="kn">import</span><span class="w"> </span><span class="nc">Ecto.Changeset</span><span class="w">

  </span><span class="na">@primary_key</span><span class="w"> </span><span class="p" data-group-id="6879844818-2">{</span><span class="ss">:id</span><span class="p">,</span><span class="w"> </span><span class="ss">:binary_id</span><span class="p">,</span><span class="w"> </span><span class="ss">autogenerate</span><span class="p">:</span><span class="w"> </span><span class="no">true</span><span class="p" data-group-id="6879844818-2">}</span><span class="w">
  </span><span class="na">@foreign_key_type</span><span class="w"> </span><span class="ss">:binary_id</span><span class="w">

  </span><span class="n">schema</span><span class="w"> </span><span class="s">&quot;users&quot;</span><span class="w"> </span><span class="k" data-group-id="6879844818-3">do</span><span class="w">
    </span><span class="n">field</span><span class="w"> </span><span class="ss">:name</span><span class="p">,</span><span class="w"> </span><span class="ss">:string</span><span class="w">

    </span><span class="n">timestamps</span><span class="p" data-group-id="6879844818-4">(</span><span class="p" data-group-id="6879844818-4">)</span><span class="w">
  </span><span class="k" data-group-id="6879844818-3">end</span><span class="w">
</span><span class="k" data-group-id="6879844818-1">end</span></code></pre>
<p>
That’s it! It should work. Ecto’s <code class="inline">schema</code> macro will use the module attributes
to configure how it should treat the primary key. <a href="https://hexdocs.pm/ecto/Ecto.Schema.html#module-schema-attributes">The documentation has more
information if you want to read more</a>.</p>
<a name="pull-into-macro"></a><h3>
Pull Into Macro</h3>
<p>
That’s a lot of boilerplate for <em>each schema</em>. Let’s make it easier. Notice at
the top of the schema definition? <code class="inline">use Ecto.Schema</code>.</p>
<p>
This injects some code into the module. We can do that too! Let’s create our own
schema file that injects our boilerplate.</p>
<pre><code class="makeup elixir"><span class="kd">defmodule</span><span class="w"> </span><span class="nc">MyApp.Schema</span><span class="w"> </span><span class="k" data-group-id="2711710484-1">do</span><span class="w">
  </span><span class="na">@moduledoc</span><span class="w"> </span><span class="s">&quot;Ecto Schema Helpers&quot;</span><span class="w">

  </span><span class="kd">defmacro</span><span class="w"> </span><span class="nf">__using__</span><span class="p" data-group-id="2711710484-2">(</span><span class="bp">_</span><span class="p" data-group-id="2711710484-2">)</span><span class="w"> </span><span class="k" data-group-id="2711710484-3">do</span><span class="w">
    </span><span class="k">quote</span><span class="w"> </span><span class="k" data-group-id="2711710484-4">do</span><span class="w">
      </span><span class="kn">use</span><span class="w"> </span><span class="nc">Ecto.Schema</span><span class="w">
      </span><span class="kn">import</span><span class="w"> </span><span class="nc">Ecto.Changeset</span><span class="w">

      </span><span class="na">@primary_key</span><span class="w"> </span><span class="p" data-group-id="2711710484-5">{</span><span class="ss">:id</span><span class="p">,</span><span class="w"> </span><span class="ss">:binary_id</span><span class="p">,</span><span class="w"> </span><span class="ss">autogenerate</span><span class="p">:</span><span class="w"> </span><span class="no">true</span><span class="p" data-group-id="2711710484-5">}</span><span class="w">
      </span><span class="na">@foreign_key_type</span><span class="w"> </span><span class="ss">:binary_id</span><span class="w">
    </span><span class="k" data-group-id="2711710484-4">end</span><span class="w">
  </span><span class="k" data-group-id="2711710484-3">end</span><span class="w">
</span><span class="k" data-group-id="2711710484-1">end</span></code></pre>
<pre><code class="makeup diff"><span class="">defmodule MyApp.RandomSchema do</span><span class="w">
</span><span class="gd">-   use Ecto.Schema</span><span class="w">
</span><span class="gd">-   import Ecto.Changeset</span><span class="w">
</span><span class="">-</span><span class="w">
</span><span class="gd">-   @primary_key {:id, :binary_id, autogenerate: true}</span><span class="w">
</span><span class="gd">-   @foreign_key_type :binary_id</span><span class="w">
</span><span class="gi">+   use MyApp.Schema</span><span class="w">

  </span><span class="">schema &quot;users&quot; do</span><span class="w">
    </span><span class="">field :name, :string</span><span class="w">

    </span><span class="">timestamps()</span><span class="w">
  </span><span class="">end</span><span class="w">
</span><span class="">end</span></code></pre>
<p>
Now for each of your schemas, use this new module and you don’t have to
remember the boilerplate anymore.</p>
<a name="db-generated-uuids"></a><h2>
DB-Generated UUIDs</h2>
<p>
Maybe you noticed above that we have <code class="inline">autogenerate: true</code> above. This is telling
<strong>Ecto</strong> to generate those ID UUIDs instead of the database. That bothered me; I
feel like that’s a database responsibility, not an app responsibility.</p>
<p>
Let’s move that into the database. I’m using PostgreSQL, but I’m sure there are
similar tools for other databases.</p>
<ol>
  <li>
<a href="#enable-pgcrypto">Enable pgcrypto</a>  </li>
  <li>
<a href="#use-pgcrypto">Update our Boilerplate</a>  </li>
</ol>
<a name="enable-pgcrypto"></a><h3>
Enable pgcrypto</h3>
<p>
Postgres unfortunately cannot generate UUIDs simply out of the box, but it does
ship with functions that you can enable. You can get away with creating a
function that’ll generate UUIDs, but I prefer some battle-tested code that ships
with <a href="https://www.postgresql.org/docs/current/contrib.html" title="">postgres contrib</a>.</p>
<p>
One of those extensions is <a href="https://www.postgresql.org/docs/current/pgcrypto.html" title="">pgcrypto</a> which supplies a function
<code class="inline">gen_random_uuid()</code>.</p>
<p>
Let’s create a migration to have Postgres enable the extension.</p>
<pre><code class="shell">$ mix ecto.gen.migration add_pgcrypto</code></pre>
<pre><code class="makeup elixir"><span class="kd">defmodule</span><span class="w"> </span><span class="nc">MyApp.Repo.Migrations.AddPgcrypto</span><span class="w"> </span><span class="k" data-group-id="6692785889-1">do</span><span class="w">
  </span><span class="na">@moduledoc</span><span class="w"> </span><span class="s">&quot;Add PgCrypto so we can have Postgres generate it&#39;s own IDs&quot;</span><span class="w">
  </span><span class="kn">use</span><span class="w"> </span><span class="nc">Ecto.Migration</span><span class="w">

  </span><span class="kd">def</span><span class="w"> </span><span class="nf">change</span><span class="w"> </span><span class="k" data-group-id="6692785889-2">do</span><span class="w">
    </span><span class="n">execute</span><span class="p" data-group-id="6692785889-3">(</span><span class="w">
      </span><span class="s">&quot;CREATE EXTENSION IF NOT EXISTS </span><span class="se">\&quot;</span><span class="s">pgcrypto</span><span class="se">\&quot;</span><span class="s">&quot;</span><span class="p">,</span><span class="w">
      </span><span class="s">&quot;DROP EXTENSION IF EXISTS </span><span class="se">\&quot;</span><span class="s">pgcrypto</span><span class="se">\&quot;</span><span class="s">&quot;</span><span class="w">
    </span><span class="p" data-group-id="6692785889-3">)</span><span class="w">
  </span><span class="k" data-group-id="6692785889-2">end</span><span class="w">
</span><span class="k" data-group-id="6692785889-1">end</span></code></pre>
<p>
This sometimes requires special permissions on the database user. If this
doesn’t work for you, then you might need an additional procedure to enable the
extension for you. However, this should work for your local database for
development.</p>
<a name="use-pgcrypto"></a><h3>
Use pgcrypto</h3>
<p>
Now that the database can generate UUIDs, let’s use it our ID columns!</p>
<p>
First we’ll tell ID column to default its value. Looking at our earlier
migration, let’s modify it.</p>
<pre><code class="makeup diff"><span class="">defmodule MyApp.Repo.Migrations.CreateUsers do</span><span class="w">
  </span><span class="">@moduledoc &quot;Creating Users in the database&quot;</span><span class="w">
  </span><span class="">use Ecto.Migration</span><span class="w">

  </span><span class="">def change do</span><span class="w">
    </span><span class="">create table(:users, primary_key: false) do</span><span class="w">
</span><span class="gd">-      add :id, :binary_id, primary_key: true</span><span class="w">
</span><span class="gi">+      add :id, :binary_id, primary_key: true, default: fragment(&quot;gen_random_uuid()&quot;)</span><span class="w">

      </span><span class="">timestamps()</span><span class="w">
    </span><span class="">end</span><span class="w">
  </span><span class="">end</span><span class="w">
</span><span class="">end</span></code></pre>
<p>
Now it’ll generate it’s own ID. <strong>If you stopped here, you’ll notice that
when you insert records with Ecto, none of your returned structs will have an
ID!</strong> What happened?!</p>
<p>
We need to tell Ecto to <a href="https://hexdocs.pm/ecto/Ecto.Schema.html#field/3-options">read the ID back into the struct after
writing</a>.
Thankfully, that’s easy. Earlier we pulled some boilerplate into a
<code class="inline">MyApp.Schema</code>; let’s modify it:</p>
<pre><code class="makeup diff"><span class="">defmodule MyApp.Schema do</span><span class="w">
  </span><span class="">@moduledoc &quot;Ecto Schema Helpers&quot;</span><span class="w">

  </span><span class="">defmacro __using__(_) do</span><span class="w">
    </span><span class="">quote do</span><span class="w">
      </span><span class="">use Ecto.Schema</span><span class="w">
      </span><span class="">import Ecto.Changeset</span><span class="w">

</span><span class="gd">-      @primary_key {:id, :binary_id, autogenerate: true}</span><span class="w">
</span><span class="gi">+      @primary_key {:id, :binary_id, read_after_writes: true}</span><span class="w">
      </span><span class="">@foreign_key_type :binary_id</span><span class="w">
    </span><span class="">end</span><span class="w">
  </span><span class="">end</span><span class="w">
</span><span class="">end</span></code></pre>
<p>
Now Ecto will pull the database-generated UUID back. Yes! Now we have IDs
generated in the database again.</p>
<h3>
Avoiding Collisions</h3>
<p>
Ok, so it’s works, but what happens in the low low chance that it generated a
duplicate UUID? It will fail to insert since the primary key is unique. In this
case, you’ll need to handle retrying with application code to retry once. The
probability of it failing a second time is galaxy-scale low. Probably not worth
handling in new code for smallish tables, and worth revisiting if you have very
large-scale tables in the millions or billions.</p>
<a name="interpolate-your-docs"></a><h2>
Have Elixir Write Docs For You</h2>
<p>
One tedious task I find myself doing is trying to remember what fields I need,
don’t need, and which ones have a default value if not supplied when interacting
with a changeset function. I’ll constantly flip back and forth between my forms,
contexts, and schemas.</p>
<p>
In VIM, I’m using <a href="https://github.com/neoclide/coc.nvim" title="">coc.nvim</a> to enable Language Server integration. You can do
this easily in <a href="https://marketplace.visualstudio.com/items?itemName=elixir-lsp.elixir-ls" title="">VSCode</a> as well. One of the great features of this is
that you can lookup the documentation on a function with a keypress or hover.</p>
<p>
  <img src="/images/documentation-hover.png" alt="Documentation Hover">
</p>
<p>
See the screenshot and how it’s listing out the required, optional, and default
fields? Let’s make that happen.</p>
<pre><code class="makeup elixir"><span class="kd">defmodule</span><span class="w"> </span><span class="nc">MyApp.RandomSchema</span><span class="w"> </span><span class="k" data-group-id="4540847050-1">do</span><span class="w">
  </span><span class="na">@moduledoc</span><span class="w"> </span><span class="s">&quot;The Random Schema.&quot;</span><span class="w">
  </span><span class="kn">use</span><span class="w"> </span><span class="nc">MyApp.Schema</span><span class="w">

  </span><span class="na">@defaults</span><span class="w"> </span><span class="p" data-group-id="4540847050-2">%{</span><span class="w">
    </span><span class="ss">type</span><span class="p">:</span><span class="w"> </span><span class="s">&quot;topical&quot;</span><span class="p">,</span><span class="w">
    </span><span class="ss">is_invite_only</span><span class="p">:</span><span class="w"> </span><span class="no">false</span><span class="p">,</span><span class="w">
    </span><span class="ss">is_screening</span><span class="p">:</span><span class="w"> </span><span class="no">false</span><span class="w">
  </span><span class="p" data-group-id="4540847050-2">}</span><span class="w">

  </span><span class="n">schema</span><span class="w"> </span><span class="s">&quot;organizations&quot;</span><span class="w"> </span><span class="k" data-group-id="4540847050-3">do</span><span class="w">
    </span><span class="n">field</span><span class="w"> </span><span class="ss">:name</span><span class="p">,</span><span class="w"> </span><span class="ss">:string</span><span class="w">
    </span><span class="n">field</span><span class="w"> </span><span class="ss">:type</span><span class="p">,</span><span class="w"> </span><span class="ss">:string</span><span class="p">,</span><span class="w"> </span><span class="ss">default</span><span class="p">:</span><span class="w"> </span><span class="na">@defaults</span><span class="p" data-group-id="4540847050-4">[</span><span class="ss">:type</span><span class="p" data-group-id="4540847050-4">]</span><span class="w">

    </span><span class="n">field</span><span class="w"> </span><span class="ss">:audience_description</span><span class="p">,</span><span class="w"> </span><span class="ss">:string</span><span class="w">
    </span><span class="n">field</span><span class="w"> </span><span class="ss">:is_invite_only</span><span class="p">,</span><span class="w"> </span><span class="ss">:boolean</span><span class="p">,</span><span class="w"> </span><span class="ss">default</span><span class="p">:</span><span class="w"> </span><span class="na">@defaults</span><span class="p" data-group-id="4540847050-5">[</span><span class="ss">:is_invite_only</span><span class="p" data-group-id="4540847050-5">]</span><span class="w">
    </span><span class="n">field</span><span class="w"> </span><span class="ss">:is_screening</span><span class="p">,</span><span class="w"> </span><span class="ss">:boolean</span><span class="p">,</span><span class="w"> </span><span class="ss">default</span><span class="p">:</span><span class="w"> </span><span class="na">@defaults</span><span class="p" data-group-id="4540847050-6">[</span><span class="ss">:is_screening</span><span class="p" data-group-id="4540847050-6">]</span><span class="w">

    </span><span class="n">timestamps</span><span class="p" data-group-id="4540847050-7">(</span><span class="p" data-group-id="4540847050-7">)</span><span class="w">
  </span><span class="k" data-group-id="4540847050-3">end</span><span class="w">

  </span><span class="na">@optional_fields</span><span class="w"> </span><span class="sx">~w[is_invite_only is_screening screen_questions]a</span><span class="w">
  </span><span class="na">@required_fields</span><span class="w"> </span><span class="sx">~w[name type audience_description]a</span><span class="w">

  </span><span class="na">@doc</span><span class="w"> </span><span class="s">&quot;&quot;&quot;
  Required fields: </span><span class="si" data-group-id="4540847050-8">#{</span><span class="n">inspect</span><span class="w"> </span><span class="na">@required_fields</span><span class="si" data-group-id="4540847050-8">}</span><span class="s">
  Optional fields: </span><span class="si" data-group-id="4540847050-9">#{</span><span class="n">inspect</span><span class="w"> </span><span class="na">@optional_fields</span><span class="si" data-group-id="4540847050-9">}</span><span class="s">
  Defaults: </span><span class="si" data-group-id="4540847050-10">#{</span><span class="n">inspect</span><span class="w"> </span><span class="na">@defaults</span><span class="si" data-group-id="4540847050-10">}</span><span class="s">
  &quot;&quot;&quot;</span><span class="w">
  </span><span class="na">@spec</span><span class="w"> </span><span class="n">changeset</span><span class="p" data-group-id="4540847050-11">(</span><span class="p">%</span><span class="bp">__MODULE__</span><span class="p" data-group-id="4540847050-12">{</span><span class="p" data-group-id="4540847050-12">}</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="nc">Ecto.Changeset</span><span class="o">.</span><span class="n">t</span><span class="p" data-group-id="4540847050-13">(</span><span class="p" data-group-id="4540847050-13">)</span><span class="p">,</span><span class="w"> </span><span class="n">map</span><span class="p" data-group-id="4540847050-14">(</span><span class="p" data-group-id="4540847050-14">)</span><span class="p" data-group-id="4540847050-11">)</span><span class="w"> </span><span class="o">::</span><span class="w">
    </span><span class="nc">Ecto.Changeset</span><span class="o">.</span><span class="n">t</span><span class="p" data-group-id="4540847050-15">(</span><span class="p" data-group-id="4540847050-15">)</span><span class="w">
  </span><span class="kd">def</span><span class="w"> </span><span class="nf">changeset</span><span class="p" data-group-id="4540847050-16">(</span><span class="n">struct</span><span class="w"> </span><span class="o">\\</span><span class="w"> </span><span class="p">%</span><span class="bp">__MODULE__</span><span class="p" data-group-id="4540847050-17">{</span><span class="p" data-group-id="4540847050-17">}</span><span class="p">,</span><span class="w"> </span><span class="n">attrs</span><span class="p" data-group-id="4540847050-16">)</span><span class="w"> </span><span class="k" data-group-id="4540847050-18">do</span><span class="w">
    </span><span class="n">struct</span><span class="w">
    </span><span class="o">|&gt;</span><span class="w"> </span><span class="n">cast</span><span class="p" data-group-id="4540847050-19">(</span><span class="n">attrs</span><span class="p">,</span><span class="w"> </span><span class="na">@optional_fields</span><span class="w"> </span><span class="o">++</span><span class="w"> </span><span class="na">@required_fields</span><span class="p" data-group-id="4540847050-19">)</span><span class="w">
    </span><span class="o">|&gt;</span><span class="w"> </span><span class="n">validate_required</span><span class="p" data-group-id="4540847050-20">(</span><span class="na">@required_fields</span><span class="p" data-group-id="4540847050-20">)</span><span class="w">
  </span><span class="k" data-group-id="4540847050-18">end</span><span class="w">
</span><span class="k" data-group-id="4540847050-1">end</span></code></pre>
<p>
It’s not hard at all! It’s a feature I often forget exists with ExDocs and
Elixir: you can interpolate compiled values into documentation. In this case,
I’m defining my <code class="inline">@optional_fields</code>, <code class="inline">@required_fields</code>, and <code class="inline">@defaults</code>, and
then interpolating them into the changeset docs.</p>
<p>
Easy peasy!</p>
<a name="compose-changesets"></a><h2>
Compose Changesets</h2>
<p>
Did you know that changesets can chain together?</p>
<p>
For example:</p>
<p>
Let’s say you have a “main” changeset that performs the basic validations. These
validations should occur <em>every single time</em> an insert or update occurs for this
schema. This happens for both admins and users.</p>
<p>
Let’s also say that you have another changeset that runs additional validations
if the User is updating the record versus the Admin updating the same record.
You don’t have to duplicate that code!</p>
<pre><code class="makeup elixir"><span class="c1"># In MySchema</span><span class="w">
</span><span class="na">@spec</span><span class="w"> </span><span class="n">changeset</span><span class="p" data-group-id="2243876627-1">(</span><span class="p">%</span><span class="bp">__MODULE__</span><span class="p" data-group-id="2243876627-2">{</span><span class="p" data-group-id="2243876627-2">}</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="nc">Ecto.Changeset</span><span class="o">.</span><span class="n">t</span><span class="p" data-group-id="2243876627-3">(</span><span class="p" data-group-id="2243876627-3">)</span><span class="p">,</span><span class="w"> </span><span class="n">map</span><span class="p" data-group-id="2243876627-4">(</span><span class="p" data-group-id="2243876627-4">)</span><span class="p" data-group-id="2243876627-1">)</span><span class="w"> </span><span class="o">::</span><span class="w">
  </span><span class="nc">Ecto.Changeset</span><span class="o">.</span><span class="n">t</span><span class="p" data-group-id="2243876627-5">(</span><span class="p" data-group-id="2243876627-5">)</span><span class="w">
</span><span class="kd">def</span><span class="w"> </span><span class="nf">changeset</span><span class="p" data-group-id="2243876627-6">(</span><span class="n">struct_or_changeset</span><span class="w"> </span><span class="o">\\</span><span class="w"> </span><span class="p">%</span><span class="bp">__MODULE__</span><span class="p" data-group-id="2243876627-7">{</span><span class="p" data-group-id="2243876627-7">}</span><span class="p">,</span><span class="w"> </span><span class="n">attrs</span><span class="p" data-group-id="2243876627-6">)</span><span class="w"> </span><span class="k" data-group-id="2243876627-8">do</span><span class="w">
  </span><span class="n">struct_or_changeset</span><span class="w">
  </span><span class="o">|&gt;</span><span class="w"> </span><span class="n">cast</span><span class="p" data-group-id="2243876627-9">(</span><span class="n">attrs</span><span class="p">,</span><span class="w"> </span><span class="na">@optional_fields</span><span class="w"> </span><span class="o">++</span><span class="w"> </span><span class="na">@required_fields</span><span class="p" data-group-id="2243876627-9">)</span><span class="w">
  </span><span class="o">|&gt;</span><span class="w"> </span><span class="n">validate_required</span><span class="p" data-group-id="2243876627-10">(</span><span class="na">@required_fields</span><span class="p" data-group-id="2243876627-10">)</span><span class="w">
</span><span class="k" data-group-id="2243876627-8">end</span><span class="w">

</span><span class="na">@spec</span><span class="w"> </span><span class="n">additional_restrictions_changeset</span><span class="p" data-group-id="2243876627-11">(</span><span class="p">%</span><span class="bp">__MODULE__</span><span class="p" data-group-id="2243876627-12">{</span><span class="p" data-group-id="2243876627-12">}</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="nc">Ecto.Changeset</span><span class="o">.</span><span class="n">t</span><span class="p" data-group-id="2243876627-13">(</span><span class="p" data-group-id="2243876627-13">)</span><span class="p">,</span><span class="w"> </span><span class="n">map</span><span class="p" data-group-id="2243876627-14">(</span><span class="p" data-group-id="2243876627-14">)</span><span class="p" data-group-id="2243876627-11">)</span><span class="w"> </span><span class="o">::</span><span class="w">
  </span><span class="nc">Ecto.Changeset</span><span class="o">.</span><span class="n">t</span><span class="p" data-group-id="2243876627-15">(</span><span class="p" data-group-id="2243876627-15">)</span><span class="w">
</span><span class="kd">def</span><span class="w"> </span><span class="nf">additional_restrictions_changeset</span><span class="p" data-group-id="2243876627-16">(</span><span class="n">struct_or_changeset</span><span class="w"> </span><span class="o">\\</span><span class="w"> </span><span class="p">%</span><span class="bp">__MODULE__</span><span class="p" data-group-id="2243876627-17">{</span><span class="p" data-group-id="2243876627-17">}</span><span class="p">,</span><span class="w"> </span><span class="n">attrs</span><span class="p" data-group-id="2243876627-16">)</span><span class="w"> </span><span class="k" data-group-id="2243876627-18">do</span><span class="w">
  </span><span class="n">struct_or_changeset</span><span class="w">
  </span><span class="o">|&gt;</span><span class="w"> </span><span class="n">changeset</span><span class="p" data-group-id="2243876627-19">(</span><span class="n">attrs</span><span class="p" data-group-id="2243876627-19">)</span><span class="w">
  </span><span class="o">|&gt;</span><span class="w"> </span><span class="n">more_validations</span><span class="p" data-group-id="2243876627-20">(</span><span class="p" data-group-id="2243876627-20">)</span><span class="w">
</span><span class="k" data-group-id="2243876627-18">end</span><span class="w">

</span><span class="na">@spec</span><span class="w"> </span><span class="n">user_changeset</span><span class="p" data-group-id="2243876627-21">(</span><span class="p">%</span><span class="bp">__MODULE__</span><span class="p" data-group-id="2243876627-22">{</span><span class="p" data-group-id="2243876627-22">}</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="nc">Ecto.Changeset</span><span class="o">.</span><span class="n">t</span><span class="p" data-group-id="2243876627-23">(</span><span class="p" data-group-id="2243876627-23">)</span><span class="p">,</span><span class="w"> </span><span class="n">map</span><span class="p" data-group-id="2243876627-24">(</span><span class="p" data-group-id="2243876627-24">)</span><span class="p" data-group-id="2243876627-21">)</span><span class="w"> </span><span class="o">::</span><span class="w">
  </span><span class="nc">Ecto.Changeset</span><span class="o">.</span><span class="n">t</span><span class="p" data-group-id="2243876627-25">(</span><span class="p" data-group-id="2243876627-25">)</span><span class="w">
</span><span class="kd">def</span><span class="w"> </span><span class="nf">user_changeset</span><span class="p" data-group-id="2243876627-26">(</span><span class="n">changeset</span><span class="p">,</span><span class="w"> </span><span class="n">attrs</span><span class="p" data-group-id="2243876627-26">)</span><span class="w"> </span><span class="k" data-group-id="2243876627-27">do</span><span class="w">
  </span><span class="n">changeset</span><span class="w">
  </span><span class="o">|&gt;</span><span class="w"> </span><span class="n">cast</span><span class="p" data-group-id="2243876627-28">(</span><span class="n">attrs</span><span class="p">,</span><span class="w"> </span><span class="p" data-group-id="2243876627-29">[</span><span class="ss">:some_more_fields</span><span class="p" data-group-id="2243876627-29">]</span><span class="p" data-group-id="2243876627-28">)</span><span class="w">
  </span><span class="o">|&gt;</span><span class="w"> </span><span class="n">even_more_validations</span><span class="p" data-group-id="2243876627-30">(</span><span class="p" data-group-id="2243876627-30">)</span><span class="w">
</span><span class="k" data-group-id="2243876627-27">end</span><span class="w">

</span><span class="c1">## ... In some other app code</span><span class="w">

</span><span class="kd">def</span><span class="w"> </span><span class="nf">create</span><span class="p" data-group-id="2243876627-31">(</span><span class="n">params</span><span class="p" data-group-id="2243876627-31">)</span><span class="w"> </span><span class="k" data-group-id="2243876627-32">do</span><span class="w">
  </span><span class="nc">MySchema</span><span class="o">.</span><span class="n">changeset</span><span class="p" data-group-id="2243876627-33">(</span><span class="p" data-group-id="2243876627-34">%</span><span class="nc" data-group-id="2243876627-34">MySchema</span><span class="p" data-group-id="2243876627-34">{</span><span class="p" data-group-id="2243876627-34">}</span><span class="p">,</span><span class="w"> </span><span class="n">params</span><span class="p" data-group-id="2243876627-33">)</span><span class="w">
</span><span class="k" data-group-id="2243876627-32">end</span><span class="w">

</span><span class="kd">def</span><span class="w"> </span><span class="nf">restricted_create</span><span class="p" data-group-id="2243876627-35">(</span><span class="n">params</span><span class="p" data-group-id="2243876627-35">)</span><span class="w"> </span><span class="k" data-group-id="2243876627-36">do</span><span class="w">
  </span><span class="nc">MySchema</span><span class="o">.</span><span class="n">additional_restrictions_changeset</span><span class="p" data-group-id="2243876627-37">(</span><span class="p" data-group-id="2243876627-38">%</span><span class="nc" data-group-id="2243876627-38">MySchema</span><span class="p" data-group-id="2243876627-38">{</span><span class="p" data-group-id="2243876627-38">}</span><span class="p">,</span><span class="w"> </span><span class="n">params</span><span class="p" data-group-id="2243876627-37">)</span><span class="w">
</span><span class="k" data-group-id="2243876627-36">end</span><span class="w">

</span><span class="kd">def</span><span class="w"> </span><span class="nf">user_create</span><span class="p" data-group-id="2243876627-39">(</span><span class="n">params</span><span class="p" data-group-id="2243876627-39">)</span><span class="w"> </span><span class="k" data-group-id="2243876627-40">do</span><span class="w">
  </span><span class="p" data-group-id="2243876627-41">%</span><span class="nc" data-group-id="2243876627-41">MySchema</span><span class="p" data-group-id="2243876627-41">{</span><span class="p" data-group-id="2243876627-41">}</span><span class="w">
  </span><span class="o">|&gt;</span><span class="w"> </span><span class="nc">MySchema</span><span class="o">.</span><span class="n">changeset</span><span class="p" data-group-id="2243876627-42">(</span><span class="n">params</span><span class="p" data-group-id="2243876627-42">)</span><span class="w">
  </span><span class="o">|&gt;</span><span class="w"> </span><span class="nc">MySchema</span><span class="o">.</span><span class="n">user_changeset</span><span class="p" data-group-id="2243876627-43">(</span><span class="n">params</span><span class="p" data-group-id="2243876627-43">)</span><span class="w">
</span><span class="k" data-group-id="2243876627-40">end</span></code></pre>
<p>
This way, you can use <code class="inline">additional_restrictions_changeset/2</code> by itself and get
all the same logic within <code class="inline">changeset/2</code>. Or alternatively, compose them together
from the outside like in <code class="inline">user_create/1</code></p>
<p>
A common mistake that prevents changesets from being composable is that we’ll
write our function signatures to require the struct as the first param:</p>
<pre><code class="makeup elixir"><span class="c1"># don&#39;t do this:</span><span class="w">
</span><span class="na">@spec</span><span class="w"> </span><span class="n">changeset</span><span class="p" data-group-id="9130789263-1">(</span><span class="p">%</span><span class="bp">__MODULE__</span><span class="p" data-group-id="9130789263-2">{</span><span class="p" data-group-id="9130789263-2">}</span><span class="p">,</span><span class="w"> </span><span class="n">map</span><span class="p" data-group-id="9130789263-3">(</span><span class="p" data-group-id="9130789263-3">)</span><span class="p" data-group-id="9130789263-1">)</span><span class="w"> </span><span class="o">::</span><span class="w"> </span><span class="nc">Ecto.Changeset</span><span class="o">.</span><span class="n">t</span><span class="p" data-group-id="9130789263-4">(</span><span class="p" data-group-id="9130789263-4">)</span><span class="w">
</span><span class="kd">def</span><span class="w"> </span><span class="nf">changeset</span><span class="p" data-group-id="9130789263-5">(</span><span class="p">%</span><span class="bp">__MODULE__</span><span class="p" data-group-id="9130789263-6">{</span><span class="p" data-group-id="9130789263-6">}</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">struct</span><span class="p">,</span><span class="w"> </span><span class="n">attrs</span><span class="p" data-group-id="9130789263-5">)</span><span class="w"> </span><span class="k" data-group-id="9130789263-7">do</span><span class="w">
  </span><span class="n">struct</span><span class="w">
  </span><span class="o">|&gt;</span><span class="w"> </span><span class="n">cast</span><span class="p" data-group-id="9130789263-8">(</span><span class="n">attrs</span><span class="p">,</span><span class="w"> </span><span class="na">@optional_fields</span><span class="w"> </span><span class="o">++</span><span class="w"> </span><span class="na">@required_fields</span><span class="p" data-group-id="9130789263-8">)</span><span class="w">
  </span><span class="o">|&gt;</span><span class="w"> </span><span class="n">validate_required</span><span class="p" data-group-id="9130789263-9">(</span><span class="na">@required_fields</span><span class="p" data-group-id="9130789263-9">)</span><span class="w">
</span><span class="k" data-group-id="9130789263-7">end</span></code></pre>
<p>
This makes it more restrictive and keeps it from being composable.</p>
]]></content:encoded>
</item>
<item>
<title><![CDATA[DateTimeParser on Elixir Mix podcast]]></title>
<dc:creator>David Bernheisel</dc:creator>
<description><![CDATA[I was invited to talk on the Elixir Mix podcast. We talked about the Elixir
community, how to contribute, and the DateTimeParser library I released.
]]></description>
<link>https://bernheisel.com/blog/date-time-parser-podcast</link>
<guid isPermaLink="true">https://bernheisel.com/blog/date-time-parser-podcast</guid>
<pubDate>Mon, 09 Sep 2019 00:00:00 -0400</pubDate>
<content:encoded><![CDATA[<p>
Check it out here:</p>
<p>
<a href="https://topenddevs.com/podcasts/elixir-mix/episodes/emx-068-contributing-to-the-elixir-community-with-david-bernheisel-cory-schmitt">https://topenddevs.com/podcasts/elixir-mix/episodes/emx-068-contributing-to-the-elixir-community-with-david-bernheisel-cory-schmitt</a></p>
<p>
<a href="https://schmitty.me" title="">Cory</a> and I talk about our adventures in the Elixir community, Meetup.com, and
how we’ve contributed. The big contribution in here is the <a href="https://github.com/taxjar/date_time_parser" title="">DateTimeParser</a> that
we built.</p>
<iframe width="560" height="315" src="https://www.youtube-nocookie.com/embed/fFEqMqBwTE4" frameborder="0" allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture" allowfullscreen="true">
</iframe>
]]></content:encoded>
</item>
<item>
<title><![CDATA[VIM Testing and Workflow]]></title>
<dc:creator>David Bernheisel</dc:creator>
<description><![CDATA[If you develop in Ruby or Elixir or write Markdown, you might find this
helpful!
]]></description>
<link>https://bernheisel.com/blog/vim-workflow</link>
<guid isPermaLink="true">https://bernheisel.com/blog/vim-workflow</guid>
<pubDate>Wed, 28 Aug 2019 00:00:00 -0400</pubDate>
<content:encoded><![CDATA[<p>
I realized that I love my VIM workflow, so I want to share that with you.
I am by-no-means a VIM expert or purist – my <a href="https://neovim.io" title="">neovim</a> files are not slim and
are a melting pot of stolen code from others, sometimes modified or not,
sometimes found in GitHub comments, sometimes found in others’ dotfiles, or
Reddit comments.</p>
<p>
If you develop in Ruby or Elixir or write Markdown, you might find this helpful!</p>
<p>
Let’s start with some basics:</p>
<h2>
Environment</h2>
<ul>
  <li>
<a href="https://neovim.io" title="">neovim</a> - right now I’m using 0.4.0 so I can use floating windows.  </li>
  <li>
<a href="https://sw.kovidgoyal.net/kitty/" title="">kitty</a> Terminal Emulator  </li>
  <li>
built-in terminal in neovim  </li>
  <li>
<a href="https://archlinux.org/" title="">ArchLinux</a> - My distro of choice. Shouldn’t matter for this article.  </li>
  <li>
<a href="https://github.com/Airblader/i3" title="">i3</a> - My window manager. Shouldn’t matter for this article.  </li>
  <li>
<a href="https://github.com/junegunn/vim-plug" title="">Plug</a> - for managing neovim plugins.  </li>
  <li>
<a href="https://github.com/neoclide/coc.nvim" title="">coc.nvim</a> for languageserver integration.  </li>
  <li>
<a href="https://github.com/dbernheisel/dotfiles" title="">dotfiles</a> - My dotfiles if you want the complete picture  </li>
</ul>
<p>
There are three ways that I can split my workspace: (1) via my window manager
<a href="https://github.com/Airblader/i3" title="">i3</a>, (2) via my terminal emulator <a href="https://sw.kovidgoyal.net/kitty/" title="">kitty</a>, (3) via <a href="https://neovim.io" title="">neovim</a> with
splits/buffers. Generally I adhere to this practice:</p>
<ol>
  <li>
Split with i3 if it’s an application, especially a GUI app. This gives me
the ability to move the window to another desktop if I want.  </li>
  <li>
Split with built-in neovim terminal for tests.  </li>
  <li>
Don’t split with kitty ever. It’d be too confusing for me to have 3 sets of
keyboard shortcuts to keep track of for switching windows/panes/splits.
Maybe one day I’ll replace the built-in neovim terminal with a kitty split
or an i3 split.  </li>
</ol>
<h2>
Testing</h2>
<p>
I use <a href="https://github.com/vim-test/vim-test" title="">vim-test</a> and it’s pretty incredible. I picked this workflow up while
working at thoughtbot from some good friends and the <a href="https://github.com/thoughtbot/dotfiles" title="">thoughtbot dotfiles</a>,
and it changed the way I code. The whole TDD workflow is great despite that I
still have trouble actually writing tests first - I tend to spike, iterate,
iterate, THEN write tests, then open a PR. Yea– this probably means I’m not a
10x developer 😛</p>
<p>
I also use neoterm to help open up terminal splits. When I’m at home, I have an
ultrawide that I use so splitting windows vertically is the way to go; but when
I’m mobile with my laptop then I typically split horizontally. I want tests to
be visible either way, so I need this to be flexible.</p>
<p>
The vim-test neoterm strategy defaults to sending tests to the last-used neoterm
buffer; I can have more terminal buffers, but the first one I open is what
vim-test will use going forward.</p>
<p>
Here’s how I configure vim-test with neoterm. (my <a href="https://github.com/dbernheisel/dotfiles" title="">dotfiles</a> for reference)</p>
<pre><code class="vim">&quot; ~/.config/nvim/init.vim

&quot; Test
let g:test#strategy = &quot;neoterm&quot;
let g:neoterm_shell = &#39;$SHELL -l&#39; &quot; use the login shell
let g:neoterm_default_mod = &#39;vert&#39;
let g:neoterm_autoscroll = 1      &quot; autoscroll to the bottom when entering insert mode
let g:neoterm_size = 80
let g:neoterm_fixedsize = 1       &quot; fixed size. The autosizing was wonky for me
let g:neoterm_keep_term_open = 0  &quot; when buffer closes, exit the terminal too.
let test#ruby#rspec#options = { &#39;suite&#39;: &#39;--profile 5&#39; }

&quot; Create some commands that makes the splits easy

function! OpenTermV(...)
  let g:neoterm_size = 80
  let l:cmd = a:1 == &#39;&#39; ? &#39;pwd&#39; : a:1
  execute &#39;vert T &#39;.l:cmd
endfunction

function! OpenTermH(...)
  let g:neoterm_size = 10
  let l:cmd = a:1 == &#39;&#39; ? &#39;pwd&#39; : a:1
  execute &#39;belowright T &#39;.l:cmd
endfunction

command! -nargs=? VT call OpenTermV(&lt;q-args&gt;)
command! -nargs=? HT call OpenTermH(&lt;q-args&gt;)

&quot; Use the project&#39;s test suite script if it exists

function! RunTestSuite()
  Tclear
  if filereadable(&#39;bin/test_suite&#39;)
    T echo &#39;bin/test_suite&#39;
    T bin/test_suite
  elseif filereadable(&quot;bin/test&quot;)
    T echo &#39;bin/test&#39;
    T bin/test
  else
    TestSuite
  endif
endfunction

nmap &lt;silent&gt; &lt;leader&gt;t :call TestNearest&lt;CR&gt;
nmap &lt;silent&gt; &lt;leader&gt;T :call TestFile&lt;CR&gt;
nmap &lt;silent&gt; &lt;leader&gt;a :call RunTestSuite()&lt;CR&gt;
nmap &lt;silent&gt; &lt;leader&gt;l :call TestLast&lt;CR&gt;</code></pre>
<p>
I’ve found it conventional to have a <code class="inline">bin/test_suite</code> or <code class="inline">bin/test</code> script in
the project that takes care of a lot of details like environment exports,
cleanup, or making sure the test environment’s database is setup as well as
running all the tests. Even if the test suite isn’t complicated, it’s still
helpful for new developers on the project.</p>
<p>
If that script is present and when I want to run all tests, I should execute
that file; otherwise use the default vim-test suite command. For non-suite
tests, I use the default vim-test commands.</p>
<p>
<code class="inline">&lt;space&gt;a</code> triggers <strong>a</strong>ll tests. If a neoterm split isn’t open,
then it’ll automatically open one with the default settings– in my case, a
vertical split at 80 columns wide. If a neoterm split is already open, then
it’ll send the test to that split. In situations where my vertical space is
lacking, I prep by opening up a split, and then hit my test shortcut. <code class="inline">:HT</code> to
open the terminal up.</p>
<p>
If I’m testing a method or function, then <code class="inline">&lt;space&gt;t</code> to send the nearest
<strong>t</strong>est to it. If I’m trying to make a test pass, I’ll modify the code and then
<code class="inline">&lt;space&gt;l</code> to run the <strong>l</strong>ast test. If I’m refactoring a class or module, I’ll
run all the <strong>T</strong>ests for it. I haven’t found myself using vim-test’s TestVisit.
If you have some examples on where that command helps, I’d love to hear it!</p>
<p>
<a href="https://asciinema.org/a/gs6r5QlC8oR8HPNYhRPDypY6n">  <img src="https://asciinema.org/a/gs6r5QlC8oR8HPNYhRPDypY6n.svg" alt="asciicast">
</a></p>
<h2>
Transformations</h2>
<p>
This is a great start! But eventually there might be a pesky app where I need to
opt-into an environment variable, but only when I’m running a small number of
tests. vim-test lets me define my own transformations to the commands. I can
check for a certain file and string in it to determine what project I’m in. If
I’m in that project, then change the command where I can.</p>
<pre><code class="vim">&quot; ~/.config/nvim/after/ftplugin/ruby.vim

function! MyAppRspec(cmd) abort
  &quot; If I&#39;m in the pesky app and
  &quot; not running the entire test suite indicated by the --profile flag
  &quot; Add the SKIP_FIXTURES env var.
  call system(&quot;cat README.md | grep &#39;MyApp&#39;&quot;)
  if match(a:cmd, &#39;--profile&#39;) == -1 &amp;&amp; v:shell_error == 0
    return substitute(a:cmd, &#39;bundle exec&#39;, &#39;SKIP_FIXTURES=true bundle exec&#39;, &#39;&#39;)
  else
    return a:cmd
  endif
endfunction

let g:test#custom_transformations = {
      \ &#39;myapp_ruby&#39;: function(&#39;MyAppRspec&#39;)
      \ }
let g:test#transformation = &#39;myapp_ruby&#39;</code></pre>
<p>
On the Elixir side, umbrella apps can be a little tricky. vim-test will send the
path of the test to <code class="inline">mix test {file}</code>, but <code class="inline">mix</code> will run that command for each
of the apps in the umbrella. That’s probably not what we want to do since that
test exists for only one for apps. Again, we can solve it with a transformation.</p>
<pre><code class="vim">&quot; ~/.config/nvim/after/ftplugin/elixir.vim

function! ElixirUmbrellaTransform(cmd) abort
  &quot; if in an umbrella project indicated by the existence of an ./apps folder
  &quot; limit the mix command to the app to which the test belongs
  if match(a:cmd, &#39;apps/&#39;) != -1
    &quot; capture the app from the file path, and send it to the --app flag instead
    return substitute(a:cmd, &#39;mix test apps/\([^/]*\)/&#39;, &#39;mix cmd --app \1 mix test --color &#39;, &#39;&#39;)
  else
    return a:cmd
  end
endfunction

let g:test#custom_transformations = {
       \ &#39;elixir_umbrella&#39;: function(&#39;ElixirUmbrellaTransform&#39;)
       \ }
let g:test#transformation = &#39;elixir_umbrella&#39;</code></pre>
<p>
That’s it for tests!</p>
<p>
Hope you picked up something nifty. If you have any tips for me, send them my
way <a href="https://twitter.com/bernheisel" title="">@bernheisel</a></p>
]]></content:encoded>
</item>
<item>
<title><![CDATA[HTTPoison and Decompression]]></title>
<dc:creator>David Bernheisel</dc:creator>
<description><![CDATA[I learned the hard way that the popular HTTP client for Elixir doesn't
automatically decompress or re-encode responses. I had to fix it myself.
]]></description>
<link>https://bernheisel.com/blog/httpoison-and-decompression</link>
<guid isPermaLink="true">https://bernheisel.com/blog/httpoison-and-decompression</guid>
<pubDate>Sat, 01 Jun 2019 00:00:00 -0400</pubDate>
<content:encoded><![CDATA[<p>
Did you know that Ruby’s
<a href="https://ruby-doc.org/stdlib-2.6.3/libdoc/net/http/rdoc/Net/HTTP.html#class-Net::HTTP-label-Compression">Net::HTTP</a>
class automatically decompresses responses? It handles a lot of use cases that
we don’t have to remember ourselves. It’s built into Ruby!</p>
<p>
When I came across a JSON API service that was returning binary, I was a bit
puzzled; “what is this binary? I’m supposed to be getting text back…” and,
it’s not consistent either: sometimes I get text <em>on the same exact request</em> a
minute later. Baffling.</p>
<p>
On top of that, I was using <a href="https://github.com/parroty/exvcr" title="">ExVCR</a> in some tests which serializes the
request/response chain into JSON. ExVCR takes binary responses, encodes it into
Erlang Term format, then Base64 encodes that, and then puts <em>that</em> in the JSON
file it writes; and does all that in reverse when it’s replaying the “cassette”
when the tests run.</p>
<p>
I’ve seen text before on this endpoint, and now I’m getting binary
sometimes; and wait-a-second, don’t HTTP clients decompress responses? I’m
pretty sure I didn’t worry about this with Ruby’s Net::HTTP or HTTParty.</p>
<p>
Turns out, in the Elixir ecosystem, HTTPoison along with other common HTTP
clients like HTTPotion, the new Mint, Gun, and probably others don’t do this
automatically.</p>
<h2>
Let’s back up</h2>
<p>
HTTP requests and responses have some headers that tell the client/server what
format of content we’re looking for. The ones we care about here is
<strong><a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Accept-Encoding" title="">Accept-Encoding</a></strong> and <strong><a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Encoding" title="">Content-Encoding</a></strong>. There’s another one
that’s related called <strong><a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Type" title="">Content-Type</a></strong>, but that’s not exactly about
compression, but we’ll get to this one later.</p>
<p>
Accept-Encoding is what the client will use to say “YO SERVER! I need some of
this resource, and I can handle it compressed with <a href="https://github.com/google/brotli" title="">brotli</a>“</p>
<p>
Content-Encoding is what the server will respond with, as in “Oh hay Client!
Nice to see you; here’s your content as requested. I even compressed it in
brotli”</p>
<p>
What <em>REALLY</em> happens (in my experience), is that Accept-Encoding is ignored,
and <strong>the server’s gonna give whatever it wants to you</strong>. To complicate it more,
there are layers between the client and server that may compress data and modify
headers (or not). So, the server might have sent plaintext and provided a
Content-Encoding of <code class="inline">identity</code> or not a Content-Encoding at all (both of these
mean there is no compression.), but a load balancer, router, CDN, whatever,
might have compressed the body of data on the way back from the server to the
client.</p>
<p>
So what’s the client to do? It has to guess. This is probably why some clients
don’t automatically decompress data for you.</p>
<p>
Here are some of the options for <code class="inline">Content-Encoding</code>:</p>
<table>
  <thead>
    <tr>
      <th style="text-align: left;">
value      </th>
      <th style="text-align: left;">
meaning      </th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td style="text-align: left;">
<code class="inline">gzip</code>      </td>
      <td style="text-align: left;">
Compressed with Lempel-Ziv (LZ77). On desktops, this is a <code class="inline">.gz</code> file      </td>
    </tr>
    <tr>
      <td style="text-align: left;">
<code class="inline">x-gzip</code>      </td>
      <td style="text-align: left;">
Same as above, just an older expression      </td>
    </tr>
    <tr>
      <td style="text-align: left;">
<code class="inline">compress</code>      </td>
      <td style="text-align: left;">
Compressed with Lempel-Ziv-Welch (LZW)      </td>
    </tr>
    <tr>
      <td style="text-align: left;">
<code class="inline">deflate</code>      </td>
      <td style="text-align: left;">
Compressed with zlib. On desktops, this is a normal <code class="inline">.zip</code> file      </td>
    </tr>
    <tr>
      <td style="text-align: left;">
<code class="inline">br</code>      </td>
      <td style="text-align: left;">
Compressed with brotli.      </td>
    </tr>
    <tr>
      <td style="text-align: left;">
<code class="inline">identity</code>      </td>
      <td style="text-align: left;">
No compression      </td>
    </tr>
    <tr>
      <td style="text-align: left;">
(missing)      </td>
      <td style="text-align: left;">
No compression      </td>
    </tr>
  </tbody>
</table>
<p>
As an interesting sidenote, Phoenix supports compression into Brotli, but
otherwise there’s not yet built-in support for decompressing Brotli in
Erlang/Elixir. There’s also no built-in support for LZW, but that’s ok because
it’s not as good or popular as the other formats. The only built-ins for Erlang
and Elixir are <code class="inline">gzip</code> and <code class="inline">deflate</code> so that’s what I’ll support on this first
iteration.</p>
<h2>
HTTPoison</h2>
<p>
In Elixir, the most popular HTTP client is <a href="https://github.com/edgurgel/httpoison" title="">HTTPoison</a> according to <a href="https://hex.pm/packages" title="">hex.pm</a>.
Actually, let me clarify: HTTPoison itself doesn’t do any HTTP requests itself;
what I mean is it’s a wrapper for the Erlang HTTP client called <a href="https://github.com/benoitc/hackney" title="">hackney</a> which
actually does the HTTP requests, and HTTPoison wraps around that to make the API
a bit friendlier for Elixir.</p>
<p>
Let me re-word that for my use-case: I’m making a wrapper for a wrapper.</p>
<p>
I’m not the first to notice that it doesn’t decompress responses automatically.
There’s been an <a href="https://github.com/edgurgel/httpoison/issues/81">issue</a> open
since 2015 for them to auto-decompress, but the author has decided, (me
paraphrasing), “I’m not going to do it, but hackney is so we’ll get to benefit
from it soon enough!”, and <em>that</em>
<a href="https://github.com/benoitc/hackney/issues/155">issue</a> has been open for a since
Jan 2015. We’re still waiting. It’s June 2019. 4.5 years.</p>
<p>
Ok, cool, but I need to handle this now, and it doesn’t seem like there’s
movement in the popular library of choice.</p>
<h2>
It’s a little unfair</h2>
<p>
It’s unfair for me to suggest these libraries should absolutely support
decompression out of the box, because these clients are really powerful. They
also support streaming which complicates decompression. But, for simple JSON
request/responses and for most APIs, we’re not streaming.</p>
<p>
Major props for these libraries making my life easier; my issue is that I didn’t
know about these concepts before diving in, and through an issue I learned
about HTTP decompression.</p>
<p>
  <img src="/images/til.gif" alt="TIL">
</p>
<h2>
One more problem: Character Encoding</h2>
<p>
I am also working with an API service that responds with characters encoded in
ISO-8859-1 sometimes; not in UTF-8. In Elixir, strings are UTF-8 so I need to
make sure I can convert those characters to something readable for my logs, and
ultimately the clients. This character encoding is indicated in the HTTP header
<a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Type" title="">Content-Type</a>, paired with the format of content, like JSON or XML. It’s going
to look something like <code class="inline">text/plain;charset=utf-8</code> or
<code class="inline">application/json;charset=ISO-8859-1</code>.</p>
<h2>
Let’s do it</h2>
<p>
Let’s stick with HTTPoison out of pure laziness. If you’re implementing from
scratch, I’d recommend you to look at <a href="https://github.com/elixir-mint/mint" title="">Mint</a> first because it has no
dependencies and has a better philosophy with OTP, which is a <em>good thing</em>.
<a href="https://github.com/elixir-tesla/tesla" title="">Tesla</a> is also a good HTTP client to consider.</p>
<p>
Let me re-word that again: I’m going to write a wrapper (MyApp.HTTPClient) for a
wrapper (HTTPoison) of hackney for a wrapper (my layer that covers the 3rd party
API) of a 3rd party service. Exciting.</p>
<p>
Also please know that my project also includes Phoenix and Plug, so you might
see some helpers in the tests and implementation. If you’re not using Phoenix or
Plug, it should be pretty easy to replace these functions with your own.</p>
<h2>
My interface</h2>
<p>
It’s going to be exactly like HTTPoison’s. Creative, I know. But, this way I can
replace any usage of <code class="inline">HTTPoison.get</code> or <code class="inline">HTTPoison.post</code> with my own
<code class="inline">HttpClient.get</code> or <code class="inline">HttpClient.post</code>. Easy peasy.</p>
<p>
I’m also going to give room for dependency-injection so I can test this easily.
And, maybe one day I won’t want to use HTTPoison anymore, so this new interface
might also help transition my app to another HTTP client without disrupting too
much.</p>
<p>
Let’s write tests first:</p>
<pre><code class="makeup elixir"><span class="kd">defmodule</span><span class="w"> </span><span class="nc">MyApp.HttpClientTest</span><span class="w"> </span><span class="k" data-group-id="9377547365-1">do</span><span class="w">
  </span><span class="kn">use</span><span class="w"> </span><span class="nc">MyApp.DataCase</span><span class="p">,</span><span class="w"> </span><span class="ss">async</span><span class="p">:</span><span class="w"> </span><span class="no">true</span><span class="w">
  </span><span class="kn">import</span><span class="w"> </span><span class="nc">ExUnit.CaptureLog</span><span class="w">
  </span><span class="kn">alias</span><span class="w"> </span><span class="nc">MyApp.HTTPClient</span><span class="w">
  </span><span class="kn">require</span><span class="w"> </span><span class="nc">HTTPoison</span><span class="w">

  </span><span class="c1"># I used Erlang&#39;s :zlib.gzip(&quot;Hello&quot;)</span><span class="w">
  </span><span class="na">@gzipped_response</span><span class="w"> </span><span class="p" data-group-id="9377547365-2">&lt;&lt;</span><span class="mi">31</span><span class="p">,</span><span class="w"> </span><span class="mi">139</span><span class="p">,</span><span class="w"> </span><span class="mi">8</span><span class="p">,</span><span class="w"> </span><span class="mi">0</span><span class="p">,</span><span class="w"> </span><span class="mi">0</span><span class="p">,</span><span class="w"> </span><span class="mi">0</span><span class="p">,</span><span class="w"> </span><span class="mi">0</span><span class="p">,</span><span class="w"> </span><span class="mi">0</span><span class="p">,</span><span class="w"> </span><span class="mi">0</span><span class="p">,</span><span class="w"> </span><span class="mi">3</span><span class="p">,</span><span class="w"> </span><span class="mi">243</span><span class="p">,</span><span class="w"> </span><span class="mi">72</span><span class="p">,</span><span class="w"> </span><span class="mi">205</span><span class="p">,</span><span class="w"> </span><span class="mi">201</span><span class="p">,</span><span class="w"> </span><span class="mi">201</span><span class="p">,</span><span class="w"> </span><span class="mi">7</span><span class="p">,</span><span class="w"> </span><span class="mi">0</span><span class="p">,</span><span class="w"> </span><span class="mi">130</span><span class="p">,</span><span class="w"> </span><span class="mi">137</span><span class="p">,</span><span class="w"> </span><span class="mi">209</span><span class="p">,</span><span class="w"> </span><span class="mi">247</span><span class="p">,</span><span class="w"> </span><span class="mi">5</span><span class="p">,</span><span class="w"> </span><span class="mi">0</span><span class="p">,</span><span class="w"> </span><span class="mi">0</span><span class="p">,</span><span class="w"> </span><span class="mi">0</span><span class="p" data-group-id="9377547365-2">&gt;&gt;</span><span class="w">
  </span><span class="c1"># I used Erlang&#39;s :zlib.zip(&quot;Hello&quot;)</span><span class="w">
  </span><span class="na">@zipped_response</span><span class="w"> </span><span class="p" data-group-id="9377547365-3">&lt;&lt;</span><span class="mi">243</span><span class="p">,</span><span class="w"> </span><span class="mi">72</span><span class="p">,</span><span class="w"> </span><span class="mi">205</span><span class="p">,</span><span class="w"> </span><span class="mi">201</span><span class="p">,</span><span class="w"> </span><span class="mi">201</span><span class="p">,</span><span class="w"> </span><span class="mi">7</span><span class="p">,</span><span class="w"> </span><span class="mi">0</span><span class="p" data-group-id="9377547365-3">&gt;&gt;</span><span class="w">
  </span><span class="na">@decoded</span><span class="w"> </span><span class="s">&quot;Hello&quot;</span><span class="w">

  </span><span class="n">describe</span><span class="w"> </span><span class="s">&quot;get&quot;</span><span class="w"> </span><span class="k" data-group-id="9377547365-4">do</span><span class="w">
    </span><span class="n">test</span><span class="w"> </span><span class="s">&quot;decompresses a gzipped body&quot;</span><span class="w"> </span><span class="k" data-group-id="9377547365-5">do</span><span class="w">
      </span><span class="n">requester</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k" data-group-id="9377547365-6">fn</span><span class="w"> </span><span class="c">_url</span><span class="p">,</span><span class="w"> </span><span class="c">_headers</span><span class="p">,</span><span class="w"> </span><span class="c">_options</span><span class="w"> </span><span class="o">-&gt;</span><span class="w">
        </span><span class="p" data-group-id="9377547365-7">{</span><span class="ss">:ok</span><span class="p">,</span><span class="w"> </span><span class="p" data-group-id="9377547365-8">%</span><span class="nc" data-group-id="9377547365-8">HTTPoison.Response</span><span class="p" data-group-id="9377547365-8">{</span><span class="w">
          </span><span class="ss">body</span><span class="p">:</span><span class="w"> </span><span class="na">@gzipped_response</span><span class="p">,</span><span class="w">
          </span><span class="ss">headers</span><span class="p">:</span><span class="w"> </span><span class="p" data-group-id="9377547365-9">[</span><span class="p" data-group-id="9377547365-10">{</span><span class="s">&quot;Content-Encoding&quot;</span><span class="p">,</span><span class="w"> </span><span class="s">&quot;gzip&quot;</span><span class="p" data-group-id="9377547365-10">}</span><span class="p" data-group-id="9377547365-9">]</span><span class="w">
        </span><span class="p" data-group-id="9377547365-8">}</span><span class="p" data-group-id="9377547365-7">}</span><span class="w">
      </span><span class="k" data-group-id="9377547365-6">end</span><span class="w">

      </span><span class="n">assert</span><span class="w"> </span><span class="p" data-group-id="9377547365-11">{</span><span class="ss">:ok</span><span class="p">,</span><span class="w"> </span><span class="p" data-group-id="9377547365-12">%{</span><span class="ss">body</span><span class="p">:</span><span class="w"> </span><span class="na">@decoded</span><span class="p" data-group-id="9377547365-12">}</span><span class="p" data-group-id="9377547365-11">}</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nc">HTTPClient</span><span class="o">.</span><span class="n">get</span><span class="p" data-group-id="9377547365-13">(</span><span class="no">nil</span><span class="p">,</span><span class="w"> </span><span class="p" data-group-id="9377547365-14">[</span><span class="p" data-group-id="9377547365-14">]</span><span class="p">,</span><span class="w"> </span><span class="ss">requester</span><span class="p">:</span><span class="w"> </span><span class="n">requester</span><span class="p" data-group-id="9377547365-13">)</span><span class="w">
    </span><span class="k" data-group-id="9377547365-5">end</span><span class="w">

    </span><span class="n">test</span><span class="w"> </span><span class="s">&quot;decompresses a gzipped body with x-gzip header&quot;</span><span class="w"> </span><span class="k" data-group-id="9377547365-15">do</span><span class="w">
      </span><span class="n">requester</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k" data-group-id="9377547365-16">fn</span><span class="w"> </span><span class="c">_url</span><span class="p">,</span><span class="w"> </span><span class="c">_headers</span><span class="p">,</span><span class="w"> </span><span class="c">_options</span><span class="w"> </span><span class="o">-&gt;</span><span class="w">
        </span><span class="p" data-group-id="9377547365-17">{</span><span class="ss">:ok</span><span class="p">,</span><span class="w"> </span><span class="p" data-group-id="9377547365-18">%</span><span class="nc" data-group-id="9377547365-18">HTTPoison.Response</span><span class="p" data-group-id="9377547365-18">{</span><span class="w">
          </span><span class="ss">body</span><span class="p">:</span><span class="w"> </span><span class="na">@gzipped_response</span><span class="p">,</span><span class="w">
          </span><span class="ss">headers</span><span class="p">:</span><span class="w"> </span><span class="p" data-group-id="9377547365-19">[</span><span class="p" data-group-id="9377547365-20">{</span><span class="s">&quot;Content-Encoding&quot;</span><span class="p">,</span><span class="w"> </span><span class="s">&quot;x-gzip&quot;</span><span class="p" data-group-id="9377547365-20">}</span><span class="p" data-group-id="9377547365-19">]</span><span class="w">
        </span><span class="p" data-group-id="9377547365-18">}</span><span class="p" data-group-id="9377547365-17">}</span><span class="w">
      </span><span class="k" data-group-id="9377547365-16">end</span><span class="w">

      </span><span class="n">assert</span><span class="w"> </span><span class="p" data-group-id="9377547365-21">{</span><span class="ss">:ok</span><span class="p">,</span><span class="w"> </span><span class="p" data-group-id="9377547365-22">%{</span><span class="ss">body</span><span class="p">:</span><span class="w"> </span><span class="na">@decoded</span><span class="p" data-group-id="9377547365-22">}</span><span class="p" data-group-id="9377547365-21">}</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nc">HTTPClient</span><span class="o">.</span><span class="n">get</span><span class="p" data-group-id="9377547365-23">(</span><span class="no">nil</span><span class="p">,</span><span class="w"> </span><span class="p" data-group-id="9377547365-24">[</span><span class="p" data-group-id="9377547365-24">]</span><span class="p">,</span><span class="w"> </span><span class="ss">requester</span><span class="p">:</span><span class="w"> </span><span class="n">requester</span><span class="p" data-group-id="9377547365-23">)</span><span class="w">
    </span><span class="k" data-group-id="9377547365-15">end</span><span class="w">

    </span><span class="n">test</span><span class="w"> </span><span class="s">&quot;does not attempt to decompress a plain body with gzip header&quot;</span><span class="w"> </span><span class="k" data-group-id="9377547365-25">do</span><span class="w">
      </span><span class="n">body</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s">&quot;Hallo&quot;</span><span class="w">
      </span><span class="n">requester</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k" data-group-id="9377547365-26">fn</span><span class="w"> </span><span class="c">_url</span><span class="p">,</span><span class="w"> </span><span class="c">_headers</span><span class="p">,</span><span class="w"> </span><span class="c">_options</span><span class="w"> </span><span class="o">-&gt;</span><span class="w">
        </span><span class="p" data-group-id="9377547365-27">{</span><span class="ss">:ok</span><span class="p">,</span><span class="w"> </span><span class="p" data-group-id="9377547365-28">%</span><span class="nc" data-group-id="9377547365-28">HTTPoison.Response</span><span class="p" data-group-id="9377547365-28">{</span><span class="w">
          </span><span class="ss">body</span><span class="p">:</span><span class="w"> </span><span class="n">body</span><span class="p">,</span><span class="w">
          </span><span class="ss">headers</span><span class="p">:</span><span class="w"> </span><span class="p" data-group-id="9377547365-29">[</span><span class="p" data-group-id="9377547365-30">{</span><span class="s">&quot;Content-Encoding&quot;</span><span class="p">,</span><span class="w"> </span><span class="s">&quot;gzip&quot;</span><span class="p" data-group-id="9377547365-30">}</span><span class="p" data-group-id="9377547365-29">]</span><span class="w">
        </span><span class="p" data-group-id="9377547365-28">}</span><span class="p" data-group-id="9377547365-27">}</span><span class="w">
      </span><span class="k" data-group-id="9377547365-26">end</span><span class="w">

      </span><span class="n">assert</span><span class="w"> </span><span class="p" data-group-id="9377547365-31">{</span><span class="ss">:ok</span><span class="p">,</span><span class="w"> </span><span class="p" data-group-id="9377547365-32">%{</span><span class="ss">body</span><span class="p">:</span><span class="w"> </span><span class="o">^</span><span class="n">body</span><span class="p" data-group-id="9377547365-32">}</span><span class="p" data-group-id="9377547365-31">}</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nc">HTTPClient</span><span class="o">.</span><span class="n">get</span><span class="p" data-group-id="9377547365-33">(</span><span class="no">nil</span><span class="p">,</span><span class="w"> </span><span class="p" data-group-id="9377547365-34">[</span><span class="p" data-group-id="9377547365-34">]</span><span class="p">,</span><span class="w"> </span><span class="ss">requester</span><span class="p">:</span><span class="w"> </span><span class="n">requester</span><span class="p" data-group-id="9377547365-33">)</span><span class="w">
    </span><span class="k" data-group-id="9377547365-25">end</span><span class="w">

    </span><span class="n">test</span><span class="w"> </span><span class="s">&quot;decompresses a zipped body&quot;</span><span class="w"> </span><span class="k" data-group-id="9377547365-35">do</span><span class="w">
      </span><span class="n">requester</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k" data-group-id="9377547365-36">fn</span><span class="w"> </span><span class="c">_url</span><span class="p">,</span><span class="w"> </span><span class="c">_headers</span><span class="p">,</span><span class="w"> </span><span class="c">_options</span><span class="w"> </span><span class="o">-&gt;</span><span class="w">
        </span><span class="p" data-group-id="9377547365-37">{</span><span class="ss">:ok</span><span class="p">,</span><span class="w"> </span><span class="p" data-group-id="9377547365-38">%</span><span class="nc" data-group-id="9377547365-38">HTTPoison.Response</span><span class="p" data-group-id="9377547365-38">{</span><span class="w">
          </span><span class="ss">body</span><span class="p">:</span><span class="w"> </span><span class="na">@zipped_response</span><span class="p">,</span><span class="w">
          </span><span class="ss">headers</span><span class="p">:</span><span class="w"> </span><span class="p" data-group-id="9377547365-39">[</span><span class="p" data-group-id="9377547365-40">{</span><span class="s">&quot;Content-Encoding&quot;</span><span class="p">,</span><span class="w"> </span><span class="s">&quot;deflate&quot;</span><span class="p" data-group-id="9377547365-40">}</span><span class="p" data-group-id="9377547365-39">]</span><span class="w">
        </span><span class="p" data-group-id="9377547365-38">}</span><span class="p" data-group-id="9377547365-37">}</span><span class="w">
      </span><span class="k" data-group-id="9377547365-36">end</span><span class="w">

      </span><span class="n">assert</span><span class="w"> </span><span class="p" data-group-id="9377547365-41">{</span><span class="ss">:ok</span><span class="p">,</span><span class="w"> </span><span class="p" data-group-id="9377547365-42">%{</span><span class="ss">body</span><span class="p">:</span><span class="w"> </span><span class="na">@decoded</span><span class="p" data-group-id="9377547365-42">}</span><span class="p" data-group-id="9377547365-41">}</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nc">HTTPClient</span><span class="o">.</span><span class="n">get</span><span class="p" data-group-id="9377547365-43">(</span><span class="no">nil</span><span class="p">,</span><span class="w"> </span><span class="p" data-group-id="9377547365-44">[</span><span class="p" data-group-id="9377547365-44">]</span><span class="p">,</span><span class="w"> </span><span class="ss">requester</span><span class="p">:</span><span class="w"> </span><span class="n">requester</span><span class="p" data-group-id="9377547365-43">)</span><span class="w">
    </span><span class="k" data-group-id="9377547365-35">end</span><span class="w">

    </span><span class="n">test</span><span class="w"> </span><span class="s">&quot;emits log when encountering unsupported compression&quot;</span><span class="w"> </span><span class="k" data-group-id="9377547365-45">do</span><span class="w">
      </span><span class="n">requester</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k" data-group-id="9377547365-46">fn</span><span class="w"> </span><span class="c">_url</span><span class="p">,</span><span class="w"> </span><span class="c">_headers</span><span class="p">,</span><span class="w"> </span><span class="c">_options</span><span class="w"> </span><span class="o">-&gt;</span><span class="w">
        </span><span class="p" data-group-id="9377547365-47">{</span><span class="ss">:ok</span><span class="p">,</span><span class="w"> </span><span class="p" data-group-id="9377547365-48">%</span><span class="nc" data-group-id="9377547365-48">HTTPoison.Response</span><span class="p" data-group-id="9377547365-48">{</span><span class="w">
          </span><span class="ss">body</span><span class="p">:</span><span class="w"> </span><span class="s">&quot;Hallo&quot;</span><span class="p">,</span><span class="w">
          </span><span class="ss">headers</span><span class="p">:</span><span class="w"> </span><span class="p" data-group-id="9377547365-49">[</span><span class="p" data-group-id="9377547365-50">{</span><span class="s">&quot;Content-Encoding&quot;</span><span class="p">,</span><span class="w"> </span><span class="s">&quot;br&quot;</span><span class="p" data-group-id="9377547365-50">}</span><span class="p" data-group-id="9377547365-49">]</span><span class="w">
        </span><span class="p" data-group-id="9377547365-48">}</span><span class="p" data-group-id="9377547365-47">}</span><span class="w">
      </span><span class="k" data-group-id="9377547365-46">end</span><span class="w">

      </span><span class="n">assert</span><span class="w"> </span><span class="n">capture_log</span><span class="p" data-group-id="9377547365-51">(</span><span class="k" data-group-id="9377547365-52">fn</span><span class="w"> </span><span class="o">-&gt;</span><span class="w">
        </span><span class="n">assert</span><span class="w"> </span><span class="p" data-group-id="9377547365-53">{</span><span class="ss">:ok</span><span class="p">,</span><span class="w"> </span><span class="bp">_</span><span class="p" data-group-id="9377547365-53">}</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nc">HTTPClient</span><span class="o">.</span><span class="n">get</span><span class="p" data-group-id="9377547365-54">(</span><span class="no">nil</span><span class="p">,</span><span class="w"> </span><span class="p" data-group-id="9377547365-55">[</span><span class="p" data-group-id="9377547365-55">]</span><span class="p">,</span><span class="w"> </span><span class="ss">requester</span><span class="p">:</span><span class="w"> </span><span class="n">requester</span><span class="p" data-group-id="9377547365-54">)</span><span class="w">
      </span><span class="k" data-group-id="9377547365-52">end</span><span class="p" data-group-id="9377547365-51">)</span><span class="w"> </span><span class="o">=~</span><span class="w"> </span><span class="s">&quot;No support for decompression of body using &#39;br&#39; algorithm&quot;</span><span class="w">
    </span><span class="k" data-group-id="9377547365-45">end</span><span class="w">

    </span><span class="n">test</span><span class="w"> </span><span class="s">&quot;emits log when failing to decompress&quot;</span><span class="w"> </span><span class="k" data-group-id="9377547365-56">do</span><span class="w">
      </span><span class="n">requester</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k" data-group-id="9377547365-57">fn</span><span class="w"> </span><span class="c">_url</span><span class="p">,</span><span class="w"> </span><span class="c">_headers</span><span class="p">,</span><span class="w"> </span><span class="c">_options</span><span class="w"> </span><span class="o">-&gt;</span><span class="w">
        </span><span class="p" data-group-id="9377547365-58">{</span><span class="ss">:ok</span><span class="p">,</span><span class="w"> </span><span class="p" data-group-id="9377547365-59">%</span><span class="nc" data-group-id="9377547365-59">HTTPoison.Response</span><span class="p" data-group-id="9377547365-59">{</span><span class="w">
          </span><span class="ss">body</span><span class="p">:</span><span class="w"> </span><span class="p" data-group-id="9377547365-60">&lt;&lt;</span><span class="mi">1</span><span class="p">,</span><span class="w"> </span><span class="mi">2</span><span class="p">,</span><span class="w"> </span><span class="mi">3</span><span class="p" data-group-id="9377547365-60">&gt;&gt;</span><span class="p">,</span><span class="w">
          </span><span class="ss">headers</span><span class="p">:</span><span class="w"> </span><span class="p" data-group-id="9377547365-61">[</span><span class="p" data-group-id="9377547365-62">{</span><span class="s">&quot;Content-Encoding&quot;</span><span class="p">,</span><span class="w"> </span><span class="s">&quot;deflate&quot;</span><span class="p" data-group-id="9377547365-62">}</span><span class="p" data-group-id="9377547365-61">]</span><span class="w">
        </span><span class="p" data-group-id="9377547365-59">}</span><span class="p" data-group-id="9377547365-58">}</span><span class="w">
      </span><span class="k" data-group-id="9377547365-57">end</span><span class="w">

      </span><span class="n">assert</span><span class="w"> </span><span class="n">capture_log</span><span class="p" data-group-id="9377547365-63">(</span><span class="k" data-group-id="9377547365-64">fn</span><span class="w"> </span><span class="o">-&gt;</span><span class="w">
        </span><span class="n">assert</span><span class="w"> </span><span class="p" data-group-id="9377547365-65">{</span><span class="ss">:ok</span><span class="p">,</span><span class="w"> </span><span class="bp">_</span><span class="p" data-group-id="9377547365-65">}</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nc">HTTPClient</span><span class="o">.</span><span class="n">get</span><span class="p" data-group-id="9377547365-66">(</span><span class="no">nil</span><span class="p">,</span><span class="w"> </span><span class="p" data-group-id="9377547365-67">[</span><span class="p" data-group-id="9377547365-67">]</span><span class="p">,</span><span class="w"> </span><span class="ss">requester</span><span class="p">:</span><span class="w"> </span><span class="n">requester</span><span class="p" data-group-id="9377547365-66">)</span><span class="w">
      </span><span class="k" data-group-id="9377547365-64">end</span><span class="p" data-group-id="9377547365-63">)</span><span class="w"> </span><span class="o">=~</span><span class="w"> </span><span class="s">&quot;Failed to decompress response&quot;</span><span class="w">
    </span><span class="k" data-group-id="9377547365-56">end</span><span class="w">

    </span><span class="n">test</span><span class="w"> </span><span class="s">&quot;re-encodes a latin1 body to UTF-8&quot;</span><span class="w"> </span><span class="k" data-group-id="9377547365-68">do</span><span class="w">
      </span><span class="n">latin1</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p" data-group-id="9377547365-69">&lt;&lt;</span><span class="mi">163</span><span class="p">,</span><span class="w"> </span><span class="mi">233</span><span class="p">,</span><span class="w"> </span><span class="mi">100</span><span class="p">,</span><span class="w"> </span><span class="mi">117</span><span class="p">,</span><span class="w"> </span><span class="mi">102</span><span class="p">,</span><span class="w"> </span><span class="mi">102</span><span class="p" data-group-id="9377547365-69">&gt;&gt;</span><span class="w">
      </span><span class="n">utf8</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s">&quot;£éduff&quot;</span><span class="w">
      </span><span class="n">requester</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k" data-group-id="9377547365-70">fn</span><span class="w"> </span><span class="c">_url</span><span class="p">,</span><span class="w"> </span><span class="c">_headers</span><span class="p">,</span><span class="w"> </span><span class="c">_options</span><span class="w"> </span><span class="o">-&gt;</span><span class="w">
        </span><span class="p" data-group-id="9377547365-71">{</span><span class="ss">:ok</span><span class="p">,</span><span class="w"> </span><span class="p" data-group-id="9377547365-72">%</span><span class="nc" data-group-id="9377547365-72">HTTPoison.Response</span><span class="p" data-group-id="9377547365-72">{</span><span class="w">
          </span><span class="ss">body</span><span class="p">:</span><span class="w"> </span><span class="n">latin1</span><span class="p">,</span><span class="w">
          </span><span class="ss">headers</span><span class="p">:</span><span class="w"> </span><span class="p" data-group-id="9377547365-73">[</span><span class="p" data-group-id="9377547365-74">{</span><span class="s">&quot;Content-Type&quot;</span><span class="p">,</span><span class="w"> </span><span class="s">&quot;text/plain;charset=ISO-8859-1&quot;</span><span class="p" data-group-id="9377547365-74">}</span><span class="p" data-group-id="9377547365-73">]</span><span class="w">
        </span><span class="p" data-group-id="9377547365-72">}</span><span class="p" data-group-id="9377547365-71">}</span><span class="w">
      </span><span class="k" data-group-id="9377547365-70">end</span><span class="w">

      </span><span class="n">assert</span><span class="w"> </span><span class="p" data-group-id="9377547365-75">{</span><span class="ss">:ok</span><span class="p">,</span><span class="w"> </span><span class="p" data-group-id="9377547365-76">%{</span><span class="ss">body</span><span class="p">:</span><span class="w"> </span><span class="o">^</span><span class="n">utf8</span><span class="p" data-group-id="9377547365-76">}</span><span class="p" data-group-id="9377547365-75">}</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nc">HTTPClient</span><span class="o">.</span><span class="n">get</span><span class="p" data-group-id="9377547365-77">(</span><span class="no">nil</span><span class="p">,</span><span class="w"> </span><span class="p" data-group-id="9377547365-78">[</span><span class="p" data-group-id="9377547365-78">]</span><span class="p">,</span><span class="w"> </span><span class="ss">requester</span><span class="p">:</span><span class="w"> </span><span class="n">requester</span><span class="p" data-group-id="9377547365-77">)</span><span class="w">
    </span><span class="k" data-group-id="9377547365-68">end</span><span class="w">

    </span><span class="n">test</span><span class="w"> </span><span class="s">&quot;does not re-encode utf8 bodies&quot;</span><span class="w"> </span><span class="k" data-group-id="9377547365-79">do</span><span class="w">
      </span><span class="n">utf8</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s">&quot;£éduff&quot;</span><span class="w">
      </span><span class="n">requester</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k" data-group-id="9377547365-80">fn</span><span class="w"> </span><span class="c">_url</span><span class="p">,</span><span class="w"> </span><span class="c">_headers</span><span class="p">,</span><span class="w"> </span><span class="c">_options</span><span class="w"> </span><span class="o">-&gt;</span><span class="w">
        </span><span class="p" data-group-id="9377547365-81">{</span><span class="ss">:ok</span><span class="p">,</span><span class="w"> </span><span class="p" data-group-id="9377547365-82">%</span><span class="nc" data-group-id="9377547365-82">HTTPoison.Response</span><span class="p" data-group-id="9377547365-82">{</span><span class="w">
          </span><span class="ss">body</span><span class="p">:</span><span class="w"> </span><span class="n">utf8</span><span class="p">,</span><span class="w">
          </span><span class="ss">headers</span><span class="p">:</span><span class="w"> </span><span class="p" data-group-id="9377547365-83">[</span><span class="p" data-group-id="9377547365-84">{</span><span class="s">&quot;Content-Type&quot;</span><span class="p">,</span><span class="w"> </span><span class="s">&quot;text/plain;charset=utf-8&quot;</span><span class="p" data-group-id="9377547365-84">}</span><span class="p" data-group-id="9377547365-83">]</span><span class="w">
        </span><span class="p" data-group-id="9377547365-82">}</span><span class="p" data-group-id="9377547365-81">}</span><span class="w">
      </span><span class="k" data-group-id="9377547365-80">end</span><span class="w">

      </span><span class="n">assert</span><span class="w"> </span><span class="p" data-group-id="9377547365-85">{</span><span class="ss">:ok</span><span class="p">,</span><span class="w"> </span><span class="p" data-group-id="9377547365-86">%{</span><span class="ss">body</span><span class="p">:</span><span class="w"> </span><span class="o">^</span><span class="n">utf8</span><span class="p" data-group-id="9377547365-86">}</span><span class="p" data-group-id="9377547365-85">}</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nc">HTTPClient</span><span class="o">.</span><span class="n">get</span><span class="p" data-group-id="9377547365-87">(</span><span class="no">nil</span><span class="p">,</span><span class="w"> </span><span class="p" data-group-id="9377547365-88">[</span><span class="p" data-group-id="9377547365-88">]</span><span class="p">,</span><span class="w"> </span><span class="ss">requester</span><span class="p">:</span><span class="w"> </span><span class="n">requester</span><span class="p" data-group-id="9377547365-87">)</span><span class="w">
    </span><span class="k" data-group-id="9377547365-79">end</span><span class="w">

    </span><span class="n">test</span><span class="w"> </span><span class="s">&quot;emits log when encountering unknown encoding&quot;</span><span class="w"> </span><span class="k" data-group-id="9377547365-89">do</span><span class="w">
      </span><span class="n">requester</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k" data-group-id="9377547365-90">fn</span><span class="w"> </span><span class="c">_url</span><span class="p">,</span><span class="w"> </span><span class="c">_headers</span><span class="p">,</span><span class="w"> </span><span class="c">_options</span><span class="w"> </span><span class="o">-&gt;</span><span class="w">
        </span><span class="p" data-group-id="9377547365-91">{</span><span class="ss">:ok</span><span class="p">,</span><span class="w"> </span><span class="p" data-group-id="9377547365-92">%</span><span class="nc" data-group-id="9377547365-92">HTTPoison.Response</span><span class="p" data-group-id="9377547365-92">{</span><span class="w">
          </span><span class="ss">body</span><span class="p">:</span><span class="w"> </span><span class="s">&quot;Hallo&quot;</span><span class="p">,</span><span class="w">
          </span><span class="ss">headers</span><span class="p">:</span><span class="w"> </span><span class="p" data-group-id="9377547365-93">[</span><span class="p" data-group-id="9377547365-94">{</span><span class="s">&quot;Content-Type&quot;</span><span class="p">,</span><span class="w"> </span><span class="s">&quot;text/plain;charset=duurf&quot;</span><span class="p" data-group-id="9377547365-94">}</span><span class="p" data-group-id="9377547365-93">]</span><span class="w">
        </span><span class="p" data-group-id="9377547365-92">}</span><span class="p" data-group-id="9377547365-91">}</span><span class="w">
      </span><span class="k" data-group-id="9377547365-90">end</span><span class="w">

      </span><span class="n">assert</span><span class="w"> </span><span class="n">capture_log</span><span class="p" data-group-id="9377547365-95">(</span><span class="k" data-group-id="9377547365-96">fn</span><span class="w"> </span><span class="o">-&gt;</span><span class="w">
        </span><span class="n">assert</span><span class="w"> </span><span class="p" data-group-id="9377547365-97">{</span><span class="ss">:ok</span><span class="p">,</span><span class="w"> </span><span class="bp">_</span><span class="p" data-group-id="9377547365-97">}</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nc">HTTPClient</span><span class="o">.</span><span class="n">get</span><span class="p" data-group-id="9377547365-98">(</span><span class="no">nil</span><span class="p">,</span><span class="w"> </span><span class="p" data-group-id="9377547365-99">[</span><span class="p" data-group-id="9377547365-99">]</span><span class="p">,</span><span class="w"> </span><span class="ss">requester</span><span class="p">:</span><span class="w"> </span><span class="n">requester</span><span class="p" data-group-id="9377547365-98">)</span><span class="w">
      </span><span class="k" data-group-id="9377547365-96">end</span><span class="p" data-group-id="9377547365-95">)</span><span class="w"> </span><span class="o">=~</span><span class="w"> </span><span class="s">&quot;Need to implement re-encoding support for: duurf&quot;</span><span class="w">
    </span><span class="k" data-group-id="9377547365-89">end</span><span class="w">

    </span><span class="n">test</span><span class="w"> </span><span class="s">&quot;emits log when failing to reencode&quot;</span><span class="w"> </span><span class="k" data-group-id="9377547365-100">do</span><span class="w">
      </span><span class="n">body</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p" data-group-id="9377547365-101">&lt;&lt;</span><span class="mi">163</span><span class="p">,</span><span class="w"> </span><span class="mi">233</span><span class="p">,</span><span class="w"> </span><span class="mi">100</span><span class="p">,</span><span class="w"> </span><span class="mi">117</span><span class="p">,</span><span class="w"> </span><span class="mi">102</span><span class="p">,</span><span class="w"> </span><span class="mi">102</span><span class="p">,</span><span class="w"> </span><span class="mi">833</span><span class="o">::</span><span class="mi">3</span><span class="p" data-group-id="9377547365-101">&gt;&gt;</span><span class="w">
      </span><span class="n">requester</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k" data-group-id="9377547365-102">fn</span><span class="w"> </span><span class="c">_url</span><span class="p">,</span><span class="w"> </span><span class="c">_headers</span><span class="p">,</span><span class="w"> </span><span class="c">_options</span><span class="w"> </span><span class="o">-&gt;</span><span class="w">
        </span><span class="p" data-group-id="9377547365-103">{</span><span class="ss">:ok</span><span class="p">,</span><span class="w"> </span><span class="p" data-group-id="9377547365-104">%</span><span class="nc" data-group-id="9377547365-104">HTTPoison.Response</span><span class="p" data-group-id="9377547365-104">{</span><span class="w">
          </span><span class="ss">body</span><span class="p">:</span><span class="w"> </span><span class="n">body</span><span class="p">,</span><span class="w">
          </span><span class="ss">headers</span><span class="p">:</span><span class="w"> </span><span class="p" data-group-id="9377547365-105">[</span><span class="p" data-group-id="9377547365-106">{</span><span class="s">&quot;Content-Type&quot;</span><span class="p">,</span><span class="w"> </span><span class="s">&quot;text/plain;charset=ISO-8859-1&quot;</span><span class="p" data-group-id="9377547365-106">}</span><span class="p" data-group-id="9377547365-105">]</span><span class="w">
        </span><span class="p" data-group-id="9377547365-104">}</span><span class="p" data-group-id="9377547365-103">}</span><span class="w">
      </span><span class="k" data-group-id="9377547365-102">end</span><span class="w">

      </span><span class="n">assert</span><span class="w"> </span><span class="n">capture_log</span><span class="p" data-group-id="9377547365-107">(</span><span class="k" data-group-id="9377547365-108">fn</span><span class="w"> </span><span class="o">-&gt;</span><span class="w">
        </span><span class="n">assert</span><span class="w"> </span><span class="p" data-group-id="9377547365-109">{</span><span class="ss">:ok</span><span class="p">,</span><span class="w"> </span><span class="bp">_</span><span class="p" data-group-id="9377547365-109">}</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nc">HTTPClient</span><span class="o">.</span><span class="n">get</span><span class="p" data-group-id="9377547365-110">(</span><span class="no">nil</span><span class="p">,</span><span class="w"> </span><span class="p" data-group-id="9377547365-111">[</span><span class="p" data-group-id="9377547365-111">]</span><span class="p">,</span><span class="w"> </span><span class="ss">requester</span><span class="p">:</span><span class="w"> </span><span class="n">requester</span><span class="p" data-group-id="9377547365-110">)</span><span class="w">
      </span><span class="k" data-group-id="9377547365-108">end</span><span class="p" data-group-id="9377547365-107">)</span><span class="w"> </span><span class="o">=~</span><span class="w"> </span><span class="s">&quot;Failed to re-encode response&quot;</span><span class="w">
    </span><span class="k" data-group-id="9377547365-100">end</span><span class="w">

    </span><span class="n">test</span><span class="w"> </span><span class="s">&quot;does not re-encode un-specified bodies&quot;</span><span class="w"> </span><span class="k" data-group-id="9377547365-112">do</span><span class="w">
      </span><span class="n">body</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s">&quot;£éduff&quot;</span><span class="w"> </span><span class="o">&lt;&gt;</span><span class="w"> </span><span class="p" data-group-id="9377547365-113">&lt;&lt;</span><span class="mi">163</span><span class="p">,</span><span class="w"> </span><span class="mi">233</span><span class="p">,</span><span class="w"> </span><span class="mi">100</span><span class="p">,</span><span class="w"> </span><span class="mi">117</span><span class="p">,</span><span class="w"> </span><span class="mi">102</span><span class="p">,</span><span class="w"> </span><span class="mi">102</span><span class="p" data-group-id="9377547365-113">&gt;&gt;</span><span class="w">
      </span><span class="n">requester</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k" data-group-id="9377547365-114">fn</span><span class="w"> </span><span class="c">_url</span><span class="p">,</span><span class="w"> </span><span class="c">_headers</span><span class="p">,</span><span class="w"> </span><span class="c">_options</span><span class="w"> </span><span class="o">-&gt;</span><span class="w">
        </span><span class="p" data-group-id="9377547365-115">{</span><span class="ss">:ok</span><span class="p">,</span><span class="w"> </span><span class="p" data-group-id="9377547365-116">%</span><span class="nc" data-group-id="9377547365-116">HTTPoison.Response</span><span class="p" data-group-id="9377547365-116">{</span><span class="w">
          </span><span class="ss">body</span><span class="p">:</span><span class="w"> </span><span class="n">body</span><span class="p">,</span><span class="w">
          </span><span class="ss">headers</span><span class="p">:</span><span class="w"> </span><span class="p" data-group-id="9377547365-117">[</span><span class="p" data-group-id="9377547365-117">]</span><span class="w">
        </span><span class="p" data-group-id="9377547365-116">}</span><span class="p" data-group-id="9377547365-115">}</span><span class="w">
      </span><span class="k" data-group-id="9377547365-114">end</span><span class="w">

      </span><span class="n">assert</span><span class="w"> </span><span class="p" data-group-id="9377547365-118">{</span><span class="ss">:ok</span><span class="p">,</span><span class="w"> </span><span class="p" data-group-id="9377547365-119">%{</span><span class="ss">body</span><span class="p">:</span><span class="w"> </span><span class="o">^</span><span class="n">body</span><span class="p" data-group-id="9377547365-119">}</span><span class="p" data-group-id="9377547365-118">}</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nc">HTTPClient</span><span class="o">.</span><span class="n">get</span><span class="p" data-group-id="9377547365-120">(</span><span class="no">nil</span><span class="p">,</span><span class="w"> </span><span class="p" data-group-id="9377547365-121">[</span><span class="p" data-group-id="9377547365-121">]</span><span class="p">,</span><span class="w"> </span><span class="ss">requester</span><span class="p">:</span><span class="w"> </span><span class="n">requester</span><span class="p" data-group-id="9377547365-120">)</span><span class="w">
    </span><span class="k" data-group-id="9377547365-112">end</span><span class="w">
  </span><span class="k" data-group-id="9377547365-4">end</span><span class="w">

  </span><span class="c1"># copy/paste above, but adjust it for the post/4 function. Or, if you</span><span class="w">
  </span><span class="c1"># want to be creative, make a macro to generate these tests for you.</span><span class="w">
</span><span class="k" data-group-id="9377547365-1">end</span></code></pre>
<p>
Cool. That’s a lot of tests.</p>
<p>
Let’s make those tests pass:</p>
<pre><code class="makeup elixir"><span class="kd">defmodule</span><span class="w"> </span><span class="nc">MyApp.HTTPClient</span><span class="w"> </span><span class="k" data-group-id="2366217429-1">do</span><span class="w">
  </span><span class="na">@moduledoc</span><span class="w"> </span><span class="s">&quot;&quot;&quot;
  A wrapper around HTTPoison that takes care of post-processing depending on the response, namely:
    1) decompress the response if gzipped
    2) re-encode the body into UTF-8 if ISO-8859-1
  &quot;&quot;&quot;</span><span class="w">
  </span><span class="na">@default_getter</span><span class="w"> </span><span class="o">&amp;</span><span class="nc">HTTPoison</span><span class="o">.</span><span class="n">get</span><span class="o">/</span><span class="mi">3</span><span class="w">
  </span><span class="na">@default_poster</span><span class="w"> </span><span class="o">&amp;</span><span class="nc">HTTPoison</span><span class="o">.</span><span class="n">post</span><span class="o">/</span><span class="mi">4</span><span class="w">
  </span><span class="na">@default_options</span><span class="w"> </span><span class="p" data-group-id="2366217429-2">[</span><span class="ss">timeout</span><span class="p">:</span><span class="w"> </span><span class="mi">300_000</span><span class="p">,</span><span class="w"> </span><span class="ss">recv_timeout</span><span class="p">:</span><span class="w"> </span><span class="mi">60_000</span><span class="p" data-group-id="2366217429-2">]</span><span class="w">

  </span><span class="kn">require</span><span class="w"> </span><span class="nc">Logger</span><span class="w">

  </span><span class="na">@doc</span><span class="w"> </span><span class="s">&quot;&quot;&quot;
  GET a URL with headers. Supported options:
    requester: fn(url, headers, request_options)
  &quot;&quot;&quot;</span><span class="w">
  </span><span class="na">@spec</span><span class="w"> </span><span class="n">get</span><span class="p" data-group-id="2366217429-3">(</span><span class="nc">String</span><span class="o">.</span><span class="n">t</span><span class="p" data-group-id="2366217429-4">(</span><span class="p" data-group-id="2366217429-4">)</span><span class="p">,</span><span class="w"> </span><span class="nc">HTTPoison</span><span class="o">.</span><span class="n">headers</span><span class="p" data-group-id="2366217429-5">(</span><span class="p" data-group-id="2366217429-5">)</span><span class="p">,</span><span class="w"> </span><span class="n">list</span><span class="p" data-group-id="2366217429-6">(</span><span class="p" data-group-id="2366217429-6">)</span><span class="p" data-group-id="2366217429-3">)</span><span class="w"> </span><span class="o">::</span><span class="w">
    </span><span class="p" data-group-id="2366217429-7">{</span><span class="ss">:ok</span><span class="p">,</span><span class="w"> </span><span class="nc">HTTPoison.Response</span><span class="o">.</span><span class="n">t</span><span class="p" data-group-id="2366217429-8">(</span><span class="p" data-group-id="2366217429-8">)</span><span class="p" data-group-id="2366217429-7">}</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="p" data-group-id="2366217429-9">{</span><span class="ss">:error</span><span class="p">,</span><span class="w"> </span><span class="nc">HTTPoison.Error</span><span class="o">.</span><span class="n">t</span><span class="p" data-group-id="2366217429-10">(</span><span class="p" data-group-id="2366217429-10">)</span><span class="p" data-group-id="2366217429-9">}</span><span class="w">
  </span><span class="kd">def</span><span class="w"> </span><span class="nf">get</span><span class="p" data-group-id="2366217429-11">(</span><span class="n">url</span><span class="p">,</span><span class="w"> </span><span class="n">headers</span><span class="p">,</span><span class="w"> </span><span class="n">request_options</span><span class="w"> </span><span class="o">\\</span><span class="w"> </span><span class="p" data-group-id="2366217429-12">[</span><span class="p" data-group-id="2366217429-12">]</span><span class="p" data-group-id="2366217429-11">)</span><span class="w"> </span><span class="k" data-group-id="2366217429-13">do</span><span class="w">
    </span><span class="n">opts</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">request_options</span><span class="w"> </span><span class="o">++</span><span class="w"> </span><span class="na">@default_options</span><span class="w">
    </span><span class="p" data-group-id="2366217429-14">{</span><span class="n">get</span><span class="p">,</span><span class="w"> </span><span class="n">opts</span><span class="p" data-group-id="2366217429-14">}</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nc">Keyword</span><span class="o">.</span><span class="n">pop</span><span class="p" data-group-id="2366217429-15">(</span><span class="n">opts</span><span class="p">,</span><span class="w"> </span><span class="ss">:requester</span><span class="p">,</span><span class="w"> </span><span class="na">@default_getter</span><span class="p" data-group-id="2366217429-15">)</span><span class="w">

    </span><span class="n">url</span><span class="w">
    </span><span class="o">|&gt;</span><span class="w"> </span><span class="n">get</span><span class="o">.</span><span class="p" data-group-id="2366217429-16">(</span><span class="n">headers</span><span class="p">,</span><span class="w"> </span><span class="n">opts</span><span class="p" data-group-id="2366217429-16">)</span><span class="w">
    </span><span class="o">|&gt;</span><span class="w"> </span><span class="n">process_response</span><span class="w">
  </span><span class="k" data-group-id="2366217429-13">end</span><span class="w">

  </span><span class="na">@doc</span><span class="w"> </span><span class="s">&quot;&quot;&quot;
  POST a URL with a body, headers. Supported options:
    requester: fn(url, body, headers, request_options)
  &quot;&quot;&quot;</span><span class="w">
  </span><span class="na">@spec</span><span class="w"> </span><span class="n">post</span><span class="p" data-group-id="2366217429-17">(</span><span class="nc">String</span><span class="o">.</span><span class="n">t</span><span class="p" data-group-id="2366217429-18">(</span><span class="p" data-group-id="2366217429-18">)</span><span class="p">,</span><span class="w"> </span><span class="nc">HTTPoison</span><span class="o">.</span><span class="n">body</span><span class="p" data-group-id="2366217429-19">(</span><span class="p" data-group-id="2366217429-19">)</span><span class="p">,</span><span class="w"> </span><span class="nc">HTTPoison</span><span class="o">.</span><span class="n">headers</span><span class="p" data-group-id="2366217429-20">(</span><span class="p" data-group-id="2366217429-20">)</span><span class="p">,</span><span class="w"> </span><span class="n">list</span><span class="p" data-group-id="2366217429-21">(</span><span class="p" data-group-id="2366217429-21">)</span><span class="p" data-group-id="2366217429-17">)</span><span class="w"> </span><span class="o">::</span><span class="w">
    </span><span class="p" data-group-id="2366217429-22">{</span><span class="ss">:ok</span><span class="p">,</span><span class="w"> </span><span class="nc">HTTPoison.Response</span><span class="o">.</span><span class="n">t</span><span class="p" data-group-id="2366217429-23">(</span><span class="p" data-group-id="2366217429-23">)</span><span class="p" data-group-id="2366217429-22">}</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="p" data-group-id="2366217429-24">{</span><span class="ss">:error</span><span class="p">,</span><span class="w"> </span><span class="nc">HTTPoison.Error</span><span class="o">.</span><span class="n">t</span><span class="p" data-group-id="2366217429-25">(</span><span class="p" data-group-id="2366217429-25">)</span><span class="p" data-group-id="2366217429-24">}</span><span class="w">
  </span><span class="kd">def</span><span class="w"> </span><span class="nf">post</span><span class="p" data-group-id="2366217429-26">(</span><span class="n">url</span><span class="p">,</span><span class="w"> </span><span class="n">body</span><span class="p">,</span><span class="w"> </span><span class="n">headers</span><span class="p">,</span><span class="w"> </span><span class="n">request_options</span><span class="w"> </span><span class="o">\\</span><span class="w"> </span><span class="p" data-group-id="2366217429-27">[</span><span class="p" data-group-id="2366217429-27">]</span><span class="p" data-group-id="2366217429-26">)</span><span class="w"> </span><span class="k" data-group-id="2366217429-28">do</span><span class="w">
    </span><span class="n">opts</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">request_options</span><span class="w"> </span><span class="o">++</span><span class="w"> </span><span class="na">@default_options</span><span class="w">
    </span><span class="p" data-group-id="2366217429-29">{</span><span class="n">post</span><span class="p">,</span><span class="w"> </span><span class="n">opts</span><span class="p" data-group-id="2366217429-29">}</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nc">Keyword</span><span class="o">.</span><span class="n">pop</span><span class="p" data-group-id="2366217429-30">(</span><span class="n">opts</span><span class="p">,</span><span class="w"> </span><span class="ss">:requester</span><span class="p">,</span><span class="w"> </span><span class="na">@default_poster</span><span class="p" data-group-id="2366217429-30">)</span><span class="w">

    </span><span class="n">url</span><span class="w">
    </span><span class="o">|&gt;</span><span class="w"> </span><span class="n">post</span><span class="o">.</span><span class="p" data-group-id="2366217429-31">(</span><span class="n">body</span><span class="p">,</span><span class="w"> </span><span class="n">headers</span><span class="p">,</span><span class="w"> </span><span class="n">opts</span><span class="p" data-group-id="2366217429-31">)</span><span class="w">
    </span><span class="o">|&gt;</span><span class="w"> </span><span class="n">process_response</span><span class="w">
  </span><span class="k" data-group-id="2366217429-28">end</span><span class="w">

  </span><span class="kd">defp</span><span class="w"> </span><span class="nf">process_response</span><span class="p" data-group-id="2366217429-32">(</span><span class="n">response</span><span class="p" data-group-id="2366217429-32">)</span><span class="w"> </span><span class="k" data-group-id="2366217429-33">do</span><span class="w">
    </span><span class="n">response</span><span class="w">
    </span><span class="o">|&gt;</span><span class="w"> </span><span class="n">decompress_response</span><span class="w">
    </span><span class="o">|&gt;</span><span class="w"> </span><span class="n">reencode_response_to_utf8</span><span class="w">
  </span><span class="k" data-group-id="2366217429-33">end</span><span class="w">

  </span><span class="kd">defp</span><span class="w"> </span><span class="nf">decompress_response</span><span class="p" data-group-id="2366217429-34">(</span><span class="p" data-group-id="2366217429-35">{</span><span class="ss">:error</span><span class="p">,</span><span class="w"> </span><span class="n">response</span><span class="p" data-group-id="2366217429-35">}</span><span class="p" data-group-id="2366217429-34">)</span><span class="p">,</span><span class="w"> </span><span class="ss">do</span><span class="p">:</span><span class="w"> </span><span class="p" data-group-id="2366217429-36">{</span><span class="ss">:error</span><span class="p">,</span><span class="w"> </span><span class="n">response</span><span class="p" data-group-id="2366217429-36">}</span><span class="w">
  </span><span class="kd">defp</span><span class="w"> </span><span class="nf">decompress_response</span><span class="p" data-group-id="2366217429-37">(</span><span class="p" data-group-id="2366217429-38">{</span><span class="n">status</span><span class="p">,</span><span class="w"> </span><span class="p" data-group-id="2366217429-39">%{</span><span class="ss">headers</span><span class="p">:</span><span class="w"> </span><span class="n">headers</span><span class="p">,</span><span class="w"> </span><span class="ss">body</span><span class="p">:</span><span class="w"> </span><span class="n">body</span><span class="p" data-group-id="2366217429-39">}</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">response</span><span class="p" data-group-id="2366217429-38">}</span><span class="p" data-group-id="2366217429-37">)</span><span class="w"> </span><span class="k" data-group-id="2366217429-40">do</span><span class="w">
    </span><span class="k">try</span><span class="w"> </span><span class="k" data-group-id="2366217429-41">do</span><span class="w">
      </span><span class="n">decompressed_body</span><span class="w"> </span><span class="o">=</span><span class="w">
        </span><span class="n">headers</span><span class="w">
        </span><span class="o">|&gt;</span><span class="w"> </span><span class="n">find_header</span><span class="p" data-group-id="2366217429-42">(</span><span class="s">&quot;content-encoding&quot;</span><span class="p" data-group-id="2366217429-42">)</span><span class="w">
        </span><span class="o">|&gt;</span><span class="w"> </span><span class="n">decompress_body</span><span class="p" data-group-id="2366217429-43">(</span><span class="n">body</span><span class="p" data-group-id="2366217429-43">)</span><span class="w">

      </span><span class="p" data-group-id="2366217429-44">{</span><span class="n">status</span><span class="p">,</span><span class="w"> </span><span class="p" data-group-id="2366217429-45">%{</span><span class="n">response</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="ss">body</span><span class="p">:</span><span class="w"> </span><span class="n">decompressed_body</span><span class="p" data-group-id="2366217429-45">}</span><span class="p" data-group-id="2366217429-44">}</span><span class="w">

    </span><span class="k" data-group-id="2366217429-41">rescue</span><span class="w">
      </span><span class="bp">_</span><span class="w"> </span><span class="o">-&gt;</span><span class="w">
        </span><span class="nc">Logger</span><span class="o">.</span><span class="n">error</span><span class="p" data-group-id="2366217429-46">(</span><span class="s">&quot;Failed to decompress response: </span><span class="si" data-group-id="2366217429-47">#{</span><span class="n">inspect</span><span class="w"> </span><span class="n">response</span><span class="si" data-group-id="2366217429-47">}</span><span class="s">&quot;</span><span class="p" data-group-id="2366217429-46">)</span><span class="w">
        </span><span class="p" data-group-id="2366217429-48">{</span><span class="n">status</span><span class="p">,</span><span class="w"> </span><span class="n">response</span><span class="p" data-group-id="2366217429-48">}</span><span class="w">
    </span><span class="k" data-group-id="2366217429-41">end</span><span class="w">
  </span><span class="k" data-group-id="2366217429-40">end</span><span class="w">

  </span><span class="kd">defp</span><span class="w"> </span><span class="nf">reencode_response_to_utf8</span><span class="p" data-group-id="2366217429-49">(</span><span class="p" data-group-id="2366217429-50">{</span><span class="ss">:error</span><span class="p">,</span><span class="w"> </span><span class="n">response</span><span class="p" data-group-id="2366217429-50">}</span><span class="p" data-group-id="2366217429-49">)</span><span class="p">,</span><span class="w"> </span><span class="ss">do</span><span class="p">:</span><span class="w"> </span><span class="p" data-group-id="2366217429-51">{</span><span class="ss">:error</span><span class="p">,</span><span class="w"> </span><span class="n">response</span><span class="p" data-group-id="2366217429-51">}</span><span class="w">
  </span><span class="kd">defp</span><span class="w"> </span><span class="nf">reencode_response_to_utf8</span><span class="p" data-group-id="2366217429-52">(</span><span class="p" data-group-id="2366217429-53">{</span><span class="n">status</span><span class="p">,</span><span class="w"> </span><span class="p" data-group-id="2366217429-54">%{</span><span class="ss">headers</span><span class="p">:</span><span class="w"> </span><span class="n">headers</span><span class="p">,</span><span class="w"> </span><span class="ss">body</span><span class="p">:</span><span class="w"> </span><span class="n">body</span><span class="p" data-group-id="2366217429-54">}</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">response</span><span class="p" data-group-id="2366217429-53">}</span><span class="p" data-group-id="2366217429-52">)</span><span class="w"> </span><span class="k" data-group-id="2366217429-55">do</span><span class="w">
    </span><span class="k">try</span><span class="w"> </span><span class="k" data-group-id="2366217429-56">do</span><span class="w">
      </span><span class="n">reencoded_body</span><span class="w"> </span><span class="o">=</span><span class="w">
        </span><span class="n">headers</span><span class="w">
        </span><span class="o">|&gt;</span><span class="w"> </span><span class="n">find_header</span><span class="p" data-group-id="2366217429-57">(</span><span class="s">&quot;content-type&quot;</span><span class="p" data-group-id="2366217429-57">)</span><span class="w">
        </span><span class="o">|&gt;</span><span class="w"> </span><span class="n">parse_charset</span><span class="p" data-group-id="2366217429-58">(</span><span class="p" data-group-id="2366217429-58">)</span><span class="w">
        </span><span class="o">|&gt;</span><span class="w"> </span><span class="n">reencode_body</span><span class="p" data-group-id="2366217429-59">(</span><span class="n">body</span><span class="p" data-group-id="2366217429-59">)</span><span class="w">

      </span><span class="p" data-group-id="2366217429-60">{</span><span class="n">status</span><span class="p">,</span><span class="w"> </span><span class="p" data-group-id="2366217429-61">%{</span><span class="n">response</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="ss">body</span><span class="p">:</span><span class="w"> </span><span class="n">reencoded_body</span><span class="p" data-group-id="2366217429-61">}</span><span class="p" data-group-id="2366217429-60">}</span><span class="w">

    </span><span class="k" data-group-id="2366217429-56">rescue</span><span class="w">
      </span><span class="bp">_</span><span class="w"> </span><span class="o">-&gt;</span><span class="w">
        </span><span class="nc">Logger</span><span class="o">.</span><span class="n">error</span><span class="p" data-group-id="2366217429-62">(</span><span class="s">&quot;Failed to re-encode response: </span><span class="si" data-group-id="2366217429-63">#{</span><span class="n">inspect</span><span class="w"> </span><span class="n">response</span><span class="si" data-group-id="2366217429-63">}</span><span class="s">&quot;</span><span class="p" data-group-id="2366217429-62">)</span><span class="w">
        </span><span class="p" data-group-id="2366217429-64">{</span><span class="n">status</span><span class="p">,</span><span class="w"> </span><span class="n">response</span><span class="p" data-group-id="2366217429-64">}</span><span class="w">
    </span><span class="k" data-group-id="2366217429-56">end</span><span class="w">
  </span><span class="k" data-group-id="2366217429-55">end</span><span class="w">

  </span><span class="kd">defp</span><span class="w"> </span><span class="nf">find_header</span><span class="p" data-group-id="2366217429-65">(</span><span class="n">headers</span><span class="p">,</span><span class="w"> </span><span class="n">header_name</span><span class="p" data-group-id="2366217429-65">)</span><span class="w"> </span><span class="k" data-group-id="2366217429-66">do</span><span class="w">
    </span><span class="nc">Enum</span><span class="o">.</span><span class="n">find_value</span><span class="p" data-group-id="2366217429-67">(</span><span class="w">
      </span><span class="n">headers</span><span class="p">,</span><span class="w">
      </span><span class="k" data-group-id="2366217429-68">fn</span><span class="w"> </span><span class="p" data-group-id="2366217429-69">{</span><span class="n">name</span><span class="p">,</span><span class="w"> </span><span class="n">value</span><span class="p" data-group-id="2366217429-69">}</span><span class="w"> </span><span class="o">-&gt;</span><span class="w">
        </span><span class="n">name</span><span class="w"> </span><span class="o">=~</span><span class="w"> </span><span class="sr">~r/</span><span class="si" data-group-id="2366217429-70">#{</span><span class="n">header_name</span><span class="si" data-group-id="2366217429-70">}</span><span class="sr">/i</span><span class="w"> </span><span class="o">&amp;&amp;</span><span class="w"> </span><span class="nc">String</span><span class="o">.</span><span class="n">downcase</span><span class="p" data-group-id="2366217429-71">(</span><span class="n">value</span><span class="p" data-group-id="2366217429-71">)</span><span class="w">
      </span><span class="k" data-group-id="2366217429-68">end</span><span class="w">
    </span><span class="p" data-group-id="2366217429-67">)</span><span class="w">
  </span><span class="k" data-group-id="2366217429-66">end</span><span class="w">

  </span><span class="c1"># gzip&#39;s magic header is 0x1F 0x8B, with the 3rd byte specifying the compression method, 0x08</span><span class="w">
  </span><span class="c1"># meaning &quot;deflate&quot;. More info https://en.wikipedia.org/wiki/Gzip</span><span class="w">
  </span><span class="kd">defp</span><span class="w"> </span><span class="nf">decompress_body</span><span class="p" data-group-id="2366217429-72">(</span><span class="no">nil</span><span class="p">,</span><span class="w"> </span><span class="n">body</span><span class="p" data-group-id="2366217429-72">)</span><span class="p">,</span><span class="w"> </span><span class="ss">do</span><span class="p">:</span><span class="w"> </span><span class="n">body</span><span class="w">
  </span><span class="kd">defp</span><span class="w"> </span><span class="nf">decompress_body</span><span class="p" data-group-id="2366217429-73">(</span><span class="s">&quot;identity&quot;</span><span class="p">,</span><span class="w"> </span><span class="n">body</span><span class="p" data-group-id="2366217429-73">)</span><span class="p">,</span><span class="w"> </span><span class="ss">do</span><span class="p">:</span><span class="w"> </span><span class="n">body</span><span class="w">
  </span><span class="kd">defp</span><span class="w"> </span><span class="nf">decompress_body</span><span class="p" data-group-id="2366217429-74">(</span><span class="s">&quot;gzip&quot;</span><span class="p">,</span><span class="w"> </span><span class="p" data-group-id="2366217429-75">&lt;&lt;</span><span class="mi">31</span><span class="p">,</span><span class="w"> </span><span class="mi">139</span><span class="p">,</span><span class="w"> </span><span class="mi">8</span><span class="p">,</span><span class="w"> </span><span class="bp">_</span><span class="o">::</span><span class="n">binary</span><span class="p" data-group-id="2366217429-75">&gt;&gt;</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">body</span><span class="p" data-group-id="2366217429-74">)</span><span class="p">,</span><span class="w"> </span><span class="ss">do</span><span class="p">:</span><span class="w"> </span><span class="nc">:zlib</span><span class="o">.</span><span class="n">gunzip</span><span class="p" data-group-id="2366217429-76">(</span><span class="n">body</span><span class="p" data-group-id="2366217429-76">)</span><span class="w">
  </span><span class="kd">defp</span><span class="w"> </span><span class="nf">decompress_body</span><span class="p" data-group-id="2366217429-77">(</span><span class="s">&quot;gzip&quot;</span><span class="p">,</span><span class="w"> </span><span class="n">body</span><span class="p" data-group-id="2366217429-77">)</span><span class="p">,</span><span class="w"> </span><span class="ss">do</span><span class="p">:</span><span class="w"> </span><span class="n">body</span><span class="w">
  </span><span class="kd">defp</span><span class="w"> </span><span class="nf">decompress_body</span><span class="p" data-group-id="2366217429-78">(</span><span class="s">&quot;x-gzip&quot;</span><span class="p">,</span><span class="w"> </span><span class="p" data-group-id="2366217429-79">&lt;&lt;</span><span class="mi">31</span><span class="p">,</span><span class="w"> </span><span class="mi">139</span><span class="p">,</span><span class="w"> </span><span class="mi">8</span><span class="p">,</span><span class="w"> </span><span class="bp">_</span><span class="o">::</span><span class="n">binary</span><span class="p" data-group-id="2366217429-79">&gt;&gt;</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">body</span><span class="p" data-group-id="2366217429-78">)</span><span class="p">,</span><span class="w"> </span><span class="ss">do</span><span class="p">:</span><span class="w"> </span><span class="nc">:zlib</span><span class="o">.</span><span class="n">gunzip</span><span class="p" data-group-id="2366217429-80">(</span><span class="n">body</span><span class="p" data-group-id="2366217429-80">)</span><span class="w">
  </span><span class="kd">defp</span><span class="w"> </span><span class="nf">decompress_body</span><span class="p" data-group-id="2366217429-81">(</span><span class="s">&quot;x-gzip&quot;</span><span class="p">,</span><span class="w"> </span><span class="n">body</span><span class="p" data-group-id="2366217429-81">)</span><span class="p">,</span><span class="w"> </span><span class="ss">do</span><span class="p">:</span><span class="w"> </span><span class="n">body</span><span class="w">
  </span><span class="kd">defp</span><span class="w"> </span><span class="nf">decompress_body</span><span class="p" data-group-id="2366217429-82">(</span><span class="s">&quot;deflate&quot;</span><span class="p">,</span><span class="w"> </span><span class="n">body</span><span class="p" data-group-id="2366217429-82">)</span><span class="p">,</span><span class="w"> </span><span class="ss">do</span><span class="p">:</span><span class="w"> </span><span class="nc">:zlib</span><span class="o">.</span><span class="n">unzip</span><span class="p" data-group-id="2366217429-83">(</span><span class="n">body</span><span class="p" data-group-id="2366217429-83">)</span><span class="w">
  </span><span class="kd">defp</span><span class="w"> </span><span class="nf">decompress_body</span><span class="p" data-group-id="2366217429-84">(</span><span class="n">other</span><span class="p">,</span><span class="w"> </span><span class="n">body</span><span class="p" data-group-id="2366217429-84">)</span><span class="w"> </span><span class="k" data-group-id="2366217429-85">do</span><span class="w">
    </span><span class="nc">Logger</span><span class="o">.</span><span class="n">error</span><span class="p" data-group-id="2366217429-86">(</span><span class="s">&quot;No support for decompression of body using &#39;</span><span class="si" data-group-id="2366217429-87">#{</span><span class="n">other</span><span class="si" data-group-id="2366217429-87">}</span><span class="s">&#39; algorithm.&quot;</span><span class="p" data-group-id="2366217429-86">)</span><span class="w">
    </span><span class="n">body</span><span class="w">
  </span><span class="k" data-group-id="2366217429-85">end</span><span class="w">

  </span><span class="kd">defp</span><span class="w"> </span><span class="nf">parse_charset</span><span class="p" data-group-id="2366217429-88">(</span><span class="no">nil</span><span class="p" data-group-id="2366217429-88">)</span><span class="p">,</span><span class="w"> </span><span class="ss">do</span><span class="p">:</span><span class="w"> </span><span class="no">nil</span><span class="w">
  </span><span class="kd">defp</span><span class="w"> </span><span class="nf">parse_charset</span><span class="p" data-group-id="2366217429-89">(</span><span class="n">content_type</span><span class="p" data-group-id="2366217429-89">)</span><span class="w"> </span><span class="k" data-group-id="2366217429-90">do</span><span class="w">
    </span><span class="k">case</span><span class="w"> </span><span class="nc">Plug.Conn.Utils</span><span class="o">.</span><span class="n">content_type</span><span class="p" data-group-id="2366217429-91">(</span><span class="n">content_type</span><span class="p" data-group-id="2366217429-91">)</span><span class="w"> </span><span class="k" data-group-id="2366217429-92">do</span><span class="w">
      </span><span class="p" data-group-id="2366217429-93">{</span><span class="ss">:ok</span><span class="p">,</span><span class="w"> </span><span class="bp">_</span><span class="p">,</span><span class="w"> </span><span class="bp">_</span><span class="p">,</span><span class="w"> </span><span class="p" data-group-id="2366217429-94">%{</span><span class="s">&quot;charset&quot;</span><span class="w"> </span><span class="p">=&gt;</span><span class="w"> </span><span class="n">charset</span><span class="p" data-group-id="2366217429-94">}</span><span class="p" data-group-id="2366217429-93">}</span><span class="w"> </span><span class="o">-&gt;</span><span class="w">
        </span><span class="k">cond</span><span class="w"> </span><span class="k" data-group-id="2366217429-95">do</span><span class="w">
          </span><span class="n">charset</span><span class="w"> </span><span class="o">=~</span><span class="w"> </span><span class="sr">~r/utf-?8/</span><span class="w"> </span><span class="o">-&gt;</span><span class="w"> </span><span class="ss">:utf8</span><span class="w">
          </span><span class="n">charset</span><span class="w"> </span><span class="o">=~</span><span class="w"> </span><span class="sr">~r/iso-?8859-?1/</span><span class="w"> </span><span class="o">-&gt;</span><span class="w"> </span><span class="ss">:latin1</span><span class="w">
          </span><span class="no">true</span><span class="w"> </span><span class="o">-&gt;</span><span class="w"> </span><span class="n">charset</span><span class="w">
        </span><span class="k" data-group-id="2366217429-95">end</span><span class="w">
      </span><span class="bp">_</span><span class="w"> </span><span class="o">-&gt;</span><span class="w">
        </span><span class="no">nil</span><span class="w">
    </span><span class="k" data-group-id="2366217429-92">end</span><span class="w">
  </span><span class="k" data-group-id="2366217429-90">end</span><span class="w">

  </span><span class="c1"># When the header isn&#39;t sent, the RFC spec says we should assume ISO-8859-1, but the default is</span><span class="w">
  </span><span class="c1"># actually different per format, eg, XML should be assumed UTF-8. We&#39;re going to not re-encode</span><span class="w">
  </span><span class="c1"># if it&#39;s not sent and assume UTF-8. This should be safe for most cases.</span><span class="w">
  </span><span class="kd">defp</span><span class="w"> </span><span class="nf">reencode_body</span><span class="p" data-group-id="2366217429-96">(</span><span class="no">nil</span><span class="p">,</span><span class="w"> </span><span class="n">body</span><span class="p" data-group-id="2366217429-96">)</span><span class="p">,</span><span class="w"> </span><span class="ss">do</span><span class="p">:</span><span class="w"> </span><span class="n">body</span><span class="w">
  </span><span class="kd">defp</span><span class="w"> </span><span class="nf">reencode_body</span><span class="p" data-group-id="2366217429-97">(</span><span class="ss">:utf8</span><span class="p">,</span><span class="w"> </span><span class="n">body</span><span class="p" data-group-id="2366217429-97">)</span><span class="p">,</span><span class="w"> </span><span class="ss">do</span><span class="p">:</span><span class="w"> </span><span class="n">body</span><span class="w">
  </span><span class="kd">defp</span><span class="w"> </span><span class="nf">reencode_body</span><span class="p" data-group-id="2366217429-98">(</span><span class="ss">:latin1</span><span class="p">,</span><span class="w"> </span><span class="n">body</span><span class="p" data-group-id="2366217429-98">)</span><span class="w"> </span><span class="k" data-group-id="2366217429-99">do</span><span class="w">
    </span><span class="k">case</span><span class="w"> </span><span class="nc">:unicode</span><span class="o">.</span><span class="n">characters_to_binary</span><span class="p" data-group-id="2366217429-100">(</span><span class="n">body</span><span class="p">,</span><span class="w"> </span><span class="ss">:latin1</span><span class="p">,</span><span class="w"> </span><span class="ss">:utf8</span><span class="p" data-group-id="2366217429-100">)</span><span class="w"> </span><span class="k" data-group-id="2366217429-101">do</span><span class="w">
      </span><span class="p" data-group-id="2366217429-102">{</span><span class="ss">:error</span><span class="p">,</span><span class="w"> </span><span class="n">binary</span><span class="p">,</span><span class="w"> </span><span class="n">rest</span><span class="p" data-group-id="2366217429-102">}</span><span class="w"> </span><span class="o">-&gt;</span><span class="w">
        </span><span class="nc">Logger</span><span class="o">.</span><span class="n">error</span><span class="p" data-group-id="2366217429-103">(</span><span class="s">&quot;Failed to re-encode text. BODY: </span><span class="si" data-group-id="2366217429-104">#{</span><span class="n">inspect</span><span class="w"> </span><span class="n">binary</span><span class="si" data-group-id="2366217429-104">}</span><span class="s"> REST: </span><span class="si" data-group-id="2366217429-105">#{</span><span class="n">inspect</span><span class="w"> </span><span class="n">rest</span><span class="si" data-group-id="2366217429-105">}</span><span class="s">&quot;</span><span class="p" data-group-id="2366217429-103">)</span><span class="w">
        </span><span class="n">body</span><span class="w">

      </span><span class="p" data-group-id="2366217429-106">{</span><span class="ss">:incomplete</span><span class="p">,</span><span class="w"> </span><span class="n">reencoded_text</span><span class="p">,</span><span class="w"> </span><span class="n">rest</span><span class="p" data-group-id="2366217429-106">}</span><span class="w"> </span><span class="o">-&gt;</span><span class="w">
        </span><span class="nc">Logger</span><span class="o">.</span><span class="n">warn</span><span class="p" data-group-id="2366217429-107">(</span><span class="s">&quot;Failed to re-encode entire text. Dropping characters: </span><span class="si" data-group-id="2366217429-108">#{</span><span class="n">inspect</span><span class="w"> </span><span class="n">rest</span><span class="si" data-group-id="2366217429-108">}</span><span class="s">&quot;</span><span class="p" data-group-id="2366217429-107">)</span><span class="w">
        </span><span class="n">reencoded_text</span><span class="w">

      </span><span class="n">reencoded_text</span><span class="w"> </span><span class="o">-&gt;</span><span class="w">
        </span><span class="n">reencoded_text</span><span class="w">
    </span><span class="k" data-group-id="2366217429-101">end</span><span class="w">
  </span><span class="k" data-group-id="2366217429-99">end</span><span class="w">
  </span><span class="kd">defp</span><span class="w"> </span><span class="nf">reencode_body</span><span class="p" data-group-id="2366217429-109">(</span><span class="n">other</span><span class="p">,</span><span class="w"> </span><span class="n">body</span><span class="p" data-group-id="2366217429-109">)</span><span class="w"> </span><span class="k" data-group-id="2366217429-110">do</span><span class="w">
    </span><span class="nc">Logger</span><span class="o">.</span><span class="n">error</span><span class="p" data-group-id="2366217429-111">(</span><span class="s">&quot;Need to implement re-encoding support for: </span><span class="si" data-group-id="2366217429-112">#{</span><span class="n">other</span><span class="si" data-group-id="2366217429-112">}</span><span class="s">&quot;</span><span class="p" data-group-id="2366217429-111">)</span><span class="w">
    </span><span class="n">body</span><span class="w">
  </span><span class="k" data-group-id="2366217429-110">end</span><span class="w">
</span><span class="k" data-group-id="2366217429-1">end</span></code></pre>
<h2>
and… Done!</h2>
<p>
Hope this helps. Hit me up at <a href="https://twitter.com/bernheisel">@bernheisel</a> if I
missed anything or you have another cool idea.</p>
<p>
I’m not really interested in pulling this into a library, because that’s exactly
what we don’t need: yet another HTTP client. But, if you found yourself needing
to decompress responses with HTTPoison, then this will be a good start.</p>
<p>
You’ll notice that this doesn’t implement all the calls (we’re missing the ones
like <code class="inline">delete</code> and <code class="inline">head</code>), nor is it the smartest way to solve the problem. I’ll
leave the gaps to inspire you.</p>
<h2>
Or use <a href="https://github.com/elixir-tesla/tesla" title="">Tesla</a></h2>
<p>
Tesla supports decompression out of the box; so if you started on that HTTP
client, you probably didn’t have to worry about any of this :)</p>
]]></content:encoded>
</item>
<item>
<title><![CDATA[Ruby, Rails, and CircleCI 2.0 Workflows]]></title>
<dc:creator>David Bernheisel</dc:creator>
<description><![CDATA[Know your tools -- CircleCI 2.0 Workflows]]></description>
<link>https://bernheisel.com/blog/ruby-rails-and-circle-ci-workflows</link>
<guid isPermaLink="true">https://bernheisel.com/blog/ruby-rails-and-circle-ci-workflows</guid>
<pubDate>Sun, 26 Aug 2018 00:00:00 -0400</pubDate>
<content:encoded><![CDATA[<p>
There’s a movement lately in the development world that I’ve really enjoyed:</p>
<blockquote>
  <p>
“Know your tools”
– Tool authors, probably  </p>
</blockquote>
<p>
It’s encouraged me to dive deeper into the tools that I use every day and
understand more how they work.</p>
<ul>
  <li>
Ruby? I should know how <a href="https://medium.com/rubycademy/openstruct-in-ruby-ab6ba3aff9a4" title="">OpenStruct works</a>.  </li>
  <li>
Rails? I should understand the <a href="https://www.rubypigeon.com/posts/examining-internals-of-rails-request-response-cycle/" title="">request cycle</a>.  </li>
  <li>
SQL? I should understand <a href="https://www.postgresql.org/docs/current/tutorial-views.html" title="">database views</a> and <a href="https://www.postgresql.org/docs/10/sql-createtrigger.html" title="">triggers</a>.  </li>
  <li>
Git? I should understand <a href="https://git-scm.com/docs/git-rebase" title="">how to rebase</a>.  </li>
  <li>
CI? I should understand my <strong>tool of choice</strong>.  </li>
</ul>
<p>
Why? Because it makes me a better developer. I expose myself to new patterns,
learn new concepts, and have those solution-patterns in my toolbox for when
I need to solve problems.</p>
<p>
Today, I want to talk about my tool of choice for Continuous Integration.</p>
<h2>
CircleCI 2.0</h2>
<p>
My tool of choice for Continuous Integration lately is CircleCI 2.0. I’ve
really enjoyed it. I don’t think I went out of my way to pick them, honestly,
but a couple of co-workers before me setup projects that I work on with
CircleCI; I think they made a good choice.</p>
<p>
Depending on how long ago the project was setup for CI, you might be using
CircleCI 1.0 configurations, which is <a href="https://circleci.com/sunset1-0/" title="">deprecating on EOD August 31,
2018</a>.</p>
<p>
Enter my problem: I had a project using CircleCI 1.0, so I needed to migrated
quickly. Our deployment process also happens via CI, so I didn’t want that to
stop.</p>
<p>
Time for me to dive into CircleCI.</p>
<h3>
What is CircleCI?</h3>
<p>
I’ll keep it short here, <a href="https://circleci.com/product/" title="">because they do a better job explaining who they
are</a>. CircleCI is a service that uses containers to build your code,
test your code, and deploy your code.</p>
<p>
What are containers? <a href="https://www.docker.com/resources/what-container/" title="">They are isolated environments that run
software.</a> CircleCI 2.0 uses containers to manage your code, and
extends that container configurability to your projects.</p>
<h3>
How do I configure <em>my</em> container?</h3>
<p>
This is the “know your tools” part. Thankfully, CircleCI provides a lot of
great <a href="https://circleci.com/docs/" title="">documentation</a> and <a href="https://circleci.com/docs/examples-and-guides-overview/" title="">samples</a>. Since this is an article about Ruby and
Rails, using their Rails tutorial will be enough to get you going.</p>
<p>
In their Rails tutorial, they are using one container to build your project
and test your code. I could modify it to also deploy my project; it’s simple to
add another step:</p>
<pre><code class="yaml"># replace &quot;production&quot; with integration, staging, or whatever else environment
# you need to deploy.
- run: git config --global user.name &quot;CircleCI&quot;
- run: bundle exec cap production deploy</code></pre>
<p>
Using Capistrano makes it easy to deploy for most Rails applications. Above
assumes you’ve configured the CircleCI project to <a href="https://circleci.com/docs/add-ssh-key/" title="">have the appropriate SSH
keys</a>, so it can log into the server and perform the needed steps.</p>
<p>
At this point you you could be done if you’re in the same boat as me and need
to move off of CircleCI 1.0 and onto CircleCI 2.0. But, this isn’t really about
migrating from CircleCI 1.0 to CircleCI 2.0; we’re here to learn!</p>
<h2>
Not everything has to be together</h2>
<p>
I decided to explore a little further and use another CircleCI feature:
<a href="https://circleci.com/docs/jobs-steps/" title="">workflows</a></p>
<p>
In my development environment, everything happens on my machine. I like my
machine because it’s the way I made it. I like using <a href="https://brew.sh/" title="">brew</a> to install
dependencies, but the CI environment runs Linux, which my production
environment also runs.</p>
<p>
CircleCI leverages containers to help keep “software units” separate and pure.
This is smart because I need these environments to be accurately reproducible
and not fragile to some other dependency being introduced. Have you ever
installed a different version of OpenSSL to “fix” one project, but totally
borked other projects on your machine? Containers prevent this problem.</p>
<p>
When you think about what your CI process does, there’s probably a couple of
large tasks happening:</p>
<ul>
  <li>
Install language versions  </li>
  <li>
Install dependencies outside of your language (like imagemagick)  </li>
  <li>
Install dependencies in your language (with bundler)  </li>
  <li>
Install dependencies in your <em>other</em> language (with npm)  </li>
  <li>
Precompile assets  </li>
  <li>
Run Ruby tests  </li>
  <li>
Run JavaScript tests (you wrote those, right?)  </li>
  <li>
Run linting checks (your team agrees on the same style, right?)  </li>
  <li>
Deploy the working code  </li>
</ul>
<p>
With containers, we can separate these tasks. This has some benefits because we
can run some of these in parallel instead of serially; that might give us
a speed boost; more importantly, it communicates better where the failure is
happening, if it happens 🤞</p>
<p>
  <img src="/images/circleci2-github.png" alt="GitHub Integration">
</p>
<p>
Also, what if your ruby tests fail– wouldn’t you also like to know if your
JavaScript tests fail? Most of the CI scripts I’ve seen will stop on the first
failed command which doesn’t give you a complete picture.</p>
<h2>
Gotta keep ‘em separated</h2>
<p>
For installing the language versions, I used to rely on tools like <a href="https://github.com/asdf-vm/asdf" title="">asdf</a> and
<a href="https://github.com/nvm-sh/nvm" title="">nvm</a> and <a href="https://github.com/rbenv/rbenv" title="">rbenv</a>, but I realized that I <em>shouldn’t</em> need to use those tools
when using containers. I need those tools for my local development environment
so I can switch between projects, but not when I can use a container image that
has the language versions I need already installed for <em>this</em> project.</p>
<p>
Enter: <a href="https://circleci.com/docs/circleci-images/" title="">CircleCI docker images</a>.</p>
<p>
CircleCI provides some great container images with good tools included like
<a href="https://github.com/jwilder/dockerize" title="">dockerize</a>, <a href="https://packages.debian.org/stretch/xvfb" title="">xvfb</a>, and <a href="https://chromedriver.chromium.org/" title="">chromedriver</a>, which saves you from having to worry
about installing those manually. Let’s use their images.</p>
<p>
<strong>What is <a href="https://github.com/jwilder/dockerize" title="">Dockerize</a>?</strong>
It’s a small bash utility that is used to wait on database containers to be
ready <em>before</em> trying to have tests run against them.</p>
<p>
<strong>What is <a href="https://packages.debian.org/stretch/xvfb" title="">xvfb</a> and <a href="https://chromedriver.chromium.org/" title="">chromedriver</a>?</strong>
Good question, I don’t really understand it, but I know I need it to allow some
browser tests to run. Just roll with it.</p>
<p>
In my case, my project is using Ruby 2.5.1 but unfortunately requires Node 6.x
because of an older node-based asset pipeline; CircleCI’s Ruby image
<a href="https://github.com/CircleCI-Public/circleci-dockerfiles/blob/16a3d488ce42027c38f6ef5f419e2eaf9df2f35b/ruby/images/2.5.1-stretch/node/Dockerfile#L36" title="">Dockerfile</a> includes Node 8.x. I can’t run <code class="inline">npm install</code> under Node 8.x.
That’s OK though, because we can separate them with containers.</p>
<h2>
Let’s try this out</h2>
<p>
According to CircleCI docs, I’ll need to define multiple jobs. I’ll try to keep
the jobs focused on one task. Let’s start with two tasks:</p>
<ol>
  <li>
Bundle Install  </li>
  <li>
NPM Install. The production environment doesn’t use Yarn, so I shouldn’t
either in CI yet. Also, Node 6.x doesn’t ship with npm that supports
<code class="inline">package-lock.json</code>. ::sigh:: I’ll leave some commented-out code though in
case you can use it.  </li>
</ol>
<pre><code class="yaml">jobs:
  bundle-install:
    working_directory: ~/repo
    docker:
      - image: circleci/ruby:2.5.1-node-browsers
        environment:
          BUNDLE_JOBS: 4
          BUNDLE_RETRY: 3
          BUNDLE_PATH: vendor/bundle
          DATABASE_URL: &quot;postgresql://root@localhost/my_project_test?pool=5&quot;
    steps:
      - checkout
      - attach_workspace:
          at: ~/repo
      - restore_cache:
          keys:
          - bundle-{{ arch }}-{{ checksum &quot;Gemfile.lock&quot; }}
          # We add &quot;arch&quot; as a key since some gems compile native code, like
          # nokogiri. If for whatever reason CircleCI runs the job on a machine
          # with a different architecture, the cache would be invalid.
      - run: bundle install
      - save_cache:
          paths:
            - ./vendor/bundle
          key: bundle-{{ arch }}-{{ checksum &quot;Gemfile.lock&quot; }}
      - persist_to_workspace:
          root: .
          paths:
            - vendor/bundle

  npm-install:
    working_directory: ~/repo
    docker:
      - image: circleci/node:6
    steps:
      - checkout
      - attach_workspace:
          at: ~/repo
      # Enable when using npm5+
      # - restore_cache:
      #     keys:
      #     - node-modules-{{ arch }}-{{ checksum &quot;package-lock.json&quot; }}
      #       We add &quot;arch&quot; as a key since some packages compile native code, like
      #       node-sass. If for whatever reason CircleCI runs the job on a machine
      #       with a different architecture, the cache would be invalid.
      - run: npm --version
      - run: node --version
      - run: npm install
      - run: mkdir -p public/assets/frontend
      - run: npm run production
      # Enable when using npm5+
      # - save_cache:
      #     paths:
      #       - node_modules
      #     key: node-modules-{{ arch }}-{{ checksum &quot;package-lock.lock&quot; }}
      - persist_to_workspace:
          root: .
          paths:
            - node_modules
            - public/assets/frontend</code></pre>
<p>
Looking good so far. I’m not doing anything special but there are a couple of
concepts we should know:</p>
<ul>
  <li>
Cache: This is used to save dependencies across runs.  </li>
  <li>
Workspace: This is used to save data as the workflow continues, so the next
step can use the results of a previous step like <code class="inline">node_modules</code> and
<code class="inline">vendor/bundle</code>. We attach the workspace at the beginning, and then persist
some resulting files to the workspace at the end of the task.  </li>
</ul>
<p>
For my project, we when we run <code class="inline">npm run production</code>, it compiles assets into
the folder <code class="inline">public/assets/frontend</code>. I need that to run my tests later.</p>
<p>
Let’s move on to the test step:</p>
<pre><code class="yaml">test:
  working_directory: ~/repo
  docker:
    # The first image is the primary image. This is where the commands below
    # will run within.
    - image: circleci/ruby:2.5.1-node-browsers
      environment:
        BUNDLE_JOBS: 4
        BUNDLE_RETRY: 3
        BUNDLE_PATH: vendor/bundle
        DATABASE_URL: &quot;postgresql://root@localhost/my_project_test?pool=5&quot;
    # For more information about how these images work, check our their READMEs
    # https://hub.docker.com/r/circleci/ruby/
    # https://hub.docker.com/r/circleci/postgres/
    # https://hub.docker.com/_/postgres/
    - image: circleci/postgres:9.3-alpine-ram
      environment:
        POSTGRES_USER: root
        POSTGRES_DB: my_project_test
  steps:
    - checkout
    - attach_workspace:
        at: ~/repo
    - run: dockerize -wait tcp://localhost:5432 -timeout 1m
    - run: cp config/secrets.yml{.example,}
    - run: cp config/database.yml{.example,}
    - run: bundle install
    - run: RAILS_ENV=test bundle exec rake db:schema:load --trace
    - run: |
        mkdir -p ~/repo/tmp/test-results/rspec
        bundle exec rspec --profile 10 --format RspecJunitFormatter --out ~/repo/tmp/test-results/rspec/results.xml --format progress
    - store_artifacts:
        path: ~/repo/tmp/screenshots
        destination: test-screenshots
    - store_test_results:
        path: ~/repo/tmp/test-results</code></pre>
<p>
Since we’re attaching the workspace, we can assume that we have our
<code class="inline">node_modules</code> and <code class="inline">vendor/bundle</code> already present. We still run <code class="inline">bundle install</code>, but it’s incredibly quick since the workspace already has the info–
bundler just needs to recognize it.</p>
<p>
I want to point out another CircleCI feature: <a href="https://circleci.com/docs/collect-test-data/" title="">collecting test metadata</a>.</p>
<p>
There’s a popular XML format for expressing test outputs and metadata, like the
time it took to run each test; <a href="https://www.ibm.com/docs/en/developer-for-zos/14.1.0?topic=formats-junit-xml-format" title="">JUnit</a> popularized this format. To get it for
your tests, you can install a gem. All of the information is on the CircleCI
site.</p>
<p>
This feature gives us this:</p>
<p>
  <img src="/images/circleci2-failures.png" alt="Test metadata at the top">
</p>
<p>
One more CircleCI feature: <a href="https://circleci.com/docs/artifacts/" title="">storing artifacts</a>. I think this is normally for
compiled binaries of your app, but I’m going to use it for storing screenshots
of failed browser-based tests. <a href="https://guides.rubyonrails.org/v5.1/testing.html#screenshot-helper" title="">Rails 5.1 system specs</a> will do this
automatically, but you can also add it to older Rails projects with
<a href="https://github.com/mattheworiordan/capybara-screenshot" title="">capybara-screenshot</a>. All we have to do is tell CircleCI where to find
artifacts.</p>
<p>
Ideally, you could set up two more jobs here:</p>
<ul>
  <li>
job: <code class="inline">lint-rubocop</code>  </li>
  <li>
job: <code class="inline">lint-eslint</code>  </li>
</ul>
<p>
But that’s up to you.</p>
<p>
Lastly, we need to deploy the app:</p>
<pre><code class="yaml">deploy-integration:
  docker:
    - image: circleci/ruby:2.5.1-node-browsers
      environment:
        BUNDLE_JOBS: 4
        BUNDLE_RETRY: 3
        BUNDLE_PATH: vendor/bundle
        DATABASE_URL: &quot;postgresql://root@localhost/my_project_test?pool=5&quot;
    - image: circleci/postgres:9.3-alpine-ram
      environment:
        POSTGRES_USER: root
        POSTGRES_DB: my_project_test
  steps:
    - checkout
    - attach_workspace:
        at: ~/repo
    - run: bundle install
    - run: |
        git config --global user.name &quot;CircleCI&quot;
        bundle exec cap integration deploy

# copy and paste and modify for each environment</code></pre>
<p>
Nothing special here, but I do want to have CI identify itself as CircleCI for
the Capistrano process that uses git. Just like before, above assumes you’ve
configured the CircleCI project to <a href="https://circleci.com/docs/add-ssh-key/" title="">have the appropriate SSH keys</a>, so it
can log into the server and perform the needed steps.</p>
<p>
We’ve described the jobs, but we haven’t described the workflow. Let’s do that
now.</p>
<h2>
Putting it together</h2>
<pre><code class="yaml">workflows:
  version: 2
  build-test-deploy:
    jobs:
      - bundle-install
      - npm-install
      - test:
          requires:
            - bundle-install
            - npm-install
      - deploy-integration:
          requires:
            - test
          filters:
            branches:
              only: master</code></pre>
<p>
This describes the shape of your workflow. <code class="inline">bundle-install</code> and <code class="inline">npm-install</code>
don’t have any requirements, so they can run in parallel. <code class="inline">test</code> requires the
installation steps to succeed before it can run, and <code class="inline">deploy-integration</code>
requires the test step to succeed, and only run when CI is running from
a commit on the <code class="inline">master</code> branch.</p>
<p>
That gives us this shape:</p>
<p>
  <img src="/images/circleci2-workflow.png" alt="Workflow Shape">
</p>
<p>
Remember, if you also linted the code and had JavaScript tests, you could have
4 jobs running in parallel since they don’t depend on each other (only
<code class="inline">npm-install</code> and/or <code class="inline">bundle-install</code>)</p>
<h2>
YAML</h2>
<p>
We can make it a little better with a little bit of <a href="https://en.wikipedia.org/wiki/YAML" title="">YAML</a> knowledge. With
workflows, you’ll repeat a lot of the same steps, and some of those steps can
be verbose. We can help that.</p>
<p>
For example, we repeated <code class="inline">working_directory: ~/repo</code> a lot, as well as the
docker images for several jobs. Let’s shorten it with a named anchor.</p>
<pre><code class="yaml">defaults: &amp;defaults
  working_directory: ~/repo
  docker:
    - image: circleci/ruby:2.5.1-node-browsers
      environment:
        BUNDLE_JOBS: 4
        BUNDLE_RETRY: 3
        BUNDLE_PATH: vendor/bundle
        DATABASE_URL: &quot;postgresql://root@localhost/my_project_test?pool=5&quot;

    - image: circleci/postgres:9.3-alpine-ram
      environment:
        POSTGRES_USER: root
        POSTGRES_DB: my_project_test


## and then use it like this:

jobs:
  bundle-install:
    &lt;&lt;: *defaults
    steps: ...

  test:
    &lt;&lt;: *defaults
    steps: ...

  deploy-integration:
    &lt;&lt;: *defaults
    steps: ...</code></pre>
<p>
Nice!</p>
<h2>
Encore: What if I fork the repo from the client?</h2>
<p>
CircleCI is so helpful; they provide <a href="https://circleci.com/docs/env-vars/" title="">environment variables</a> for you to check
for situations like this. We can ask some questions against those variables
with something like this:</p>
<pre><code class="yaml">deploy-integration:
  &lt;&lt;: *defaults
  steps:
    - checkout
    - attach_workspace:
        at: ~/repo
    - run: |
        set -e
        # only deploy from upstream
        if [ &quot;$CIRCLE_PROJECT_USERNAME&quot; == &quot;ClientName&quot; ]; then
          git config --global user.name &quot;CircleCI&quot;
          bundle install
          bundle exec cap integration deploy
        fi</code></pre>
<p>
Just add a dash of bash to make it work.</p>
<h2>
Wrapping up</h2>
<p>
That concludes this episode of “know your tools”. If you find anything wrong,
another cool feature, or an alternative approach, then tweet <a href="https://twitter.com/bernheisel" title="">@bernheisel</a> with
your recommendation.</p>
<p>
By the way, there are other great CI platforms like <a href="https://www.travis-ci.com/" title="">Travis CI</a>, <a href="https://about.gitlab.com/features/continuous-integration/" title="">GitLab</a>,
<a href="https://cloud.google.com/build/" title="">Google Cloud Build</a>, and more. Today I’m using <a href="https://circleci.com/product/" title="">CircleCI</a>, but tomorrow
I might be trying out another platform. If above is way too much for you,
I would consider looking at <a href="https://www.travis-ci.com/" title="">Travis CI</a>; they did a great job simplifying to
the essentials.</p>
<hr class="thin">
<p>
Here’s the entire config file for my project. Feel free to modify it for yours!</p>
<pre><code class="yaml"># Ruby CircleCI 2.0 configuration file
#
# Check https://circleci.com/docs/language-ruby/ for more details
#
version: 2

defaults: &amp;defaults
  working_directory: ~/repo
  docker:
    - image: circleci/ruby:2.5.1-node-browsers
      environment:
        BUNDLE_JOBS: 4
        BUNDLE_RETRY: 3
        BUNDLE_PATH: vendor/bundle
        DATABASE_URL: &quot;postgresql://root@localhost/project_test?pool=5&quot;

    - image: circleci/postgres:9.3-alpine-ram
      environment:
        POSTGRES_USER: root
        POSTGRES_DB: project_test

jobs:
  bundle-install:
    &lt;&lt;: *defaults
    steps:
      - checkout
      - attach_workspace:
          at: ~/repo
      - restore_cache:
          keys:
          - bundle-{{ arch }}-{{ checksum &quot;Gemfile.lock&quot; }}
      - run: bundle install
      - save_cache:
          paths:
            - ./vendor/bundle
          key: bundle-{{ arch }}-{{ checksum &quot;Gemfile.lock&quot; }}
      - persist_to_workspace:
          root: .
          paths:
            - vendor/bundle

  npm-install:
    working_directory: ~/repo
    docker:
      - image: circleci/node:6
    steps:
      - checkout
      - attach_workspace:
          at: ~/repo
      # Enable when using npm5+
      # - restore_cache:
      #     keys:
      #     - node-modules-{{ arch }}-{{ checksum &quot;package-lock.json&quot; }}
      - run: npm --version
      - run: node --version
      - run: npm install
      - run: mkdir -p public/assets/frontend
      - run: npm run production
      # Enable when using npm5+
      # - save_cache:
      #     paths:
      #       - node_modules
      #     key: node-modules-{{ arch }}-{{ checksum &quot;package-lock.lock&quot; }}
      - persist_to_workspace:
          root: .
          paths:
            - node_modules
            - public/assets/frontend

  test:
    &lt;&lt;: *defaults
    steps:
      - checkout
      - attach_workspace:
          at: ~/repo
      - run: dockerize -wait tcp://localhost:5432 -timeout 1m
      - run: cp config/secrets.yml{.example,}
      - run: cp config/database.yml{.example,}
      - run: bundle install
      - run: RAILS_ENV=test bundle exec rake db:schema:load --trace
      - run: |
          mkdir -p ~/repo/tmp/test-results/rspec
          bundle exec rspec --profile 10 --format RspecJunitFormatter --out ~/repo/tmp/test-results/rspec/results.xml --format progress
      - store_artifacts:
          path: ~/repo/tmp/screenshots
          destination: test-screenshots
      - store_test_results:
          path: ~/repo/tmp/test-results

  deploy-integration:
    &lt;&lt;: *defaults
    steps:
      - checkout
      - attach_workspace:
          at: ~/repo
      - run: |
          set -e
          # only deploy from upstream
          if [ &quot;$CIRCLE_PROJECT_USERNAME&quot; == &quot;ClientName&quot; ]; then
            git config --global user.name &quot;CircleCI&quot;
            bundle install
            bundle exec cap integration deploy
          fi

  deploy-staging:
    &lt;&lt;: *defaults
    steps:
      - checkout
      - attach_workspace:
          at: ~/repo
      - run: |
          set -e
          # only deploy from upstream
          if [ &quot;$CIRCLE_PROJECT_USERNAME&quot; == &quot;ClientName&quot; ]; then
            git config --global user.name &quot;CircleCI&quot;
            bundle install
            bundle exec cap staging deploy
          fi

  deploy-production:
    &lt;&lt;: *defaults
    steps:
      - checkout
      - attach_workspace:
          at: ~/repo
      - run: |
          set -e
          # only deploy from upstream
          if [ &quot;$CIRCLE_PROJECT_USERNAME&quot; == &quot;ClientName&quot; ]; then
            git config --global user.name &quot;CircleCI&quot;
            bundle install
            bundle exec cap production deploy
          fi

workflows:
  version: 2
  build-test-deploy:
    jobs:
      - bundle-install
      - npm-install
      - test:
          requires:
            - bundle-install
            - npm-install
      - deploy-integration:
          requires:
            - test
          filters:
            branches:
              only: master
      - deploy-staging:
          requires:
            - test
          filters:
            branches:
              only: staging
      - deploy-production:
          requires:
            - test
          filters:
            branches:
              only: production</code></pre>
]]></content:encoded>
</item>
<item>
<title><![CDATA[Querying an Embedded Map in PostgreSQL with Ecto]]></title>
<dc:creator>David Bernheisel</dc:creator>
<description><![CDATA[Structs and maps are easy to work with in Elixir, but if they are stored
in the database as JSON and accessed via an Ecto Schema, it's not as
clear how to query them. We're going to explore how to do that, and make
it clear and easy.
]]></description>
<link>https://bernheisel.com/blog/querying-embedded-maps-in-postgres-with-ecto</link>
<guid isPermaLink="true">https://bernheisel.com/blog/querying-embedded-maps-in-postgres-with-ecto</guid>
<pubDate>Fri, 09 Mar 2018 00:00:00 -0500</pubDate>
<content:encoded><![CDATA[<p>
Structs and maps are easy to work with in Elixir, but if they are stored
in the database as JSON and accessed via an Ecto Schema, it’s not as
clear how to query them. We’re going to explore how to do that, and make
it clear and easy.</p>
<p>
PostgreSQL has <a href="https://www.postgresql.org/docs/current/functions-json.html" title="">great support for objects stored as JSON</a>.
This is useful for those moments when you need to store data that could
be variably structured, such as responses from other services’ APIs, or
data that frequently travels together within your relational tables.</p>
<p>
A common trade-off for mixing scalar column data types (like <code class="inline">varchar</code>
or <code class="inline">integer</code>) with column data types that handle more-complicated
objects (like &lt;abbr title=”JavaScript Object Notation”&gt;JSON&lt;/abbr&gt;) is
that &lt;abbr title=”Object-relational Mapping”&gt;ORMs&lt;/abbr&gt; or data mappers
sometimes can’t introspect on them for you, which means it becomes much
harder to query that data.</p>
<p>
Using Ecto’s <code class="inline">embedded_schema</code> helps introspect on those known values,
but it doesn’t really assist you with querying those fields in SQL. This
is where I became extremely greatful for <a href="https://hexdocs.pm/ecto/Ecto.Query.html#module-fragments" title="">Ecto’s escape
hatch</a>: <code class="inline">fragment()</code>.</p>
<h2>
Define the Struct or Map in Ecto</h2>
<p>
Let’s dive into some code as an example:</p>
<p>
I have a <code class="inline">Vehicle.Photo</code> schema that has several versions of the photo:</p>
<ul>
  <li>
craigslist_ad  </li>
  <li>
facebook_ad  </li>
  <li>
facebook_carousel_ad  </li>
  <li>
extra_large  </li>
  <li>
extra_small  </li>
  <li>
large  </li>
  <li>
medium  </li>
  <li>
original  </li>
  <li>
small  </li>
</ul>
<p>
We decided to store the versions’ &lt;abbr title=”Uniform Resource
Locators”&gt;URLs&lt;/abbr&gt; inside a map in the database,
because we’re going to use a set of the URLs at the same time inside of
an HTML <code class="inline">&lt;img srcset /&gt;</code>. <a href="https://developer.mozilla.org/en-US/docs/Learn/HTML/Multimedia_and_embedding/Responsive_images" title="">You can read more about srcset from
MDN and how it helps with responsive images</a>.</p>
<p>
The Ecto migration looks like this:</p>
<pre><code class="makeup elixir"><span class="kd">def</span><span class="w"> </span><span class="nf">up</span><span class="w"> </span><span class="k" data-group-id="7218113008-1">do</span><span class="w">
  </span><span class="n">alter</span><span class="w"> </span><span class="n">table</span><span class="p" data-group-id="7218113008-2">(</span><span class="ss">:vehicle_photos</span><span class="p" data-group-id="7218113008-2">)</span><span class="w"> </span><span class="k" data-group-id="7218113008-3">do</span><span class="w">
    </span><span class="n">add</span><span class="w"> </span><span class="ss">:standard_urls</span><span class="p">,</span><span class="w"> </span><span class="ss">:map</span><span class="w">
    </span><span class="n">add</span><span class="w"> </span><span class="ss">:facebook_urls</span><span class="p">,</span><span class="w"> </span><span class="ss">:map</span><span class="w">
    </span><span class="n">add</span><span class="w"> </span><span class="ss">:craigslist_urls</span><span class="p">,</span><span class="w"> </span><span class="ss">:map</span><span class="w">
  </span><span class="k" data-group-id="7218113008-3">end</span><span class="w">
</span><span class="k" data-group-id="7218113008-1">end</span></code></pre>
<p>
The Ecto schema looks like this:</p>
<pre><code class="makeup elixir"><span class="n">schema</span><span class="w"> </span><span class="s">&quot;vehicle_photos&quot;</span><span class="w"> </span><span class="k" data-group-id="4013514497-1">do</span><span class="w">
  </span><span class="n">field</span><span class="p" data-group-id="4013514497-2">(</span><span class="ss">:file</span><span class="p">,</span><span class="w"> </span><span class="nc">PhotoUploader.Type</span><span class="p" data-group-id="4013514497-2">)</span><span class="w">

  </span><span class="n">embeds_one</span><span class="w"> </span><span class="ss">:standard_urls</span><span class="p">,</span><span class="w"> </span><span class="nc">StandardUrls</span><span class="p">,</span><span class="w"> </span><span class="ss">on_replace</span><span class="p">:</span><span class="w"> </span><span class="ss">:update</span><span class="w"> </span><span class="k" data-group-id="4013514497-3">do</span><span class="w">
    </span><span class="n">field</span><span class="p" data-group-id="4013514497-4">(</span><span class="ss">:extra_large</span><span class="p">,</span><span class="w"> </span><span class="ss">:string</span><span class="p" data-group-id="4013514497-4">)</span><span class="w">
    </span><span class="n">field</span><span class="p" data-group-id="4013514497-5">(</span><span class="ss">:extra_small</span><span class="p">,</span><span class="w"> </span><span class="ss">:string</span><span class="p" data-group-id="4013514497-5">)</span><span class="w">
    </span><span class="n">field</span><span class="p" data-group-id="4013514497-6">(</span><span class="ss">:large</span><span class="p">,</span><span class="w"> </span><span class="ss">:string</span><span class="p" data-group-id="4013514497-6">)</span><span class="w">
    </span><span class="n">field</span><span class="p" data-group-id="4013514497-7">(</span><span class="ss">:medium</span><span class="p">,</span><span class="w"> </span><span class="ss">:string</span><span class="p" data-group-id="4013514497-7">)</span><span class="w">
    </span><span class="n">field</span><span class="p" data-group-id="4013514497-8">(</span><span class="ss">:original</span><span class="p">,</span><span class="w"> </span><span class="ss">:string</span><span class="p" data-group-id="4013514497-8">)</span><span class="w">
    </span><span class="n">field</span><span class="p" data-group-id="4013514497-9">(</span><span class="ss">:small</span><span class="p">,</span><span class="w"> </span><span class="ss">:string</span><span class="p" data-group-id="4013514497-9">)</span><span class="w">
  </span><span class="k" data-group-id="4013514497-3">end</span><span class="w">

  </span><span class="n">embeds_one</span><span class="w"> </span><span class="ss">:facebook_urls</span><span class="p">,</span><span class="w"> </span><span class="nc">FacebookUrls</span><span class="p">,</span><span class="w"> </span><span class="ss">on_replace</span><span class="p">:</span><span class="w"> </span><span class="ss">:update</span><span class="w"> </span><span class="k" data-group-id="4013514497-10">do</span><span class="w">
    </span><span class="n">field</span><span class="p" data-group-id="4013514497-11">(</span><span class="ss">:hero_ad</span><span class="p">,</span><span class="w"> </span><span class="ss">:string</span><span class="p" data-group-id="4013514497-11">)</span><span class="w">
    </span><span class="n">field</span><span class="p" data-group-id="4013514497-12">(</span><span class="ss">:carousel_ad</span><span class="p">,</span><span class="w"> </span><span class="ss">:string</span><span class="p" data-group-id="4013514497-12">)</span><span class="w">
  </span><span class="k" data-group-id="4013514497-10">end</span><span class="w">

  </span><span class="n">embeds_one</span><span class="w"> </span><span class="ss">:craigslist_urls</span><span class="p">,</span><span class="w"> </span><span class="nc">CraigslistUrls</span><span class="p">,</span><span class="w"> </span><span class="ss">on_replace</span><span class="p">:</span><span class="w"> </span><span class="ss">:update</span><span class="w"> </span><span class="k" data-group-id="4013514497-13">do</span><span class="w">
    </span><span class="n">field</span><span class="p" data-group-id="4013514497-14">(</span><span class="ss">:ad</span><span class="p">,</span><span class="w"> </span><span class="ss">:string</span><span class="p" data-group-id="4013514497-14">)</span><span class="w">
  </span><span class="k" data-group-id="4013514497-13">end</span><span class="w">
</span><span class="k" data-group-id="4013514497-1">end</span></code></pre>
<p>
Since this is a known structure, Ecto can introspect on the JSON values and
cast and dump them to the appropriate Elixir data types, which is immensely
helpful. Here I am achieving that by using <code class="inline">embeds_one</code> and specifying the
struct. Once pulled from the database, Ecto will decode them.</p>
<p>
Other times, you may not be able to do this ahead of time, so the schema
might look like this (the <code class="inline">api_response</code> field):</p>
<pre><code class="makeup elixir"><span class="n">schema</span><span class="w"> </span><span class="s">&quot;vehicle_photos&quot;</span><span class="w"> </span><span class="k" data-group-id="3107382297-1">do</span><span class="w">
  </span><span class="n">field</span><span class="p" data-group-id="3107382297-2">(</span><span class="ss">:file</span><span class="p">,</span><span class="w"> </span><span class="nc">PhotoUploader.Type</span><span class="p" data-group-id="3107382297-2">)</span><span class="w">
  </span><span class="n">field</span><span class="p" data-group-id="3107382297-3">(</span><span class="ss">:api_response</span><span class="p">,</span><span class="w"> </span><span class="ss">:map</span><span class="p" data-group-id="3107382297-3">)</span><span class="w">
</span><span class="k" data-group-id="3107382297-1">end</span></code></pre>
<h2>
Query the JSON</h2>
<p>
Continuing with the struct example schema, we found out that some of our
URLs weren’t being populated like we expected, so I had to find those
photos and fix them. How do I query for them since they’re stored in
PostgreSQL as JSON? We need to drop down into raw SQL:</p>
<pre><code class="makeup elixir"><span class="kd">def</span><span class="w"> </span><span class="nf">where_photo_urls_have_a_null</span><span class="p" data-group-id="1651942043-1">(</span><span class="n">query</span><span class="p" data-group-id="1651942043-1">)</span><span class="w"> </span><span class="k" data-group-id="1651942043-2">do</span><span class="w">
  </span><span class="n">query</span><span class="w">
  </span><span class="o">|&gt;</span><span class="w"> </span><span class="n">where</span><span class="p" data-group-id="1651942043-3">(</span><span class="p" data-group-id="1651942043-4">[</span><span class="c">_q</span><span class="p" data-group-id="1651942043-4">]</span><span class="p">,</span><span class="w"> </span><span class="n">fragment</span><span class="p" data-group-id="1651942043-5">(</span><span class="w">
    </span><span class="s">&quot;&quot;&quot;
    (facebook_urls IS NULL) OR
    (facebook_urls-&gt;&gt;&#39;ad_version&#39; IS NULL) OR
    (facebook_urls-&gt;&gt;&#39;hero_version&#39; IS NULL) OR
    (craigslist_urls-&gt;&gt;&#39;ad&#39; IS NULL)
    &quot;&quot;&quot;</span><span class="w">
  </span><span class="p" data-group-id="1651942043-5">)</span><span class="p" data-group-id="1651942043-3">)</span><span class="w">
</span><span class="k" data-group-id="1651942043-2">end</span></code></pre>
<p>
The SQL operator <code class="inline">-&gt;&gt;</code> will leverage <a href="https://www.postgresql.org/docs/current/functions-json.html" title="">PostgreSQL’s JSON
functions</a> to retrieve the text or integers that are stored
in the JSON. You can access them using this syntax: <code class="inline">column-&gt;&gt;key</code>. In
my case, I needed to find if the column was null, or it wasn’t null,
then to ask if the JSON object has any keys that are null.  This will
work regardless of whether you use an embedded struct or a map, because
PostgreSQL sees it as the same thing: JSON.</p>
<p>
Here’s an example that checks for substrings:</p>
<pre><code class="makeup elixir"><span class="kd">def</span><span class="w"> </span><span class="nf">where_photo_url_wrong</span><span class="p" data-group-id="2764417918-1">(</span><span class="n">query</span><span class="p" data-group-id="2764417918-1">)</span><span class="w"> </span><span class="k" data-group-id="2764417918-2">do</span><span class="w">
  </span><span class="n">query</span><span class="w">
  </span><span class="o">|&gt;</span><span class="w"> </span><span class="n">where</span><span class="p" data-group-id="2764417918-3">(</span><span class="p" data-group-id="2764417918-4">[</span><span class="c">_q</span><span class="p" data-group-id="2764417918-4">]</span><span class="p">,</span><span class="w"> </span><span class="n">fragment</span><span class="p" data-group-id="2764417918-5">(</span><span class="w">
    </span><span class="s">&quot;&quot;&quot;
    (facebook_urls-&gt;&gt;&#39;hero_ad&#39; NOT ILIKE ?) OR
    (facebook_urls-&gt;&gt;&#39;carousel_ad&#39; NOT ILIKE ?) OR
    (craigslist_urls-&gt;&gt;&#39;ad&#39; NOT ILIKE ?)
    &quot;&quot;&quot;</span><span class="p">,</span><span class="w">
    </span><span class="s">&quot;%facebook_hero_ad%&quot;</span><span class="p">,</span><span class="w">
    </span><span class="s">&quot;%facebook_carousel_ad%&quot;</span><span class="p">,</span><span class="w">
    </span><span class="s">&quot;%craigslist_ad%&quot;</span><span class="w">
  </span><span class="p" data-group-id="2764417918-5">)</span><span class="p" data-group-id="2764417918-3">)</span><span class="w">
</span><span class="k" data-group-id="2764417918-2">end</span></code></pre>
<h2>
Make the Query Composable</h2>
<p>
Above is all I needed for my use case, but I wondered how I could continue
querying those fields in a reusable way. For example, how do I chain these
together in an <code class="inline">OR</code> statement that uses both of these fragments?</p>
<p>
To do that, I’ll need to extract the <code class="inline">fragment</code> expressions and put them
into a macro so they can be used within Ecto’s functions.</p>
<pre><code class="makeup elixir"><span class="kd">defmodule</span><span class="w"> </span><span class="nc">MyProject.SampleQuery.Fragments</span><span class="w"> </span><span class="k" data-group-id="8901852106-1">do</span><span class="w">
  </span><span class="kn">import</span><span class="w"> </span><span class="nc">Ecto.Query.API</span><span class="p">,</span><span class="w"> </span><span class="ss">only</span><span class="p">:</span><span class="w"> </span><span class="p" data-group-id="8901852106-2">[</span><span class="ss">fragment</span><span class="p">:</span><span class="w"> </span><span class="mi">1</span><span class="p" data-group-id="8901852106-2">]</span><span class="w">

  </span><span class="kd">defmacro</span><span class="w"> </span><span class="nf">photo_urls_have_a_null</span><span class="w"> </span><span class="k" data-group-id="8901852106-3">do</span><span class="w">
    </span><span class="k">quote</span><span class="w"> </span><span class="k" data-group-id="8901852106-4">do</span><span class="w">
      </span><span class="n">fragment</span><span class="p" data-group-id="8901852106-5">(</span><span class="w">
        </span><span class="s">&quot;&quot;&quot;
        (facebook_urls IS NULL) OR
        (facebook_urls-&gt;&gt;&#39;ad_version&#39; IS NULL) OR
        (facebook_urls-&gt;&gt;&#39;hero_version&#39; IS NULL) OR
        (craigslist_urls-&gt;&gt;&#39;ad&#39; IS NULL)
        &quot;&quot;&quot;</span><span class="w">
      </span><span class="p" data-group-id="8901852106-5">)</span><span class="w">
    </span><span class="k" data-group-id="8901852106-4">end</span><span class="w">
  </span><span class="k" data-group-id="8901852106-3">end</span><span class="w">

  </span><span class="kd">defmacro</span><span class="w"> </span><span class="nf">photo_urls_not_contain</span><span class="p" data-group-id="8901852106-6">(</span><span class="p" data-group-id="8901852106-7">[</span><span class="n">hero_ad_value</span><span class="p">,</span><span class="w"> </span><span class="n">carousel_ad_value</span><span class="p">,</span><span class="w"> </span><span class="n">ad_value</span><span class="p" data-group-id="8901852106-7">]</span><span class="p" data-group-id="8901852106-6">)</span><span class="w"> </span><span class="k" data-group-id="8901852106-8">do</span><span class="w">
    </span><span class="k">quote</span><span class="w"> </span><span class="k" data-group-id="8901852106-9">do</span><span class="w">
      </span><span class="n">fragment</span><span class="p" data-group-id="8901852106-10">(</span><span class="w">
        </span><span class="s">&quot;&quot;&quot;
        (facebook_urls-&gt;&gt;&#39;hero_ad&#39; NOT ILIKE ?) OR
        (facebook_urls-&gt;&gt;&#39;carousel_ad&#39; NOT ILIKE ?) OR
        (craigslist_urls-&gt;&gt;&#39;ad&#39; NOT ILIKE ?)
        &quot;&quot;&quot;</span><span class="p">,</span><span class="w">
        </span><span class="o">^</span><span class="s">&quot;%</span><span class="si" data-group-id="8901852106-11">#{</span><span class="k">unquote</span><span class="p" data-group-id="8901852106-12">(</span><span class="n">hero_ad_value</span><span class="p" data-group-id="8901852106-12">)</span><span class="si" data-group-id="8901852106-11">}</span><span class="s">%&quot;</span><span class="p">,</span><span class="w">
        </span><span class="o">^</span><span class="s">&quot;%</span><span class="si" data-group-id="8901852106-13">#{</span><span class="k">unquote</span><span class="p" data-group-id="8901852106-14">(</span><span class="n">carousel_ad_value</span><span class="p" data-group-id="8901852106-14">)</span><span class="si" data-group-id="8901852106-13">}</span><span class="s">%&quot;</span><span class="p">,</span><span class="w">
        </span><span class="o">^</span><span class="s">&quot;%</span><span class="si" data-group-id="8901852106-15">#{</span><span class="k">unquote</span><span class="p" data-group-id="8901852106-16">(</span><span class="n">ad_value</span><span class="p" data-group-id="8901852106-16">)</span><span class="si" data-group-id="8901852106-15">}</span><span class="s">%&quot;</span><span class="w">
      </span><span class="p" data-group-id="8901852106-10">)</span><span class="w">
    </span><span class="k" data-group-id="8901852106-9">end</span><span class="w">
  </span><span class="k" data-group-id="8901852106-8">end</span><span class="w">
</span><span class="k" data-group-id="8901852106-1">end</span></code></pre>
<p>
Now that those fragments are extracted, let’s use them:</p>
<pre><code class="makeup elixir"><span class="kn">import</span><span class="w"> </span><span class="nc">MyProject.SampleQuery.Fragments</span><span class="w">
</span><span class="kn">alias</span><span class="w"> </span><span class="nc">MyProject.Photo</span><span class="w">

</span><span class="kd">defmodule</span><span class="w"> </span><span class="nc">MyProject.SampleQuery</span><span class="w"> </span><span class="k" data-group-id="7891253815-1">do</span><span class="w">
  </span><span class="kd">def</span><span class="w"> </span><span class="nf">find_bad_photos</span><span class="p" data-group-id="7891253815-2">(</span><span class="n">query</span><span class="w"> </span><span class="o">\\</span><span class="w"> </span><span class="nc">Photo</span><span class="p" data-group-id="7891253815-2">)</span><span class="w"> </span><span class="k" data-group-id="7891253815-3">do</span><span class="w">
    </span><span class="n">query</span><span class="w">
    </span><span class="o">|&gt;</span><span class="w"> </span><span class="n">where</span><span class="p" data-group-id="7891253815-4">(</span><span class="p" data-group-id="7891253815-5">[</span><span class="c">_p</span><span class="p" data-group-id="7891253815-5">]</span><span class="p">,</span><span class="w"> </span><span class="n">photo_urls_have_a_null</span><span class="p" data-group-id="7891253815-6">(</span><span class="p" data-group-id="7891253815-6">)</span><span class="p" data-group-id="7891253815-4">)</span><span class="w">
    </span><span class="o">|&gt;</span><span class="w"> </span><span class="n">or_where</span><span class="p" data-group-id="7891253815-7">(</span><span class="p" data-group-id="7891253815-8">[</span><span class="c">_p</span><span class="p" data-group-id="7891253815-8">]</span><span class="p">,</span><span class="w"> </span><span class="n">photo_urls_not_contain</span><span class="p" data-group-id="7891253815-9">(</span><span class="p" data-group-id="7891253815-10">[</span><span class="w">
      </span><span class="s">&quot;facebook_hero_ad&quot;</span><span class="p">,</span><span class="w">
      </span><span class="s">&quot;facebook_carousel_ad&quot;</span><span class="p">,</span><span class="w">
      </span><span class="s">&quot;craigslist_ad&quot;</span><span class="w">
    </span><span class="p" data-group-id="7891253815-10">]</span><span class="p" data-group-id="7891253815-9">)</span><span class="p" data-group-id="7891253815-7">)</span><span class="w">
    </span><span class="o">|&gt;</span><span class="w"> </span><span class="nc">Repo</span><span class="o">.</span><span class="n">all</span><span class="w">
  </span><span class="k" data-group-id="7891253815-3">end</span><span class="w">
</span><span class="k" data-group-id="7891253815-1">end</span></code></pre>
<p>
<strong>Beautiful.</strong></p>
<p>
If you’d like to check out the code a little more, you can see this
<a href="https://github.com/dbernheisel/sample_json_ecto_queries" title="">sample Ecto and Phoenix repo with tests</a>.</p>
<p>
This article only explains how to query a JSON object in the database
and how it works with Ecto querying. If you’re needing to store an array
of maps or structs, then check out Jon’s post <a href="https://thoughtbot.com/blog/why-ecto-s-way-of-storing-embedded-lists-of-maps-makes-querying-hard" title="">Why Ecto’s Way of
Storing Embedded Lists of Maps Makes Querying Hard</a>.</p>
]]></content:encoded>
</item>
<item>
<title><![CDATA[Testing Random Data in Emails with Bamboo]]></title>
<dc:creator>David Bernheisel</dc:creator>
<description><![CDATA[Testing a scenario where an app sends an email is easy, but how do you
test something random in an email, like a password reset token? When we
test a function that intentionally returns random data, it's a little
tougher.
]]></description>
<link>https://bernheisel.com/blog/testing-emails-with-bamboo</link>
<guid isPermaLink="true">https://bernheisel.com/blog/testing-emails-with-bamboo</guid>
<pubDate>Mon, 16 Oct 2017 00:00:00 -0400</pubDate>
<content:encoded><![CDATA[<p>
Testing a scenario where an app sends an email is easy, but how do you
test something random in an email, like a password reset token? When we
test a function that intentionally returns random data, it’s a little
tougher.</p>
<p>
In those times, we often tackle the problem by:</p>
<ol>
  <li>
Testing behavior and static data, ignoring the dynamic data.  </li>
  <li>
Using a mock to rid the randomized data, and then test everything.  </li>
</ol>
<p>
Let’s walk through how to do this with
<a href="https://github.com/thoughtbot/bamboo">Bamboo</a>.</p>
<p>
Bamboo provides test helpers to help you assert behavior and data in
your app. A really common email scenario is sending users password reset
links. The idea behind these reset links is that they’re <em>secure</em> and
<em>unique</em>, and we ensure this by generating a random token and signing it
with user’s data to make it secure. How do we test this then?</p>
<p>
There are two ways!</p>
<h2>
Use regex to cover the static text and skip dynamic text.</h2>
<p>
Here we are testing the behavior and static data, ignoring the dynamic
data.  Bamboo provides <code class="inline">assert_email_delivered_with()</code> which accepts a
keyword list of parts of the email, and what those parts should match.
We can match the email entirely by supplying a string like <code class="inline">[subject: &quot;Password reset link for MyApp&quot;]</code>, or we can supply a regex, <code class="inline">[text: ~r/reset_token=/)</code>, and the assertion will check if the regex matches.</p>
<p>
Here’s a fuller integration test example:</p>
<pre><code class="makeup elixir"><span class="n">test</span><span class="w"> </span><span class="s">&quot;customers can request a password reset link&quot;</span><span class="p">,</span><span class="w"> </span><span class="p" data-group-id="4666490007-1">%{</span><span class="ss">session</span><span class="p">:</span><span class="w"> </span><span class="n">session</span><span class="p" data-group-id="4666490007-1">}</span><span class="w"> </span><span class="k" data-group-id="4666490007-2">do</span><span class="w">
  </span><span class="n">customer</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">insert</span><span class="p" data-group-id="4666490007-3">(</span><span class="ss">:customer</span><span class="p" data-group-id="4666490007-3">)</span><span class="w">
  </span><span class="n">session</span><span class="w"> </span><span class="o">=</span><span class="w">
    </span><span class="n">session</span><span class="w">
    </span><span class="o">|&gt;</span><span class="w"> </span><span class="n">visit</span><span class="p" data-group-id="4666490007-4">(</span><span class="n">password_reset_path</span><span class="p" data-group-id="4666490007-5">(</span><span class="nc">MyApp.Endpoint</span><span class="p">,</span><span class="w"> </span><span class="ss">:new</span><span class="p" data-group-id="4666490007-5">)</span><span class="p" data-group-id="4666490007-4">)</span><span class="w">
    </span><span class="o">|&gt;</span><span class="w"> </span><span class="n">fill_in</span><span class="p" data-group-id="4666490007-6">(</span><span class="ss">:password_reset</span><span class="p">,</span><span class="w"> </span><span class="ss">:email</span><span class="p">,</span><span class="w"> </span><span class="ss">with</span><span class="p">:</span><span class="w"> </span><span class="n">customer</span><span class="o">.</span><span class="n">email</span><span class="p" data-group-id="4666490007-6">)</span><span class="w">
    </span><span class="o">|&gt;</span><span class="w"> </span><span class="n">click_on</span><span class="p" data-group-id="4666490007-7">(</span><span class="s">&quot;Send link&quot;</span><span class="p" data-group-id="4666490007-7">)</span><span class="w">

  </span><span class="n">assert_email_delivered_with</span><span class="p" data-group-id="4666490007-8">(</span><span class="ss">subject</span><span class="p">:</span><span class="w"> </span><span class="s">&quot;Password reset link for MyApp&quot;</span><span class="p" data-group-id="4666490007-8">)</span><span class="w">
  </span><span class="n">assert_email_delivered_with</span><span class="p" data-group-id="4666490007-9">(</span><span class="ss">text_body</span><span class="p">:</span><span class="w"> </span><span class="sr">~r/reset_token=/</span><span class="p" data-group-id="4666490007-9">)</span><span class="w">
  </span><span class="n">assert_email_delivered_with</span><span class="p" data-group-id="4666490007-10">(</span><span class="ss">html_body</span><span class="p">:</span><span class="w"> </span><span class="sr">~r/reset_token=/</span><span class="p" data-group-id="4666490007-10">)</span><span class="w">
</span><span class="k" data-group-id="4666490007-2">end</span></code></pre>
<h2>
Use a mock to rid the random data, and test the whole thing!</h2>
<p>
Here you can guarantee the behavior and (mocked) data, but it’s a little
more setup.</p>
<p>
Here’s an example:</p>
<pre><code class="makeup elixir"><span class="c1"># lib/mock_token_generator.ex</span><span class="w">
</span><span class="kd">defmodule</span><span class="w"> </span><span class="nc">MyApp.MockTokenGenerator</span><span class="w"> </span><span class="k" data-group-id="9433384371-1">do</span><span class="w">
  </span><span class="na">@token</span><span class="w"> </span><span class="s">&quot;123&quot;</span><span class="w">

  </span><span class="c1"># This should match the interface of the real TokenGenerator</span><span class="w">
  </span><span class="kd">def</span><span class="w"> </span><span class="nf">generate</span><span class="p">,</span><span class="w"> </span><span class="ss">do</span><span class="p">:</span><span class="w"> </span><span class="na">@token</span><span class="w">

  </span><span class="c1"># We&#39;re going to expose this in the mock so we can get the assertion</span><span class="w">
  </span><span class="c1"># right</span><span class="w">
  </span><span class="kd">def</span><span class="w"> </span><span class="nf">token</span><span class="p">,</span><span class="w"> </span><span class="ss">do</span><span class="p">:</span><span class="w"> </span><span class="na">@token</span><span class="w">
</span><span class="k" data-group-id="9433384371-1">end</span><span class="w">


</span><span class="c1"># config/config.exs</span><span class="w">
</span><span class="n">config</span><span class="w"> </span><span class="ss">:my_app</span><span class="p">,</span><span class="w"> </span><span class="ss">token_generator</span><span class="p">:</span><span class="w"> </span><span class="nc">MyApp.TokenGenerator</span><span class="w">


</span><span class="c1"># config/test.exs</span><span class="w">
</span><span class="n">config</span><span class="w"> </span><span class="ss">:my_app</span><span class="p">,</span><span class="w"> </span><span class="ss">token_generator</span><span class="p">:</span><span class="w"> </span><span class="nc">MyApp.MockTokenGenerator</span><span class="w">


</span><span class="c1"># web/controllers/password_reset_controller.ex</span><span class="w">
</span><span class="kd">defmodule</span><span class="w"> </span><span class="nc">MyApp.PasswordResetController</span><span class="w"> </span><span class="k" data-group-id="9433384371-2">do</span><span class="w">
  </span><span class="na">@generator</span><span class="w"> </span><span class="nc">Application</span><span class="o">.</span><span class="n">get_env</span><span class="p" data-group-id="9433384371-3">(</span><span class="ss">:my_app</span><span class="p">,</span><span class="w"> </span><span class="ss">:token_generator</span><span class="p" data-group-id="9433384371-3">)</span><span class="w">

  </span><span class="kd">def</span><span class="w"> </span><span class="nf">create</span><span class="p" data-group-id="9433384371-4">(</span><span class="n">conn</span><span class="p">,</span><span class="w"> </span><span class="n">params</span><span class="p" data-group-id="9433384371-4">)</span><span class="w"> </span><span class="k" data-group-id="9433384371-5">do</span><span class="w">
    </span><span class="c1">#...</span><span class="w">
    </span><span class="c1"># use the @generator.generate function</span><span class="w">
    </span><span class="c1"># do your email thing</span><span class="w">
    </span><span class="c1">#...</span><span class="w">
  </span><span class="k" data-group-id="9433384371-5">end</span><span class="w">
</span><span class="k" data-group-id="9433384371-2">end</span><span class="w">


</span><span class="c1"># test/features/password_reset_test.exs</span><span class="w">
</span><span class="c1"># ...</span><span class="w">
</span><span class="kn">alias</span><span class="w"> </span><span class="nc">MyApp.MockTokenGenerator</span><span class="w">

</span><span class="n">test</span><span class="w"> </span><span class="s">&quot;customers can request a password reset link&quot;</span><span class="p">,</span><span class="w"> </span><span class="p" data-group-id="9433384371-6">%{</span><span class="ss">session</span><span class="p">:</span><span class="w"> </span><span class="n">session</span><span class="p" data-group-id="9433384371-6">}</span><span class="w"> </span><span class="k" data-group-id="9433384371-7">do</span><span class="w">
  </span><span class="n">customer</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">insert</span><span class="p" data-group-id="9433384371-8">(</span><span class="ss">:customer</span><span class="p" data-group-id="9433384371-8">)</span><span class="w">
  </span><span class="n">session</span><span class="w"> </span><span class="o">=</span><span class="w">
    </span><span class="n">session</span><span class="w">
    </span><span class="o">|&gt;</span><span class="w"> </span><span class="n">visit</span><span class="p" data-group-id="9433384371-9">(</span><span class="n">password_reset_path</span><span class="p" data-group-id="9433384371-10">(</span><span class="nc">MyApp.Endpoint</span><span class="p">,</span><span class="w"> </span><span class="ss">:new</span><span class="p" data-group-id="9433384371-10">)</span><span class="p" data-group-id="9433384371-9">)</span><span class="w">
    </span><span class="o">|&gt;</span><span class="w"> </span><span class="n">fill_in</span><span class="p" data-group-id="9433384371-11">(</span><span class="ss">:password_reset</span><span class="p">,</span><span class="w"> </span><span class="ss">:email</span><span class="p">,</span><span class="w"> </span><span class="ss">with</span><span class="p">:</span><span class="w"> </span><span class="n">customer</span><span class="o">.</span><span class="n">email</span><span class="p" data-group-id="9433384371-11">)</span><span class="w">
    </span><span class="o">|&gt;</span><span class="w"> </span><span class="n">click_on</span><span class="p" data-group-id="9433384371-12">(</span><span class="s">&quot;Send link&quot;</span><span class="p" data-group-id="9433384371-12">)</span><span class="w">

  </span><span class="n">assert_email_delivered_with</span><span class="p" data-group-id="9433384371-13">(</span><span class="ss">subject</span><span class="p">:</span><span class="w"> </span><span class="s">&quot;Password reset link for MyApp&quot;</span><span class="p" data-group-id="9433384371-13">)</span><span class="w">
  </span><span class="n">assert_email_delivered_with</span><span class="p" data-group-id="9433384371-14">(</span><span class="ss">text_body</span><span class="p">:</span><span class="w"> </span><span class="s">&quot;&quot;&quot;
    Here&#39;s the entire body of the text email. You might test the entire
    text version of the email, and use regex to test the HTML version

    Here&#39;s your password reset link: https://myapp.com/password?reset_token=</span><span class="si" data-group-id="9433384371-15">#{</span><span class="nc">MockTokenGenerator</span><span class="o">.</span><span class="n">token</span><span class="si" data-group-id="9433384371-15">}</span><span class="s">
  &quot;&quot;&quot;</span><span class="p" data-group-id="9433384371-14">)</span><span class="w">
  </span><span class="n">assert_email_delivered_with</span><span class="p" data-group-id="9433384371-16">(</span><span class="ss">html_body</span><span class="p">:</span><span class="w"> </span><span class="sr">~r|https://myapp.com/password?reset_token=</span><span class="si" data-group-id="9433384371-17">#{</span><span class="nc">MockTokenGenerator</span><span class="o">.</span><span class="n">token</span><span class="si" data-group-id="9433384371-17">}</span><span class="sr">|</span><span class="p" data-group-id="9433384371-16">)</span><span class="w">
</span><span class="k" data-group-id="9433384371-7">end</span></code></pre>
<p>
See? <a href="https://github.com/thoughtbot/bamboo">Bamboo</a> makes it easy. Give
it a shot and let us know what you think.</p>
]]></content:encoded>
</item>
<item>
<title><![CDATA[Lessons From Using Phoenix 1.3]]></title>
<dc:creator>David Bernheisel</dc:creator>
<description><![CDATA[Phoenix 1.3 introduces contexts, which has been met with some resistance. I've
developed an application using it and learned some lessons.
]]></description>
<link>https://bernheisel.com/blog/lessons-from-using-phoenix-1-3</link>
<guid isPermaLink="true">https://bernheisel.com/blog/lessons-from-using-phoenix-1-3</guid>
<pubDate>Tue, 01 Aug 2017 00:00:00 -0400</pubDate>
<content:encoded><![CDATA[<p>
Phoenix 1.3 introduces contexts, which has been met with some resistance. I’ve
developed an application using it and learned some lessons.</p>
<p>
<strong>[WARNING]</strong> I like contexts.</p>
<p>
  <img src="/images/homer-backing-up.gif" alt="me, now, ashamed and hiding because I am probably going against the grain that
other smarter-than-me people likely established already">
</p>
<p>
Phew… I just wanted to admit that up front. Now that I got that out of the
way, I am going to share my journey about using Phoenix 1.3.0 and contexts.</p>
<hr class="thin">
<ul>
  <li>
    <p>
<a href="#experience">My context</a>    </p>
  </li>
  <li>
    <p>
Lessons:    </p>
    <ul>
      <li>
<a href="#dontgenerators">Don’t use the generators</a>      </li>
      <li>
<a href="#dothedomain">Embrace the domain vocabulary</a>      </li>
      <li>
<a href="#bloated">Avoid the bloat</a>      </li>
      <li>
<a href="#maybeumbrella">Consider before umbrellas</a>      </li>
    </ul>
  </li>
  <li>
    <p>
<a href="#giveitago">You should give it a shot</a>    </p>
  </li>
</ul>
<a name="experience"></a><h2>
Experience</h2>
<p>
I worked on a greenfield project and had an opportunity to use Phoenix
1.3.0-rc2. With Phoenix 1.3.0 just released, I thought it might be timely to
inform other developers what it’s like to work with contexts, and some
recommendations I have after working on a project using contexts for several
months.</p>
<p>
If you don’t know what a context is:</p>
<ul>
  <li>
    <p>
<a href="https://youtu.be/tMO28ar0lW8?t=12m21s">watch Chris McCord talk about
it</a>    </p>
  </li>
  <li>
    <p>
or <a href="https://martinfowler.com/bliki/BoundedContext.html">read Martin Fowler’s explanation of Bounded
Context</a>    </p>
  </li>
  <li>
    <p>
or <a href="https://www.phoenixframework.org/blog/phoenix-1-3-0-released">read the Phoenix 1.3 release
post</a>    </p>
  </li>
  <li>
    <p>
or read my tldr version:    </p>
    <blockquote>
      <p>
A context is a module that defines the interface between a set of
inter-related models/schemas to the rest of the application (like other
contexts). A context is an internal API that provides opportunity to name
things better and organize code.      </p>
    </blockquote>
  </li>
</ul>
<p>
A practical example: instead of your controller talking to the database, your
controller will talk to the context, and the context will interface with
necessary functions and schemas and modules to accomplish the task.</p>
<p>
<strong>NOTE:</strong> I did not use 1.3.0-rc3 which changes the <code class="inline">Web</code> namespace, so I will
skip that part. I think that’s a good change, but I have no real experience with
those tweaks yet.</p>
<h2>
Lessons</h2>
<a name="dontgenerators"></a><h3>
Don’t use the generators more than once</h3>
<p>
With Phoenix 1.3, I only recommend using the <code class="inline">phx</code> generators ONCE in a
greenfield project. After that, ditch them. Ditch them because once you’ve
adjusted the code to your liking (and you’ll definitely need to edit the
generated code), using the generators again may <em><strong>inject</strong></em> generated code into
your existing files, which likely don’t follow your patterns anymore.</p>
<p>
Since I’m recommending against something, let’s jump into examples and find out
why.</p>
<p>
Here’s what I had to do with the generated files:</p>
<ul>
  <li>
    <p>
<strong>Rewrite the tests because they setup a fixture for the schema.</strong>    </p>
    <p>
This isn’t a bad idea in itself, but I wanted to use
<a href="https://github.com/thoughtbot/ex_machina">ex_machina</a> for setting up test
scenarios. At first, I thought the generated fixture was a great idea.  I’m
providing an interface for creating widgets, so I should use it in my tests,
right?    </p>
    <p>
Here’s the problem:    </p>
    <p>
Imagine if someone introduced a bug in <code class="inline">create_widget()</code>– now all your tests
that involve inserting a <code class="inline">widget</code> breaks. That’s unreasonable, because I’m not
testing <em>getting to that state</em> most of the time, I’m testing the unit of
functionality or integration between functions. Instead, I want the tests for
<code class="inline">create_widget()</code> to fail (and any reliant integration tests), as opposed to
the <strong>WHOLE TEST SUITE</strong> breaking and thus freaking me out. When the whole
test suite breaks, it’s harder to discern where the problem is.    </p>
  </li>
  <li>
    <p>
<strong>Separate the schema-specific tests into their own test file.</strong>    </p>
    <p>
The new context organization only generates a test file for the <em>context</em>, and
not a schema. As I kept building the application, it became evident that the
context file and context test file were getting too large. I felt compelled to
isolate and organize this big bag-o’-functions into smaller bags-o’-functions.
I decided to start splitting the tests into different schema-related files,
like <code class="inline">{context}/{schema}_test.exs</code>. Since I split the test files, it became
clearer where I should place tests for custom changeset functions as well.    </p>
    <p>
I also want to be more careful about how I use <code class="inline">describe</code> and <code class="inline">test</code> blocks,
since ExUnit doesn’t support nested <code class="inline">describe</code> or context blocks.  The
generated test names were also a bit long for my taste, so I moved the
function name to the <code class="inline">describe</code> block, and then used the test title to
describe the context and the expected result.    </p>
    <p>
Lastly, the generated style was … different.    </p>
    <ul>
      <li>
I don’t like aliasing modules in the middle of the file. I feel
they belong at the top of the file.      </li>
      <li>
I keep module attributes near the top of the file.      </li>
      <li>
I avoid the function parenthesis unless I need them.      </li>
    </ul>
    <p>
Here is an example of how I changed things:    </p>
    <pre><code class="makeup elixir"><span class="c1"># BEFORE</span><span class="w">
</span><span class="c1"># test/my_app/things_test.exs</span><span class="w">
</span><span class="kd">defmodule</span><span class="w"> </span><span class="nc">MyApp.ThingsTest</span><span class="w"> </span><span class="k" data-group-id="5485271880-1">do</span><span class="w">
  </span><span class="kn">use</span><span class="w"> </span><span class="nc">MyApp.DataCase</span><span class="w">
  </span><span class="kn">alias</span><span class="w"> </span><span class="nc">MyApp.Things</span><span class="w">

  </span><span class="n">describe</span><span class="w"> </span><span class="s">&quot;widgets&quot;</span><span class="w"> </span><span class="k" data-group-id="5485271880-2">do</span><span class="w">
    </span><span class="kn">alias</span><span class="w"> </span><span class="nc">MyApp.Things.Widget</span><span class="w">

    </span><span class="na">@valid_attrs</span><span class="w"> </span><span class="p" data-group-id="5485271880-3">%{</span><span class="p" data-group-id="5485271880-3">}</span><span class="w">
    </span><span class="na">@update_attrs</span><span class="w"> </span><span class="p" data-group-id="5485271880-4">%{</span><span class="p" data-group-id="5485271880-4">}</span><span class="w">
    </span><span class="na">@invalid_attrs</span><span class="w"> </span><span class="p" data-group-id="5485271880-5">%{</span><span class="p" data-group-id="5485271880-5">}</span><span class="w">

    </span><span class="kd">def</span><span class="w"> </span><span class="nf">widget_fixture</span><span class="p" data-group-id="5485271880-6">(</span><span class="n">attrs</span><span class="w"> </span><span class="o">\\</span><span class="w"> </span><span class="p" data-group-id="5485271880-7">%{</span><span class="p" data-group-id="5485271880-7">}</span><span class="p" data-group-id="5485271880-6">)</span><span class="w"> </span><span class="k" data-group-id="5485271880-8">do</span><span class="w">
      </span><span class="p" data-group-id="5485271880-9">{</span><span class="ss">:ok</span><span class="p">,</span><span class="w"> </span><span class="n">widget</span><span class="p" data-group-id="5485271880-9">}</span><span class="w"> </span><span class="o">=</span><span class="w">
        </span><span class="n">attrs</span><span class="w">
        </span><span class="o">|&gt;</span><span class="w"> </span><span class="nc">Enum</span><span class="o">.</span><span class="n">into</span><span class="p" data-group-id="5485271880-10">(</span><span class="na">@valid_attrs</span><span class="p" data-group-id="5485271880-10">)</span><span class="w">
        </span><span class="o">|&gt;</span><span class="w"> </span><span class="nc">Things</span><span class="o">.</span><span class="n">create_widget</span><span class="p" data-group-id="5485271880-11">(</span><span class="p" data-group-id="5485271880-11">)</span><span class="w">

      </span><span class="n">widget</span><span class="w">
    </span><span class="k" data-group-id="5485271880-8">end</span><span class="w">

    </span><span class="n">test</span><span class="w"> </span><span class="s">&quot;list_widgets/0 returns all widgets&quot;</span><span class="w"> </span><span class="k" data-group-id="5485271880-12">do</span><span class="w">
      </span><span class="n">widget_one</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">widget_fixture</span><span class="p" data-group-id="5485271880-13">(</span><span class="p" data-group-id="5485271880-13">)</span><span class="w">
      </span><span class="n">widget_two</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">widget_fixture</span><span class="p" data-group-id="5485271880-14">(</span><span class="p" data-group-id="5485271880-14">)</span><span class="w">

      </span><span class="n">assert</span><span class="w"> </span><span class="nc">Things</span><span class="o">.</span><span class="n">list_widgets</span><span class="p" data-group-id="5485271880-15">(</span><span class="p" data-group-id="5485271880-15">)</span><span class="w"> </span><span class="o">==</span><span class="w"> </span><span class="p" data-group-id="5485271880-16">[</span><span class="n">widget_one</span><span class="p">,</span><span class="w"> </span><span class="n">widget_two</span><span class="p" data-group-id="5485271880-16">]</span><span class="w">
    </span><span class="k" data-group-id="5485271880-12">end</span><span class="w">

    </span><span class="c1"># I added this, just to go along with their style and to show</span><span class="w">
    </span><span class="c1"># what a typical new developer would do with this existing pattern</span><span class="w">
    </span><span class="n">test</span><span class="w"> </span><span class="s">&quot;list_widgets/1 returns all widgets limited by list of id&quot;</span><span class="w"> </span><span class="k" data-group-id="5485271880-17">do</span><span class="w">
      </span><span class="n">widget_one</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">widget_fixture</span><span class="p" data-group-id="5485271880-18">(</span><span class="p" data-group-id="5485271880-18">)</span><span class="w">
      </span><span class="c">_widget_two</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">widget_fixture</span><span class="p" data-group-id="5485271880-19">(</span><span class="p" data-group-id="5485271880-19">)</span><span class="w">

      </span><span class="n">assert</span><span class="w"> </span><span class="nc">Things</span><span class="o">.</span><span class="n">list_widgets</span><span class="p" data-group-id="5485271880-20">(</span><span class="p" data-group-id="5485271880-21">[</span><span class="n">widget_one</span><span class="o">.</span><span class="n">id</span><span class="p" data-group-id="5485271880-21">]</span><span class="p" data-group-id="5485271880-20">)</span><span class="w"> </span><span class="o">==</span><span class="w"> </span><span class="p" data-group-id="5485271880-22">[</span><span class="n">widget_one</span><span class="p" data-group-id="5485271880-22">]</span><span class="w">
    </span><span class="k" data-group-id="5485271880-17">end</span><span class="w">

    </span><span class="c1">#...</span><span class="w">
  </span><span class="k" data-group-id="5485271880-2">end</span><span class="w">
</span><span class="k" data-group-id="5485271880-1">end</span><span class="w">

</span><span class="c1"># AFTER</span><span class="w">
</span><span class="c1"># test/my_app/things/widget_test.exs</span><span class="w">
</span><span class="kd">defmodule</span><span class="w"> </span><span class="nc">MyApp.Things.WidgetTest</span><span class="w"> </span><span class="k" data-group-id="5485271880-23">do</span><span class="w">
  </span><span class="kn">use</span><span class="w"> </span><span class="nc">MyApp.DataCase</span><span class="w">
  </span><span class="kn">import</span><span class="w"> </span><span class="nc">MyApp.Factory</span><span class="w">
  </span><span class="kn">alias</span><span class="w"> </span><span class="nc">MyApp.Things</span><span class="w">
  </span><span class="kn">alias</span><span class="w"> </span><span class="nc">MyApp.Things.Widget</span><span class="w">

  </span><span class="n">describe</span><span class="w"> </span><span class="s">&quot;list_widgets&quot;</span><span class="w"> </span><span class="k" data-group-id="5485271880-24">do</span><span class="w">
    </span><span class="n">test</span><span class="w"> </span><span class="s">&quot;returns all widgets&quot;</span><span class="w"> </span><span class="k" data-group-id="5485271880-25">do</span><span class="w">
      </span><span class="p" data-group-id="5485271880-26">[</span><span class="n">widget_one</span><span class="p">,</span><span class="w"> </span><span class="n">widget_two</span><span class="p" data-group-id="5485271880-26">]</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">insert_pair</span><span class="p" data-group-id="5485271880-27">(</span><span class="ss">:widget</span><span class="p" data-group-id="5485271880-27">)</span><span class="w">

      </span><span class="n">assert</span><span class="w"> </span><span class="nc">Things</span><span class="o">.</span><span class="n">list_widgets</span><span class="w"> </span><span class="o">==</span><span class="w"> </span><span class="p" data-group-id="5485271880-28">[</span><span class="n">widget_one</span><span class="p">,</span><span class="w"> </span><span class="n">widget_two</span><span class="p" data-group-id="5485271880-28">]</span><span class="w">
    </span><span class="k" data-group-id="5485271880-25">end</span><span class="w">

    </span><span class="n">test</span><span class="w"> </span><span class="s">&quot;when given list of ids, returns all widgets in ids&quot;</span><span class="w"> </span><span class="k" data-group-id="5485271880-29">do</span><span class="w">
      </span><span class="p" data-group-id="5485271880-30">[</span><span class="n">widget_one</span><span class="p">,</span><span class="w"> </span><span class="c">_widget_two</span><span class="p" data-group-id="5485271880-30">]</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">insert_pair</span><span class="p" data-group-id="5485271880-31">(</span><span class="ss">:widget</span><span class="p" data-group-id="5485271880-31">)</span><span class="w">

      </span><span class="n">assert</span><span class="w"> </span><span class="nc">Things</span><span class="o">.</span><span class="n">list_widgets</span><span class="p" data-group-id="5485271880-32">(</span><span class="p" data-group-id="5485271880-33">[</span><span class="n">widget_one</span><span class="o">.</span><span class="n">id</span><span class="p" data-group-id="5485271880-33">]</span><span class="p" data-group-id="5485271880-32">)</span><span class="w"> </span><span class="o">==</span><span class="w"> </span><span class="p" data-group-id="5485271880-34">[</span><span class="n">widget_one</span><span class="p" data-group-id="5485271880-34">]</span><span class="w">
    </span><span class="k" data-group-id="5485271880-29">end</span><span class="w">
  </span><span class="k" data-group-id="5485271880-24">end</span><span class="w">

  </span><span class="c1">#...</span><span class="w">
</span><span class="k" data-group-id="5485271880-23">end</span></code></pre>
    <p>
I like this so much better, and I’m afraid that just going with the generated
pattern will lead newer developers down a path of bloated files.    </p>
  </li>
</ul>
<p>
It’s more apparent to me in Phoenix 1.3.0 that the generators are much more a
teaching tool for new developers than meant to be used in an ongoing fashion
throughout a project’s lifetime. If you’ve formed your opinion, or your
organization has a coding style within Phoenix, then you might appreciate
knowing you can customize the templates that the generators will use. You can do
this by copying them out of <code class="inline">deps/phoenix/priv/templates</code> and into your
project’s <code class="inline">priv/templates</code> folder. That’s pretty awesome.</p>
<p>
Recap: for new developers, Phoenix’s new generators are a great learning tool,
but I don’t recommend using them after the first use.</p>
<a name="dothedomain"></a><h3>
Embrace the domain vocabulary</h3>
<p>
I realized that my understanding of contexts at the time was flawed, and that
many of the examples out in the blog-o-sphere were not helpful for me when I was
in the trenches myself.</p>
<p>
I imagine that most (all?) projects have their own domain AND vocabulary, and to
be readable for folks in that domain it is helpful to share that vocabulary.</p>
<p>
This coming example may not apply to you, but this is the beauty of contexts:
your needs WILL differ and your domain vocabulary will help determine how to
organize your code.</p>
<p>
The project I was working on had different terms for their warehouse workers:</p>
<ul>
  <li>
Operators  </li>
  <li>
Supervisors  </li>
  <li>
Admins  </li>
  <li>
SalesAssociate  </li>
</ul>
<p>
This roughly corresponds with a typical <code class="inline">User</code> schema that <code class="inline">belongs_to</code> a <code class="inline">Role</code>
schema. I placed both schemas into a new context called <code class="inline">Accounts</code>, and all
user-related functions are in that context file. I hesitated with the domain
vocabulary thinking that generic terms were going to be more flexible later.</p>
<p>
As the project evolved, that decision turned out to be a mistake</p>
<p>
Instead of something like this (using a generic term <code class="inline">Accounts</code> as the context):</p>
<pre><code class="makeup elixir"><span class="kd">defmodule</span><span class="w"> </span><span class="nc">MyApp.Accounts</span><span class="w"> </span><span class="k" data-group-id="9073795983-1">do</span><span class="w">
  </span><span class="kn">alias</span><span class="w"> </span><span class="nc">MyApp.Accounts.User</span><span class="w">

  </span><span class="na">@operator_role_id</span><span class="w"> </span><span class="mi">2</span><span class="w">
  </span><span class="na">@supervisor_role_id</span><span class="w"> </span><span class="mi">1</span><span class="w">

  </span><span class="kd">def</span><span class="w"> </span><span class="nf">list_operators</span><span class="p" data-group-id="9073795983-2">(</span><span class="n">queryable</span><span class="w"> </span><span class="o">\\</span><span class="w"> </span><span class="nc">User</span><span class="p" data-group-id="9073795983-2">)</span><span class="w"> </span><span class="k" data-group-id="9073795983-3">do</span><span class="w">
    </span><span class="n">queryable</span><span class="w"> </span><span class="o">|&gt;</span><span class="w"> </span><span class="n">where</span><span class="p" data-group-id="9073795983-4">(</span><span class="p" data-group-id="9073795983-5">[</span><span class="n">u</span><span class="p" data-group-id="9073795983-5">]</span><span class="p">,</span><span class="w"> </span><span class="n">u</span><span class="o">.</span><span class="n">role_id</span><span class="w"> </span><span class="o">==</span><span class="w"> </span><span class="o">^</span><span class="na">@operator_role</span><span class="p" data-group-id="9073795983-4">)</span><span class="w">
  </span><span class="k" data-group-id="9073795983-3">end</span><span class="w">

  </span><span class="kd">def</span><span class="w"> </span><span class="nf">list_supervisors</span><span class="p" data-group-id="9073795983-6">(</span><span class="n">queryable</span><span class="w"> </span><span class="o">\\</span><span class="w"> </span><span class="nc">User</span><span class="p" data-group-id="9073795983-6">)</span><span class="w"> </span><span class="k" data-group-id="9073795983-7">do</span><span class="w">
    </span><span class="n">queryable</span><span class="w"> </span><span class="o">|&gt;</span><span class="w"> </span><span class="n">where</span><span class="p" data-group-id="9073795983-8">(</span><span class="p" data-group-id="9073795983-9">[</span><span class="n">u</span><span class="p" data-group-id="9073795983-9">]</span><span class="p">,</span><span class="w"> </span><span class="n">u</span><span class="o">.</span><span class="n">role_id</span><span class="w"> </span><span class="o">==</span><span class="w"> </span><span class="o">^</span><span class="na">@supervisor_role</span><span class="p" data-group-id="9073795983-8">)</span><span class="w">
  </span><span class="k" data-group-id="9073795983-7">end</span><span class="w">

  </span><span class="c1"># ...</span><span class="w">
</span><span class="k" data-group-id="9073795983-1">end</span></code></pre>
<p>
I could have done this (using domain vocabulary as context boundaries):</p>
<pre><code class="makeup elixir"><span class="kd">defmodule</span><span class="w"> </span><span class="nc">MyApp.Operators</span><span class="w"> </span><span class="k" data-group-id="3254363135-1">do</span><span class="w">
  </span><span class="kn">alias</span><span class="w"> </span><span class="nc">MyApp.Accounts.User</span><span class="w">

  </span><span class="na">@role_id</span><span class="w"> </span><span class="mi">2</span><span class="w">

  </span><span class="kd">def</span><span class="w"> </span><span class="nf">list</span><span class="p" data-group-id="3254363135-2">(</span><span class="n">queryable</span><span class="w"> </span><span class="o">\\</span><span class="w"> </span><span class="nc">User</span><span class="p" data-group-id="3254363135-2">)</span><span class="w"> </span><span class="k" data-group-id="3254363135-3">do</span><span class="w">
    </span><span class="n">queryable</span><span class="w"> </span><span class="o">|&gt;</span><span class="w"> </span><span class="n">where</span><span class="p" data-group-id="3254363135-4">(</span><span class="p" data-group-id="3254363135-5">[</span><span class="n">u</span><span class="p" data-group-id="3254363135-5">]</span><span class="p">,</span><span class="w"> </span><span class="n">u</span><span class="o">.</span><span class="n">role_id</span><span class="w"> </span><span class="o">==</span><span class="w"> </span><span class="o">^</span><span class="na">@role_id</span><span class="p" data-group-id="3254363135-4">)</span><span class="w">
  </span><span class="k" data-group-id="3254363135-3">end</span><span class="w">

  </span><span class="c1"># ...</span><span class="w">
</span><span class="k" data-group-id="3254363135-1">end</span><span class="w">

</span><span class="c1"># notice the namespace difference</span><span class="w">
</span><span class="kd">defmodule</span><span class="w"> </span><span class="nc">MyApp.Supervisors</span><span class="w"> </span><span class="k" data-group-id="3254363135-6">do</span><span class="w">
  </span><span class="kn">alias</span><span class="w"> </span><span class="nc">MyApp.Accounts.User</span><span class="w">

  </span><span class="na">@role_id</span><span class="w"> </span><span class="mi">1</span><span class="w">

  </span><span class="kd">def</span><span class="w"> </span><span class="nf">list</span><span class="p" data-group-id="3254363135-7">(</span><span class="n">queryable</span><span class="w"> </span><span class="o">\\</span><span class="w"> </span><span class="nc">User</span><span class="p" data-group-id="3254363135-7">)</span><span class="w"> </span><span class="k" data-group-id="3254363135-8">do</span><span class="w">
    </span><span class="n">queryable</span><span class="w"> </span><span class="o">|&gt;</span><span class="w"> </span><span class="n">where</span><span class="p" data-group-id="3254363135-9">(</span><span class="p" data-group-id="3254363135-10">[</span><span class="n">u</span><span class="p" data-group-id="3254363135-10">]</span><span class="p">,</span><span class="w"> </span><span class="n">u</span><span class="o">.</span><span class="n">role_id</span><span class="w"> </span><span class="o">==</span><span class="w"> </span><span class="o">^</span><span class="na">@role_id</span><span class="p" data-group-id="3254363135-9">)</span><span class="w">
  </span><span class="k" data-group-id="3254363135-8">end</span><span class="w">

  </span><span class="c1"># ...</span><span class="w">
</span><span class="k" data-group-id="3254363135-6">end</span></code></pre>
<p>
This example is really simple, but it starts to show its strength later when you
have other conditionals and need to ask your data more questions.</p>
<p>
With the example above, I’d actually argue that having one combined context is
preferable because it’s all we need–but, knowing how the application <strong>will</strong>
grow, and how a lot of questions are asked against the user’s role, then it’ll
be more apparent having the separated context <strong>will</strong> be helpful.</p>
<pre><code class="makeup elixir"><span class="kd">defmodule</span><span class="w"> </span><span class="nc">MyApp.Operators</span><span class="w"> </span><span class="k" data-group-id="1949209711-1">do</span><span class="w">
  </span><span class="kn">alias</span><span class="w"> </span><span class="nc">MyApp.Activities.Event</span><span class="w">

  </span><span class="kd">def</span><span class="w"> </span><span class="nf">update_event</span><span class="p" data-group-id="1949209711-2">(</span><span class="n">event</span><span class="p">,</span><span class="w"> </span><span class="n">params</span><span class="p" data-group-id="1949209711-2">)</span><span class="w"> </span><span class="k" data-group-id="1949209711-3">do</span><span class="w">
    </span><span class="n">event</span><span class="w">
    </span><span class="o">|&gt;</span><span class="w"> </span><span class="n">prepare_event</span><span class="p" data-group-id="1949209711-4">(</span><span class="n">params</span><span class="p" data-group-id="1949209711-4">)</span><span class="w">
    </span><span class="o">|&gt;</span><span class="w"> </span><span class="nc">Repo</span><span class="o">.</span><span class="n">update</span><span class="w">
  </span><span class="k" data-group-id="1949209711-3">end</span><span class="w">

  </span><span class="kd">def</span><span class="w"> </span><span class="nf">prepare_event</span><span class="p" data-group-id="1949209711-5">(</span><span class="n">event</span><span class="p">,</span><span class="w"> </span><span class="n">params</span><span class="p" data-group-id="1949209711-5">)</span><span class="w"> </span><span class="k" data-group-id="1949209711-6">do</span><span class="w">
    </span><span class="n">event</span><span class="w"> </span><span class="o">|&gt;</span><span class="w"> </span><span class="nc">Event</span><span class="o">.</span><span class="n">operator_changeset</span><span class="p" data-group-id="1949209711-7">(</span><span class="n">params</span><span class="p" data-group-id="1949209711-7">)</span><span class="w">
  </span><span class="k" data-group-id="1949209711-6">end</span><span class="w">

  </span><span class="c1"># ...</span><span class="w">
</span><span class="k" data-group-id="1949209711-1">end</span><span class="w">

</span><span class="kd">defmodule</span><span class="w"> </span><span class="nc">MyApp.Supervisors</span><span class="w"> </span><span class="k" data-group-id="1949209711-8">do</span><span class="w">
  </span><span class="kn">alias</span><span class="w"> </span><span class="nc">MyApp.Activities.Event</span><span class="w">

  </span><span class="kd">def</span><span class="w"> </span><span class="nf">update_event</span><span class="p" data-group-id="1949209711-9">(</span><span class="n">event</span><span class="p">,</span><span class="w"> </span><span class="n">params</span><span class="p" data-group-id="1949209711-9">)</span><span class="w"> </span><span class="k" data-group-id="1949209711-10">do</span><span class="w">
    </span><span class="n">event</span><span class="w">
    </span><span class="o">|&gt;</span><span class="w"> </span><span class="n">prepare_event</span><span class="p" data-group-id="1949209711-11">(</span><span class="n">params</span><span class="p" data-group-id="1949209711-11">)</span><span class="w">
    </span><span class="o">|&gt;</span><span class="w"> </span><span class="nc">Repo</span><span class="o">.</span><span class="n">update</span><span class="w">
  </span><span class="k" data-group-id="1949209711-10">end</span><span class="w">

  </span><span class="c1"># Notice that we&#39;re calling a different changeset</span><span class="w">
  </span><span class="kd">def</span><span class="w"> </span><span class="nf">prepare_event</span><span class="p" data-group-id="1949209711-12">(</span><span class="n">event</span><span class="p">,</span><span class="w"> </span><span class="n">params</span><span class="p" data-group-id="1949209711-12">)</span><span class="w"> </span><span class="k" data-group-id="1949209711-13">do</span><span class="w">
    </span><span class="n">event</span><span class="w"> </span><span class="o">|&gt;</span><span class="w"> </span><span class="nc">Event</span><span class="o">.</span><span class="n">supervisor_changeset</span><span class="p" data-group-id="1949209711-14">(</span><span class="n">params</span><span class="p" data-group-id="1949209711-14">)</span><span class="w">
  </span><span class="k" data-group-id="1949209711-13">end</span><span class="w">

  </span><span class="c1"># ...</span><span class="w">
</span><span class="k" data-group-id="1949209711-8">end</span></code></pre>
<p>
Above we’re adding another function that has different permissions regarding
what the user may update on an event. The <code class="inline">supervisor_changeset</code> will cast all
params, whereas the <code class="inline">operator_changeset</code> will cast only a subset of params. This
would also be reflected for preparing any forms in templates.</p>
<p>
All the above <em>requires</em> you to understand the vocabulary before building, which
is the critique I usually hear about contexts: “It requires more up-front
cognitive thought before I can be productive.” Prior to 1.3, not knowing domain
up front might not hurt so much because it’s not built into the structure, but
with 1.3 and presumably later, it may hurt more. Despite that, it’s <em>totally</em>
worth it.</p>
<a name="bloated"></a><h3>
Avoid the bloat</h3>
<p>
Above, I glossed-over what contexts help us achieve: making interfaces between
your abstractions. A context (aka domain interface) will help organize actions.
I <em>love</em> this.</p>
<p>
I decided in this experiment to <em>really</em> give contexts a go and roll with the
philosophy. At the same time, I <em>hated</em> the bloated context that it had become
after needing to interact with several schemas in the same context. At some
point, I had several hundred lines in a context file; it was easy to let the
context file grow. <strong>RESIST</strong>. Use domain-related vocabulary to keep contexts
small. I had to determine how to organize the code better.</p>
<p>
A technique that helped keep contexts small was to limit them a set of action
verbs, like <code class="inline">list</code> <code class="inline">prepare</code> <code class="inline">create</code> <code class="inline">update</code> and <code class="inline">delete</code> (some semblance to
CRUD actions). Outside of those verbs, I put them into supporting modules. For
example, <code class="inline">def list</code> actually hits the database and returns the list of things–
it did not return an Ecto query that I could further modify. I saved those
query-building functions for a <code class="inline">Context.Query</code> module. This helped keep my
<code class="inline">list</code> function simple, and helped me make composable queries.</p>
<p>
My controllers and other services then <em>only</em> call functions in context modules.</p>
<p>
For example:</p>
<pre><code class="makeup elixir"><span class="kd">defmodule</span><span class="w"> </span><span class="nc">MyApp.Operators</span><span class="w"> </span><span class="k" data-group-id="8599782827-1">do</span><span class="w">
  </span><span class="c1"># MyApp.Accounts is now a namespace for schemas, not a context</span><span class="w">
  </span><span class="kn">alias</span><span class="w"> </span><span class="nc">MyApp.Accounts.User</span><span class="w">
  </span><span class="kn">alias</span><span class="w"> </span><span class="nc">MyApp.Accounts.Role</span><span class="w">
  </span><span class="kn">alias</span><span class="w"> </span><span class="nc">MyApp.Operators.Query</span><span class="w">

  </span><span class="kd">def</span><span class="w"> </span><span class="nf">list</span><span class="p" data-group-id="8599782827-2">(</span><span class="n">queryable</span><span class="w"> </span><span class="o">\\</span><span class="w"> </span><span class="nc">User</span><span class="p" data-group-id="8599782827-2">)</span><span class="w"> </span><span class="k" data-group-id="8599782827-3">do</span><span class="w">
    </span><span class="n">queryable</span><span class="w">
    </span><span class="o">|&gt;</span><span class="w"> </span><span class="nc">Query</span><span class="o">.</span><span class="n">where_operator</span><span class="w">
    </span><span class="o">|&gt;</span><span class="w"> </span><span class="nc">Repo</span><span class="o">.</span><span class="n">all</span><span class="w">
  </span><span class="k" data-group-id="8599782827-3">end</span><span class="w">

  </span><span class="kd">def</span><span class="w"> </span><span class="nf">list_by_latest_event</span><span class="p" data-group-id="8599782827-4">(</span><span class="n">queryable</span><span class="w"> </span><span class="o">\\</span><span class="w"> </span><span class="nc">User</span><span class="p" data-group-id="8599782827-4">)</span><span class="w"> </span><span class="k" data-group-id="8599782827-5">do</span><span class="w">
    </span><span class="n">queryable</span><span class="w">
    </span><span class="o">|&gt;</span><span class="w"> </span><span class="nc">Query</span><span class="o">.</span><span class="n">order_by_event_date</span><span class="w">
    </span><span class="o">|&gt;</span><span class="w"> </span><span class="n">list</span><span class="w">
  </span><span class="k" data-group-id="8599782827-5">end</span><span class="w">

  </span><span class="kd">def</span><span class="w"> </span><span class="nf">list_currently_assigned</span><span class="p" data-group-id="8599782827-6">(</span><span class="n">queryable</span><span class="w"> </span><span class="o">\\</span><span class="w"> </span><span class="nc">User</span><span class="p" data-group-id="8599782827-6">)</span><span class="w"> </span><span class="k" data-group-id="8599782827-7">do</span><span class="w">
    </span><span class="n">queryable</span><span class="w">
    </span><span class="o">|&gt;</span><span class="w"> </span><span class="nc">Query</span><span class="o">.</span><span class="n">where_assigned</span><span class="w">
    </span><span class="o">|&gt;</span><span class="w"> </span><span class="n">list</span><span class="w">
  </span><span class="k" data-group-id="8599782827-7">end</span><span class="w">

  </span><span class="kd">def</span><span class="w"> </span><span class="nf">list_currently_assigned_for_activity</span><span class="p" data-group-id="8599782827-8">(</span><span class="n">queryable</span><span class="w"> </span><span class="o">\\</span><span class="w"> </span><span class="nc">User</span><span class="p">,</span><span class="w"> </span><span class="n">activity</span><span class="p" data-group-id="8599782827-8">)</span><span class="w"> </span><span class="k" data-group-id="8599782827-9">do</span><span class="w">
    </span><span class="n">queryable</span><span class="w">
    </span><span class="o">|&gt;</span><span class="w"> </span><span class="nc">Query</span><span class="o">.</span><span class="n">where_activity</span><span class="p" data-group-id="8599782827-10">(</span><span class="n">activity</span><span class="o">.</span><span class="n">id</span><span class="p" data-group-id="8599782827-10">)</span><span class="w">
    </span><span class="o">|&gt;</span><span class="w"> </span><span class="n">list_currently_assigned</span><span class="w">
  </span><span class="k" data-group-id="8599782827-9">end</span><span class="w">

  </span><span class="c1"># ...</span><span class="w">
</span><span class="k" data-group-id="8599782827-1">end</span><span class="w">

</span><span class="kd">defmodule</span><span class="w"> </span><span class="nc">MyApp.Operators.Query</span><span class="w"> </span><span class="k" data-group-id="8599782827-11">do</span><span class="w">
  </span><span class="kn">import</span><span class="w"> </span><span class="nc">Ecto.Query</span><span class="w">
  </span><span class="na">@role_id</span><span class="w"> </span><span class="mi">1</span><span class="w">

  </span><span class="kd">def</span><span class="w"> </span><span class="nf">where_operator</span><span class="p" data-group-id="8599782827-12">(</span><span class="n">queryable</span><span class="p" data-group-id="8599782827-12">)</span><span class="w"> </span><span class="k" data-group-id="8599782827-13">do</span><span class="w">
    </span><span class="n">queryable</span><span class="w">
    </span><span class="o">|&gt;</span><span class="w"> </span><span class="n">where</span><span class="p" data-group-id="8599782827-14">(</span><span class="p" data-group-id="8599782827-15">[</span><span class="n">q</span><span class="p" data-group-id="8599782827-15">]</span><span class="p">,</span><span class="w"> </span><span class="n">q</span><span class="o">.</span><span class="n">role_id</span><span class="w"> </span><span class="o">==</span><span class="w"> </span><span class="o">^</span><span class="na">@role_id</span><span class="p" data-group-id="8599782827-14">)</span><span class="w">
  </span><span class="k" data-group-id="8599782827-13">end</span><span class="w">

  </span><span class="kd">def</span><span class="w"> </span><span class="nf">order_by_event_date</span><span class="p" data-group-id="8599782827-16">(</span><span class="n">queryable</span><span class="p" data-group-id="8599782827-16">)</span><span class="w"> </span><span class="k" data-group-id="8599782827-17">do</span><span class="w">
    </span><span class="n">queryable</span><span class="w">
    </span><span class="o">|&gt;</span><span class="w"> </span><span class="n">join</span><span class="p" data-group-id="8599782827-18">(</span><span class="ss">:left</span><span class="p">,</span><span class="w"> </span><span class="p" data-group-id="8599782827-19">[</span><span class="n">q</span><span class="p" data-group-id="8599782827-19">]</span><span class="p">,</span><span class="w"> </span><span class="n">event</span><span class="w"> </span><span class="ow">in</span><span class="w"> </span><span class="n">assoc</span><span class="p" data-group-id="8599782827-20">(</span><span class="n">q</span><span class="p">,</span><span class="w"> </span><span class="ss">:event</span><span class="p" data-group-id="8599782827-20">)</span><span class="p" data-group-id="8599782827-18">)</span><span class="w">
    </span><span class="o">|&gt;</span><span class="w"> </span><span class="n">order_by</span><span class="p" data-group-id="8599782827-21">(</span><span class="p" data-group-id="8599782827-22">[</span><span class="bp">_</span><span class="p">,</span><span class="w"> </span><span class="n">event</span><span class="p" data-group-id="8599782827-22">]</span><span class="p">,</span><span class="w"> </span><span class="p" data-group-id="8599782827-23">[</span><span class="ss">desc</span><span class="p">:</span><span class="w"> </span><span class="n">event</span><span class="o">.</span><span class="n">inserted_at</span><span class="p" data-group-id="8599782827-23">]</span><span class="p" data-group-id="8599782827-21">)</span><span class="w">
  </span><span class="k" data-group-id="8599782827-17">end</span><span class="w">

  </span><span class="kd">def</span><span class="w"> </span><span class="nf">where_assigned</span><span class="p" data-group-id="8599782827-24">(</span><span class="n">queryable</span><span class="p" data-group-id="8599782827-24">)</span><span class="w"> </span><span class="k" data-group-id="8599782827-25">do</span><span class="w">
    </span><span class="n">queryable</span><span class="w">
    </span><span class="o">|&gt;</span><span class="w"> </span><span class="n">where</span><span class="p" data-group-id="8599782827-26">(</span><span class="p" data-group-id="8599782827-27">[</span><span class="n">q</span><span class="p" data-group-id="8599782827-27">]</span><span class="p">,</span><span class="w"> </span><span class="n">is_nil</span><span class="p" data-group-id="8599782827-28">(</span><span class="n">q</span><span class="o">.</span><span class="n">unassigned_at</span><span class="p" data-group-id="8599782827-28">)</span><span class="p" data-group-id="8599782827-26">)</span><span class="w">
  </span><span class="k" data-group-id="8599782827-25">end</span><span class="w">

  </span><span class="kd">def</span><span class="w"> </span><span class="nf">where_activity</span><span class="p" data-group-id="8599782827-29">(</span><span class="n">queryable</span><span class="p">,</span><span class="w"> </span><span class="n">activity</span><span class="p" data-group-id="8599782827-29">)</span><span class="w"> </span><span class="k" data-group-id="8599782827-30">do</span><span class="w">
    </span><span class="n">queryable</span><span class="w">
    </span><span class="o">|&gt;</span><span class="w"> </span><span class="n">join</span><span class="p" data-group-id="8599782827-31">(</span><span class="ss">:inner</span><span class="p">,</span><span class="w"> </span><span class="p" data-group-id="8599782827-32">[</span><span class="n">q</span><span class="p" data-group-id="8599782827-32">]</span><span class="p">,</span><span class="w"> </span><span class="n">assignment</span><span class="w"> </span><span class="ow">in</span><span class="w"> </span><span class="n">assoc</span><span class="p" data-group-id="8599782827-33">(</span><span class="n">q</span><span class="p">,</span><span class="w"> </span><span class="ss">:assignments</span><span class="p" data-group-id="8599782827-33">)</span><span class="p" data-group-id="8599782827-31">)</span><span class="w">
    </span><span class="o">|&gt;</span><span class="w"> </span><span class="n">where</span><span class="p" data-group-id="8599782827-34">(</span><span class="p" data-group-id="8599782827-35">[</span><span class="bp">_</span><span class="p">,</span><span class="w"> </span><span class="n">assignment</span><span class="p" data-group-id="8599782827-35">]</span><span class="p">,</span><span class="w"> </span><span class="n">assignment</span><span class="o">.</span><span class="n">activity_id</span><span class="w"> </span><span class="o">==</span><span class="w"> </span><span class="o">^</span><span class="n">activity_id</span><span class="p" data-group-id="8599782827-34">)</span><span class="w">
  </span><span class="k" data-group-id="8599782827-30">end</span><span class="w">
</span><span class="k" data-group-id="8599782827-11">end</span></code></pre>
<p>
This has been my preferred way of organizing code so far. It encourages less
god-modules that I’ve learned to dislike so much. I believe that Phoenix will
need to be more careful about their generators accidentally encouraging
god-modules, lest they start to look like monolith Rails applications with
models that know too much about the application, except now in a context.</p>
<a name="maybeumbrella"></a><h3>
Consider Before Umbrellas</h3>
<p>
Elixir allows applications to live in umbrellas, which is an awesome concept. If
you’re not familiar with umbrellas, <a href="https://elixir-lang.org/getting-started/mix-otp/dependencies-and-umbrella-apps.html">read up about
it</a>.
What I love about umbrellas is that it allows me to draw boundaries between
related applications. This is difficult to do in other frameworks and languages,
so the fact that <code class="inline">mix</code> gives this tool for free is <em>incredible</em>. Before I heard
about Phoenix contexts, I was drawn to organize my application via umbrellas
because I didn’t see other tools that made it easy.</p>
<p>
Umbrellas, in a sense, help accomplish the same thing as contexts: it helps you
draw boundaries. The difference is that umbrella applications are about
separating applications instead of APIs.</p>
<p>
A lot of typical web applications don’t need separated sub-applications. If
you’re considering one, determine if having separately-deployable applications
fixes or avoids problems, or if the boundaries need to be large enough to
deserve a separation. Avoid jumping to umbrellas like I did earlier if you only
need to organize yourself.</p>
<p>
<a href="https://youtu.be/tMO28ar0lW8?t=27m54s">Chris gives some good examples of when umbrellas could be a good
option</a></p>
<a name="giveitago"></a><h2>
Give It a Shot</h2>
<p>
At thoughtbot, we pride ourselves in the practices of designing experiences, and
<em>then</em> developing; <a href="https://thoughtbot.com/playbook">that’s what makes thoughtbot
different</a>. That process also helps establish
where these boundaries are up front, and it’s up to the artist to determine
whether it’s a new context, just a new schema, maybe a new application
altogether, maybe a support module, or none of the above.  Regardless, I believe
Phoenix 1.3 teaches <em>great</em> ideas that will win in the long run and make
developers think before doing.</p>
]]></content:encoded>
</item>
<item>
<title><![CDATA[Simple Phoenix Text Inputs with Formulator]]></title>
<dc:creator>David Bernheisel</dc:creator>
<description><![CDATA[Ugh... three lines for a simple text input for a form in Phoenix? How about
one with Formulator?
]]></description>
<link>https://bernheisel.com/blog/simple-phoenix-text-inputs-with-formulator</link>
<guid isPermaLink="true">https://bernheisel.com/blog/simple-phoenix-text-inputs-with-formulator</guid>
<pubDate>Fri, 30 Jun 2017 00:00:00 -0400</pubDate>
<content:encoded><![CDATA[<p>
Ugh… three lines for a simple text input for a form in Phoenix? How about one
with Formulator?</p>
<p>
tldr:</p>
<p>
instead of</p>
<pre><code class="makeup elixir"><span class="o">&lt;</span><span class="p">%</span><span class="o">=</span><span class="w"> </span><span class="n">label</span><span class="w"> </span><span class="n">form</span><span class="p">,</span><span class="w"> </span><span class="ss">:email_address</span><span class="w"> </span><span class="p">%</span><span class="o">&gt;</span><span class="w">
</span><span class="o">&lt;</span><span class="p">%</span><span class="o">=</span><span class="w"> </span><span class="n">email_input</span><span class="w"> </span><span class="n">form</span><span class="p">,</span><span class="w"> </span><span class="ss">:email_address</span><span class="w"> </span><span class="p">%</span><span class="o">&gt;</span><span class="w">
</span><span class="o">&lt;</span><span class="p">%</span><span class="o">=</span><span class="w"> </span><span class="n">error_tag</span><span class="w"> </span><span class="n">form</span><span class="p">,</span><span class="w"> </span><span class="ss">:email_address</span><span class="w"> </span><span class="p">%</span><span class="o">&gt;</span><span class="w">

</span><span class="o">&lt;</span><span class="p">%</span><span class="o">=</span><span class="w"> </span><span class="n">label</span><span class="w"> </span><span class="n">form</span><span class="p">,</span><span class="w"> </span><span class="ss">:first_name</span><span class="w"> </span><span class="p">%</span><span class="o">&gt;</span><span class="w">
</span><span class="o">&lt;</span><span class="p">%</span><span class="o">=</span><span class="w"> </span><span class="n">text_input</span><span class="w"> </span><span class="n">form</span><span class="p">,</span><span class="w"> </span><span class="ss">:first_name</span><span class="w"> </span><span class="p">%</span><span class="o">&gt;</span><span class="w">
</span><span class="o">&lt;</span><span class="p">%</span><span class="o">=</span><span class="w"> </span><span class="n">error_tag</span><span class="w"> </span><span class="n">form</span><span class="p">,</span><span class="w"> </span><span class="ss">:first_name</span><span class="w"> </span><span class="p">%</span><span class="o">&gt;</span><span class="w">

</span><span class="o">&lt;</span><span class="p">%</span><span class="o">=</span><span class="w"> </span><span class="n">label</span><span class="w"> </span><span class="n">form</span><span class="p">,</span><span class="w"> </span><span class="ss">:last_name</span><span class="w"> </span><span class="p">%</span><span class="o">&gt;</span><span class="w">
</span><span class="o">&lt;</span><span class="p">%</span><span class="o">=</span><span class="w"> </span><span class="n">text_input</span><span class="w"> </span><span class="n">form</span><span class="p">,</span><span class="w"> </span><span class="ss">:last_name</span><span class="w"> </span><span class="p">%</span><span class="o">&gt;</span><span class="w">
</span><span class="o">&lt;</span><span class="p">%</span><span class="o">=</span><span class="w"> </span><span class="n">error_tag</span><span class="w"> </span><span class="n">form</span><span class="p">,</span><span class="w"> </span><span class="ss">:last_name</span><span class="w"> </span><span class="p">%</span><span class="o">&gt;</span></code></pre>
<p>
do this with Formulator:</p>
<pre><code class="makeup elixir"><span class="o">&lt;</span><span class="p">%</span><span class="o">=</span><span class="w"> </span><span class="n">input</span><span class="w"> </span><span class="n">form</span><span class="p">,</span><span class="w"> </span><span class="ss">:email_address</span><span class="p">,</span><span class="w"> </span><span class="ss">as</span><span class="p">:</span><span class="w"> </span><span class="ss">:email</span><span class="w"> </span><span class="p">%</span><span class="o">&gt;</span><span class="w">
</span><span class="o">&lt;</span><span class="p">%</span><span class="o">=</span><span class="w"> </span><span class="n">input</span><span class="w"> </span><span class="n">form</span><span class="p">,</span><span class="w"> </span><span class="ss">:first_name</span><span class="w"> </span><span class="p">%</span><span class="o">&gt;</span><span class="w">
</span><span class="o">&lt;</span><span class="p">%</span><span class="o">=</span><span class="w"> </span><span class="n">input</span><span class="w"> </span><span class="n">form</span><span class="p">,</span><span class="w"> </span><span class="ss">:last_name</span><span class="w"> </span><span class="p">%</span><span class="o">&gt;</span></code></pre>
<!--excerpt-->
<p>
<a href="http://blog.plataformatec.com.br/2016/09/dynamic-forms-with-phoenix/">Platformatec has a great post about dynamic forms with
Phoenix</a>
that teaches developers how to extract some common steps out to their own
functions.  This is helpful because developers can skip the tedious parts that
they tend to repeat, which also helps keep style consistent across a larger
framework for an application.</p>
<p>
Other times, developers don’t need (or want) to build CSS classes into the
back-end, or they want to give more flexibility to designers later, or they just
don’t want to start from scratch again when they start another application.
(It’s hard to find that balance sometimes, isn’t it?)</p>
<p>
Enter: <a href="https://hexdocs.pm/formulator/index.html">Formulator</a></p>
<p>
Formulator brings some simplicity to making form inputs for Phoenix, while still
giving the developer some customization options.</p>
<p>
For example, need a specific class for an input field?</p>
<pre><code class="makeup elixir"><span class="o">&lt;</span><span class="p">%</span><span class="o">=</span><span class="w"> </span><span class="n">input</span><span class="w"> </span><span class="n">form</span><span class="p">,</span><span class="w"> </span><span class="ss">:email_address</span><span class="p">,</span><span class="w"> </span><span class="ss">as</span><span class="p">:</span><span class="w"> </span><span class="ss">:email</span><span class="p">,</span><span class="w"> </span><span class="ss">class</span><span class="p">:</span><span class="w"> </span><span class="s">&quot;magical-email-input&quot;</span><span class="w"> </span><span class="p">%</span><span class="o">&gt;</span></code></pre>
<p>
or need to class up a label?</p>
<pre><code class="makeup elixir"><span class="o">&lt;</span><span class="p">%</span><span class="o">=</span><span class="w"> </span><span class="n">input</span><span class="w"> </span><span class="n">form</span><span class="p">,</span><span class="w"> </span><span class="ss">:email_address</span><span class="p">,</span><span class="w"> </span><span class="ss">as</span><span class="p">:</span><span class="w"> </span><span class="ss">:email</span><span class="p">,</span><span class="w"> </span><span class="ss">class</span><span class="p">:</span><span class="w"> </span><span class="s">&quot;magical-email-input&quot;</span><span class="p">,</span><span class="w"> </span><span class="ss">label</span><span class="p">:</span><span class="w"> </span><span class="p" data-group-id="4822719020-1">[</span><span class="ss">class</span><span class="p">:</span><span class="w">
</span><span class="s">&quot;magical-email-label&quot;</span><span class="w"> </span><span class="p">%</span><span class="o">&gt;</span></code></pre>
<p>
If you’re a Rails developer and new to Phoenix, you’ll soon discover that
Phoenix tries to get the form errors closer to the input tags themselves, as
opposed to Rails where error messages are typically flashed near the top of a
page. Getting used to this difference is like putting my toothbrush on the other
side of the sink, I tend to forget the <code class="inline">error_tag</code> when making a new form and
wonder for about 5 minutes why my test can’t find the error text I’m expecting.
Formulator saves me some keystrokes and keeps me from forgetting error tags.</p>
<p>
We made Formulator because we realized we were repeating ourselves waaaay too
much for simple stuff. It’s a ridiculously simple library (just check out the
<a href="https://github.com/thoughtbot/formulator">source code</a>). Give it and try and
let us know what you think!</p>
]]></content:encoded>
</item>
<item>
<title><![CDATA[Introduction to Elixir, Phoenix, and Umbrella Apps]]></title>
<dc:creator>David Bernheisel</dc:creator>
<description><![CDATA[Introduction of Elixir and Phoenix for a local coding boot camp. Slides]]></description>
<link>https://bernheisel.com/blog/intro-to-elixir-phoenix-and-umbrella-apps</link>
<guid isPermaLink="true">https://bernheisel.com/blog/intro-to-elixir-phoenix-and-umbrella-apps</guid>
<pubDate>Mon, 06 Feb 2017 00:00:00 -0500</pubDate>
<content:encoded><![CDATA[<p>
<a href="https://docs.google.com/presentation/d/1VQoM62tjpJy_SBwX-6q2GJmW9V09zxquLADxwZ6s0e4/edit?usp=sharing">Presentation</a></p>
<iframe src="https://docs.google.com/presentation/d/e/2PACX-1vQBgBVnoYhtAco7dG0roWlc6pnaECZ3M1pfYVwAC9VDlFj_PShrAEW0SCydVKGufM5EAWPM04Cxm6By/embed?start=false&loop=false&delayms=3000" frameborder="0" width="480" height="299" allowfullscreen="true" mozallowfullscreen="true" webkitallowfullscreen="true">
</iframe>
]]></content:encoded>
</item>
<item>
<title><![CDATA[Developer Life After A Bootcamp]]></title>
<dc:creator>David Bernheisel</dc:creator>
<description><![CDATA[What a boot camp developer can expect after graduating their course. Slides]]></description>
<link>https://bernheisel.com/blog/developer-life-after-bootcamp</link>
<guid isPermaLink="true">https://bernheisel.com/blog/developer-life-after-bootcamp</guid>
<pubDate>Mon, 28 Mar 2016 00:00:00 -0400</pubDate>
<content:encoded><![CDATA[<p>
<a href="https://docs.google.com/presentation/d/1UK91lesVdxyekFZOMWrd1_oxJj7tGURZcnmNuk3Qjd0/edit?usp=sharing">Presentation</a></p>
<iframe src="https://docs.google.com/presentation/d/e/2PACX-1vRooaUphFuVbw288iqfBNiH5nwsbAZlTDvYRU7jSINQkLiWcEvqoCnfp4EDMSQPtYoqDTohcpKQQRiF/embed?start=false&loop=false&delayms=3000" frameborder="0" width="480" height="299" allowfullscreen="true" mozallowfullscreen="true" webkitallowfullscreen="true">
</iframe>
]]></content:encoded>
</item>
<item>
<title><![CDATA[Refactor]]></title>
<dc:creator>David Bernheisel</dc:creator>
<description><![CDATA[Refactoring is scary. I've seen some comments on Twitter indicating that it's
generally something really risky, sending tremors through the rest of the
team. It's true, it's risky, but it's generally for the better. But I often
need to refactor something a bit more scary: life.
]]></description>
<link>https://bernheisel.com/blog/refactor</link>
<guid isPermaLink="true">https://bernheisel.com/blog/refactor</guid>
<pubDate>Thu, 24 Sep 2015 00:00:00 -0400</pubDate>
<content:encoded><![CDATA[<p>
Refactoring is scary. I’ve seen some comments on Twitter indicating that it’s generally something really risky, sending tremors through the rest of the team. It’s true, it’s risky, but it’s generally for the better. But I often need to refactor something a bit more scary: <strong>life</strong>.</p>
<p>
Here’s what I see:</p>
<blockquote class="twitter-tweet tw-align-center" lang="en">
  <p lang="en" dir="ltr">Don&#39;t you hate it when one of your bits of code is pivotal, but also a big scary mess? I need to edit it, but I&#39;m scared to refactor. *sob*</p>&mdash; Hayden Scott-Baron (@docky) <a href="https://twitter.com/docky/status/622462937849036800">July 18, 2015</a></blockquote>
<blockquote class="twitter-tweet tw-align-center" lang="en">
  <p lang="en" dir="ltr">&quot;I&#39;m scared. I just did a 40 file refactor and it worked.&quot; -me&#10;&quot;First time?&quot; -<a href="https://twitter.com/sangster">@sangster</a>&#10;&quot;First compile&quot; -me&#10;&quot;...thats not good&quot; - <a href="https://twitter.com/sangster">@sangster</a></p>&mdash; Will Long (@DerailedLogic) <a href="https://twitter.com/DerailedLogic/status/509788312953815040">September 10, 2014</a></blockquote>
<blockquote class="twitter-tweet tw-align-center" lang="en">
  <p lang="en" dir="ltr">One of the reasons I love TDD, Java, and IntelliJ: you are never scared to rename and refactor anything. Big refactorings done in seconds.</p>&mdash; Sandro Mancuso (@sandromancuso) <a href="https://twitter.com/sandromancuso/status/621435441271672832">July 15, 2015</a></blockquote>
<p>
Oy. Too bad there aren’t automated tests for life.</p>
<p>
  <img src="/images/continue.png" alt="Continue">
</p>
<h2>
Summary</h2>
<p>
<strong>TL;DR</strong></p>
<ol>
  <li>
Find what you enjoy.  </li>
  <li>
Trim what’s in the way.  </li>
  <li>
Leaps of faith  </li>
  <li>
Go camping to get feedback.  </li>
</ol>
<h2>
Why life is worth retrying</h2>
<p>
I’ve been chasing something for 10 years, but haven’t quite figured out what it is. I suppose I still don’t <em>really</em> know, but it’s a journey.</p>
<p>
Something clicked in college. College: that time when all your social structure upends from high school and you really “find yourself;” It’s a real Louis-and-Clark moment for determining what life should be for yourself. TLC would suggest that you need to <a href="https://www.youtube.com/watch?v=8WEtxJ4-sh4">stick to the lakes and the rivers that you’re used to</a>.</p>
<p>
I didn’t though; I wanted to be a better person. One of my friends once told me that I had zero tact. Guess I was a little too rough around the edges. So I went to Johnson University to chase a Church Leadership and Preaching degree. Didn’t care much for being a pastor, but I knew that they often possessed the skills for navigating the one thing I was apparently terrible at: <em>people</em>.</p>
<p>
Then graduation from college, but I had no real career path. Again, my social structure upended. I found a job at a book company working with eBooks. It’s pretty neat.</p>
<p>
Reaching back to my interests, computers and logical flow always seemed to pull me in. I exceled at work because it was techy and computer-y. There was a ton of vacuum that I was able to explore and fill the vacuum.</p>
<p>
But that’s not WHY yet…</p>
<p>
I found what I enjoyed. Once you find what you enjoy, life becomes worth retrying. It’s not something you chase, it’s an outcome of enjoying who you are, enjoying what you do, and being enjoyed. Life’s worth it. I wanted more of it.</p>
<p>
Sorry it’s not more philosophical, but on the other hand, do you really want it to be? I like simplicity.</p>
<p>
  <img src="/images/thispleasesme.gif" alt="This pleases me">
</p>
<h2>
What are the big blockers?</h2>
<p>
My experience: the blockers are yourself and fear. I think I started caring too much about what other people thought and had a deep problem called <a href="https://en.wikipedia.org/wiki/Impostor_syndrome">Imposter Syndrome</a>. Ultimately, I reasoned somehow that my accomplishments weren’t valid, and that I constantly needed to be <del>liked</del>, <del>cared</del>, <strong>LOVED</strong> by everyone. When that happens, you stop knowing about yourself and what <em>you’re</em> about since you’re constantly thinking about what others want.</p>
<p>
Tough place to be.</p>
<p>
Naming the issue helped me. Articulating it. Talking about it. Sharing it. Pushing your emotional boundaries and revealing yourself a bit more—but you know that will come with its own set of risks. That’s where the fear feels intense.</p>
<p>
It took me <em>years</em> to do this.</p>
<h2>
Who’s helping, who’s not?</h2>
<p>
I’ve been really lucky; I have a distint (not the lucky part) but encouraging family. When I think about families, I have this ideology that they’re close, bonded, and ‘all up in yo businass’; but I didn’t get born into that family. Yes my family cares, but they have their own lives and interests, and time only adds more distance if effort isn’t applied. In my case, not much effort (regrettably) has been applied.</p>
<p>
<em>They help</em>. Regardless of all above, they care and they help so they’re sticking around and I always remind myself to add effort to the relationships. Same goes for friendships.</p>
<p>
At work, there are certain folks that are really enjoyable to be around in moments. They really rock at their jobs and they’re pleasant to work around. That’s where it stops though, because on the personal level they are absolute <em>drags</em> to be around. Debbie-downers, Dan-drowners.</p>
<p>
<em>They don’t help</em>. They’re at work, so you can’t toss them, and they’re definitely not worthless. They’re just not helping you refactor your life to find joy. Honestly, they might just need to refactor their own lives. If you are drawn to them, then help identify areas of refactoring-need but try to remember that they are not a charity, they are people.</p>
<p>
  <img src="/images/newman.gif" alt="Newman.">
</p>
<p>
<strong>TL;DR</strong>: Stay with those that encourage and help you. Lose the ones that don’t enourage you and add needless stress. Stress is definitely something that needs to refactored OUT.</p>
<h2>
When to refactor life</h2>
<p>
You’ll know. It’s when you determine if you’re unhappy or depressed. Pretty simple. No long section here.</p>
<h2>
How I did it</h2>
<p>
Refactor sometimes means <code class="inline">.strip</code>. Sometimes it means <code class="inline">.sort</code> or even <code class="inline">.sort!</code>, or <code class="inline">.order</code>-ing your priorities according to their <code class="inline">:worth</code> so you can make <code class="inline">self.satisfied?</code> true.</p>
<p>
  <img src="/images/sokoban.png" alt="Just movin boxes">
</p>
<h3>
Leap of faith</h3>
<p>
I think this one is universal. You’ll have to take some leapy faiths if you want change. This will look like a couple things:</p>
<ul>
  <li>
Leaving behind someone you’ve known for years.  </li>
  <li>
Moving.  </li>
  <li>
Get a different job.  </li>
  <li>
Going somewhere you’ve never been.  </li>
  <li>
Being social, like going to a meetup.  </li>
  <li>
Spending gobs of money to go back to school.  </li>
  <li>
Counseling. Yes, this is a leap of faith.  </li>
  <li>
Talking about your darkness to someone you don’t know whether you trust yet.  </li>
  <li>
Talking about your bright moments to someone in the dark. (careful)  </li>
</ul>
<p>
Out of these experiences, you’ll naturally have to refactor. Moving and going to school is obvious. The others are just risking your “perfect image” you’ve crafted. Believe me, it’s worth breaking that mirror. There is no perfection of self. <a href="https://www.esv.org/Ecclesiastes+1/">Don’t chase after the wind</a>.</p>
<p>
Be careful about talking about your bright moments to someone who’s depressed; this may not have the result you’re hoping. Depression is a maze of confusion.</p>
<h3>
Trim</h3>
<p>
Once you’ve faithfully leaped toward experiences, you’ll have a better idea of what wasn’t helping. This is your moment to take another leap of faith and <code class="inline">self.strip!</code> out some of the things that weren’t helping.</p>
<p>
You know another big thing I trimmed? My beard.</p>
<p>
  <img src="/images/cutitout.gif" alt="Cut it out">
</p>
<p>
Don’t just keep reflecting on your experiences. Take action on those and keep doing the things you enjoy, and stop doing the things that are not enjoyable (except for adulting; unfortunately that is the curse of adulthood).</p>
<p>
  <img src="/images/camping.jpg" alt="Go camping">
</p>
<h3>
Go Camping</h3>
<p>
Short feedback loops are good. Don’t spend too much time exploring and trimming without knowing the path is good.</p>
<p>
For example, when you’re hiking, if you choose a 10 mile route, you’re pretty much stuck on that path until you decide it’s not the one, or you reach the end of it, or you go off the path entirely. I’m not saying don’t take long hikes, I’m just saying that you need to hike 1 mile and decide if the journey is worth it, and if not then you’ve only gone 1 mile and not 10. Turn around and try a different path or make your own.</p>
<p>
Camping is one of those excellent moments to get some feedback from yourself and others. I don’t know what it is, but something about staring into a fire and eating marshmallows causes a group to talk about deep stuff.</p>
<p>
<em>Don’t trust everything though. Some feedback is biased</em>.</p>
<h3>
Conclusion</h3>
<p>
I really hope this helps someone out there! Hit me up <a href="https://twitter.com/bernheisel">@bernheisel</a></p>
]]></content:encoded>
</item>
<item>
<title><![CDATA[Syncing an External USB Drive with a Network NAS]]></title>
<dc:creator>David Bernheisel</dc:creator>
<description><![CDATA[I recently bought a MacBook Pro with a limited 256GB SSD. It's great, btw, but it requires me to now store all my music, movies, and archival-type files on an external drive. It scares me a bit to have all that stuff on a single USB-powered drive, so I also set up a network NAS that contains 2 mirrored 1TB drives (I salvaged these from my desktop that I sold to buy my MacBook).

Enter problem: I'm lazy. I don't like manually backing everything up. I just want to manage the stuff I put on the external drive, not the NAS drive. Enter solution: BASH script, and launchd.
]]></description>
<link>https://bernheisel.com/blog/syncing-external-drive-with-network-nas</link>
<guid isPermaLink="true">https://bernheisel.com/blog/syncing-external-drive-with-network-nas</guid>
<pubDate>Sun, 02 Jun 2013 00:00:00 -0400</pubDate>
<content:encoded><![CDATA[<p>
I recently bought a MacBook Pro with a limited 256GB SSD. It’s great, btw, but it requires me to now store all my music, movies, and archival-type files on an external drive. It scares me a bit to have all that stuff on a single USB-powered drive, so I also set up a network NAS that contains 2 mirrored 1TB drives (I salvaged these from my desktop that I sold to buy my MacBook).</p>
<p>
Enter problem: I’m lazy. I don’t like manually backing everything up. I just want to manage the stuff I put on the external drive, not the NAS drive. Enter solution: BASH script, and launchd.</p>
<h2>
Overview</h2>
<ol>
  <li>
I write BASH scripts at work, so this was my goto place for automating something I don’t want to do.  </li>
  <li>
I use a mac, so I used launchd to kick off the script when relevant  </li>
  <li>
I want to know when its running, so I used an icon I found on the internet to make it look pretty, and placed the lock folder on my desktop.  </li>
</ol>
<h2>
Getting the core logic:</h2>
<p>
Here’s the BASH Script in its entirety:</p>
<pre><code class="sh">#!/bin/bash
remote=&quot;/Volumes/Volume_1&quot;
LOCAL=&quot;/Volumes/Storage&quot;
LOCK=~/Desktop/Syncing
logging=~/backup-rsync-log.txt
sleeptime=20
maxthreads=20
set -e

function cleanup {
  echo &quot;Removing Syncing folder&quot;
  rm -rf $LOCK
}

trap cleanup EXIT
mkdir $LOCK || { echo &quot;Backup already running&quot; ; exit 1 ; }
Rez -append ~/backup.rsrc -o ~/Desktop/Syncing/$&#39;Icon\r&#39; # set the icon to lock folder
SetFile -a C ~/Desktop/Syncing # initiate the icon
SetFile -a V ~/Desktop/Syncing/$&#39;Icon\r&#39; # hide the icon file
sleep 15 # give time for mounting

# Check if external drive is mounted
if mount | grep &quot;on ${LOCAL}&quot; &gt; /dev/null; then
  echo &quot;Storage drive is mounted&quot;

  # Check if network NAS is mounted
  if mount | grep &quot;on ${remote}&quot; &gt; /dev/null; then
    echo &quot;Remote NAS is mounted&quot;
    echo &quot;Running rsync&quot;

    # loop over all root external drive folders and do an rsync for each, up to a limit
    for dir in &quot;$LOCAL&quot;/*/; do
      folder=`basename &quot;$dir&quot;`

      # create the folder if it doesn&#39;t exist, keeping permissions
      if [ ! -d &quot;${remote}/${folder}&quot; ]; then
        mkdir -p &quot;$remote&quot;/&quot;$folder&quot;
        chown --reference=&quot;$dir&quot; &quot;${remote}/${folder}&quot;
        chmod --reference=&quot;$dir&quot; &quot;${remote}/${folder}&quot;
      fi
      echo -e &quot;\nStarting rsync $(date +%Y%m%d_%H%M%S) of&quot; $dir &gt;&gt; $logging

      # don&#39;t go crazy with rsync, so limit it
      while [ `ps -ef | grep -c [r]sync` -gt ${maxthreads} ]; do
        sleep ${sleeptime}
      done

      # start rsync parallel threads.
      nohup rsync -avh --delete &quot;$dir&quot; &quot;$remote&quot;/&quot;$folder&quot;/ &gt;$logging 2&gt;&amp;amp;1 &amp;amp;
    done
    wait
    echo &quot;Done&quot;
  else
    echo &quot;Remote NAS is NOT mounted&quot;
  fi
else
  echo &quot;Storage drive is NOT mounted&quot;
fi</code></pre>
<p>
There’s a good amount of logic in there, so let’s go through it.</p>
<p>
I don’t like hardcoding stuff I have to say over and over, so I start by naming the variables in case I need to reuse this code later. The variables are</p>
<ul>
  <li>
My external hard drive, put into <code class="inline">$LOCAL</code>  </li>
  <li>
My network NAS drive, put into <code class="inline">$remote</code>  </li>
  <li>
My rsync limiter, put into <code class="inline">$maxthreads</code>  </li>
  <li>
How long I want rsync to wait before trying to launch another thread, put into <code class="inline">$sleeptime</code>  </li>
  <li>
Where I want the double-purposed folder, 1) to ensure I only have one of these scripts running at a time, and 2) visually tell me when the script is running; this is put into <code class="inline">$LOCK</code>  </li>
  <li>
Lastly, so I can debug if needed, I log all the actions into a text file, put into <code class="inline">$logging</code>  </li>
</ul>
<p>
So now that I have everything named. Now it’s time for some real logic.</p>
<p>
I start out by checking whether my script is already running. I check by attempting to create a folder. If the folder is already created, it’ll fail, and then exit the script. Pretty simple. I can’t tell you the specifics, but creating a folder is a great way of setting up a lock without conflicts, from what I’ve read online. In my script, I also used the folder as a status to inform me when the script is running. I also thought about creating a simple menubar app that’ll show a spinning gear, but I don’t really feel like overengineering this.</p>
<p>
So, I have my safety set so I’m not executing this script over on top of itself. Now I need to determine whether my necessary drives are mounted, I do this by nesting two if statements; one to check if the external drive is mounted, and another to check if the NAS is mounted. Obviously, I don’t want anything to happen unless the drives are mounted. If they’re not mounted, I exit.</p>
<p>
Next, I go through the folders in the root of the external drive. I want to be somewhat recursive, so I do a for loop over the root folders on the drive, and start an rsync on each folder. For that to work, I need to make sure the destination folder is created, preferably with the same permissions.</p>
<p>
If you have one root folder with a bunch of subfolders (for example a Music folder with a bunch of Artist subfolders), then it might be good for you to start your loop there so this is a more-effective script.</p>
<p>
Inside every folder, I start an rsync thread distributed among the folders. However, I don’t want to stress my computer out with a thousand rsync threads, so before I start a new thread, I check to see how many there are and possibly limit it (for me, I arbitrarily chose a 20 thread cap). I determine this by counting how many rsync processes there are, so I execute ps, pipe it to grep to grab what I need. the grep -c flag counts them. Then, I compare it to my previously-defined cap. If it’s at the limit, then I put it in a while loop that sleeps until the thread count is back down.</p>
<p>
If you’re familiar with command line operations, then you should read up on the rsync manual (run “man rsync” in the terminal). There’s a lot of nifty things you can do with this, for example, you can backup over SSH or SFTP. I opted for the flags -a (archive, aka carry over all the permissions and recurse any subdirectories), -v (be verbose, I want this so I can catch everything it’s doing and redirect that to my log file in <code class="inline">$logging</code>), and -h (output human-readable stuff, so I don’t have to count the zeros in all the file sizes).</p>
<p>
Lastly (as far as the script goes), I want to make sure that whenever my script exits, be it because of an error or anything else, I want my lock/status folder is removed. To accomplish this, I just trapped any kind of exit by executing a small function that removes the lock folder. I put it near the top since it needs to be defined before it could be used. Sometimes I forget that you can create functions in BASH.</p>
<p>
This can be optimized more, but this is where I’m finished since it achieves my goal of lazily backing up my external hard drive. If you have ideas, drop them in the comments or link out to your explanation!</p>
<h2>
Launching it when I need to.</h2>
<p>
So far, we just have a script that we have to manually kick off in order for it to work. I am still too lazy and instead want my $2000 computer to do the work for me. I was tempted to just use cron to have this script run automatically every 5 minutes, but I thought that was wasteful, since it’s probable that this script will fail most times it’s launched. I read around and found Mac’s tool called launchd, which watches for events, and then launches actions.</p>
<p>
I’m unfamiliar with plists still, so I really just piggybacked on the backs of other giants. Step 3 on this page shows me how to do this (btw, this  guy solves the same problem here, but does it differently; check his solution out if you’re interested). Summed up, create a plist, place it in your ~/Library/LaunchAgents folder (tilde represents your home folder), and you’re done!</p>
<p>
The important bits of the .plist file are the</p>
<ul>
  <li>
Program  </li>
  <li>
WatchPaths  </li>
  <li>
LowPriorityIO  </li>
</ul>
<p>
For the program key, I’m putting the path to the BASH script I created above.</p>
<p>
For the WatchPaths key, I’m putting in /Volumes directory, since I want this script to launch every time something changes in /Volumes. You could change this to the specific name of your drive as well–it should work the same and probably better.</p>
<p>
For the LowPriorityIO key, I’m putting the “YES” value, so I can tell my computer to not really give a lot of resources to this script. Again, I want this to be nearly invisible to me–I’d hate to feel my computer choke just because of a backup script.</p>
<p>
You still need to add a label to the plist, so call it whatever you want.</p>
<p>
Save it, and place it in your ~/Library/LaunchAgents folder. Restart.</p>
<p>
Now you should have a fully working solution.</p>
<h2>
Making it look <em>slightly</em> better</h2>
<p>
I’m not <em>quite</em> finished yet. I wanted to have my lock folder be pretty with an icon, so I went back to my BASH script and added a couple lines to set the folder icon. I found the commands here <a href="https://apple.stackexchange.com/questions/6901/how-can-i-change-a-file-or-folder-icon-using-the-terminal">Stack Exchange</a>.</p>
<p>
First, grab an icon you want. I supplied one already, but you might have a different preference. I found mine by googling, and you might find it helpful to google with “filetype:png” so you find an icon with transparency. Find an icns conerter and convert it to Mac-compatible .icns.</p>
<p>
Second, create a new temporary folder. We’re going to apply this icon to it so we can grab a file we need from it once it’s set. Then, right-click the folder, and drag-and-drop the .icns file to the Icon in the top left.</p>
<p>
Third, run a command-line tool against the folder.</p>
<p>
<code class="inline">DeRez -only icns path/to/tempfolder/$&#39;Icon\r&#39; &gt; backup.rsrc</code></p>
<p>
This will give you what you need in order to set the lock folder icon. Store this file somewhere you don’t look often (I stored mine next to the backup script in my home folder). You can delete the temporary folder now and the original image you downloaded.</p>
<p>
Fourth, modify the backup script to apply the icon.</p>
<p>
After the line where we make the lock folder, run a Rez command and a couple SetFile commands.</p>
<p>
<code class="inline">Rez -append ~/backup.rsrc -o ~/Desktop/Syncing/$&#39;Icon\r&#39; # set the icon to lock folder</code></p>
<p>
<code class="inline">SetFile -a C ~/Desktop/Syncing # initiate the icon</code></p>
<p>
<code class="inline">SetFile -a V ~/Desktop/Syncing/$&#39;Icon\r&#39; # hide the icon file</code></p>
<p>
The first command adds the icon to the lock directory.</p>
<p>
The second command sets the folder attributes, which allows Finder to appreciate your new icon (otherwise, it won’t recognize that cool icon you added)</p>
<p>
The third command sets the folder to invisible. This is totally optional, but if you ever double-click the “Syncing” folder you’ll find the icon resource, and that just seems ugly to me–so let’s hide it.</p>
<p>
<em>OK! YOU’RE DONE!</em></p>
<p>
I guess I’m not so lazy after all.</p>
]]></content:encoded>
</item>
</channel>
</rss>
