<?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" xmlns:media="http://search.yahoo.com/mrss/"><channel><title><![CDATA[Julien's DevRel corner]]></title><description><![CDATA[DevRel, Thoughts, Climate and Code]]></description><link>https://lengrand.fr/</link><image><url>https://lengrand.fr/favicon.png</url><title>Julien&apos;s DevRel corner</title><link>https://lengrand.fr/</link></image><generator>Ghost 5.54</generator><lastBuildDate>Fri, 23 Feb 2024 06:25:27 GMT</lastBuildDate><atom:link href="https://lengrand.fr/rss/" rel="self" type="application/rss+xml"/><ttl>60</ttl><item><title><![CDATA[A retrospective on getting solar panels installed]]></title><description><![CDATA[A retrospective on getting solar panels installed. The installation cost around 5k€ (4k€ after government subsidies), and we were able to generate over 80% of our yearly consumption and save just short of 1k€ in year 1 as well as shave off our CO2 footprint by over a ton]]></description><link>https://lengrand.fr/a-retrospective-on-getting-solar-panels-installed/</link><guid isPermaLink="false">65a6465f02fb1c43eb7ff04d</guid><category><![CDATA[electric]]></category><category><![CDATA[side-project]]></category><category><![CDATA[sustainability]]></category><category><![CDATA[solar]]></category><dc:creator><![CDATA[Julien]]></dc:creator><pubDate>Tue, 16 Jan 2024 11:28:03 GMT</pubDate><media:content url="https://images.unsplash.com/photo-1624397640148-949b1732bb0a?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wxMTc3M3wwfDF8c2VhcmNofDJ8fHNvbGFyfGVufDB8fHx8MTcwNTQwNDMwOXww&amp;ixlib=rb-4.0.3&amp;q=80&amp;w=2000" medium="image"/><content:encoded><![CDATA[<img src="https://images.unsplash.com/photo-1624397640148-949b1732bb0a?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wxMTc3M3wwfDF8c2VhcmNofDJ8fHNvbGFyfGVufDB8fHx8MTcwNTQwNDMwOXww&amp;ixlib=rb-4.0.3&amp;q=80&amp;w=2000" alt="A retrospective on getting solar panels installed"><p>As you may know, we&apos;re trying many ways to reduce our impact on the environment as a family. For example, we didn&apos;t fly for several years, focussing instead on vacations that are easier to reach. We&apos;re also growing part of our own food organically, making a lot of our own products and almost exclusively using public transport.</p><p>A year ago now, we got solar panels installed on our roof and it&apos;s time for a small retrospective. </p><p><strong>TL;DR : The installation cost around 5&#x20AC; (4k&#x20AC; after government subsidies), and we were able to generate over 80% of our yearly consumption (used 30% straight up) and save just short of 1k&#x20AC; in year 1.</strong></p><h2 id="first-our-consumption">First, our consumption</h2><p></p><p>In this post, we&apos;ll only be talking about electricity. Our house heating (and warm water) is done using hot water pipes from the city, so it won&apos;t be part of the equation here.</p><p>As you may know already, the first thing to do if you are trying to lower your impact on the environment is not to recycle, but to start by reducing your consumption. Yup, it all starts by <em>reducing</em>.</p><figure class="kg-card kg-image-card"><img src="https://lengrand.fr/content/images/2024/01/image-4.png" class="kg-image" alt="A retrospective on getting solar panels installed" loading="lazy" width="1920" height="1080" srcset="https://lengrand.fr/content/images/size/w600/2024/01/image-4.png 600w, https://lengrand.fr/content/images/size/w1000/2024/01/image-4.png 1000w, https://lengrand.fr/content/images/size/w1600/2024/01/image-4.png 1600w, https://lengrand.fr/content/images/2024/01/image-4.png 1920w" sizes="(min-width: 720px) 720px"></figure><p>We leave in a 100m2 house that is connected on both sides. We do not own an electric car, and we have two kids. <strong>In 2022, our yearly consumption was just above 3000kWh</strong>. If I believe the data from our energy contractor, our monthly consumption is about 45% smaller than similar families, which is kind of nice. (If you&apos;re interested btw, <a href="https://www.agwayenergy.com/blog/average-kwh-per-day/?ref=lengrand.fr#:~:text=According%20to%20the%20United%20States,kWh%20per%20day%20was%2029.">the average American household consumed 10,632-kilowatt hours (kWh) of electricity last year</a> &#x1F613;&#x1F631;) . </p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://lengrand.fr/content/images/2024/01/image-2.png" class="kg-image" alt="A retrospective on getting solar panels installed" loading="lazy" width="1122" height="884" srcset="https://lengrand.fr/content/images/size/w600/2024/01/image-2.png 600w, https://lengrand.fr/content/images/size/w1000/2024/01/image-2.png 1000w, https://lengrand.fr/content/images/2024/01/image-2.png 1122w" sizes="(min-width: 720px) 720px"><figcaption>Our energy usage compared to average families in the Netherlands.</figcaption></figure><p>This is all done without many sacrifices btw, we&apos;re just using mostly &quot;dumb&quot; appliances and avoiding to run the dryer, for example. </p><h2 id="the-experience-and-the-setup">The experience, and the setup</h2><p></p><p>Interested to further see how to reduce our house&apos;s footprint, we invited <a href="https://energie-n.nl/?ref=lengrand.fr">Energie N</a> at home, a group of volunteers who comes and tells you based on your house installation which optimizations are the most impactful. (If you&apos;re searching for help by the way, I recommend checking for similar groups in your area. They have lots of experience visiting many houses and it&apos;s been extremely helpful). </p><p>For example, <a href="https://energyforums.net/hvac/how-humidity-affects-heating-and-cooling/?ref=lengrand.fr#:~:text=Water%20has%20more%20thermal%20mass,it%20can%20store%20more%20heat.&amp;text=This%20same%20concept%20applies%20to,humid%20air%20than%20dry%20air.">I learnt that dry air is cheaper to heat that humid air</a>. Which is very logical if you think about it, but I never had before &#x1F913;. </p><p>Based on their recommendations, we decided to look into installing solar panels on our house. Solar panels are extremely common in the Netherlands (<a href="https://en.wikipedia.org/wiki/Solar_power_in_the_Netherlands?ref=lengrand.fr#:~:text=By%202018%20residential%20Solar%20PV,had%20solar%20rooftop%20systems%20installed.">more than 25% of households have them now!!</a>) so there&apos;s a plethora of companies to choose from. They do everything online, using cadaster and aerial imagery to plan your future installation. Here is our part of the contract looked for us, without them even having to come</p><figure class="kg-card kg-image-card"><img src="https://lengrand.fr/content/images/2024/01/image-5.png" class="kg-image" alt="A retrospective on getting solar panels installed" loading="lazy" width="626" height="878" srcset="https://lengrand.fr/content/images/size/w600/2024/01/image-5.png 600w, https://lengrand.fr/content/images/2024/01/image-5.png 626w"></figure><p>Now, I have to say that not everything is flawless with those methods. As you can see, the space we have to install those panels is limited because of the roof extension we have in our bedroom. There&apos;s also legal requirements for margins on each side of your roof to avoid taking space off of your neighbor&apos;s roof. The first contractor who came over realized that there was a 5cm discrepancy between his plans and our actual roof and had to cancel the installation...</p><p>We contracted a second company, <strong>who back then had a 16 months waiting time</strong> (due to the war in Ukraine and the energy prices exploding)! Luckily, a few weeks later another customer cancelled their installation date in our street and we could get them installed very fast. </p><p>The whole installation took less than 3 hours, and was flawless. Super happy with our contractor.</p><figure class="kg-card kg-image-card"><img src="https://lengrand.fr/content/images/2024/01/4E01796B-185A-44B2-9551-C46E4B3BD179_1_102.jpeg" class="kg-image" alt="A retrospective on getting solar panels installed" loading="lazy" width="2000" height="1500" srcset="https://lengrand.fr/content/images/size/w600/2024/01/4E01796B-185A-44B2-9551-C46E4B3BD179_1_102.jpeg 600w, https://lengrand.fr/content/images/size/w1000/2024/01/4E01796B-185A-44B2-9551-C46E4B3BD179_1_102.jpeg 1000w, https://lengrand.fr/content/images/size/w1600/2024/01/4E01796B-185A-44B2-9551-C46E4B3BD179_1_102.jpeg 1600w, https://lengrand.fr/content/images/2024/01/4E01796B-185A-44B2-9551-C46E4B3BD179_1_102.jpeg 2048w" sizes="(min-width: 720px) 720px"></figure><h2 id="a-year-later-the-results">A year later, the results</h2><p>We&apos;re now a year later. Let&apos;s have a look at the results!</p><p>We do not have batteries in our installation. The way it works in the Netherlands at the moment, is that the energy you generate is either used direction by your house, or sent back to the grid. The energy you send back to the grid is removed from your bill on a 1/1 ratio. </p><p>Last year, <strong>our 6 panels generated a total of 2393kWh</strong>. The cool thing is that you really can see we had a very sunny spring, but a crap summer &#x1F605;. I&apos;m curious how it&apos;ll compare to next year.</p><figure class="kg-card kg-image-card"><img src="https://lengrand.fr/content/images/2024/01/image-6.png" class="kg-image" alt="A retrospective on getting solar panels installed" loading="lazy" width="591" height="656"></figure><p>Out of this energy, we sent almost 1600kWh back to the grid. </p><figure class="kg-card kg-image-card"><img src="https://lengrand.fr/content/images/2024/01/image.png" class="kg-image" alt="A retrospective on getting solar panels installed" loading="lazy" width="1816" height="994" srcset="https://lengrand.fr/content/images/size/w600/2024/01/image.png 600w, https://lengrand.fr/content/images/size/w1000/2024/01/image.png 1000w, https://lengrand.fr/content/images/size/w1600/2024/01/image.png 1600w, https://lengrand.fr/content/images/2024/01/image.png 1816w" sizes="(min-width: 720px) 720px"></figure><p><strong>This means we used directly just over 30%, or 800kWh</strong>. If the conditions of energy delivery were to change, it might actually be worth installing batteries, because it seems that we sent back over two thirds of what we generated. </p><p><strong>After all this, our leftover usage is 2226 -&#xA0; 1591 = 635kWh, which means we generated almost 80% of our total consumption!</strong> In terms of financials, we saved just shy of 1k<strong>&#x20AC;</strong> this year, meaning that <strong>I&apos;d expect our installation to be profitable within the next 4 years if all goes well.</strong></p><p>However, if we </p><figure class="kg-card kg-image-card"><img src="https://lengrand.fr/content/images/2024/01/image-1.png" class="kg-image" alt="A retrospective on getting solar panels installed" loading="lazy" width="1524" height="766" srcset="https://lengrand.fr/content/images/size/w600/2024/01/image-1.png 600w, https://lengrand.fr/content/images/size/w1000/2024/01/image-1.png 1000w, https://lengrand.fr/content/images/2024/01/image-1.png 1524w" sizes="(min-width: 720px) 720px"></figure><p></p><h2 id="and-in-terms-of-co2-emissions">And in terms of CO2 emissions?</h2><p>This will be a very rough calculation here, because I&apos;m not including the cost of the solar panels manufacturing, and the values I will be giving change all the time, but overall <a href="https://www.nowtricity.com/country/netherlands/?ref=lengrand.fr">if we look at the numbers</a> : </p><p>On a normal day, 1kWh generates roughly 500g of CO2 (In 2023 the average emissions of Netherlands were 421 g CO2eq/kWh.). Meaning that us generating 2393kWh this year leads to a reduction of 1150kg of our CO2 footprint, 400kg of which we didn&apos;t even send back to the grid. Not bad, that&apos;s almost 10% of our combined footprint!</p><figure class="kg-card kg-image-card"><img src="https://lengrand.fr/content/images/2024/01/image-7.png" class="kg-image" alt="A retrospective on getting solar panels installed" loading="lazy" width="1150" height="808" srcset="https://lengrand.fr/content/images/size/w600/2024/01/image-7.png 600w, https://lengrand.fr/content/images/size/w1000/2024/01/image-7.png 1000w, https://lengrand.fr/content/images/2024/01/image-7.png 1150w" sizes="(min-width: 720px) 720px"></figure><h2 id="next-steps-and-conclusion">Next steps and conclusion</h2><p>Overall, I&apos;m super happy about the whole setup. We&apos;re reducing our footprint, while saving money, raising the value of the house and for absolutely no trouble. Hard to do better.</p><p>I think we&apos;ve about done what we could for energy production in the household. I think the next big thing for us will revolve around insulation, looking at getting better windows and more. I&apos;ll post about this another time!</p><p>Hope you liked this post, maybe it can motivate you too!</p>]]></content:encoded></item><item><title><![CDATA[Visualizing your AAARRP priorities as a way to manage up in your DevRel team]]></title><description><![CDATA[In this article, I'm describing how we used the AAARRRP framework to visually describe our DevRel team's strategy and clarify the reason for  our day to day activities to our internal stakeholders, and more focus on impact instead. ]]></description><link>https://lengrand.fr/aaarrp-metrics-as-a-way-to-manage-expectations-up/</link><guid isPermaLink="false">6523b75f02fb1c43eb7fea1b</guid><category><![CDATA[devrel]]></category><category><![CDATA[strategy]]></category><category><![CDATA[development]]></category><dc:creator><![CDATA[Julien]]></dc:creator><pubDate>Tue, 12 Dec 2023 09:46:10 GMT</pubDate><media:content url="https://lengrand.fr/content/images/2023/12/image-8-1.png" medium="image"/><content:encoded><![CDATA[<img src="https://lengrand.fr/content/images/2023/12/image-8-1.png" alt="Visualizing your AAARRP priorities as a way to manage up in your DevRel team"><p><em>TL;DR : we have been using <a href="https://www.leggetter.co.uk/aaarrrp/?ref=lengrand.fr">the AAARRRP framework</a> as a visual and easy way to align with stakeholders which types of activities are the most relevant for our customers. It can look like this</em></p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://lengrand.fr/content/images/2023/12/image-8.png" class="kg-image" alt="Visualizing your AAARRP priorities as a way to manage up in your DevRel team" loading="lazy" width="1190" height="736" srcset="https://lengrand.fr/content/images/size/w600/2023/12/image-8.png 600w, https://lengrand.fr/content/images/size/w1000/2023/12/image-8.png 1000w, https://lengrand.fr/content/images/2023/12/image-8.png 1190w" sizes="(min-width: 720px) 720px"><figcaption>Possible DevRel priorities focussing on activation, retention and product</figcaption></figure><p>Let&apos;s dive into it!</p><h3 id="some-context">Some context</h3><p>Even though I have been doing Developer Relations for a few years unofficially or semi-officially, it was only two years ago that I got my first official role as a Developer Advocate. And after just a few months, I was offered to lead the team. We&apos;re a small team of three (me included) and our team falls under engineering.</p><p>It was never really an issue for us to create a strategy on what to achieve, or how to achieve it. We focussed on our 3 pillars: <a href="https://www.whatisdevrel.com/?ref=lengrand.fr">Code, Content and Community</a> and selected the most relevant activities.</p><p>For example, being a B2B company we knew that running the conference circuit wasn&apos;t the most efficient way to create value, for the simple reason that people wouldn&apos;t be able to sign up for our services. We quickly decided instead to embed ourselves close to documentation and provide many samples for the companies who were using us, bringing a lot of internal feedback at the same time.</p><p>But even though we had no doubt about our priorities, it wasn&apos;t always as clear for higher management, nor other stakeholders. After all, Developer Relations was a relatively new thing inside the company and only a few people had direct experience with the domain in the past. And we all know that even for companies seasoned with DevRel, measuring impact and making sure activities align are a difficult topic. </p><p>We received a lot of challenging questions over time regarding our priorities :</p><ul><li>We see that the <em>insert competitor</em> DevRel team is very present on Youtube. How is it that your are not doing the same? </li><li>How is it that your Twitter account gets very few likes compared to <em>insert other company</em>.</li><li>Why do we not see many community contributions on your GitHub account, compared to <em>insert large company</em>? </li><li>Would you please join us at the booths for <em>insert conference, </em>we want to increase hiring.</li></ul><p>All those questions are very fair, and they deserve clear answers. Thing is, they were quite obvious for us given the context we had and it was a struggle for me to communicate those answers at scale and in a strategic way (understand : Find a way that people understand those answers, and hopefully give enough context that they come to the same decisions by themselves). </p><p>It&apos;s after reading about the AAARRRP framework that a potential solution came to mind &#x1F60A;.</p><h3 id="quick-sideline-about-priorities-activities">Quick sideline about priorities / activities</h3><p>My point with this method is to focus on how to communicate in a scalable way <strong>the type of activity</strong> we plan on focussing on, <strong>not the exact activity performed</strong>. </p><p>For example, we might want people to understand that writing code samples is the most impactful activity we can do. </p><p>However, we will not use it to communicate <strong>which</strong> code sample to write. For this, we might instead use a combination of data sources like documentation statistics, or support tickets for example. I may write more about this later in another article.</p><h3 id="a-quick-intro-about-the-aaarrrp-framework"> A quick intro about the AAARRRP framework</h3><p>I&apos;m not going to expand myself much about this, because I would be ripping off the original article. I recommend you to <a href="https://www.leggetter.co.uk/aaarrrp/?ref=lengrand.fr">go read it</a> instead if you have never heard of it before.</p><p>Here is the very minimal version. As Developer Relations team, we can operate on several key moments of the customer journey : </p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://lengrand.fr/content/images/2023/12/image-4.png" class="kg-image" alt="Visualizing your AAARRP priorities as a way to manage up in your DevRel team" loading="lazy" width="2000" height="555" srcset="https://lengrand.fr/content/images/size/w600/2023/12/image-4.png 600w, https://lengrand.fr/content/images/size/w1000/2023/12/image-4.png 1000w, https://lengrand.fr/content/images/size/w1600/2023/12/image-4.png 1600w, https://lengrand.fr/content/images/2023/12/image-4.png 2284w" sizes="(min-width: 720px) 720px"><figcaption>The AAARRRP framework funnel</figcaption></figure><p>Depending on the type of company we&apos;re working on and the strategic priorities of that company, some of those parts will be more crucial than others. </p><p>Early on, many SAAS products want to focus on <strong>awareness</strong> and <strong>acquisition</strong> to drive as many new customers in as possible, and grow fast (The famous <a href="https://www.swyx.io/measuring-devrel?ref=lengrand.fr">MAD</a>) .</p><p>As your product grows in maturity, you may want to increase the &quot;stickiness&quot; of your product and increase <strong>retention</strong>, or work on <strong>activation</strong> of additional features for your existing customer base.</p><p>Typical DevRel activities that we carry have impact of different parts of that funnel (usually several at the same time). </p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://lengrand.fr/content/images/2023/12/image-2.png" class="kg-image" alt="Visualizing your AAARRP priorities as a way to manage up in your DevRel team" loading="lazy" width="2000" height="1393" srcset="https://lengrand.fr/content/images/size/w600/2023/12/image-2.png 600w, https://lengrand.fr/content/images/size/w1000/2023/12/image-2.png 1000w, https://lengrand.fr/content/images/size/w1600/2023/12/image-2.png 1600w, https://lengrand.fr/content/images/2023/12/image-2.png 2042w" sizes="(min-width: 720px) 720px"><figcaption>Activities and funnel mapping. Original from https://www.leggetter.co.uk/aaarrrp/</figcaption></figure><p><em>Note: The original post also adds weight to the equation in order to select activities. We will not here, for simplicity&apos;s sake. The whole idea stays valid here</em>. </p><h3 id="from-aaarrrp-to-strategic-communication"> From AAARRRP to strategic communication</h3><p><em>Note: All those charts have been altered and do not contain internal information </em>&#x1F60A; </p><p>So far, nothing new under the sun. That&apos;s where we&apos;ll go just one step further and use the content above for strategic communication. </p><p>Let&apos;s say that based on the current context, <strong>we decide to focus on activation, retention and product</strong>. </p><ul><li>We can draw this as a chart to help communication with stakeholders. </li><li>We can also draw (rough, given we do not have internal knowledge) equivalent charts for other companies in the space for comparison. </li><li>We can even draw our own chart year on year, to show how our activities vary as priorities shift, or impact decreased (<a href="https://en.wikipedia.org/wiki/Diminishing_returns?ref=lengrand.fr">law of diminishing returns</a>).</li><li><strong>Now for the key part :</strong> We use these charts to validate assumptions with &#xA0;stakeholders, <strong>who will be able to derive the corresponding activities themselves.</strong></li></ul><p>For example, given the decision above we can draw this: </p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://lengrand.fr/content/images/2023/12/image-8.png" class="kg-image" alt="Visualizing your AAARRP priorities as a way to manage up in your DevRel team" loading="lazy" width="1190" height="736" srcset="https://lengrand.fr/content/images/size/w600/2023/12/image-8.png 600w, https://lengrand.fr/content/images/size/w1000/2023/12/image-8.png 1000w, https://lengrand.fr/content/images/2023/12/image-8.png 1190w" sizes="(min-width: 720px) 720px"><figcaption>Possible DevRel priorities focussing on activation, retention and product</figcaption></figure><p>With those priorities agreed, the common understanding is that we should focus on activities like Code Samples, Guides, Tutorials, and answering Stack Overflow questions.</p><p>Again, we can more easily explain why we have a lesser focus on social media and the conference circuit that others by comparing main focuses between entities. </p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://lengrand.fr/content/images/2023/12/data-src-image-6dd98e93-7b30-4f89-acd5-424eb4c60376.png" class="kg-image" alt="Visualizing your AAARRP priorities as a way to manage up in your DevRel team" loading="lazy" width="1200" height="742" srcset="https://lengrand.fr/content/images/size/w600/2023/12/data-src-image-6dd98e93-7b30-4f89-acd5-424eb4c60376.png 600w, https://lengrand.fr/content/images/size/w1000/2023/12/data-src-image-6dd98e93-7b30-4f89-acd5-424eb4c60376.png 1000w, https://lengrand.fr/content/images/2023/12/data-src-image-6dd98e93-7b30-4f89-acd5-424eb4c60376.png 1200w" sizes="(min-width: 720px) 720px"><figcaption>possible DevRel team profile from another company focussing more on MAD</figcaption></figure><p>Once the shape of our strategy is agreed to, the type of activities expected become clearer for everyone and we can focus on the impact of those activities. And if the priorities change, it&apos;s always time to come back to the drawing board &#x1F389;.</p><p>Let&apos;s imagine now that our company&apos;s internal focus heavily shifts towards &quot;closing deals&quot; because we need to extend our runway. It might be time for the DevRel team to <strong>start acting on the Revenue </strong>part of the funnel, reduce Product efforts <strong>and do Pre-Sales activities</strong>. We can update the plan, propose a new shape and double check with stakeholders that the change fits the new landscape : &#xA0;</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://lengrand.fr/content/images/2023/12/image-7.png" class="kg-image" alt="Visualizing your AAARRP priorities as a way to manage up in your DevRel team" loading="lazy" width="2000" height="614" srcset="https://lengrand.fr/content/images/size/w600/2023/12/image-7.png 600w, https://lengrand.fr/content/images/size/w1000/2023/12/image-7.png 1000w, https://lengrand.fr/content/images/size/w1600/2023/12/image-7.png 1600w, https://lengrand.fr/content/images/2023/12/image-7.png 2398w" sizes="(min-width: 720px) 720px"><figcaption>DevRel team proposing to shift priorities following a company strategic change</figcaption></figure><p>Of course, whether the type of activities to be carried on fits DevRel well, is sustainable long term and more is another discussion altogether. The main point here is to make your priorities clear and communicate them accordingly.</p><h3 id="about-the-scale-of-priorities">About the scale of priorities </h3><p>One question you might wonder is : How do you calculate the scale of each priority in your bar chart?</p><p> Well, so far our method isn&apos;t very scientific &#x1F60A;. We mostly want to show relative priorities of activities and as such we&apos;re using a set number of points and sharing them across the different axes of the chart. For example, pick 70 points (10 per angle) and subdivide. The chart then helps us make decisions quarter to quarter or day to day. Of course, when drawing other team&apos;s chart, all numbers are best effort and based on impressions. </p><p>As we use these graphs more, we&apos;re experimenting with other ways to measure that could be more relevant. For example incorporating alignment and relevant weights that <a href="https://www.leggetter.co.uk/aaarrrp/?ref=lengrand.fr">the original AAARRRP article</a> mentions.</p><h3 id="a-word-of-conclusion">A word of conclusion</h3><p>Overall, the only thing we&apos;ve been doing is taking the AAARRRP framework as an inspiration to clearly visualise what our team focusses on and bring some clarity internally. We&apos;ve been using it on all of our presentations and internal pages focussing on our team&apos;s strategy. </p><p>It has helped to focus discussions with stakeholders more on how much impact we can bring, rather than what activities to carry and why and it&apos;s really been useful for us.</p><p>Having joined DevRelCon this year I know that internal communication is always a strong topic in DevRel teams. Maybe this can help bring some clarity on the topic! </p><p>Cheers, and talk soon. </p><p>Julien</p>]]></content:encoded></item><item><title><![CDATA[Creating an OpenAPI generator from scratch : From YAML to JetBrains HTTP Client]]></title><description><![CDATA[In this article, I'll be implementing an openapi generator from scratch so you can too! We'll be creating a very simple generator for the Jetbrains HTTP Client]]></description><link>https://lengrand.fr/creating-an-openapi-generator-from-scratch-from-yaml-to-jetbrains-http-client/</link><guid isPermaLink="false">6544f72902fb1c43eb7fec8f</guid><category><![CDATA[openapi]]></category><category><![CDATA[jetbrains]]></category><category><![CDATA[http]]></category><category><![CDATA[development]]></category><category><![CDATA[cli]]></category><dc:creator><![CDATA[Julien]]></dc:creator><pubDate>Sat, 04 Nov 2023 17:40:33 GMT</pubDate><media:content url="https://lengrand.fr/content/images/2023/11/F9_7HZZW4AEvcHF.jpeg" medium="image"/><content:encoded><![CDATA[<img src="https://lengrand.fr/content/images/2023/11/F9_7HZZW4AEvcHF.jpeg" alt="Creating an OpenAPI generator from scratch : From YAML to JetBrains HTTP Client"><p>This is the online version of the article with the same name I wrote for the Dutch Java Magazine &#x1F60A;.</p><figure class="kg-card kg-embed-card"><blockquote class="twitter-tweet"><p lang="en" dir="ltr">Creating your own code generator for the <a href="https://twitter.com/jetbrains?ref_src=twsrc%5Etfw&amp;ref=lengrand.fr">@jetbrains</a> http client from an <a href="https://twitter.com/hashtag/openapi?src=hash&amp;ref_src=twsrc%5Etfw&amp;ref=lengrand.fr">#openapi</a> specification? It&#x2019;s possible and you can read how in the latest edition of the Dutch <a href="https://twitter.com/hashtag/java?src=hash&amp;ref_src=twsrc%5Etfw&amp;ref=lengrand.fr">#java</a> magazine!<br>And yes, it&#x2019;s Jetbrains, not yet brains &#x1F602; <a href="https://t.co/zLGkpibLtN?ref=lengrand.fr">pic.twitter.com/zLGkpibLtN</a></p>&#x2014; Julien Lengrand-Lambert &#x1F951;&#x1F44B; (@jlengrand) <a href="https://twitter.com/jlengrand/status/1720369690105590201?ref_src=twsrc%5Etfw&amp;ref=lengrand.fr">November 3, 2023</a></blockquote>
<script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>
</figure><p>In the previous edition of the magazine, we discussed how the <a href="https://www.jetbrains.com/help/idea/http-client-reference.html?ref=lengrand.fr">JetBrains HTTP Client</a> could be used to run HTTP Queries, automate them and even use them in your CI/CD pipelines. Just like <a href="https://www.postman.com/?ref=lengrand.fr">Postman</a>, but text based and can be part of your source code. Pretty cool.</p><p>For reference, it could look like this. </p><pre><code class="language-http">### Github API - Traffic per day

GET https://api.github.com/repos/{{owner}}/{{repo}}/traffic/views?per=day
Accept: application/vnd.github+json
X-GitHub-Api-Version: 2022-11-28
Authorization: Bearer {{github_key}}</code></pre><p>With an environment file that looks like this : </p><pre><code class="language-json">{
  &quot;dev&quot;: {
    &quot;github_key&quot;: &quot;not_that_easy&quot;,
    &quot;owner&quot;: &quot;jlengrand&quot;,
    &quot;repo&quot;: &quot;elm-firebase&quot;
  }}</code></pre><p>Now, that is very nice, but it requires a lot of manual work.<strong> Wouldn&#x2019;t it be nice to be able to automate this?</strong> Fortunately, most of us developing APIs also generate <a href="https://www.openapis.org/?ref=lengrand.fr">OpenAPI</a> Specifications for them. When I looked however, there was no OpenAPI generator yet available for the Jetbrains HTTP Client. This is the story of how I&apos;ve implemented it from scratch, and how you could too if you find yourself in the same situation! We&apos;ll use the JetBrains HTTP Client as a practical example, but the knowledge is transferable &#x1F642;.</p><p>The OpenAPI generator project contains a core engine, as well as many packages with each a specific generator (Java, Ada, &#x2026;). The Jetbrains HTTP Client generator is actually published, and you can find <a href="https://github.com/OpenAPITools/openapi-generator/pull/14477/files?ref=lengrand.fr">the merge request</a> as well as <a href="https://openapi-generator.tech/docs/generators/jetbrains-http-client?ref=lengrand.fr">the documentation</a> on GitHub. If you have installed the last available OpenAPI generator release, you can actually try it out in a terminal as such : </p><pre><code class="language-bash">$ openapi-generator generate -i https://api.opendota.com/api  -g jetbrains-http-client -o dotaClient</code></pre><p>At its core, the idea of the OpenAPI generator is quite simple : It takes a specification file (JSON or YAML), transforms it into a set of objects in memory, and uses those objects to generate code / files using <a href="https://mustache.github.io/?ref=lengrand.fr">mustache</a> template files. </p><p>You can actually find most of that logic in the <code>DefaultGenerator</code> source file of the library. There, you can see that actions are separated into 3 groups : </p><ul><li>models (basically data types)</li><li>operations (actual operations)</li><li>supporting files (environments, READMEs, &#x2026;). </li></ul><p>Each of those is illustrated by a method, and takes separate objects as inputs :</p><pre><code class="language-java"> &#x2026;
  void generateModels(List&lt;File&gt; files, List&lt;ModelMap&gt; allModels, List&lt;String&gt; unusedModels) {&#x2026;}
&#x2026;
    void generateApis(List&lt;File&gt; files, List&lt;OperationsMap&gt; allOperations, List&lt;ModelMap&gt; allModels) {...}
&#x2026;
    private void generateSupportingFiles(List&lt;File&gt; files, Map&lt;String, Object&gt; bundle) {...}
&#x2026;</code></pre><p>You can find <a href="https://github.com/OpenAPITools/openapi-generator/blob/78f3b19b58df699ef883b89a7a44531407377719/modules/openapi-generator/src/main/java/org/openapitools/codegen/DefaultGenerator.java?ref=lengrand.fr#L433">the actual source file on GitHub</a>. The objects for each of those methods are large <code>Map</code> classes that contain the necessary data in a semi-structured format. Here is an example of how <code>allModels</code> looks like: </p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://lh7-us.googleusercontent.com/NVgYf1bDqjbDEuvOkdmDReLlE0pVj2M3A64JpNQncZmFvosFOlA4tGzb1idJWD8cWexTV-tlzd18VJwJ0lgkqHYi80GZFqmj3S-oJtXwa9Y2LrRG1mU-gdlKfBIV0hZsIBm2arP9QlVTQlfIvuwS_Tc" class="kg-image" alt="Creating an OpenAPI generator from scratch : From YAML to JetBrains HTTP Client" loading="lazy" width="624" height="360"><figcaption>a debug view of the allModels object</figcaption></figure><p>As you can see, the object is essentially a lot of key/value pairs that are quite recognisable and directly come from the OpenAPI specification file. </p><p>To create our own client, we will take advantage of this nice work. Let&apos;s dive into it. We first clone the repository</p><pre><code class="language-bash">$ git@github.com:OpenAPITools/openapi-generator.git; cd openapi-generator</code></pre><p>We can then use the <code>/new.sh</code> script to generate a few placeholder files for us. We&apos;ll be generating a client, and since we&apos;re not creating any bugs we won&apos;t be generating test files.</p><pre><code class="language-bash">$  ./new.sh -n java-magazine-client -c

Creating modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/JavaMagazineClientClientCodegen.java
Creating modules/openapi-generator/src/main/resources/java-magazine-client/README.mustache
Creating modules/openapi-generator/src/main/resources/java-magazine-client/model.mustache
Creating modules/openapi-generator/src/main/resources/java-magazine-client/api.mustache
Creating bin/configs/java-magazine-client-petstore-new.yaml
Finished.</code></pre><p>The library nicely generates a client generator for us, as well as some template files and even a config so we can test it easily! The config uses the well known <a href="https://spring-framework-petclinic-qctjpkmzuq-od.a.run.app/?ref=lengrand.fr">petstore</a> by default.</p><p>This is how the config file looks like : </p><pre><code class="language-yaml">generatorName: java-magazine-client
outputDir: samples/client/petstore/java/magazine/client
inputSpec: modules/openapi-generator/src/test/resources/3_0/petstore.yaml
templateDir: modules/openapi-generator/src/main/resources/java-magazine-client
additionalProperties:
  hideGenerationTimestamp: &quot;true&quot;</code></pre><p>It nicely mentions to the OpenAPI generator library which generator to use, which sample OpenAPI file to use as input, where the mustache template files are located and where to store the output</p><p><strong>Let&apos;s run it! </strong></p><pre><code class="language-bash">$ ./mvnw clean package # package once to have the generator inside the generated jar
$ ./bin/generate-samples.sh bin/configs/java-magazine-client-petstore-new.yaml</code></pre><p>Let&apos;s see what the generated output looks like : </p><figure class="kg-card kg-image-card kg-width-wide kg-card-hascaption"><img src="https://lh7-us.googleusercontent.com/QdjBEGu-dpqVoX_EMfZ5DZPG9ND4g3KjY1HR6Yo0LvChQmnRAPQBpBV-MAp6HNteXRbAb4ZOjQkQs9LhxBkj9KiZ7iRFdVKNTwNyNzNjCAQUWoc4RnipISl2kJcsKpauctW-D-Atkq7J5jEOyXcsA98" class="kg-image" alt="Creating an OpenAPI generator from scratch : From YAML to JetBrains HTTP Client" loading="lazy" width="624" height="197"><figcaption>a tree view of the generated client</figcaption></figure><p>We haven&apos;t done any work yet, and our generator is already spitting out things! Unfortunately, as we can see, all those files are empty. That&apos;s because our mustache template files also are empty. Let&apos;s fix that now!</p><p>We&apos;ll start by customizing the <code>JavaMagazineClientClientCodegen</code> to fit our needs. We want a very minimal implementation that fits in this article, so we&apos;ll actually decide to NOT implement any supporting files (the README), nor Models and instead focus solely on the API. The way to do this in a custom generator is to extend the <code>postProcessOperationsWithModels</code> from the <code>CodeGenConfig</code> interface. We change the <code>.zz</code> extension into <code>.http</code> files that will be recognised by IntelliJ. And because in this specific (simplistic) case, we will not need any alterations to the <code>OperationsMap</code> object we can actually only call the super method. Our final class looks like this : </p><pre><code class="language-java">package org.openapitools.codegen.languages;
import org.openapitools.codegen.*;
import java.io.File;
import java.util.*;
import org.openapitools.codegen.model.ModelMap;
import org.openapitools.codegen.model.OperationsMap;

public class JavaMagazineClientClientCodegen extends DefaultCodegen implements CodegenConfig {

    public CodegenType getTag() { return CodegenType.CLIENT; }

    public String getName() { return &quot;java-magazine-client&quot;; }

    public String getHelp() { return &quot;Generates a java-magazine-client client.&quot;; }

    public JavaMagazineClientClientCodegen() {
        super();

        outputFolder = &quot;generated-code&quot; + File.separator + &quot;java-magazine-client&quot;;
        apiTemplateFiles.put(&quot;api.mustache&quot;, &quot;.http&quot;);
        embeddedTemplateDir = templateDir = &quot;java-magazine-client&quot;;
        apiPackage = &quot;Apis&quot;;
    }

    @Override
    public OperationsMap postProcessOperationsWithModels(OperationsMap objs, List&lt;ModelMap&gt; allModels) {
        return super.postProcessOperationsWithModels(objs, allModels);
    }
}</code></pre><p><em>Note: At first glance, the method and variable names may look a bit like magic. It is because most of the logic comes from DefaultGenerator, and CodeGenConfig. If you feel lost, those two classes are where it&apos;s at.</em></p><p>Now that we have our baseline, what we want to do is work on our mustache files. Those files are basically templates that will be fed into the processing pipeline to generate our <code>.http</code> files.</p><p>We know we want one file per main API endpoint, with some documentation. We also want the <code>@name</code> unique identifier from the Jetbrains HTTP Client to be able <a href="https://www.jetbrains.com/help/idea/exploring-http-syntax.html?ref=lengrand.fr#http_request_names">to reference our code</a>. Finally, we want to add the supported content type for the calls.</p><p>If we look at the data object available for operations, we end up with this, where each <code>{{item}}</code> notation is the value of the item key inside the object.</p><pre><code class="language-mustache">## {{classname}}
{{#operations}}
{{#operation}}

### {{#summary}}{{summary}}{{/summary}}
# @name {{operationId}}
{{httpMethod}} {{basePath}}{{path}}
{{#consumes}}Content-Type: {{{mediaType}}}
{{/consumes}}
{{/operation}}
{{/operations}}</code></pre><p>We can see it clearly if we look at the object during processing</p><figure class="kg-card kg-image-card"><img src="https://lh7-us.googleusercontent.com/LW_g2JLAtaj1yq2LJeg0LBb0yTOy4ufSyurPNWW6XjKcdsv5GhVSASr6yWS7vYv3vmEuS9xmbZqzBa4Mqt76dg_Bv47gMoifUjxInC0-z1WkJYJRU3grz4RBApXJAZl4ZCx1irLQ69axWx5CQAe1fM0" class="kg-image" alt="Creating an OpenAPI generator from scratch : From YAML to JetBrains HTTP Client" loading="lazy" width="624" height="152"></figure><figure class="kg-card kg-image-card"><img src="https://lh7-us.googleusercontent.com/HL2UN4ZvPrLgENa7ergdKqrLfcLFEp_N8I46e973y1QsKWO7nzaiqSyjypk-YAdA7_fA_HdjIJR2PwI9zWAym9tVMtSLksXMMZYVmWj6oJbr84V4A90PfMWjqQZ468lQmc9eN8CJEY8h3L4apRAGcgw" class="kg-image" alt="Creating an OpenAPI generator from scratch : From YAML to JetBrains HTTP Client" loading="lazy" width="255" height="210"></figure><p><em>Note: Unfortunately, to my knowledge the best way to dive into the data model is still to go pause at runtime, I haven&apos;t yet found a complete data model documentation online. If you do, let me know!</em></p><p>Let&apos;s rerun the generation and see what we get now : </p><pre><code class="language-bash">$ ./mvnw package 
$ ./bin/generate-samples.sh bin/configs/java-magazine-client-petstore-new.yaml</code></pre><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://lh7-us.googleusercontent.com/hJ-53Q53ibGC0DuCaqu7yuoWpnLJB3d6g9SYUQ26-XeAVLW5JgavLfljBo08hnuyMwUsQo4Hgz5aBthp8L8jqFCpq1RBFWCz-PWFvpdofXDgR4o7QI_iyFKYMz4Afbet38-rEnzAuCXL4aCaL7ZUnZE" class="kg-image" alt="Creating an OpenAPI generator from scratch : From YAML to JetBrains HTTP Client" loading="lazy" width="624" height="111"><figcaption>the result of the generation of our client</figcaption></figure><p>Great! Only API files, and one per API, as wanted. Let&apos;s see what they contain! </p><pre><code class="language-http">## PetApi

### Add a new pet to the store
# @name addPet
POST http://petstore.swagger.io/v2/pet
Content-Type: application/json
Content-Type: application/xml

### Deletes a pet
# @name deletePet
DELETE http://petstore.swagger.io/v2/pet/{petId}</code></pre><p>Looks great to me! Let&apos;s try to run one of the calls</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://lh7-us.googleusercontent.com/LrHrgXoLj4y9_lyYCzAbtLGle6eDCqv-eyDWOVQLkCkdsb2LskNOrdhBEO0c0wDMuRb9EHbh3i21TpLEcntMyd_qhHqeYgIsQDzDOD7FCrf6VNsaxe3RY1OzrzB21uqo1SxwzlF7lXZ7oqyno3FAGSo" class="kg-image" alt="Creating an OpenAPI generator from scratch : From YAML to JetBrains HTTP Client" loading="lazy" width="624" height="272"><figcaption>Running one of the calls that&apos;s just been generated</figcaption></figure><p>It works just fine, and we get a 200 response as well. Success! </p><p>Now, there&apos;s only one little issue. The variables! &#xA0;In the Jetbrains HTTP Client format, <a href="https://www.jetbrains.com/help/idea/exploring-http-syntax.html?ref=lengrand.fr#using_request_vars">variables are written as</a> <code>{{variable}}</code>. OpenAPI implementations only have single braces. We need to fix that!</p><p>What we&apos;ll be doing here is implement <a href="https://mustache.github.io/mustache.5.html?ref=lengrand.fr">a custom mustache lambda</a> that doubles up the braces when it finds them. The lambda is essentially a string replacement</p><pre><code class="language-java">public static class DoubleMustacheLambda implements Mustache.Lambda {
        @Override
        public void execute(Template.Fragment fragment, Writer writer) throws IOException {
            String text = fragment.execute();
            writer.write(text
                    .replaceAll(&quot;\\{&quot;, &quot;{{&quot;)
                    .replaceAll(&quot;}&quot;, &quot;}}&quot;)
            );
        }
    }</code></pre><p>In order to make it available in our Generator, the openapi generator library offers the same mechanism as for the rest : We have to override a ready-made method.</p><pre><code class="language-java">    @Override
    protected ImmutableMap.Builder&lt;String, Mustache.Lambda&gt; addMustacheLambdas() {

        return super.addMustacheLambdas()
                .put(&quot;doubleMustache&quot;, new JavaMagazineClientClientCodegen.DoubleMustacheLambda());
    }</code></pre><p>The code above is added to our <code>JavaMagazineClientClientCodegen</code> class. </p><p>Next, we also need to modify our mustache template to add that lambda at the right location (around the <code>path</code> parameter). If that path is a variable, the braces will then be doubled </p><pre><code class="language-mustache">## {{classname}}
{{#operations}}
{{#operation}}

### {{#summary}}{{summary}}{{/summary}}
# @name {{operationId}}
{{httpMethod}} {{basePath}}{{#lambda.doubleMustache}}{{path}}{{/lambda.doubleMustache}}
{{#consumes}}Content-Type: {{{mediaType}}}
{{/consumes}}
{{/operation}}
{{/operations}}</code></pre><p>Et voil&#xE0;! Running the sample again, we can now use variables as they are meant to be inside IntelliJ!</p><p>In the sample below, I&apos;m using the following local environment file : </p><pre><code class="language-json">{
  &quot;dev&quot;: {
    &quot;petId&quot;: 3
  }
}</code></pre><p>There is still a lot more to do with this generator. READMEs, payload, auth, headers, &#x2026; But now it&apos;s a matter of updating the mustache files as we want. </p><p>I&apos;d love to have a more fleshed out generator, because it&apos;d be an amazing and cheap way together with <a href="https://www.jetbrains.com/help/idea/http-client-cli.html?ref=lengrand.fr">the client CLI</a> to have a great automated integration tests pipeline and get people running in second with your API.</p><p>I hope this article made you feel like trying to create your own OpenAPI generator. The only limit is your imagination! And as you can see, the merging process is actually relatively pleasant, because the volunteers of the project LOVE to see people bringing out new ideas to life.</p><p>Happy to hear your thoughts, as always!</p>]]></content:encoded></item><item><title><![CDATA[[Unit] Testing Supabase in Kotlin using Test Containers - PART 2]]></title><description><![CDATA[In this article we continue diving into TestContainers and Supabase, and run unit tests against a full local self-hosted Supabase.  ]]></description><link>https://lengrand.fr/unit-testing-supabase-in-kotlin-using-test-containers-part-2/</link><guid isPermaLink="false">6532e76102fb1c43eb7febc4</guid><category><![CDATA[kotlin]]></category><category><![CDATA[supabase]]></category><category><![CDATA[testcontainers]]></category><dc:creator><![CDATA[Julien]]></dc:creator><pubDate>Fri, 20 Oct 2023 21:16:46 GMT</pubDate><media:content url="https://lengrand.fr/content/images/2023/10/Screenshot-2023-10-11-at-23.57.36-1.png" medium="image"/><content:encoded><![CDATA[<img src="https://lengrand.fr/content/images/2023/10/Screenshot-2023-10-11-at-23.57.36-1.png" alt="[Unit] Testing Supabase in Kotlin using Test Containers - PART 2"><p><em>TL;DR : You can run a full Supabase instance inside Test Containers quite easily. <a href="https://github.com/jlengrand/supabase-testcontainers-kotlin?ref=lengrand.fr">See this repository</a>.</em></p><p><a href="https://lengrand.fr/unit-testing-supabase-in-kotlin/">In my last article</a>, I was listing a few attempts I had done at running tests against my Kotlin Supabase application. The way the Supabase-Kt library is built makes it hard to mock, and I ended up building a minimal Docker Compose setup that was mimicking a Supabase instance. </p><p>In this second part, we&apos;re gonna push the madness further and actually run a FULL SUPABASE instance locally, still using <a href="https://testcontainers.com/?ref=lengrand.fr">Test Containers</a>. </p><p>Right after I finished pushing my repository last week, I realised that Supabase actually offered <a href="https://supabase.com/docs/guides/self-hosting/docker?ref=lengrand.fr">a Docker Compose file to self-host their platform</a>. So I decided to push the madness further and see how easy it was to use that file inside TestContainers. In short : Relatively easy. </p><h3 id="the-setup">The setup</h3><p>The setup isn&apos;t actually much different from my homecrafted Docker Compose. version. Here it is in its entirety. </p><p>A few notable things: </p><ul><li>I&apos;m relying on a local clone of Supabase, and point a ComposeContainer to the <code>src/test/resources/supabase/docker/docker-compose.yml</code> file. </li><li>The setup uses an <code>.env</code> file, so I use a <code>dotenv</code> implementation to grab the parameters there and make the code slightly dynamic.</li><li>I have to run a database statement to populate and flush my database in between tests. The Docker Compose setup from Supabase comes with persistent volumes, which needs to be accounted for. </li><li>I don&apos;t have test for those here, but all services (auth, functions, storage), ... should actually be supported, given that we&apos;re running a full local instance.</li></ul><pre><code class="language-kotlin">import io.github.cdimascio.dotenv.dotenv
import io.github.jan.supabase.SupabaseClient
import io.github.jan.supabase.createSupabaseClient
import io.github.jan.supabase.postgrest.Postgrest
import kotlinx.coroutines.runBlocking
import org.junit.jupiter.api.AfterAll
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.BeforeAll
import org.junit.jupiter.api.Test
import org.testcontainers.containers.ComposeContainer
import org.testcontainers.junit.jupiter.Container
import org.testcontainers.junit.jupiter.Testcontainers
import java.io.File
import java.sql.DriverManager


@Testcontainers
class MainKtTest {

    @Test
    fun testEmptyPersonTable(){
        runBlocking {
            val result = getPerson(supabaseClient)
            assertEquals(0, result.size)
        }
    }

    @Test
    fun testSavePersonAndRetrieve(){
        val randomPersons = listOf(Person(&quot;Jan&quot;, 30), Person(&quot;Jane&quot;, 42))

        runBlocking {
            val result = savePerson(randomPersons, supabaseClient)
            assertEquals(2, result.size)
            assertEquals(randomPersons, result.map { it.toPerson() })

            val fetchResult = getPerson(supabaseClient)
            assertEquals(2, fetchResult.size)
            assertEquals(randomPersons, fetchResult.map { it.toPerson() })
        }
    }

    companion object {

        private const val DOCKER_COMPOSE_FILE = &quot;src/test/resources/supabase/docker/docker-compose.yml&quot;
        private const val ENV_LOCATION = &quot;src/test/resources/supabase/docker/.env&quot; // We grab the JWT token from here

        val dotenv = dotenv{
            directory = File(ENV_LOCATION).toString()
        }

        private val jwtToken = dotenv[&quot;SERVICE_ROLE_KEY&quot;]
        private val dbPassword = dotenv[&quot;POSTGRES_PASSWORD&quot;]
        private val db = dotenv[&quot;POSTGRES_DB&quot;]

        private lateinit var supabaseClient: SupabaseClient

        @Container
        var container: ComposeContainer = ComposeContainer(File(DOCKER_COMPOSE_FILE))
            .withExposedService(&quot;kong&quot;, 8000)
            .withExposedService(&quot;db&quot;, 5432)   // Handy but not required

        @JvmStatic
        @AfterAll
        fun tearDown() {
            val dbUrl = container.getServiceHost(&quot;db&quot;, 5432) + &quot;:&quot; + container.getServicePort(&quot;db&quot;, 5432)

            val jdbcUrl = &quot;jdbc:postgresql://$dbUrl/$db&quot;
            val connection = DriverManager.getConnection(jdbcUrl, &quot;postgres&quot;, dbPassword)

            try {
                val query = connection.prepareStatement(
                    &quot;&quot;&quot;
            drop table public.person;
        &quot;&quot;&quot;
                )

                query.executeQuery()
            } catch (ex: Exception) {
                println(ex)
            }
        }

        @JvmStatic
        @BeforeAll
        fun setUp() {
            val supabaseUrl = container.getServiceHost(&quot;kong&quot;, 8000) + &quot;:&quot; + container.getServicePort(&quot;kong&quot;, 8000)
            val dbUrl = container.getServiceHost(&quot;db&quot;, 5432) + &quot;:&quot; + container.getServicePort(&quot;db&quot;, 5432)

            supabaseClient = createSupabaseClient(
                supabaseUrl = &quot;http://$supabaseUrl&quot;,
                supabaseKey = jwtToken
            ) {
                install(Postgrest)
            }

            val jdbcUrl = &quot;jdbc:postgresql://$dbUrl/$db&quot;
            val connection = DriverManager.getConnection(jdbcUrl, &quot;postgres&quot;, dbPassword)


            try {
                val query = connection.prepareStatement(
                    &quot;&quot;&quot;
                create table
                    public.person (
                                    id bigint generated by default as identity not null,
                                    timestamp timestamp with time zone null default now(),
                                    name character varying null,
                                    age bigint null
                ) tablespace pg_default;
                &quot;&quot;&quot;
                )

                query.executeQuery()
            } catch (ex: Exception) {
                println(&quot;Error is fine here. This should actually run only once&quot;)
                println(ex) // Might be fine, this should actually run only once
            }
        }
    }
}</code></pre><p>To achieve those results, a few manual steps are required. The Docker Compose file provided by Supabase uses <code>container_name</code> parameters, <a href="https://github.com/testcontainers/testcontainers-java/pull/2741?ref=lengrand.fr">which aren&apos;t supported by Test Containers</a>. </p><p>I needed to : </p><ul><li>Clone the Supabase repository locally</li><li>Copy the env file</li><li>Run some magic to remove <code>container_name</code></li><li>Once in a while, the Supabase repository will have to be pulled </li></ul><h3 id="the-results">The results</h3><p>The results are as outrageous as I expected them to be, if not more : <strong>All tests are running fine, though it takes almost 1 minute to run them</strong>. The <code>ComposeContainer</code> is starting no less than 12 containers (!!!) so it is to be expected.</p><p>Obviously, that setup is not to be used for unit testing. That being said, I find it absolutely freaking cool to be able to recreate your complete environment locally that easily, and I&apos;d definitely consider that an option for bigger integration tests. The confidence I didn&apos;t have with my home brewed Docker Compose file is much higher now, given that it&apos;s directly provided by Supabase. No network needed to run my tests, pretty cool.</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://lengrand.fr/content/images/2023/10/image.png" class="kg-image" alt="[Unit] Testing Supabase in Kotlin using Test Containers - PART 2" loading="lazy" width="2000" height="343" srcset="https://lengrand.fr/content/images/size/w600/2023/10/image.png 600w, https://lengrand.fr/content/images/size/w1000/2023/10/image.png 1000w, https://lengrand.fr/content/images/size/w1600/2023/10/image.png 1600w, https://lengrand.fr/content/images/size/w2400/2023/10/image.png 2400w" sizes="(min-width: 720px) 720px"><figcaption>Gradle running those massive tests just fine</figcaption></figure><h3 id="what-more">What more</h3><p>My original complete intent was to build a small layer on top of the Docker Compose file, kinda like AtomoicJar does it with its <a href="https://github.com/testcontainers/testcontainers-java/blob/57ca6ad4f0a6ee8bb5feaf428b5c66c7d25259c4/modules/kafka/src/main/java/org/testcontainers/containers/KafkaContainer.java?ref=lengrand.fr#L28">modules</a>. It would have been cool to have a simple interface for a Supabase instance to start, while providing a locally for starting scripts, user roles, maybe a new set of credentials, ...</p><p>Here is how they describe it for NGinx for example. I would have loved to have something similar : </p><pre><code class="language-Java">@Rule
public NginxContainer&lt;?&gt; nginx = new NginxContainer&lt;&gt;(NGINX_IMAGE)
    .withCopyFileToContainer(MountableFile.forHostPath(tmpDirectory), &quot;/usr/share/nginx/html&quot;)
    .waitingFor(new HttpWaitStrategy());</code></pre><p>All of the implementation I&apos;ve seen extend from <code>GenericContainer</code> though, not <code>ComposeContainer</code> so I&apos;ve decided to hold that off and keep it simple for now. </p><p>Could maybe be something for the future, who knows. </p><h3 id="in-conclusion">In conclusion</h3><p>That was a fun experiment, in which I&apos;ve learnt more about TestContainers &#x1F60A;. I&apos;m as happy as usual with the way Supabase shows love for their users. Providing a seemless Docker Compose like this allows for a great experience. And I&apos;m also impressed with TestContainers and how they can run such complex flaws without breaking a sweat! </p><p>If anything, I&apos;d like them to at least ignore the <code>container_name</code> parameter if possible. I&apos;ve seen many folks being blocked by it, and I can imagine many cases, like this one where people are not in control of their compose file. I don&apos;t necessarily ask for support, but an option to ignore without throwing an exception would be great.</p><p>That&apos;s it folks, till next time! </p>]]></content:encoded></item><item><title><![CDATA[[Unit] Testing Supabase in Kotlin using Test Containers]]></title><description><![CDATA[In this article, I'll dive into several methods I've been looking into to unit test a Kotlin application using Supabase and why I finally decided to go for a Docker Compose / Test Containers solution.]]></description><link>https://lengrand.fr/unit-testing-supabase-in-kotlin/</link><guid isPermaLink="false">6526bacc02fb1c43eb7fea20</guid><category><![CDATA[supabase]]></category><category><![CDATA[kotlin]]></category><category><![CDATA[docker]]></category><category><![CDATA[jvm]]></category><dc:creator><![CDATA[Julien]]></dc:creator><pubDate>Wed, 11 Oct 2023 22:01:10 GMT</pubDate><media:content url="https://lengrand.fr/content/images/2023/10/Screenshot-2023-10-11-at-23.57.36.png" medium="image"/><content:encoded><![CDATA[<img src="https://lengrand.fr/content/images/2023/10/Screenshot-2023-10-11-at-23.57.36.png" alt="[Unit] Testing Supabase in Kotlin using Test Containers"><p><em>TL;DR : The easiest way I found to test my database service is to mimick Supabase using Docker Compose and Test Containers. <a href="https://github.com/jlengrand/supabase-mock-demo-kotlin?ref=lengrand.fr">Here&apos;s the code</a></em></p><p><em>(Also, have a look at Part 2 of that blog <a href="https://lengrand.fr/unit-testing-supabase-in-kotlin-using-test-containers-part-2/">here</a>).</em></p><p>In case you don&apos;t know it, I&apos;m a big fan of <a href="https://supabase.com/?ref=lengrand.fr">Supabase</a>. I love that they&apos;re a viable alternative to Firebase. I love that they&apos;re <a href="https://github.com/supabase/supabase/blob/master/apps/docs/public/img/supabase-architecture.png?ref=lengrand.fr">built on top of Open-Source</a> pieces. I love <a href="https://supabase.com/blog/chatgpt-supabase-docs?ref=lengrand.fr">how innovative they are</a>, and <a href="https://github.com/supabase/postgres_lsp?ref=lengrand.fr">how much they give back to the community</a>. And as you already know it, I love <a href="https://kotlinlang.org/?ref=lengrand.fr">Kotlin</a> as well. </p><p>Lately, I&apos;ve been building a side-project which consists of a <a href="https://ktor.io/?ref=lengrand.fr">Ktor</a> webapp, and uses <a href="https://github.com/supabase-community/supabase-kt?ref=lengrand.fr">Supabase-Kt</a> to communicate with the database. I&apos;ve been looking into ways to test my Kotlin component that interacts with the database, and it&apos;s been harder than expected.</p><p>In this article, I&apos;ll dive into several methods I&apos;ve been looking into and why I finally decided to go for a <a href="https://docs.docker.com/compose/?ref=lengrand.fr">Docker Compose</a> / <a href="https://testcontainers.com/?ref=lengrand.fr">Test Containers</a> solution. You can check the example repository <a href="https://github.com/jlengrand/supabase-mock-demo-kotlin?ref=lengrand.fr">here</a> AND THE FINAL CODE <a href="https://lengrand.fr/p/f6b63121-9525-4f77-a31f-f11b22f50809/#final-solution-docker-compose-and-test-containers">HERE</a>.</p><h3 id="what-i-want-to-achieve">What I want to achieve</h3><p>Let&apos;s imagine a minimal code example that contains a <code>Person</code> data class, and wants to save/fetch persons via a <code>SupabaseClient</code>. It can look like this:</p><pre><code class="language-kotlin">@Serializable
data class Person (val name: String, val age: Int)

@Serializable
data class ResultPerson (
    val id: Int,
    val name: String,
    val age: Int,
    val timestamp: String
)

fun main() {
    val supabaseClient = createSupabaseClient(
        supabaseUrl = &quot;&quot;,
        supabaseKey = &quot;&quot;
    ) {install(Postgrest)}

    runBlocking {
        savePerson(listOf(Person(&quot;Jan&quot;, 30), Person(&quot;Jane&quot;, 42)), supabaseClient)
    }
}

suspend fun getPerson(client: SupabaseClient): List&lt;ResultPerson&gt; {
    return client
        .postgrest[&quot;person&quot;]
        .select().decodeList&lt;ResultPerson&gt;()
        .filter { it.age &gt; 18 }
}


suspend fun savePerson(persons: List&lt;Person&gt;, client: SupabaseClient): List&lt;ResultPerson&gt; {
    val adults = persons.filter { it.age &gt; 18 }

    return client
        .postgrest[&quot;person&quot;]
        .insert(adults)
        .decodeList&lt;ResultPerson&gt;()
}
</code></pre><p>The SQL definition of our table looks like this</p><pre><code class="language-SQL">create table
    public.person (
                    id bigint generated by default as identity not null,
                    timestamp timestamp with time zone null default now(),
                    name character varying null,
                    age bigint null
) tablespace pg_default;</code></pre><p>We want to be able to test that our functions behave properly. For the sake of this minimal example, I&apos;ve decided to filter all non adults, but you can imagine any other use case where the functions contain some business logic. </p><h3 id="first-attempt-mock-supabase">First attempt : Mock Supabase</h3><p>When unit testing code using third parties that I don&apos;t have control over, my first reflex is to try and mock it.</p><p>It stopped being fun really quickly. The Supabase-Kt library is making use of a lot of inline function and I ended up having to &#xA0;mock more and more parts of the library and never managed to get a functional tests.</p><p>The short version is that because they are <em>as the name indicates, </em>inlined, <a href="https://stackoverflow.com/questions/56753396/does-mockk-support-suspend-inline?ref=lengrand.fr">inline functions cannot be mocked in Kotlin</a>. So that was the end of that experiment</p><p>The <code>MainKtTestMock</code> file of my example repository reflects that attempt.</p><pre><code class="language-kotlin">class MainKtTestMock {

    private lateinit var supabaseClient : SupabaseClient

    @BeforeTest
    fun setUp() {

        supabaseClient = mockk&lt;SupabaseClient&gt;()
        val postgrest = mockk&lt;Postgrest&gt;()
        val postgrestBuilder = mockk&lt;PostgrestBuilder&gt;()
        val postgrestResult = PostgrestResult(body = null, headers = Headers.Empty)

        every { supabaseClient.postgrest } returns postgrest
        every { postgrest[&quot;path&quot;] } returns postgrestBuilder
        coEvery { postgrestBuilder.insert(values = any&lt;List&lt;Path&gt;&gt;()) } returns postgrestResult
    }

    @Test
    fun testSavePerson(){
        val randomPersons = listOf(Person(&quot;Jan&quot;, 30), Person(&quot;Jane&quot;, 42))

        runBlocking {
            val result = savePerson(randomPersons, supabaseClient)
            assertEquals(2, result.size)
            assertEquals(randomPersons, result.map { it.toPerson() })
        }
    }
}</code></pre><p>Here&apos;s the final error I encountered : </p><pre><code>java.lang.IllegalStateException: Plugin rest not installed or not of type Postgrest. Consider installing Postgrest within your supabase client builder
	at io.github.jan.supabase.postgrest.PostgrestKt.getPostgrest(Postgrest.kt:172)
	at MainKtTestMock$setUp$1.invoke(MainKtTestMock.kt:34)
	at MainKtTestMock$setUp$1.invoke(MainKtTestMock.kt:34)</code></pre><h3 id="second-attempt-encapsulate-the-supabase-client">Second attempt: Encapsulate the Supabase Client</h3><p>My second attempt was to get around the problem by encapsulating the problematic client inside a class of mine that I can then control.</p><p>It can be as simple as this :</p><pre><code class="language-Kotlin">class DatabaseClient(private val client: SupabaseClient){
    suspend fun savePerson(persons: List&lt;Person&gt;): List&lt;ResultPerson&gt; {
        val adults = persons.filter { it.age &gt; 18 }

        return client
            .postgrest[&quot;person&quot;]
            .insert(adults)
            .decodeList&lt;ResultPerson&gt;()
    }
}</code></pre><p>And my test can then look like this (see <code>MainKtTestSubclass</code>):</p><pre><code class="language-kotlin">class MainKtTestSubclass {

    private lateinit var client : DatabaseClient

    @BeforeTest
    fun setUp() {
        client = mockk&lt;DatabaseClient&gt;()
        coEvery { client.savePerson(any&lt;List&lt;Person&gt;&gt;()) } returns listOf(ResultPerson(2, &quot;name_2&quot;, 2, &quot;timestamp_2&quot;))
    }

    @Test
    fun testSavePerson(){
        val fakePersons = listOf(Person(&quot;name_1&quot;, 1), Person(&quot;name_2&quot;, 2))

        runBlocking {
            val result = client.savePerson(fakePersons)
            assertEquals(2, result.size)
        }
    }
}</code></pre><p>My main issue now is that because I have to indicate every single time what my output should be. It also just displaces the problem, because I don&apos;t really have any nice and clean way to check that my business logic works as intended, since I&apos;m mocking it. </p><h3 id="third-attempt-ktor-mock">Third attempt : Ktor mock</h3><p>The main contributor of the project gave another possible workaround <a href="https://github.com/supabase-community/supabase-kt/issues/298?ref=lengrand.fr">in the GitHub issue I created</a> : mock the internal Ktor engine of the Supabase client.</p><p>See the <code>MainKtTestMockEngine</code> : </p><pre><code class="language-kotlin">class MainKtTestMockEngine {

    private val supabaseClient : SupabaseClient = createSupabaseClient(&quot;&quot;, &quot;&quot;,) {
        httpEngine = MockEngine { _ -&gt;
            respond(Json.encodeToString(Person.serializer(), Person(&quot;name_1&quot;, 16)))
        }
    }

    @Test
    fun testSavePerson(){
        val randomPersons = listOf(Person(&quot;Jan&quot;, 30), Person(&quot;Jane&quot;, 42))

        runBlocking {
            val result = savePerson(randomPersons, supabaseClient)
            assertEquals(2, result.size)
            assertEquals(randomPersons, result.map { it.toPerson() })
        }
    }
}</code></pre><p>This is actually not a bad idea, it&apos;s light and it gets the job done is a clear and readable way. Those tests are also fast to run.</p><p>My main issue with this method would be that to test my business logic I&apos;d have to dive into the received requests of the mock engine every time, which is a little cumbersome and prone to lots of maintenance.</p><p>I do want to investigate it further though.</p><h3 id="proposed-solution-test-supabase-db">Proposed solution : Test Supabase db</h3><p>Now, one semi obvious solution would be to fire up a test database in supabase itself, and test there! </p><p>That&apos;d work. I even do it to test my release deployments!</p><p>It has some obvious downsides though:</p><ul><li>We couldn&apos;t be further away from unit tests, since we&apos;re testing on the cloud</li><li>Tests run slower, require internet, and also require a setup database. Cleanup can also be a mess</li><li>I&apos;d be terrified to run that against the wrong database</li><li>It uses my bandwidth and projects, that either are limited, or I have to pay for!</li></ul><h3 id="final-solution-docker-compose-and-test-containers">Final solution : Docker Compose and Test Containers</h3><p>I had one last idea, and that&apos;s the one I&apos;ve decided to stick with for now. It leverages the fact that at its core, Supabase is built on a lot of Open-Source. And when we&apos;re using the Supabase client, we&apos;re essentially interacting with a glorified PostgreSQL / <a href="https://postgrest.org/?ref=lengrand.fr">postgrest</a> combo! </p><p>I&apos;ve decided to create a Docker Compose setup that would mimick the actual Supabase production setup and connect to this instead. </p><p>A few things had to be taken into account for this to work :</p><ul><li>I had to redirect all my postgrest calls to <code>/rest/v1</code>, which is the path that Supabase expects. So a <code>GET</code> on <code>/persons</code> should actually be on <code>/rest/v1/persons</code>.</li><li><code>postgrest</code> uses <a href="https://jwt.io/?ref=lengrand.fr">JSON Web Tokens</a> for authentication, so we have to set that up as part of the test class.</li></ul><p>One last thing to note is that I would have to do MORE work in case I start using any of the other services of Supabase (say auth for example). </p><p>The Docker Compose setup looks like this : </p><pre><code class="language-dockerfile">version: &apos;3&apos;

services:
  ################
  # postgrest-db #
  ################
  postgrest-db:
    image: postgres:16-alpine
    ports:
      - &quot;5432:5432&quot;
    environment:
      - POSTGRES_USER=${POSTGRES_USER}
      - POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
      - POSTGRES_DB=${POSTGRES_DB}
      - DB_SCHEMA=${DB_SCHEMA}
    volumes:
      - &quot;./initdb:/docker-entrypoint-initdb.d&quot;
    networks:
      - postgrest-backend
    restart: always

  #############
  # postgrest #
  #############
  postgrest:
    image: postgrest/postgrest:latest
    ports:
      - &quot;3000:3000&quot;
    environment:
      - PGRST_DB_URI=postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@postgrest-db:5432/${POSTGRES_DB}
      - PGRST_DB_SCHEMA=${DB_SCHEMA}
      - PGRST_DB_ANON_ROLE=${DB_ANON_ROLE}
      - PGRST_JWT_SECRET=${PGRST_JWT_SECRET}
    networks:
      - postgrest-backend
    restart: always

  #############
  # Nginx     #
  #############
  nginx:
    image: nginx:alpine
    restart: always
    tty: true
    volumes:
      - ./nginx.conf:/etc/nginx/conf.d/default.conf
    ports:
      - &quot;80:80&quot;
      - &quot;443:443&quot;
    networks:
      - postgrest-backend

networks:
  postgrest-backend:
    driver: bridge</code></pre><p>Note that a few auxiliary files are needed for this to work. You can find everything in the <code>test/resources</code> folder of <a href="https://github.com/jlengrand/supabase-mock-demo-kotlin/tree/main/src/test/resources?ref=lengrand.fr">the example GitHub repository</a>.</p><p>There is :</p><ul><li>A short <code>nginx.conf</code> file. </li><li>An SQL file to setup the database (note that in my actual repo, this guy already exists since I need it to setup production :)).</li><li>a <code>.env</code> file to list all my environment variables</li></ul><p>Once that is done, it is already possible to run <code>$ docker-compose up -d</code> and to run your application against <code>localhost</code> like if you were interacting with the real Supabase. &#xA0;(don&apos;t forget to call <code>$docker-compose down --remove-orphans -v</code> to kill and delete all containers once you&apos;re done).</p><p>To make the magic complete, we&apos;re gonna use the power of TestContainers to run this as unit/integration tests. My final <code>MainKtTestTestContainers</code> test class looks like this : </p><pre><code class="language-kotlin">import io.github.jan.supabase.SupabaseClient
import io.github.jan.supabase.createSupabaseClient
import io.github.jan.supabase.postgrest.Postgrest
import kotlinx.coroutines.runBlocking
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import org.testcontainers.containers.ComposeContainer
import org.testcontainers.junit.jupiter.Container
import org.testcontainers.junit.jupiter.Testcontainers
import java.io.File

@Testcontainers
class MainKtTestTestContainers {

    // The jwt token is calculated manually (https://jwt.io/) based on the private key in the docker-compose.yml file, and a payload of {&quot;role&quot;:&quot;postgres&quot;} to match the user in the database
    private val jwtToken = &quot;eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoicG9zdGdyZXMifQ.88jCdmcEuy2McbdwKPmuazNRD-dyD65WYeKIONDXlxg&quot;

    private lateinit var supabaseClient: SupabaseClient

    @Container
    var environment: ComposeContainer =
        ComposeContainer(File(&quot;src/test/resources/docker-compose.yml&quot;))
            .withExposedService(&quot;postgrest-db&quot;, 5432)
            .withExposedService(&quot;postgrest&quot;, 3000)
            .withExposedService(&quot;nginx&quot;, 80)

    @BeforeEach
    fun setUp() {
        val fakeSupabaseUrl = environment.getServiceHost(&quot;nginx&quot;, 80) +
                &quot;:&quot; + environment.getServicePort(&quot;nginx&quot;, 80)

        supabaseClient = createSupabaseClient(
            supabaseUrl = &quot;http://$fakeSupabaseUrl&quot;,
            supabaseKey = jwtToken
        ) {
            install(Postgrest)
        }
    }

    @Test
    fun testEmptyPersonTable(){
        runBlocking {
            val result = getPerson(supabaseClient)
            assertEquals(0, result.size)
        }
    }

    @Test
    fun testSavePersonAndRetrieve(){
        val randomPersons = listOf(Person(&quot;Jan&quot;, 30), Person(&quot;Jane&quot;, 42))

        runBlocking {
            val result = savePerson(randomPersons, supabaseClient)
            assertEquals(2, result.size)
            assertEquals(randomPersons, result.map { it.toPerson() })

            val fetchResult = getPerson(supabaseClient)
            assertEquals(2, fetchResult.size)
            assertEquals(randomPersons, fetchResult.map { it.toPerson() })
        }
    }
}</code></pre><p>All of the magic happens at the beginning, to setup a fake Supabase URL and connect to it. Once that is done, we can write our tests as easily as ever, since we&apos;re actually interacting with an actual light Supabase clone! (For reference, the tests take about 2 seconds to run on my machine)</p><h3 id="a-word-of-conclusion">A word of conclusion</h3><p>It took me a little while to get all these tests running, but I&apos;m very happy about the final result. It might not be the best solution for production grade apps, but the trade off of running test containers for my side project definitely makes up for the fact that I literally have no boilerplate to run and can avoid using mocks. </p><p>I&apos;ll check in the future how much I can extend the docker compose image as I get to use more Supabase services. Maybe it would be nice of Supabase to offer that image themselves so we can test easily and avoid using the cloud where not necessary :).</p><p>Check <a href="https://lengrand.fr/unit-testing-supabase-in-kotlin-using-test-containers-part-2/">here</a> for part 2 of the article.</p>]]></content:encoded></item><item><title><![CDATA[Thoughts on the Climate crisis and being a Developer (Advocate)]]></title><description><![CDATA[It's quite clear by now that we humans have a strong impact on the Earth's  climate. As a new developer advocate, my impact on the world around me is something I have in the back of my head pretty much every day. I want to go with you through some of this today.]]></description><link>https://lengrand.fr/thoughts-on-climate-change-and-being-a-developer-advocate/</link><guid isPermaLink="false">6310f67f1c11530500c795f4</guid><category><![CDATA[devrel]]></category><category><![CDATA[climate]]></category><category><![CDATA[tech]]></category><dc:creator><![CDATA[Julien]]></dc:creator><pubDate>Tue, 05 Sep 2023 17:10:08 GMT</pubDate><media:content url="https://lengrand.fr/content/images/2023/09/DEVFEST-2022-302.jpg" medium="image"/><content:encoded><![CDATA[<img src="https://lengrand.fr/content/images/2023/09/DEVFEST-2022-302.jpg" alt="Thoughts on the Climate crisis and being a Developer (Advocate)"><p></p><p>This blog is not meant to judge anyone, nor to make anyone feel guilty in any way. Apologies if it comes out the wrong way. </p><p>What I mean to share here is a <strong>personal</strong> journey we&apos;ve embarked on as a family a couple years back. I do realise how privileged we are to be able to have such a position. Oh and by the way I&apos;m always looking for ways to do better, so please do voice your opinion if you have good ideas.</p><p>It&apos;s quite clear by now that we humans have a strong impact on the Earth&apos;s &#xA0;climate. And it&apos;s also quite clear things aren&apos;t changing as fast as they need to to make a meaningful difference globally. </p><p>As a <em>new</em> Developer Advocate (I&apos;ve only been in the field for two years officially), my impact on the world around me is something I keep in the back of my head pretty much every day. I want to go with you through some of these thoughts today.</p><h2 id="advocating-through-individual-actions">Advocating through individual actions?</h2><p>As an individual, there are a few directions you can go to reduce your impact on climate: </p><ul><li><strong>Individual actions</strong> : That&apos;s the easiest thing to do, but also what has the least amount of impact. Reducing your footprint might feel good, but it doesn&apos;t really help in the grand scheme of things.</li><li><strong>Group actions</strong> : Voting, supporting political groups, protesting, boycotting, ... Basically helping change the system any way you can as a part of society. </li></ul><p>Thing is, part of my job as a Developer Advocate is to be public. Of course, I&apos;m not a Kylian Mbapp&#xE9;, or a Rihanna, ... I&apos;m pretty much a nobody. But I still have a voice in certain niches. Through my job I am spending some of my time in online communities. I like to believe that my individual actions (and those of my family) can ripple to others because I have a few means of promoting them around me.</p><p>In this blog, I will focus on the individual actions that I do to reduce my impact on the climate. I&apos;ll also limit myself &#xA0;to the ones that have a relationship with my job as Developer Advocate. I won&apos;t go into investing or producing food for example. I have enough other blogs on these topics &#x1F60A;.</p><h2 id="traveling-conferences-and-the-airports-life">Traveling, conferences and the airport&apos;s life</h2><p>Most of the Developer Advocates I meet are public folks. They travel A LOT. If you want to maximize your presence as a Developer Advocate, you want to participate in as many events as possible; which typically means flying. The funny thing is, often the public figures are the same from one conference to the next; meaning that as a speaker 60% of the folks you meet are the same in Germany as they were in UK the week before. </p><p>I can&apos;t help but think how destructive we are as a group living this completely unsustainable way of living. &quot;it&apos;s part of the job&quot;. When I see &quot; ATL &#x2708;&#xFE0F; CDG &#x2708;&#xFE0F; TYO&#x2708;&#xFE0F; WLG &#x2708;&#xFE0F; &#xA0;AMS &#x2708;&#xFE0F; ATL&quot; on my favourite social media, the first thing I think is that this flight path alone would place me <a href="https://bonpote.com/en/why-stop-flying-to-tackle-climate-change/?ref=lengrand.fr">in the top 1% of flyers worldwide</a>.</p><p>I&apos;ve managed to not board a plane since 2019. Not for holidays, not for work. If I go to a conference, I&apos;ll take the train to go there. If I can&apos;t, well then I&apos;ll either send someone who lives closer, or simply not go. And it&apos;s not even a sacrifice, actually. I love the train. I can work in it, read books, no security gates to go through, or rough passport control, not dumb dumb 2 hours before rule, no shoes removal involved. The only thing I may miss is the mini bar, but that&apos;s a me thing &#x1F37E;.</p><p>I&apos;m not saying I&apos;ll never fly. That&apos;d be hypocritical. For example I dream of visiting Korea. But I want to reduce it as much as I can, for as long as I can because it&apos;s by far the easiest way to cut my emissions. Flying accounted for 40% of my yearly footprint until 2019. And I wasn&apos;t a Developer Advocate yet back then. This is something I made clear when joining my current employer, and they agreed. If we have to go to many conferences overseas, we&apos;ll hire a developer advocate over there to do the work. </p><p>And to be fair, it&apos;s not like our company suffers from this. Instead of travelling, we&apos;re releasing more samples, more content, more experiments, and I sincerely think we&apos;re simply serving our customers better. Most of them don&apos;t go to conferences. They want stuff that works well, with a good developer experience, that is well documented, and feel welcome with the product. Heck, it&apos;s even more inclusive in a way : Not everyone can afford to take days off and travel to go and see you. Online content is available for everyone to see, and it&apos;s more scalable by definition. Of course, this is personal experience and your mileage may vary. </p><p>One of the things that infuriates me though, is that even to this day flying is the cheapest option anywhere. Taking the train from Amsterdam to Barcelona takes me 2 days and costs 400 euros, while flying there is 30 euros and only 2 hours. Our governments have to do better...</p><p>One last thing : Until about 100 years ago, we&apos;ve never been used to travel that much. People would do one trip to Italy in their life, and be amazed. Travelling has become a complete commodity. But we&apos;ve also never been as connected as today! Phones, internet, apps, MOOCs, videos, ... There are so many ways to stay in contact with each other. There has to be better options than flying somewhere to do my advocacy!</p><p>The picture is not all dark though, I&apos;ve seen several people doing so much better than me, like Liz Rice biking to go to a conference in April &#x1F92F;. There are many other folks as well taking the train to travel.</p><figure class="kg-card kg-embed-card"><blockquote class="twitter-tweet"><p lang="en" dir="ltr">Is anyone thinking about cycling from London to Amsterdam for <a href="https://twitter.com/hashtag/kubecon?src=hash&amp;ref_src=twsrc%5Etfw&amp;ref=lengrand.fr">#kubecon</a> in April? &#x1F914;&#x1F6B4;&#x200D;&#x2640;&#xFE0F;</p>&#x2014; Liz Rice &#x1F41D; (@lizrice) <a href="https://twitter.com/lizrice/status/1612808632982441985?ref_src=twsrc%5Etfw&amp;ref=lengrand.fr">January 10, 2023</a></blockquote>
<script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>
</figure><h2 id="about-conference-planning">About conference planning</h2><p>Conference planning is a hard problem. Most people go to only a few conferences a year. And they&apos;re going to the most appealing ones. If you can get huge names to come to your conference, it&apos;s typically a nice way to ensure your tickets are sold. I hear that problem very much, especially post COVID where many conference organisers have piled up huge costs due to unexpected cancellations. </p><p>At the same time, I have seen some things at conferences that sometimes disturbed me. Without having to be political, conferences are definitely a place where organisers can send a strong message. The huge majority of the talks I have given that were climate related either happened in the very first slot of the morning before most people wake up, or in the last slot while most of the attendees were already enjoying some beer. </p><p>In one instance, I gave a climate related talk at 8h in the morning. That talk was followed by 3 cryptos talks. In another case, I&apos;ve seen someone give a &quot;reduce your CO2 emissions in the cloud&quot; talk. That person flew all the way from South America to Europe to give a talk to 40 people. As much as I appreciated the chat, I found it hard to believe that one of the main cloud companies in the world didn&apos;t have someone in Europe at the ready to give that talk. That hampered the message of the speaker by a lot to me.</p><p>Again, I&apos;m not complaining at all about my spot in the conferences here. There are gazillions of speakers better than me &#x1F60A;. My main point is that the programmation of talks is definitely sending a message to a huge amount of humans at the same time. Conferences aren&apos;t typically very clean events in terms of emissions or amount of waste produced. Maybe we can make them matter even more?</p><p>The past years, I&apos;ve been part of redaction commissions for magazines, and in the review committee of a few conferences. Without being completely biased, that&apos;s one of the places where I can help sending a message. Having more local speakers, inviting speakers with strong topics on stage, .... (And this goes for much more than climate by the way, diversity, inclusion, ...).</p><p>Again, the picture is not completely dark. I&apos;ve seen great examples of conferences who really outdid themselves to make sure that all the messaging was aligned from one end to the other. There are many great examples, but the best I have seen is <a href="https://devfest.gdglille.org/?ref=lengrand.fr">DevFestLille</a>. Vegetarian options by default, with reusable cutlery, an adapted programmation, speaker dinner in a local restaurant using locally produced food, sustainable gifts and more... They go much deeper than this, putting accessibility and inclusivity at the core of their values. I admire that conference. Check it out. </p><figure class="kg-card kg-embed-card"><blockquote class="twitter-tweet"><p lang="fr" dir="ltr">Nous cl&#xF4;turons le <a href="https://twitter.com/hashtag/DevfestLille?src=hash&amp;ref_src=twsrc%5Etfw&amp;ref=lengrand.fr">#DevfestLille</a> avec <a href="https://twitter.com/fs0c131y?ref_src=twsrc%5Etfw&amp;ref=lengrand.fr">@fs0c131y</a> qui nous parle d&apos;empreinte num&#xE9;rique ! <a href="https://t.co/d4gFq7X0Sj?ref=lengrand.fr">pic.twitter.com/d4gFq7X0Sj</a></p>&#x2014; Devfest Lille (@DevfestLille) <a href="https://twitter.com/DevfestLille/status/1662121748177776642?ref_src=twsrc%5Etfw&amp;ref=lengrand.fr">May 26, 2023</a></blockquote>
<script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>
</figure><h2 id="who-to-work-for">Who to work for?</h2><p>Here&apos;s another one of my main struggles: As a Developer Advocate, you&apos;re by definition at the forefront of your company and how it presents itself to developers. As such, it is crucial that you&apos;re in line with the values they represent. </p><p>I am fortunate with the fact that this is something that I really find at Adyen today. Of course, Adyen is here to make profit, but that&apos;s obviously OK and I&apos;m definitely here to help. What&apos;s important (for me) is that it does it in an ethical, sustainable way. And in this sense it does much more than the large majority of the companies I&apos;ve been working for. I don&apos;t want to blatantly sound like I&apos;m promoting Adyen here, but &quot;building an ethical business and driving sustainable growth for our merchants&quot; is one of the core principles of Adyen since its inception. And it also adds words to action. For example Adyen dedicates 1% of its net revenue to programs that are meant to positively impact ESGs in line with the UN <a href="https://sdgs.un.org/?ref=lengrand.fr">SDG</a> commitment. And it combines global action like this, with <a href="https://www.adyen.com/social-responsibility?ref=lengrand.fr">local actions on the ground</a>. It also accomodates with my travel choices, as long as it doesn&apos;t impact my work. Of course, it&apos;s always possible to do more. But it&apos;s more than I&apos;ve ever seen in my career so far.</p><p>There&apos;s a whole lot of other jobs in DevRel in the industry and I can&apos;t say that I associate with all of them. There&apos;s been a large amount of advocacy positions open in Web 3, Crypto and NFT related tech lately for example. Historically, those technologies are not particularly known for their clean or efficient computing. Finding positions that line up with my values, is something that I personally struggle with at times, &#xA0; </p><p>I do see some VERY interesting positions as well though. For example, I admire <a href="https://asim.dev/?ref=lengrand.fr">Asim Hussain</a>&apos;s work and the <a href="https://greensoftware.foundation/?ref=lengrand.fr">Green Software Foundation</a> he founded. Most cloud providers are heavily invested in reducing their and their customer&apos;s footprints. That&apos;s partly why they love your serverless workloads so much &#x1F605;. &#xA0;</p><p>Another example who brightened my day a while back is Jamund, finding his way into sustainability at AWS : </p><figure class="kg-card kg-embed-card"><blockquote class="twitter-tweet"><p lang="en" dir="ltr">Super excited to announce that *today* I started with AWS here in London as a Sr. Frontend Engineer with a focus on supply chain and sustainability &#x1F69A;&#x26D3;&#x1F4CA;&#x1F30E;</p>&#x2014; Jamund Ferguson (@xjamundx) <a href="https://twitter.com/xjamundx/status/1566823072371458049?ref_src=twsrc%5Etfw&amp;ref=lengrand.fr">September 5, 2022</a></blockquote>
<script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>
</figure><p>I have been looking for opportunities in the sustainability domain, but fact of the matter is there doesn&apos;t seem to be much yet for Developer Advocates (and DevRel in general).</p><h2 id="were-in-a-great-position-to-challenge-the-status-quo">We&apos;re in a great position to challenge the status quo</h2><p>It may not be true for everyone, but turns out most of us are compensated very well. Of course, I&apos;m not saying we&apos;re rich. There&apos;s still rent to pay, tuition for the kids, supporting folks around you, ... Especially given how shaky the industry has been the past year. I&apos;m not saying any of us has it easy. That being said, looking at <a href="https://wid.world/income-comparator/?ref=lengrand.fr">the world inequality database</a> humbled me. I am in a favored position to make a difference on the market, simply because I have enough wealth to have all my major needs fulfilled and I have a platform available to me. </p><p>I was impressed to see the impact that many Developer Advocates did supporting Ukraine the past year for example. People like <a href="https://twitter.com/alina_yurenko?ref=lengrand.fr">Alina</a> are inspiring. As I was saying above, <a href="https://asim.dev/?ref=lengrand.fr">Asim</a> is also a great example for substainability. As shown in the past, we are in a position to help make a difference. &#xA0;</p><h2 id="educating-yourself-and-listening-to-others">Educating yourself, and listening to others</h2><p>One of the nice things of being a Developer Advocate is that whether physically or virtually, you get to meet a lot of people. And your job is pretty much to learn new stuff and share it. I like that as part of my profile I get to share some of the things I&apos;ve learnt about climate and sustainability. I&apos;ve given talks on the topic, I&apos;ve raised awareness internally and externally. I&apos;ve been sharing our journey online. </p><figure class="kg-card kg-embed-card"><blockquote class="twitter-tweet"><p lang="en" dir="ltr">This is getting completely out of hands, but there isn&apos;t a single book in that pile that I&apos;d remove &#x1F60A;. Looking forward reading them all cover to cover! <a href="https://twitter.com/hashtag/books?src=hash&amp;ref_src=twsrc%5Etfw&amp;ref=lengrand.fr">#books</a> <a href="https://t.co/tuofRLwaLq?ref=lengrand.fr">pic.twitter.com/tuofRLwaLq</a></p>&#x2014; Julien Lengrand-Lambert &#x1F951;&#x1F44B; (@jlengrand) <a href="https://twitter.com/jlengrand/status/1691828793109774774?ref_src=twsrc%5Etfw&amp;ref=lengrand.fr">August 16, 2023</a></blockquote>
<script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>
</figure><p>Of course, <a href="https://twitter.com/ClimateHuman?ref=lengrand.fr">many</a> are <a href="https://twitter.com/ClimateAd?ref=lengrand.fr">doing</a> it much <a href="https://twitter.com/BonPote?ref=lengrand.fr">better</a> than me, and I am very well aware that posting photos of potatoes won&apos;t fix the climate crisis. But I&apos;ve met people along the way who gave me energy to do more, try new things, learn new stuff. We animated a panel last years with 3 other folks on sustainability in tech. I&apos;ve helped run an online conference dedicated to <a href="https://simplewebconf.com/?ref=lengrand.fr">Simpler and Cleaner Tech</a>.</p><p>In France we say &quot;les petits ruisseaux font les grandes rivi&#xE8;res&quot; (&quot;They say that small streams make big rivers.&quot;). My only goal is to be surrounded by many more of those little streams &#x1F60A;. Not to feel good about myself and not feel guilty. But more as a way to do ever better every day.</p><h2 id="but-why-do-this-job-at-all-then">But why do this job at all then?</h2><p>Some of you might ask &quot;but after all those issues, why be a Developer Advocate at all then&quot;? And that&apos;d be a great question. I ask it to myself every day.</p><p>And the answer is relatively short as well : <a href="https://en.wikipedia.org/wiki/Ikigai?ref=lengrand.fr">because I absolutely love my job to bits, and it took me years to find something that I liked, that someone would pay me for, and that I&apos;d be good at</a>. I do think that we&apos;re also something the world needs. Human seek connection, and we have a role to play there. Given all of the positive examples I&apos;ve given above from all those inspiring folks, I &#xA0;do see a way to do my job in a way that lines up with my internal voice. You all inspire me to do better every day.</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://lengrand.fr/content/images/2023/09/image.png" class="kg-image" alt="Thoughts on the Climate crisis and being a Developer (Advocate)" loading="lazy" width="730" height="730" srcset="https://lengrand.fr/content/images/size/w600/2023/09/image.png 600w, https://lengrand.fr/content/images/2023/09/image.png 730w" sizes="(min-width: 720px) 720px"><figcaption>A photo of Ikigai (https://www.japan.go.jp/kizuna/_src/7994686/ikigai_japanese_secret_to_a_joyful_life_pic.png?v=1693289036040)</figcaption></figure><p>That&apos;s it for now. I really hope this blog doesn&apos;t come as moralizing; that really wasn&apos;t the point, <a href="https://www.theguardian.com/books/2023/sep/04/the-big-idea-how-can-we-live-ethically-in-a-world-in-crisis?ref=lengrand.fr">quite the opposite</a>. I&apos;ll be at DevRelCon next week, maybe we can chat there (or in the train on the way &#x1F61B;). Or any other time, you can hit me on <a href="https://twitter.com/jlengrand?ref=lengrand.fr">Twitter</a>, <a href="https://mastodon.online/deck/getting-started?ref=lengrand.fr">Mastodon</a>, or by subscribing :).</p><p><em>And if you&apos;ve recently been laid off, and are still searching for a new gig; I&apos;m so sorry. The industry hasn&apos;t been great for us the past year. If that helps I know that Adyen is searching for a <a href="https://careers.adyen.com/vacancies/5152703-team-lead-internal-developer-advocate?ref=lengrand.fr">Team Lead Internal Developer Advocacy</a> and a friend of mine at DHL is searching for an internal Developer Advocate in the Netherlands to kickstart their advocacy program. Hit me up.</em></p><p>Cheers,</p><p>Julien</p>]]></content:encoded></item><item><title><![CDATA[My KotlinConf experience]]></title><description><![CDATA[My experience and impressions attending KotlinConf a couple weeks back. And some thoughts on the future of Kotlin.]]></description><link>https://lengrand.fr/kotlinconf/</link><guid isPermaLink="false">644f8321187d37046afb194e</guid><category><![CDATA[kotlin]]></category><category><![CDATA[conference]]></category><category><![CDATA[devrel]]></category><category><![CDATA[android]]></category><category><![CDATA[java]]></category><dc:creator><![CDATA[Julien]]></dc:creator><pubDate>Mon, 01 May 2023 09:42:07 GMT</pubDate><media:content url="https://lengrand.fr/content/images/2023/05/IMG_7028-1.jpeg" medium="image"/><content:encoded><![CDATA[<img src="https://lengrand.fr/content/images/2023/05/IMG_7028-1.jpeg" alt="My KotlinConf experience"><p></p><p>The past week, <a href="kotlinconf.com/">KotlinConf</a> took place in Amsterdam. It was my very first KotlinConf ever, and to be fair I had been excitingly waiting for it for a long time &#x1F60A;.I&apos;ll be trying to share some of my excitement as well as my impressions here. This post is obviously biased, for official news I recommend you directly go to the source!</p><h2 id="the-community">The community</h2><p>The first and main reason for me to go to the conference was to meet all the community again, as always. I&apos;ve had a glimpse of it a couple months back at the <a href="https://twitter.com/jlengrand/status/1622577024832270339?ref=lengrand.fr">GDE summit</a>, but even more people were there this time &#x1F60A;.<br>Many known faces of course, from <a href="https://duckduckgo.com/?t=ffab&amp;q=louis+cad&amp;ia=web&amp;ref=lengrand.fr">Louis</a> to <a href="https://twitter.com/_JamesWard?ref=lengrand.fr">James</a>, <a href="https://twitter.com/jlengrand/status/1622577024832270339?ref=lengrand.fr">Duncan</a>, <a href="https://twitter.com/trisha_gee?ref=lengrand.fr">Trisha</a> or <a href="https://www.linkedin.com/in/amanda-hinchman-dominguez-4190a7a4/?ref=lengrand.fr">Amanda</a>, who was also speaker at the very first conference I spoke at (in Belarus, how times change...) but also so many people I know online for a long time and who I finally got to meet. <a href="https://twitter.com/jlengrand/status/1647181693571198976?ref=lengrand.fr">Isa</a>, a content creator from Spain, Sebastian, which content I follow not only because of the Kotlin magic but also to learn more about the craft of content creation itself. And I <em>finally</em> got to meet Margaryta, who introduced me to the GDE program and came with the whole family (seriously though, more kids at conferences!)</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://lengrand.fr/content/images/2023/05/image.png" class="kg-image" alt="My KotlinConf experience" loading="lazy" width="1024" height="768" srcset="https://lengrand.fr/content/images/size/w600/2023/05/image.png 600w, https://lengrand.fr/content/images/size/w1000/2023/05/image.png 1000w, https://lengrand.fr/content/images/2023/05/image.png 1024w" sizes="(min-width: 720px) 720px"><figcaption>Meeting up with Isa, we&apos;ve been talking to each other on Twitter for a long time ^^</figcaption></figure><p>Somehow, I still didn&apos;t manage to Josh and Svetlana ^^. That&apos;ll be for another time.</p><p>And of course, there&apos;s all the new folks. Most notably <a href="https://www.linkedin.com/in/salihgueler/?ref=lengrand.fr">Salih</a>, we nerded about developer advocacy and leadership.</p><h2 id="the-new-stuff">The new stuff</h2><p>The Keynote announcements were quite exciting to be honest. From the announcement of <a href="https://blog.jetbrains.com/kotlin/2021/04/kotlin-kernel-for-jupyter-notebook-v0-9-0/?ref=lengrand.fr">Kotlin Notebooks</a> (been wishing for this for a long time, try them out!), to obviously the <a href="https://blog.jetbrains.com/kotlin/2023/02/k2-kotlin-2-0/?ref=lengrand.fr">K2 compiler</a> and much of the goodies that come with it. The notebooks announcement (as well as some of the ML/AI related talks at the conference) also hint at Kotlin becoming a potential alternative for Python for ML engineers out there in the future. Why not?<br>I care a lot about the language features but even more about the ecosystem, and seeing more companies joining the <a href="https://kotlinfoundation.org/?ref=lengrand.fr">Kotlin Foundation</a> was the best news of the day to me. It is a great step for a long living healthy language and I&apos;m sure it&apos;ll help drive adoption further.</p><h2 id="the-venue">The venue</h2><p>I really liked all of the sponsors that were present at the event. The venue was also dope for me, with a large <strong>Jetbrains</strong> booth in the middle stuffed with Product and Engineering folks. After all, Kotlin is a creation of Jetbrains and having the product smack in the middle of everyone with demos running at all times was super cool. Of course, you know me as a big Jetbrains fan so it was really a nerd fest for me.</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://lengrand.fr/content/images/2023/05/image-1.png" class="kg-image" alt="My KotlinConf experience" loading="lazy" width="768" height="1024" srcset="https://lengrand.fr/content/images/size/w600/2023/05/image-1.png 600w, https://lengrand.fr/content/images/2023/05/image-1.png 768w" sizes="(min-width: 720px) 720px"><figcaption>The massive Jetbrains booth at the centre of the venue</figcaption></figure><p><br>Sponsors also made a lot of sense, and I liked that they were from various industries and domains. Hardware, Multiplatform, server, small and big companies, they were all represented and that was really cool. </p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://lengrand.fr/content/images/2023/05/image-4.png" class="kg-image" alt="My KotlinConf experience" loading="lazy" width="768" height="1024" srcset="https://lengrand.fr/content/images/size/w600/2023/05/image-4.png 600w, https://lengrand.fr/content/images/2023/05/image-4.png 768w" sizes="(min-width: 720px) 720px"><figcaption>Meeting the guys from KodeinKoders, with whom I&apos;ve interacted online for a while</figcaption></figure><p>Quite a few of the companies who also have a large impact on the community via their contributions made it a strong part of their booth and I liked it a lot. From Xebia (47deg) with <a href="https://github.com/arrow-kt/arrow?ref=lengrand.fr">Arrow</a>, <a href="https://github.com/kosi-libs/Kodein?ref=lengrand.fr">Kodein</a> or even <a href="https://www.http4k.org/?ref=lengrand.fr">http4K</a>, it was nice to have strong players present as well and see industry support for the ecosystem. &#xA0;It is obviously a big PR and recruitment move for them, but hey I find it smart and it&apos;s a strong win/win situation in my opinion.</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://lengrand.fr/content/images/2023/05/image-2.png" class="kg-image" alt="My KotlinConf experience" loading="lazy" width="1024" height="768" srcset="https://lengrand.fr/content/images/size/w600/2023/05/image-2.png 600w, https://lengrand.fr/content/images/size/w1000/2023/05/image-2.png 1000w, https://lengrand.fr/content/images/2023/05/image-2.png 1024w" sizes="(min-width: 720px) 720px"><figcaption>The Adyen booth at KotlinConf. Adyen uses Kotlin a lot for the payment terminals</figcaption></figure><h2 id="the-sessions">The sessions</h2><p>I spent most of my time roaming around talking to people, so I won&apos;t describe all of the sessions I joined but instead I want to give some items which I really liked or who caught my attention particularly.</p><p>First, I wanna start with the one session which was an absolute blast for me : <a href="https://kotlinconf.com/speakers/68b14adf-80d2-4a38-b6c9-d8c66b343dca/?ref=lengrand.fr#Video%20Game%20Hacking%20using%20Kotlin/Native">Video Game Hacking using Kotlin/Native</a> by Ignat Beresnev. It was fun, entertaining, I&apos;ve learnt stuff, delivery was amazing and the topic was great as well. Surprisingly, it packed a whole lot of punch for a lightning talk. Honestly though, what&apos;s more fun that learning about memory hacking and <strong>seeing cars appear straight in GTA, all of that in Kotlin</strong>. The presentation is full of well timed jokes, and honestly it grew me as a speaker. I can&apos;t recommend it enough.</p><p>Then, there was <a href="https://kotlinconf.com/speakers/a4d26fc1-9707-4c86-bc82-70b7ce05823c/?ref=lengrand.fr#Crash%20Course%20on%20the%20Kotlin%20Compiler">the crash course on compilers by my colleague Amanda</a>. Damn, it&apos;s impressive to see deep dive talks from people who are really passionate about their craft. Imposter syndrome hit hard there, and the room was absolutely packed! I have to admit that the topic was quite far from my usual dabblings and I&apos;m still unsure what I could build a compiler plugin for ^^, but I&apos;ve definitely learnt a lot about how Kotlin works under the hood!</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://lengrand.fr/content/images/2023/05/image-3.png" class="kg-image" alt="My KotlinConf experience" loading="lazy" width="1536" height="2048" srcset="https://lengrand.fr/content/images/size/w600/2023/05/image-3.png 600w, https://lengrand.fr/content/images/size/w1000/2023/05/image-3.png 1000w, https://lengrand.fr/content/images/2023/05/image-3.png 1536w" sizes="(min-width: 720px) 720px"><figcaption>Crash course on compilers, in a dope room, fully packed</figcaption></figure><h2 id="compose-compose-and-compose">Compose, compose and compose</h2><p>You probably know it, I&apos;m a backend guy. I make do with frontend if I have to, mostly using <a href="https://lit.dev/?ref=lengrand.fr">Web Components</a>. I tried <a href="https://developer.android.com/jetpack/compose?ref=lengrand.fr">Compose</a> back in the past, mostly for web but I really never got excited about it. I mean, I really do like the components idea and the language is a blast but maybe it was too early. Things weren&apos;t documented as much as I wanted to, reloading was slow, tooling wasn&apos;t quite there yet, ... I really wasn&apos;t reaching the experience that I wanted.</p><p><strong>I have to say however that they did manage to hype me up for it again during the conference.</strong></p><p>First, Sebastian Aigner adnd Nikita Lipsky announcing <a href="https://kotlinconf.com/speakers/a82c6942-6e2b-4d80-9883-0b84b932d6ef/?ref=lengrand.fr#Compose%20Multiplatform%20on%20iOS">Compose Multiplatform on iOS</a> with a banging demo and a flawless (and fun) distribution.<br>Then John O&apos;Reilly and Martin Bonnin <a href="https://kotlinconf.com/speakers/0392772c-28d4-47f6-bd39-47d743fb4a81/?ref=lengrand.fr#Confetti:%20building%20a%20Kotlin%20Multiplatform%20conference%20app%20in%2040min">building a Kotlin Multiplatform conference app</a> in 40min live on stage. I mean, of course those speakers rock, they&apos;re seasoned and it&apos;s clear they&apos;re having a blast on stage. But <strong>it&apos;s also hard not to get seduced seeing so much code reuse between the front and the backend of an app and seeing it run both on android and iOS in minutes</strong>.</p><p>I also went to see <a href="https://kotlinconf.com/speakers/e21ee3c3-2e72-4fd2-8e5c-30511e30fe66/?ref=lengrand.fr#You%20can%20do%20desktop%20too!">Victor Kropp talk about how they used compose to build the Jetbrains toolbox</a> (use it if you don&apos;t yet, it&apos;s great!). The toolbox is one application that I quite admire, it&apos;s lean and mean while still getting the job done. It&apos;s always there for you and damn, it&apos;s pretty. So when I learnt it was built using Jetpack Compose for Desktop, my interest for compose obviously grew even more.</p><p>Finally, adding the Compose for web demos (using WASM) constantly running on the Jetbrains booth, it&apos;s hard not to get a little excited about the future for Kotlin.<br><strong>I&apos;ve been into the industry for a little while and one learns to be pessimistic and cautious over time, but has the time finally coming where I can use one language (that I love, sorry Javascript) across all my stacks?</strong></p><h2 id="how-much-compose-is-too-much-though">How much compose is too much though?</h2><p><strong>One thing however bothered me a little bit during the conference, is the amount of Kotlin Multiplatform / Android relative to the server content that was present there.</strong> In terms of content, sessions, but also sponsors and visitors. I might be biased really, because that&apos;s something that I seem to see in the ecosystem already. Let&apos;s have a quick look at it.</p><p>The numbers I&apos;m about to give are not precise, because the frontier between Multiplatform and backend is not always crystal clear, but if we look at the ~80 talks present at the conference, I counted about 20 compose related talked, and 30 backend related ones. The rest is either compiler, platform, community, ... related.<br>It&apos;s honestly better than I was expecting, much likely because I used the conference as an opportunity to learn more about Compose.<br>Out of those 30 backend talks though, more than 12 of those are coroutines related one way or another. That&apos;s massive and there&apos;s clearly something to see here ^^.</p><p>If we look at the Kotlin Foundation partners, the Kotlin partners are Jetbrains, Google, and the newly announced ones are Gradle, Shopify and Touchlab.<br>Of course, all of those are invested in backend as well with Kotlin being the default for backend at Google those days for example. But even keeping Jetbrains apart (even though it obviously is MASSIVELY invested in KMM), touchlabs and shopify are heavily weighting in the KMM ecosystem. And Google is obviously behind most of the Jetpack Compose and Android technologies. That leaves us with Gradle, which arguably is mostly involved in tooling.<br>It might be too black and white statement, but as far as I see today most of the partners are more influential in the KMM part of the ecosystem.</p><p>Obviously, as I mentioned this is a bet that I&apos;d love to see succeed. <strong>The more successful KMM is, the better the whole ecosystem will become</strong>. At the same time, <strong>I can&apos;t stop thinking that there is SO MUCH backend Java going on out there, it feels a little like a missed opportunity</strong>. Java is also has been getting a fresh look lately with the faster release cadence and the accrued amount of DevRel going on around it. It looks to me like backend folks aren&apos;t being seduced by the language as much as they could be.</p><p>In any case, KotlinConf was an amazing experience for me, it was really great to be around so many folks in the ecosystem and seeing how vibrant it becomes over time. And a big thanks to Jetbrains for all of the hard work. Definitely count me in for next time :).</p><p>See ya!<br>Julien</p>]]></content:encoded></item><item><title><![CDATA[Replacing Postman with the Jetbrains HTTP Client]]></title><description><![CDATA[This blog shows a way to use the Jetbrains HTTP Client to replace Postman and test third party APIs, share requests and even run them in CI all of that within minutes! ]]></description><link>https://lengrand.fr/replacing-postman-in-seconds-with-the-jetbrains-http-client/</link><guid isPermaLink="false">63db6edb187d37046afb17d5</guid><category><![CDATA[apis]]></category><category><![CDATA[openapi]]></category><category><![CDATA[kotlin]]></category><category><![CDATA[ci]]></category><dc:creator><![CDATA[Julien]]></dc:creator><pubDate>Thu, 02 Feb 2023 10:17:10 GMT</pubDate><media:content url="https://lengrand.fr/content/images/2023/02/Screenshot-2023-02-02-at-11.13.06.png" medium="image"/><content:encoded><![CDATA[<img src="https://lengrand.fr/content/images/2023/02/Screenshot-2023-02-02-at-11.13.06.png" alt="Replacing Postman with the Jetbrains HTTP Client"><p></p><p><em>TL;DR : I&apos;ve created the first version of an openapi-generator for the jetbrains HTTP Client, and together with the CLI runner it allows you to play against APIs without ever going out of your terminal and it can even run in your CI/CD pipeline. <a href="https://github.com/jlengrand/dotaClient?ref=lengrand.fr">See repository here</a></em></p><p>I work a lot with APIs, whether for an app I develop myself, or to interact with others. Most people I know tend to use Postman for this. But <a href="https://twitter.com/jlengrand/status/1620343955127689216?ref=lengrand.fr">I don&apos;t quite like the product any more</a>, and it forces me to move out of my IntelliJ environment which really kills my productivity. </p><p>With a combination of the <a href="https://www.jetbrains.com/help/idea/http-client-in-product-code-editor.html?ref=lengrand.fr">Jetbrains HTTP Client</a> and <a href="https://openapi-generator.tech/?ref=lengrand.fr">OpenAPI generators</a> we can do much better, in a semi automated way and even reuse our code on our CI/CD. Let&apos;s dive into how!</p><h2 id="a-quick-introduction-to-the-jetbrains-http-client">A quick introduction to the Jetbrains HTTP Client</h2><p>A lot of people don&apos;t know about the <a href="https://www.jetbrains.com/help/idea/http-client-in-product-code-editor.html?ref=lengrand.fr">Jetbrains HTTP Client</a>, which is a Jetbrains proprietary text file format that allows you to run API requests easily. </p><p>One thing I love about it is that it&apos;s integrated into the IDE, and it can generate those requests for you. For example, looking at this <a href="https://github.com/spring-guides/tut-spring-boot-kotlin?ref=lengrand.fr">Spring Boot Kotlin Sample</a>, next to each <code>Mapping</code> there is a &quot;Open in HTTP Client&quot; option in the gutter. </p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://lengrand.fr/content/images/2023/02/image.png" class="kg-image" alt="Replacing Postman with the Jetbrains HTTP Client" loading="lazy" width="2000" height="563" srcset="https://lengrand.fr/content/images/size/w600/2023/02/image.png 600w, https://lengrand.fr/content/images/size/w1000/2023/02/image.png 1000w, https://lengrand.fr/content/images/size/w1600/2023/02/image.png 1600w, https://lengrand.fr/content/images/size/w2400/2023/02/image.png 2400w" sizes="(min-width: 720px) 720px"><figcaption>The &quot;Open in HTTP Client&quot; option in the gutter of our IDE</figcaption></figure><p> When clicking it, it will generate a scratch file for this request :</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://lengrand.fr/content/images/2023/02/image-1.png" class="kg-image" alt="Replacing Postman with the Jetbrains HTTP Client" loading="lazy" width="1254" height="288" srcset="https://lengrand.fr/content/images/size/w600/2023/02/image-1.png 600w, https://lengrand.fr/content/images/size/w1000/2023/02/image-1.png 1000w, https://lengrand.fr/content/images/2023/02/image-1.png 1254w" sizes="(min-width: 720px) 720px"><figcaption>The generated request</figcaption></figure><p>You can do many things with those requests, like setting Content-Type, sending body payload and more but<a href="https://www.jetbrains.com/help/idea/exploring-http-syntax.html?ref=lengrand.fr"> I&apos;ll direct you to the documentation</a> for more.</p><p>The one thing that I want to show you here is that it defines variables for you that you can fill in using a separate configuration file. Pick an environment, and fill in a value for the variables you need. You can either create a public file, or a private one for, say, API Keys.</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://lengrand.fr/content/images/2023/02/image-2.png" class="kg-image" alt="Replacing Postman with the Jetbrains HTTP Client" loading="lazy" width="1414" height="486" srcset="https://lengrand.fr/content/images/size/w600/2023/02/image-2.png 600w, https://lengrand.fr/content/images/size/w1000/2023/02/image-2.png 1000w, https://lengrand.fr/content/images/2023/02/image-2.png 1414w" sizes="(min-width: 720px) 720px"><figcaption>Me creating a Public Environment file</figcaption></figure><p>Running the request gets me the response, as expected, and it&apos;s even saved in a log file for me.</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://lengrand.fr/content/images/2023/02/image-3.png" class="kg-image" alt="Replacing Postman with the Jetbrains HTTP Client" loading="lazy" width="1110" height="1202" srcset="https://lengrand.fr/content/images/size/w600/2023/02/image-3.png 600w, https://lengrand.fr/content/images/size/w1000/2023/02/image-3.png 1000w, https://lengrand.fr/content/images/2023/02/image-3.png 1110w" sizes="(min-width: 720px) 720px"><figcaption>Running the request with the slug set to ispum returns a valid response</figcaption></figure><p>The best thing about all this is that everything is a text file, so I can put my request file with its config in a folder in my repository, pushes it to my repository together with my source files and I suddenly have an easy way to interact with my API.</p><p>I hope that by this time you&apos;re convinced of the value of the Client &#x1F60A;. But wait, we can do much more! </p><h2 id="generating-full-api-clients">Generating full API Clients</h2><p>Many of you are probably aware of the OpenAPI specification (formerly swagger files). It is basically a standard to describe your API. If you write a spec file for it, you basically write a &quot;contract&quot; for how your API works. The nice thing about it is that you can then use this contract to automagically generate a lot of things for your API! That can be clients, in the language of your choice, mock servers, or even documentation. There is a great project for this, conveniently called <a href="https://openapi-generator.tech/?ref=lengrand.fr">OpenAPI Generator</a>.</p><p>During the Christmas holidays, I set myself on a quest to create a simple generator for the <a href="https://lengrand.fr/tips-on-getting-started-with-openapi-generators/">Jetbrains HTTP Client</a>, which didn&apos;t exist yet. As of yesterday, it&apos;s released! Let&apos;s look through it with an example!</p><p>I&apos;m a big DOTA 2 player, and let&apos;s imagine I want to start playing with the <a href="https://docs.opendota.com/?ref=lengrand.fr">Open DOTA API</a>. Conveniently, it provides us with an <a href="https://api.opendota.com/api?ref=lengrand.fr">exhaustive OpenAPI specification file</a>. Let&apos;s have a quick look</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://lengrand.fr/content/images/2023/02/image-4.png" class="kg-image" alt="Replacing Postman with the Jetbrains HTTP Client" loading="lazy" width="2000" height="2000" srcset="https://lengrand.fr/content/images/size/w600/2023/02/image-4.png 600w, https://lengrand.fr/content/images/size/w1000/2023/02/image-4.png 1000w, https://lengrand.fr/content/images/size/w1600/2023/02/image-4.png 1600w, https://lengrand.fr/content/images/2023/02/image-4.png 2048w" sizes="(min-width: 720px) 720px"><figcaption>The beginning of the OpenAPI Spec file</figcaption></figure><p>As we can see, it describes the location of the API, as well as what kind of HTTP Calls you can make with the associated parameters. </p><p>Let&apos;s generate a full HTTP Client for it. First, we install the OpenAPI Generator</p><pre><code class="language-bash">$ brew install openapi-generator</code></pre><p>Then, we generate the client in a new folder</p><pre><code class="language-bash">$ openapi-generator generate -i https://api.opendota.com/api  -g jetbrains-http-client -o dotaClient</code></pre><p><strong>&#x2728;That&apos;s it!&#x2728;</strong></p><p>When we open the folder in IntelliJ, we&apos;re presented with a few very nice things (<a href="https://github.com/jlengrand/dotaClient?ref=lengrand.fr">Here is the GitHub repo if you want to see it for yourself</a>): </p><ul><li>First, a complete documentation with all available endpoints and how they work in the README. Each call is clickable and link to locations in our source code.</li></ul><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://lengrand.fr/content/images/2023/02/image-5.png" class="kg-image" alt="Replacing Postman with the Jetbrains HTTP Client" loading="lazy" width="2000" height="1658" srcset="https://lengrand.fr/content/images/size/w600/2023/02/image-5.png 600w, https://lengrand.fr/content/images/size/w1000/2023/02/image-5.png 1000w, https://lengrand.fr/content/images/size/w1600/2023/02/image-5.png 1600w, https://lengrand.fr/content/images/2023/02/image-5.png 2036w" sizes="(min-width: 720px) 720px"><figcaption>A screenshot of the Dota Client README</figcaption></figure><ul><li>Then, an Apis folder with all available calls and their related documentation. Here is the generated code for the <code>Heroes</code> endpoints :</li></ul><pre><code class="language-bash">## HeroesApi

### GET /heroes
# @name heroesGet
GET http://api.opendota.com/api/heroes

### GET /heroes/{hero_id}/durations
# @name heroesHeroIdDurationsGet
GET http://api.opendota.com/api/heroes/{{hero_id}}/durations

### GET /heroes/{hero_id}/itemPopularity
# @name heroesHeroIdItemPopularityGet
GET http://api.opendota.com/api/heroes/{{hero_id}}/itemPopularity

### GET /heroes/{hero_id}/matches
# @name heroesHeroIdMatchesGet
GET http://api.opendota.com/api/heroes/{{hero_id}}/matches

### GET /heroes/{hero_id}/matchups
# @name heroesHeroIdMatchupsGet
GET http://api.opendota.com/api/heroes/{{hero_id}}/matchups

### GET /heroes/{hero_id}/players
# @name heroesHeroIdPlayersGet
GET http://api.opendota.com/api/heroes/{{hero_id}}/players
</code></pre><p>Now let&apos;s create en environment file and run all those calls for a given hero. Let&apos;s use the <code>id</code> of my favourite hero : <code>Crystal Maiden</code>. </p><pre><code class="language-json">{
  &quot;dev&quot;: {
    &quot;hero_id&quot;: &quot;5&quot;
  }
}</code></pre><p>Just like this, I can start running queries against the API, and for example see which players have the most wins with Crytal Maiden : </p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://lengrand.fr/content/images/2023/02/image-6.png" class="kg-image" alt="Replacing Postman with the Jetbrains HTTP Client" loading="lazy" width="1924" height="602" srcset="https://lengrand.fr/content/images/size/w600/2023/02/image-6.png 600w, https://lengrand.fr/content/images/size/w1000/2023/02/image-6.png 1000w, https://lengrand.fr/content/images/size/w1600/2023/02/image-6.png 1600w, https://lengrand.fr/content/images/2023/02/image-6.png 1924w" sizes="(min-width: 720px) 720px"><figcaption>A screenshot of the results of running the Heroes to Player endpoint</figcaption></figure><p>I can also decide to run the Health endpoint and see whether the DOTA or Steam servers are down &#x1F60A;</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://lengrand.fr/content/images/2023/02/image-7.png" class="kg-image" alt="Replacing Postman with the Jetbrains HTTP Client" loading="lazy" width="1934" height="626" srcset="https://lengrand.fr/content/images/size/w600/2023/02/image-7.png 600w, https://lengrand.fr/content/images/size/w1000/2023/02/image-7.png 1000w, https://lengrand.fr/content/images/size/w1600/2023/02/image-7.png 1600w, https://lengrand.fr/content/images/2023/02/image-7.png 1934w" sizes="(min-width: 720px) 720px"><figcaption>Results of running a query against the Health endpoint</figcaption></figure><p>Objective reached! I can play against external APIs locally, in source code, without having to use any tool like Postman and without going out of IntelliJ.</p><h2 id="adding-tests">Adding tests</h2><p>Running endpoints is nice, but we also want to be able to make sure expectations are fulfilled! Thankfully, there is a way to do this the the HTTP Request client : </p><figure class="kg-card kg-code-card"><pre><code>### GET /heroes
# @name heroesGet
GET http://api.opendota.com/api/heroes


&gt; {%
    client.test(&quot;Request executed successfully&quot;, function() {
        // client.assert(response.status === 200, &quot;Response status is not 200&quot;);
        client.assert(response.body.length == 126, &quot;DOTA 2 currently has 123 heroes&quot;);
    });
%}</code></pre><figcaption>Creating a purposefully failing test</figcaption></figure><p>This should fail, as DOTA 2 currently has 123 heroes : </p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://lengrand.fr/content/images/2023/02/image-8.png" class="kg-image" alt="Replacing Postman with the Jetbrains HTTP Client" loading="lazy" width="2000" height="306" srcset="https://lengrand.fr/content/images/size/w600/2023/02/image-8.png 600w, https://lengrand.fr/content/images/size/w1000/2023/02/image-8.png 1000w, https://lengrand.fr/content/images/size/w1600/2023/02/image-8.png 1600w, https://lengrand.fr/content/images/size/w2400/2023/02/image-8.png 2400w" sizes="(min-width: 720px) 720px"><figcaption>Bingo!</figcaption></figure><h2 id="going-even-further">Going even further</h2><p>I&apos;ve always been a bit frustrated at Jetbrains for their HTTP Client, because it was completely bound to Intellij. As amazing as it is, I can&apos;t spend a lot of time crafting nice custom API calls if I still have to create tests for my CI anyways. At the end of the day, it&apos;s double the work for me. </p><p>But last month, they announced something that filled me with joy &#xA0;: <a href="https://blog.jetbrains.com/idea/2022/12/http-client-cli-run-requests-and-tests-on-ci/?ref=lengrand.fr">A CI runner for the HTTP Request files</a>! <strong>All of a sudden, all the work we did above became 1000x more useful! </strong></p><p>Let&apos;s have a look into it : </p><ul><li>We download the (preview, for now), tool and unzip it:</li></ul><!--kg-card-begin: markdown--><pre><code>$ curl -f -L -o ijhttp.zip &quot;https://jb.gg/ijhttp/latest
$ unzip ijhttp.zip
</code></pre>
<!--kg-card-end: markdown--><ul><li>Finally, we run it against the files we have generated above, with our environment file:</li></ul><pre><code class="language-bash">$ ./ijhttp/ijhttp Apis/HeroesApi.http --env-file Apis/http-client.env.json --env dev</code></pre><p>Et voil&#xE0;!</p><pre><code class="language-bash">$./ijhttp/ijhttp Apis/HeroesApi.http --env-file Apis/http-client.env.json --env dev                                &#xE0B2; &#x2714; &#x2571; 6s &#xF252; &#x2571; 10:23:37 &#xF017;
&#x250C;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2510;
&#x2502;                      Running IntelliJ HTTP Client with                      &#x2502;
&#x251C;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x252C;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2524;
&#x2502;        Files         &#x2502; HeroesApi.http                                       &#x2502;
&#x251C;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x253C;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2524;
&#x2502;  Public Environment  &#x2502; hero_id = 5                                          &#x2502;
&#x251C;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x253C;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2524;
&#x2502; Private Environment  &#x2502;                                                      &#x2502;
&#x2514;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2534;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2518;
Request &apos;heroesGet&apos; GET http://api.opendota.com/api/heroes
Failed HeroesApi.http#0 Request executed successfully
Request &apos;heroesHeroIdDurationsGet&apos; GET http://api.opendota.com/api/heroes/5/durations
Request &apos;heroesHeroIdItemPopularityGet&apos; GET http://api.opendota.com/api/heroes/5/itemPopularity
Request &apos;heroesHeroIdMatchesGet&apos; GET http://api.opendota.com/api/heroes/5/matches
Request &apos;heroesHeroIdMatchupsGet&apos; GET http://api.opendota.com/api/heroes/5/matchups
Request &apos;heroesHeroIdPlayersGet&apos; GET http://api.opendota.com/api/heroes/5/players



6 requests completed, 1 have failed tests
RUN FAILED
</code></pre><p>We still have our failing test, which will fail our CI. Task failed successfully!</p><p>The last step to have a complete setup is to setup a CI action. Let&apos;s use GitHub actions for this, fix our failing test, and see what happens: </p><pre><code class="language-yaml">name: Run API tests based on Jetbrains HTTP Client

on: push

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - uses: actions/setup-java@v3
        with:
          distribution: &apos;temurin&apos;
          java-version: &apos;17&apos;
      - name: Downloads and runs the Jetbrains HTTP Client CLI
        run: |
          curl -f -L -o ijhttp.zip &quot;https://jb.gg/ijhttp/latest&quot;
          unzip ijhttp.zip
          ./ijhttp/ijhttp Apis/HeroesApi.http --env-file Apis/http-client.env.json --env dev
</code></pre><p>We checkout, set the Java version to 17 and run the <code>ijhttp</code> package like we did locally. </p><p><a href="https://github.com/jlengrand/dotaClient/actions/runs/4073382476?ref=lengrand.fr">Success</a>! We now have reliable CI tests using Jetbrains HTTP Client requests! </p><figure class="kg-card kg-image-card"><img src="https://lengrand.fr/content/images/2023/02/image-9.png" class="kg-image" alt="Replacing Postman with the Jetbrains HTTP Client" loading="lazy" width="1788" height="900" srcset="https://lengrand.fr/content/images/size/w600/2023/02/image-9.png 600w, https://lengrand.fr/content/images/size/w1000/2023/02/image-9.png 1000w, https://lengrand.fr/content/images/size/w1600/2023/02/image-9.png 1600w, https://lengrand.fr/content/images/2023/02/image-9.png 1788w" sizes="(min-width: 720px) 720px"></figure><h2 id="going-further">Going further</h2><p>There is so much more I could talk about, like the fact that the HTTP Client supports other protocols like GraphQL, handles redirections and more, but I&apos;ll keep it to this for now.</p><p>Do keep in mind that the <a href="https://github.com/OpenAPITools/openapi-generator?ref=lengrand.fr">Jetbrains HTTP Client generator</a> is still in a very early phase at the moment. For example, I have no support for authentication or most headers at the moment. It won&apos;t break anything, but you might have to add things manually to the generated files. </p><p>But hey, I do welcome requests and contributions! </p><p>Hope you found the read useful. Feel free to ping me on <a href="https://mastodon.online/@jlengrand?ref=lengrand.fr">Mastodon</a> or <a href="https://twitter.com/jlengrand?ref=lengrand.fr">Twitter</a>!</p>]]></content:encoded></item><item><title><![CDATA[2022 retro, and what's up]]></title><description><![CDATA[This post is a recap of 2022, and a look into what's coming in 2023. 
In short, Devrel is amazing and doesn't require flying, I became a GDE and we grew over 160kg of vegetables this year!]]></description><link>https://lengrand.fr/2022-retro-and-whats-up/</link><guid isPermaLink="false">63b9f00fc361900453743e79</guid><category><![CDATA[community]]></category><category><![CDATA[development]]></category><category><![CDATA[self]]></category><category><![CDATA[reflections]]></category><dc:creator><![CDATA[Julien]]></dc:creator><pubDate>Wed, 11 Jan 2023 10:07:52 GMT</pubDate><media:content url="https://lengrand.fr/content/images/2023/01/FdlgcmgXgAAc55v.jpg" medium="image"/><content:encoded><![CDATA[<img src="https://lengrand.fr/content/images/2023/01/FdlgcmgXgAAc55v.jpg" alt="2022 retro, and what&apos;s up"><p>Just like for many other people, the end of the year is a good time to stop for a second and look back at what one has accomplished. I&apos;m notoriously bad at celebrating my successes, always instead looking at the next thing. So please bear with me for this one time humblebrag post.</p><p>So let&apos;s dive in, here&apos;s a dive into what&apos;s happened for me last year, professionally and at a more personal level (which, honestly I&apos;m even more proud of :)).</p><h2 id="at-work">At work</h2><h3 id="my-debuts-into-devrel">My debuts into DevRel</h3><p>2022 was my first professional year in the realm of Developer Relations! I can&apos;t believe it took so long for me to find what many would call <a href="https://en.wikipedia.org/wiki/Ikigai?ref=lengrand.fr">Ikigai</a>. Something I love, I&apos;m good at, and I can be paid for!</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://lengrand.fr/content/images/2023/01/image-5.png" class="kg-image" alt="2022 retro, and what&apos;s up" loading="lazy" width="1080" height="1080" srcset="https://lengrand.fr/content/images/size/w600/2023/01/image-5.png 600w, https://lengrand.fr/content/images/size/w1000/2023/01/image-5.png 1000w, https://lengrand.fr/content/images/2023/01/image-5.png 1080w" sizes="(min-width: 720px) 720px"><figcaption>An illustration of Ikigai (https://www.calmsage.com/10-rules-of-ikigai/)</figcaption></figure><p>I absolutely love my job, and even though it&apos;s clear I can do much much better, the world around me seems to agree with the fact that I&apos;m doing OK at it. I think it&apos;s genuinely the first time in my professional career where I feel like I exactly belong in the environment I evolve in, and I make a direct impact.</p><p>So there&apos;s that!</p><h3 id="from-entity-to-team">From entity, to team</h3><p>Last year ended in a rocky way, with my manager (and mentor) living the company and Adyen deciding to change it&apos;s strategy. It took me (and us) a while to figure out what we were going to be doing. After all, Adyen being a closed product, what value can Developer Relations bring ? Turns out, a lot!</p><p>A year later, we now officially are our own team, with a clear strategy. I have the chance and honor to lead 3 motivated folks with a different set of strenghts, and it really feels like we can move mountains together. And it&apos;s so much fun as well, I&apos;m super proud of them, and what we&apos;ve done together.</p><h3 id="moving-the-needle">Moving the needle</h3><p>Talking about this, our team helped create a lot of brand awareness for Adyen. Collectively, we talked to over 6 thousand (!!) people, gave over 30 conference talks and in total reached over 150k folks. It&apos;s mad to think about it, especially for a team team where everyone is basically starting up in DevRel &#x1F60A;.</p><p>But even more important, we also helped a lot bring awareness about Developer Experience inside the company, so much so that it&apos;s becoming one of the priorities for 2023. It&apos;s a great achievement, because it&apos;ll improve even further our focus for our merchants, and bring many internal teams together. We&apos;re definitely looking forward 2023.</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://lengrand.fr/content/images/2023/01/image-14.png" class="kg-image" alt="2022 retro, and what&apos;s up" loading="lazy" width="1280" height="1707" srcset="https://lengrand.fr/content/images/size/w600/2023/01/image-14.png 600w, https://lengrand.fr/content/images/size/w1000/2023/01/image-14.png 1000w, https://lengrand.fr/content/images/2023/01/image-14.png 1280w" sizes="(min-width: 720px) 720px"><figcaption>Apoorva and I speaking about Developer Experience at the internal Adyen conference</figcaption></figure><h3 id="meetups-and-conferences">Meetups, and Conferences</h3><p>I&apos;ve been quite present on the conference scene this year, even though it&apos;s definitely not one of my main objectives. It brings me crazy amounts of energy to meet folks, share knowledge and to get people excited. I&apos;m proud of the fact that I&apos;ve started speaking of things that are much closer to my heart as well (sustainability). It&apos;s also the first year that people actually started contacting me to speak at conferences, and that honestly feels amazing. This year is also the very first time I was invited <a href="https://open.spotify.com/show/6rxip23OishKIhUxtAdaw7?si=t4QPeEk_R6mAu_IKpI0V7w&amp;nd=1&amp;ref=lengrand.fr">on a podcast</a>, to talk sustainability and DevRel, and it felt great!</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://lengrand.fr/content/images/2023/01/image-6.png" class="kg-image" alt="2022 retro, and what&apos;s up" loading="lazy" width="1500" height="500" srcset="https://lengrand.fr/content/images/size/w600/2023/01/image-6.png 600w, https://lengrand.fr/content/images/size/w1000/2023/01/image-6.png 1000w, https://lengrand.fr/content/images/2023/01/image-6.png 1500w" sizes="(min-width: 720px) 720px"><figcaption>A photo of me speaking about the Climate Emergency during the DevFest Lille. The Q/A turned into a discussion for almost 30 minutes.</figcaption></figure><p>This year, I&apos;ve also helped reboot <a href="https://www.meetup.com/amsterdam-devrel-salon/?ref=lengrand.fr">the Amsterdam DevRel salon</a> and learnt a lot from the folks there. It&apos;s definitely different to organize a Meetup for people who&apos;s job it is to speak &#x1F60A;. Gaining a lot of value from it, and there&apos;s demand for it. Our first 2023 session expect almost 50 people!</p><p>Oh, and thanks to <a href="https://mastodon.online/@floord@mastodon.lol?ref=lengrand.fr">Floor</a> for helping organize!</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://lengrand.fr/content/images/2023/01/Screenshot-2023-01-11-at-11.14.53.png" class="kg-image" alt="2022 retro, and what&apos;s up" loading="lazy" width="1434" height="1926" srcset="https://lengrand.fr/content/images/size/w600/2023/01/Screenshot-2023-01-11-at-11.14.53.png 600w, https://lengrand.fr/content/images/size/w1000/2023/01/Screenshot-2023-01-11-at-11.14.53.png 1000w, https://lengrand.fr/content/images/2023/01/Screenshot-2023-01-11-at-11.14.53.png 1434w" sizes="(min-width: 720px) 720px"><figcaption>A photo of Floor and me at the Halloween edition of the DevRel Salon. We respectively wear an egg, and a lion costume.</figcaption></figure><h3 id="industry-recognition">Industry recognition</h3><p>I love spending time create awareness for tools I love. And if you know me, you definitely are aware I&apos;m in love with <a href="https://kotlinlang.org/?ref=lengrand.fr">Kotlin</a> and <a href="https://www.gitpod.io/?ref=lengrand.fr">Gitpod</a>.</p><p>Even though it&apos;s definitely not a main goal for me, it feels seeing recognition for my efforts. In 2022, I became a <a href="https://developers.google.com/community/experts/directory/profile/profile-julien-lengrand-lambert?ref=lengrand.fr">Kotlin Google Developer Expert</a> as well as a <a href="https://www.gitpod.io/community/heroes?ref=lengrand.fr">Gitpod Hero</a>. There&apos;s some nice perks to it, but being closer to the community gives me so much energy every day, I&apos;m super grateful about it.</p><h3 id="blogging-and-writing">Blogging and Writing</h3><p>Even though I definitely didn&apos;t spend as much time as I wanted to writing this year, I still managed to write a few pieces on this blog and in written magazines. Again, the pleasure I take writing is nothing compared to the joy of hearing that it helped someone or getting feedback.</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://lengrand.fr/content/images/2023/01/image-8.png" class="kg-image" alt="2022 retro, and what&apos;s up" loading="lazy" width="1536" height="2048" srcset="https://lengrand.fr/content/images/size/w600/2023/01/image-8.png 600w, https://lengrand.fr/content/images/size/w1000/2023/01/image-8.png 1000w, https://lengrand.fr/content/images/2023/01/image-8.png 1536w" sizes="(min-width: 720px) 720px"><figcaption>My face in a magazine, talking about things that are close to my heart (Gitpod)</figcaption></figure><h3 id="from-speaker-to-reviewer">From speaker, to reviewer</h3><p>It&apos;s been a couple years already that I help the Dutch Java community by being one of the editors of the Dutch Java Magazine. I also absolutely love JFall above all other conferences, and try to always have a speaking slot there.</p><p>This year, I had the honor to be part of the selection committee as well for the talks. It was great having the opportunity helping shape this conference that I love so much. That, and helping first time speakers prepare for their talk as well.</p><h2 id="and-at-home">And at home</h2><p>As I was mentioning, even though I&apos;m quite happy with how my work life turned out last year, I&apos;m even prouder of everything we achieved at home.</p><h3 id="no-compromises">No compromises</h3><p>It&apos;s been a few years already that we&apos;re conscious about out footprint on the planet, and we&apos;ve decided to drasctically reduce our impact anywhere we could. And one of the major items there is flying. As of 2018, plane travel totalled almost 40% of my CO2 footprint. I decided to stop flying, unless really necessary.</p><p>Combining this with being a Developer Advocate is not always simple. After all, a big part of our job is to be at conferences, close to our audience. Well I managed to be present at over 15 conferences this year, without ever boarding a plane. I&apos;ve also done absolutely no compromise on the topics I wanted to present.</p><p>Turns out, you can go far with the train. I&apos;ve even managed to go all the way to Barcelona &#x1F60A;. I&apos;m happy to see it&apos;s possible, and glad Adyen was nice enough to give me the extra bandwidth necessary for this.</p><h3 id="producing-a-good-chunk-of-our-food">Producing a good chunk of our food</h3><p>It&apos;s been a couple years we try to buy less, and produce some of what we consume at home. It started with many home products (washing powder, dishwasher tablets, detergent, ...) and last year we started the gardening experiment.</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://lengrand.fr/content/images/2023/01/Screenshot-2023-01-11-at-11.10.14.png" class="kg-image" alt="2022 retro, and what&apos;s up" loading="lazy" width="1440" height="1082" srcset="https://lengrand.fr/content/images/size/w600/2023/01/Screenshot-2023-01-11-at-11.10.14.png 600w, https://lengrand.fr/content/images/size/w1000/2023/01/Screenshot-2023-01-11-at-11.10.14.png 1000w, https://lengrand.fr/content/images/2023/01/Screenshot-2023-01-11-at-11.10.14.png 1440w" sizes="(min-width: 720px) 720px"><figcaption>The family together on the garden, enjoying our very first red cabbage</figcaption></figure><p>We went bigger this year, with a 200m2 garden! It was quite the task, actually much more than we expected. Especially with close to 0 previous experience ^^. Almost 60% of everything we planted died &#x1F605;.</p><p>But even then, we managed to produce over 160 kg of food this year! This is by far my proudest achievement with the family. We are, and have been quite largely feeding ourselves with our own produce. We made cans, froze a lot, and it&apos;s such a joy to create meals that come 100% from stuff you grew yourself! We&apos;re definitely planning on doubling down on this next year.</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://lengrand.fr/content/images/2023/01/Screenshot-2023-01-11-at-11.10.38.png" class="kg-image" alt="2022 retro, and what&apos;s up" loading="lazy" width="1436" height="1918" srcset="https://lengrand.fr/content/images/size/w600/2023/01/Screenshot-2023-01-11-at-11.10.38.png 600w, https://lengrand.fr/content/images/size/w1000/2023/01/Screenshot-2023-01-11-at-11.10.38.png 1000w, https://lengrand.fr/content/images/2023/01/Screenshot-2023-01-11-at-11.10.38.png 1436w" sizes="(min-width: 720px) 720px"><figcaption>That&apos;s what 30Kg of potatoes look like!</figcaption></figure><p>Gardening also made us so much more aware of the climate emergency. When you&apos;re in the field, you realize how f*ed and unreliable the weather is becoming. I&apos;m glad about it, because it&apos;s something we can&apos;t ignore anymore and we can bring our kids on the journey. Acting is definitely better than plain despair. </p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://lengrand.fr/content/images/2023/01/Screenshot-2023-01-11-at-11.10.53.png" class="kg-image" alt="2022 retro, and what&apos;s up" loading="lazy" width="1438" height="1912" srcset="https://lengrand.fr/content/images/size/w600/2023/01/Screenshot-2023-01-11-at-11.10.53.png 600w, https://lengrand.fr/content/images/size/w1000/2023/01/Screenshot-2023-01-11-at-11.10.53.png 1000w, https://lengrand.fr/content/images/2023/01/Screenshot-2023-01-11-at-11.10.53.png 1438w" sizes="(min-width: 720px) 720px"><figcaption>This is what the garden looked like back in July when it stopped raining for almost 90 days in a row. We live in the Netherlands!</figcaption></figure><h2 id="so-whats-with-2023">So what&apos;s with 2023?</h2><p>It&apos;s hard to reflect back without ending with a glimpse at what&apos;s coming. This year will be slightly different than last year, but even more exciting.</p><p>I&apos;ll most likely be less present at conferences, as we will be focusing a lot more on content creation with the rest of the team. This means that I&apos;ll get my hands much more dirty and that&apos;s great! I actually already managed to do a minor <a href="https://lengrand.fr/a-tale-of-fixing-a-tiny-openapi-bug/">Open-Source contribution</a> to OpenAPI early this year so that&apos;s a great start.</p><p>So, more content. I also will dive much deeper into Developer Experience, and especially how to measure it. Developer Advocacy definitely ties into Developer Experience, being on the forefront of the product and I want to learn better to convince stakeholders by leveraging numbers. Finally, I will learn the business side of software. This is one of the parts I have been missing in the past years, and it&apos;s a gap I want to fill. I want to be generating revenue myself this year, by learning to recognize gaps in product offerings. </p><p>So let&apos;s get to it! So what are your plans for 2023?! I&apos;m mostly available on <a href="https://mastodon.online/@jlengrand?ref=lengrand.fr">Mastodon</a> and <a href="https://www.linkedin.com/in/julienlengrand/?ref=lengrand.fr">Linkedin</a> those days &#x1F60A;, though you can still find me on <a href="https://twitter.com/jlengrand?ref=lengrand.fr">Twitter</a>...</p>]]></content:encoded></item><item><title><![CDATA[A tale of fixing a tiny OpenAPI bug]]></title><description><![CDATA[I found and fixed a tiny bug in OpenAPI, learnt about the library, jackson and git in the process. In short, get and set methods are visited by Jackson even when they aren't used anywhere in the code! Be careful how you name your methods 🔥]]></description><link>https://lengrand.fr/a-tale-of-fixing-a-tiny-openapi-bug/</link><guid isPermaLink="false">63b04a201c11530500c79ca7</guid><category><![CDATA[apis]]></category><category><![CDATA[openapi]]></category><category><![CDATA[jackson]]></category><category><![CDATA[java]]></category><category><![CDATA[kotlin]]></category><category><![CDATA[development]]></category><dc:creator><![CDATA[Julien]]></dc:creator><pubDate>Sat, 31 Dec 2022 15:35:19 GMT</pubDate><media:content url="https://lengrand.fr/content/images/2022/12/DALL-E-2022-12-31-16.50.10---a-caveman-smashing-a-tiny-insect-in-a-big-library.png" medium="image"/><content:encoded><![CDATA[<img src="https://lengrand.fr/content/images/2022/12/DALL-E-2022-12-31-16.50.10---a-caveman-smashing-a-tiny-insect-in-a-big-library.png" alt="A tale of fixing a tiny OpenAPI bug"><p><em>TL;DR : I found and fixed a tiny bug in OpenAPI, learnt about the library, <a href="https://github.com/FasterXML/jackson?ref=lengrand.fr">jackson</a> and git in the process.</em></p><p>Yesterday, I was sharing some tips on how to get started with OpenAPI generators. Turns out, within minutes of me starting to use the library, I found a bug that was causing heap space exceptions. This blog is about what I learnt while fixing it &#x1F60A;.</p><h2 id="facing-the-bug">Facing the bug</h2><p>The first thing I started doing while testing the application, before even creating my own generator was running a few existing generators for languages I know. I started with Kotlin, and added the debug flags to it to see what kind of objects were generated by the OpenAPI parser.</p><p>Within minutes, I was running the Kotlin (of course) generator, with the <code>debugSupportingFiles</code> global property activated. </p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://lengrand.fr/content/images/2022/12/image-3.png" class="kg-image" alt="A tale of fixing a tiny OpenAPI bug" loading="lazy" width="2000" height="1373" srcset="https://lengrand.fr/content/images/size/w600/2022/12/image-3.png 600w, https://lengrand.fr/content/images/size/w1000/2022/12/image-3.png 1000w, https://lengrand.fr/content/images/size/w1600/2022/12/image-3.png 1600w, https://lengrand.fr/content/images/2022/12/image-3.png 2068w" sizes="(min-width: 720px) 720px"><figcaption>IntelliJ debug configuration with the debugSupportingFiles global property</figcaption></figure><p>After a few seconds, the process crashed with a Java heap space exception:</p><pre><code class="language-java">[main] INFO  o.o.codegen.TemplateManager - writing file /Users/julienlengrand-lambert/Developer/openapi-generator/samples/client/petstore/kotlin/http/client/docs/UserApi.md
[main] INFO  o.o.codegen.DefaultGenerator - ############ Supporting file info ############
Exception in thread &quot;main&quot; java.lang.OutOfMemoryError: Java heap space
	at com.fasterxml.jackson.core.util.TextBuffer.carr(TextBuffer.java:967)
	at com.fasterxml.jackson.core.util.TextBuffer.expand(TextBuffer.java:928)
	at com.fasterxml.jackson.core.util.TextBuffer.append(TextBuffer.java:682)
	at com.fasterxml.jackson.core.io.SegmentedStringWriter.write(SegmentedStringWriter.java:58)
	at com.fasterxml.jackson.core.json.WriterBasedJsonGenerator.writeRaw(WriterBasedJsonGenerator.java:605)
	at com.fasterxml.jackson.core.util.DefaultIndenter.writeIndentation(DefaultIndenter.java:94)
	...</code></pre><p>Not knowing at all what I was doing, I asked for directions <a href="https://openapi-generator.slack.com/archives/CLSB0U0R5/p1669735856969609?ref=lengrand.fr">on the Slack channel</a> of the project and got suggestions within minutes with ideas of things to try out.</p><p>For me, the main question was :<strong> Is it only happening on my machine &#x1F62C;?</strong></p><p>Justin was nice enough to <a href="https://github.com/OpenAPITools/openapi-generator/issues/14161?ref=lengrand.fr">file a bug for me</a>, and I started diving into the problem.</p><h2 id="finding-the-culprit">Finding the culprit</h2><p>Turns out, the issue <em>only</em> happens in debug mode, with that specific flag set. It was also happening for all of the generators I tried, and all the specification files. Extending the heap size to humongous values wasn&apos;t helping. It had to come from the code somehow.</p><p>After a little while, I could track down <a href="https://github.com/OpenAPITools/openapi-generator/blob/7c587ce061f1b2ef6bcd3fc406224452f79099a9/modules/openapi-generator/src/main/java/org/openapitools/codegen/DefaultGenerator.java?ref=lengrand.fr#L831">the line</a> that was creating the crash. It was all happening in the <code>DefaultGenerator</code> (which all generators extend from), and only if the debug flag was set. Makes sense.</p><pre><code class="language-java">if (GlobalSettings.getProperty(&quot;debugSupportingFiles&quot;) != null) {
	LOGGER.info(&quot;############ Supporting file info ############&quot;);
	Json.prettyPrint(bundle);  // BIG BOUM, BIG BADABOUM 
}</code></pre><p>Jason, again, very helpful on Slack, mentioned that the objects generated by the generators can contain infinite loops. Those loops, when trying to transform them into a JSON file would crash the JVM. That&apos;s also what was showing in the stack trace of the exception. </p><p>It was time to track down when the bug was introduced!</p><h2 id="git-bisect-to-the-rescue">Git bisect to the rescue!</h2><p>I won&apos;t go deep into how <code>git bisect</code> works. You can <a href="https://www.git-tower.com/learn/git/faq/git-bisect/?ref=lengrand.fr">read about it yourself</a>. The basic idea is that you provide git with a valid commit (before the bug was there) and a faulty commit (with the bug). You&apos;ll also need a quick way to see if the bug is present (in my case, running the run configuration and seeing the heap space crash).</p><p>Git will iteratively select commits and ask you to tell him whether the bug is there, until you know precisely the ONE COMMIT which introduced the problem.</p><p>It basically looked like this : </p><pre><code class="language-bash">$ git checkout master # clean state
$ git bisect start
$ git bisect bad HEAD # bug in there
$ git bisect good v5.0.0 # no bug
$ git bisect good # no crash
$ git bisect bad # crash
$ git bisect good
$ git bisect bad
$ git bisect good
$ git bisect bad
$ git bisect good
$ git bisect end
$ git bisect reset # back to work, let&apos;s check the bug now</code></pre><p>Honestly, I never had to use it in the past in my professional life and I feel blessed to have an extra tool in my box now &#x1F60A;. The whole thing was sorted in less than 5 minutes &#x1F605;.</p><h2 id="understanding-the-bug">Understanding the bug</h2><p>Once we know the commit which introduced the issue, it&apos;s easy to track down where exactly the problem is. Look, <a href="https://github.com/OpenAPITools/openapi-generator/blob/ef8e55ca21d71dd1e68c47ea239974c75b96a4f8/modules/openapi-generator/src/main/java/org/openapitools/codegen/CodegenOperation.java?ref=lengrand.fr#LL200-L215C6">it&apos;s right there</a> :</p><figure class="kg-card kg-code-card"><pre><code class="language-java">    /**
     * @return contentTypeToOperation
     * returns a map where the key is the request body content type and the value is the current CodegenOperation
     * this is needed by templates when a different signature is needed for each request body content type
     */
    public Map&lt;String, CodegenOperation&gt; getContentTypeToOperation() {
        LinkedHashMap&lt;String, CodegenOperation&gt; contentTypeToOperation = new LinkedHashMap&lt;&gt;();
        if (bodyParam == null) {
            return null;
        }
        LinkedHashMap&lt;String, CodegenMediaType&gt; content = bodyParam.getContent();
        for (String contentType: content.keySet()) {
            contentTypeToOperation.put(contentType, this);
        }
        return contentTypeToOperation;
    }</code></pre><figcaption>The method which introduced the bug creating those heap space exceptions</figcaption></figure><p>Once you understand what the method does, it&apos;s quite clear why the heap explodes. This method is basically adding to an object, the object itself, but with a key of a certain type of format. No wonder it generates infinite loops when the JSON parser runs through it &#x1F60A;.</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://lengrand.fr/content/images/2022/12/image-4.png" class="kg-image" alt="A tale of fixing a tiny OpenAPI bug" loading="lazy" width="2000" height="248" srcset="https://lengrand.fr/content/images/size/w600/2022/12/image-4.png 600w, https://lengrand.fr/content/images/size/w1000/2022/12/image-4.png 1000w, https://lengrand.fr/content/images/size/w1600/2022/12/image-4.png 1600w, https://lengrand.fr/content/images/2022/12/image-4.png 2246w" sizes="(min-width: 720px) 720px"><figcaption>A look inside the object in debug mode at runtime</figcaption></figure><p>Now, the actual problem is not that method itself. The real issue is that it&apos;s not called anywhere &#x1F605;. It <em>shouldn&apos;t</em> be run at all. Actually, there&apos;s literally 0 direct usages of the method in the whole library (even in the <code>toString</code> method)</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://lengrand.fr/content/images/2022/12/image-5.png" class="kg-image" alt="A tale of fixing a tiny OpenAPI bug" loading="lazy" width="1478" height="532" srcset="https://lengrand.fr/content/images/size/w600/2022/12/image-5.png 600w, https://lengrand.fr/content/images/size/w1000/2022/12/image-5.png 1000w, https://lengrand.fr/content/images/2022/12/image-5.png 1478w" sizes="(min-width: 720px) 720px"><figcaption>IntelliJ indicating the method isn&apos;t used anywhere</figcaption></figure><p>Obviously, the method <em>is</em> used. But only in the Python template, where it is actually working as expected.</p><pre><code class="language-mustache">class BaseApi(api_client.Api):
{{#if bodyParam}}
    {{#each getContentTypeToOperation}} // Our method here
    {{&gt; endpoint_args_baseapi_wrapper contentType=@key this=this}}

    {{/each}}
    {{&gt; endpoint_args_baseapi_wrapper contentType=&quot;null&quot; this=this}}

{{else}}
    @typing.overload
    def _{{operationId}}_oapg(
    {{&gt; endpoint_args isOverload=true skipDeserialization=&quot;False&quot; contentType=&quot;null&quot;}}
{{/if}}</code></pre><p>Still, placing a breakpoint makes it clear enough that Jackson DOES enter the method when converting the object to JSON.</p><p>I tried upgrading Jackson and other shenanigans, until <a href="https://www.linkedin.com/in/alex-tarlovsky/?ref=lengrand.fr">one of my colleagues</a> made it all obvious : &quot;Would your method start with <code>get</code> or <code>set</code> by any chance?&quot;. </p><p>Finally, it all made sense : Jackson uses reflection to convert the object into JSON representation. Somehow, it takes the class and goes through all of the getters and setters iteratively. The method was assumed to be a getter, hence creating the crash.</p><h2 id="fixing-the-bug">Fixing the bug</h2><p>As usualy with software, once we know what the problem is it&apos;s actually quite easy to fix it. And even then, I went through 2 iterations. </p><p>My first thought was to add the <code>@JsonIgnore</code> annotation to the method, to tell Jackson to ignore it. It worked.</p><pre><code class="language-Java">
    @JsonIgnore // The magic line
    public Map&lt;String, CodegenOperation&gt; getContentTypeToOperation() {
        LinkedHashMap&lt;String, CodegenOperation&gt; contentTypeToOperation = new LinkedHashMap&lt;&gt;();
        if (bodyParam == null) {
            return null;
        }
        LinkedHashMap&lt;String, CodegenMediaType&gt; content = bodyParam.getContent();
        for (String contentType: content.keySet()) {
            contentTypeToOperation.put(contentType, this);
        }
        return contentTypeToOperation;
    }
</code></pre><p>I didn&apos;t quite like it though. It put some direct dependency to Jackson in a place there wasn&apos;t before, and there was no clear explanation either. The me from the future would have hated me for doing this. </p><p>The second attempt was, I think, better and that&apos;s the one which got merged : Rename the method to remove the get &#x1F605;.</p><figure class="kg-card kg-code-card"><pre><code class="language-Java">public Map&lt;String, CodegenOperation&gt; contentTypeToOperation() {
        LinkedHashMap&lt;String, CodegenOperation&gt; contentTypeToOperation = new LinkedHashMap&lt;&gt;();
        if (bodyParam == null) {
            return null;
        }
        LinkedHashMap&lt;String, CodegenMediaType&gt; content = bodyParam.getContent();
        for (String contentType: content.keySet()) {
            contentTypeToOperation.put(contentType, this);
        }
        return contentTypeToOperation;
    }</code></pre><figcaption>Method renamed!&#xA0;</figcaption></figure><p><a href="https://github.com/OpenAPITools/openapi-generator/pull/14331?ref=lengrand.fr">William</a> was nice enough <a href="https://github.com/OpenAPITools/openapi-generator/pull/14331/files?ref=lengrand.fr">to add a test to test for future regression</a>. I wasn&apos;t sure how to do this (how to you test for &quot;no crash of the JVM&quot;). Well, you don&apos;t, because the crash isn&apos;t there. Here is his commit : </p><pre><code class="language-bash"># test debugSupportingFiles
          ./bin/generate-samples.sh ./bin/configs/python.yaml -- --global-property debugSupportingFiles</code></pre><h2 id="a-word-of-conclusion">A word of conclusion</h2><p>Most of what I&apos;ve learnt about OpenAPI happened through this bug. The folks on Slack are super helpful and they&apos;ve been gentle giving directions while expecting me to do the work. </p><p>I write a lot of open code, but I don&apos;t work much in other people&apos;s libraries and that felt super nice. the bug<a href="https://github.com/OpenAPITools/openapi-generator/issues/14161?ref=lengrand.fr"> should be closed</a> in the coming weeks I expect.</p><p>Thanks everyone, and happy new year already ! &#x1F389;</p><p>Don&apos;t hesitate to reach out if you have any questions! I&apos;m mostly available on <a href="https://mastodon.online/@jlengrand?ref=lengrand.fr">Mastodon</a> and <a href="https://www.linkedin.com/in/julienlengrand/?ref=lengrand.fr">Linkedin</a> those days &#x1F60A;, though you can still find me on <a href="https://twitter.com/jlengrand?ref=lengrand.fr">Twitter</a>...</p>]]></content:encoded></item><item><title><![CDATA[Getting started with OpenAPI Generators : tips and tricks]]></title><description><![CDATA[ In this article, I'm sharing some tips and tricks on how to get productive with creating OpenAPI generators. How to run and debug, create your own easily and more]]></description><link>https://lengrand.fr/tips-on-getting-started-with-openapi-generators/</link><guid isPermaLink="false">63aabcc01c11530500c79aaf</guid><category><![CDATA[java]]></category><category><![CDATA[openapi]]></category><category><![CDATA[apis]]></category><category><![CDATA[development]]></category><dc:creator><![CDATA[Julien]]></dc:creator><pubDate>Fri, 30 Dec 2022 23:57:08 GMT</pubDate><media:content url="https://images.unsplash.com/photo-1623282033815-40b05d96c903?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=MnwxMTc3M3wwfDF8c2VhcmNofDF8fGFwaXxlbnwwfHx8fDE2NzIxMzM4MzM&amp;ixlib=rb-4.0.3&amp;q=80&amp;w=2000" medium="image"/><content:encoded><![CDATA[<img src="https://images.unsplash.com/photo-1623282033815-40b05d96c903?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=MnwxMTc3M3wwfDF8c2VhcmNofDF8fGFwaXxlbnwwfHx8fDE2NzIxMzM4MzM&amp;ixlib=rb-4.0.3&amp;q=80&amp;w=2000" alt="Getting started with OpenAPI Generators : tips and tricks"><p><em>TL;DR : In this article, I&apos;m sharing some tips and tricks on how to get productive with creating OpenAPI generators</em></p><p>Lately, I&apos;ve started dabbling more and more with <a href="https://github.com/OAI/OpenAPI-Specification?ref=lengrand.fr">OpenAPI</a>. The OpenAPI specification is a super useful way to describe the API that you&apos;re exposed to your users, both internally and externally.</p><p>Some of you may also have heard about those under the name of &quot;Swagger files&quot;. They&apos;re both kinda the same in the common speak, though OpenAPI is an open standard (lead with the OpenAPI Initiative) and Swagger is a set of tools built by a company (<a href="https://smartbear.com/?ref=lengrand.fr">SmartBear</a>). Some of those tools are Open-Source, while others have a pro license. I won&apos;t go into the details of the specification here, there are plenty of great getting started resources, typically for different technologies. <a href="https://www.baeldung.com/spring-rest-openapi-documentation?ref=lengrand.fr">Here&apos;s one for Java and Spring</a>.</p><p><strong>Generators</strong> are the parts who take the OpenAPI model that&apos;s being created from a definition file, and generates server / client code and / or documentation. Or anything you want, really. There&apos;s <a href="https://openapi-generator.tech/docs/generators?ref=lengrand.fr">many generators</a> available out of the box, so typically what you want is use or adapt one of them. It&apos;s not <em>always</em> the case though. For example, there is currently no generator available for the <a href="https://www.jetbrains.com/help/idea/http-client-in-product-code-editor.html?ref=lengrand.fr">Jetbrains HTTP Client</a>.</p><p>That&apos;s where my story begins &#x1F60A;, but that&apos;s for another day! </p><h2 id="using-the-debugging-flags">Using the debugging flags</h2><p>Typically, if you&apos;re playing with a generator, you either want to generate a model file (your data structure), an operation file (your logic), or a supporting file (basically anything else, READMEs, docs, ...).</p><p>At its core, the idea of OpenAPI is quite simple : It takes a specification file, transforms it into a set of objects in memory, and uses those objects to generates code / files using <a href="http://mustache.github.io/?ref=lengrand.fr">mustache</a> template files. You can read more about it <a href="https://openapi-generator.tech/docs/templating?ref=lengrand.fr">here</a>.</p><p>That&apos;s why it&apos;s crucial to have a good look into those objects, so that you can find where the data you need is located inside your mustache template. Here, debugging flags become vital. There&apos;s 3 of them, whether you want to see the data to generate models, operations or supporting files. </p><p>You can use respectively <code>debugModels</code>, <code>debugOpenAPI</code> and /or <code>debugSupportingFiles</code>. and you set them by running the desired &#xA0;<code>generate</code> command with the correct flag. For example </p><!--kg-card-begin: markdown--><pre><code class="language-bash">$ java -cp modules/openapi-generator-cli/target/openapi-generator-cli.jar org.openapitools.codegen.OpenAPIGenerator generate -g java -o out -i petstore.yaml  --global-property debugModels=true
</code></pre>
<!--kg-card-end: markdown--><p>I won&apos;t print the entire output here because it&apos;s huge, but as part of the output it will basically spit a giant json representation of all the models available inside that specified yaml file. Here&apos;s a tiny part of the beginning :</p><!--kg-card-begin: markdown--><pre><code class="language-json">[ {
  &quot;importPath&quot; : &quot;org.openapitools.client.model.Category&quot;,
  &quot;model&quot; : {
    &quot;anyOf&quot; : [ ],
    &quot;oneOf&quot; : [ ],
    &quot;allOf&quot; : [ ],
    &quot;name&quot; : &quot;Category&quot;,
    &quot;classname&quot; : &quot;Category&quot;,
    &quot;title&quot; : &quot;Pet category&quot;,
    &quot;description&quot; : &quot;A category for a pet&quot;,
    &quot;classVarName&quot; : &quot;category&quot;,
    &quot;modelJson&quot; : &quot;{\n  \&quot;title\&quot; : \&quot;Pet category\&quot;,\n  \&quot;type\&quot; : \&quot;object\&quot;,\n  \&quot;properties\&quot; : {\n    \&quot;id\&quot; : {\n      \&quot;type\&quot; : \&quot;integer\&quot;,\n      \&quot;format\&quot; : \&quot;int64\&quot;\n    },\n    \&quot;name\&quot; : {\n      \&quot;type\&quot; : \&quot;string\&quot;\n    }\n  },\n  \&quot;description\&quot; : \&quot;A category for a pet\&quot;,\n  \&quot;xml\&quot; : {\n    \&quot;name\&quot; : \&quot;Category\&quot;\n  }\n}&quot;,
    &quot;dataType&quot; : &quot;Object&quot;,
    &quot;xmlName&quot; : &quot;Category&quot;,
    &quot;classFilename&quot; : &quot;Category&quot;,
    &quot;unescapedDescription&quot; : &quot;A category for a pet&quot;,
    &quot;isAlias&quot; : false,
    &quot;isString&quot; : false,
    &quot;isInteger&quot; : false,
    &quot;isLong&quot; : false,
    &quot;isNumber&quot; : false,
    &quot;isNumeric&quot; : false,
    &quot;isFloat&quot; : false,
    &quot;isDouble&quot; : false,
    &quot;isDate&quot; : false,
    &quot;isDateTime&quot; : false,
    &quot;isDecimal&quot; : false,
    &quot;isShort&quot; : false,
    &quot;isUnboundedInteger&quot; : false,
    &quot;isPrimitiveType&quot; : false,
    &quot;isBoolean&quot; : false,
    &quot;additionalPropertiesIsAnyType&quot; : false,
    .........
</code></pre>
<!--kg-card-end: markdown--><h2 id="running-and-debugging-a-generator">Running and Debugging a generator</h2><p>Whether you want to play with an existing generator or <a href="https://openapi-generator.tech/docs/new-generator/?ref=lengrand.fr">create a new one</a>, the existing documentation tends to offer you to compile and run the generators using the generic <code>./mvnw clean package</code> followed by <code>./bin/generate-samples.sh bin/configs/spring-boot.yaml</code> (replace with the file you want to use) command. The first one will compile the source, while the second one will use the created <code>openapi-generator-cli.jar</code> and actually run the code. </p><p>For example, the config file <code>spring-boot.yaml</code> : </p><!--kg-card-begin: markdown--><pre><code class="language-yaml">generatorName: spring
outputDir: samples/server/petstore/springboot
inputSpec: modules/openapi-generator/src/test/resources/2_0/petstore.yaml
templateDir: modules/openapi-generator/src/main/resources/JavaSpring
additionalProperties:
  artifactId: springboot
</code></pre>
<!--kg-card-end: markdown--><p>will run the command</p><!--kg-card-begin: markdown--><pre><code class="language-bash">$./modules/openapi-generator-cli/target/openapi-generator-cli.jar org.openapitools.codegen.OpenAPIGenerator generate \
-g spring \
-i modules/openapi-generator/src/test/resources/2_0/petstore.yaml \
-o samples/server/petstore/springboot \
-t modules/openapi-generator/src/main/resources/JavaSpring \
--additional-properties=artifactId=springboot
</code></pre>
<!--kg-card-end: markdown--><p>Useful, but quite cumbersome. On top of this, you typically want to be able to run / debug code as you go directly in your IDE. </p><p>Using IntelliJ, you can do it by creating a run configuration as such : </p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://lengrand.fr/content/images/2022/12/image.png" class="kg-image" alt="Getting started with OpenAPI Generators : tips and tricks" loading="lazy" width="2000" height="1208" srcset="https://lengrand.fr/content/images/size/w600/2022/12/image.png 600w, https://lengrand.fr/content/images/size/w1000/2022/12/image.png 1000w, https://lengrand.fr/content/images/size/w1600/2022/12/image.png 1600w, https://lengrand.fr/content/images/size/w2400/2022/12/image.png 2400w" sizes="(min-width: 720px) 720px"><figcaption>A run configuration in IntelliJ used to run a specific generator</figcaption></figure><p>Set the <code>OpenAPIGenerator</code> class to run from the <code>openapi-generator-cli</code> jar file. Pick your generator name, input yaml file and output folder (the same as in the config file described above and set the working directory to be the root of the <code>openapi-generator</code> git project. You&apos;re done! </p><p>Now you can go in the generator&apos;s <code>CodeGen</code> file, (for example <code>JMeterClientCodegen</code> if your generator name is <code>jmeter</code> ) and set breakpoints where you want &#x1F60A;.</p><h2 id="setting-breakpoints-in-the-right-locations">Setting breakpoints in the right locations</h2><p>As described above already OpenAPI generators take a specification file, transform it into a set of objects in memory, and use those objects to generates code / files using <a href="http://mustache.github.io/?ref=lengrand.fr">mustache</a> template files. </p><p>Typically, generators will take those existing objects and add or modify some of their content to fit the output to be generated. Most generators will extend from <code>DefaultCodeGen</code>. That&apos;s where you will find the most useful locations to set breakpoints to see what to change and where.</p><p>The most interesting part of this class is located in the <code>generate</code> method. You will also be able to find the lines that are being used to print the debugging flags by searching for the <code>Json.prettyPrint</code><em> </em>calls. Here&apos;s an example for the models (as of now, <a href="https://github.com/OpenAPITools/openapi-generator/blob/7c587ce061f1b2ef6bcd3fc406224452f79099a9/modules/openapi-generator/src/main/java/org/openapitools/codegen/DefaultGenerator.java?ref=lengrand.fr#L572">line 566 of </a> <a href="https://github.com/OpenAPITools/openapi-generator/blob/7c587ce061f1b2ef6bcd3fc406224452f79099a9/modules/openapi-generator/src/main/java/org/openapitools/codegen/DefaultGenerator.java?ref=lengrand.fr#L572"><code>DefaultCodeGen</code></a> : </p><!--kg-card-begin: markdown--><pre><code class="language-java">if (GlobalSettings.getProperty(&quot;debugModels&quot;) != null) {
    LOGGER.info(&quot;############ Model info ############&quot;);
    Json.prettyPrint(allModels);
}
</code></pre>
<!--kg-card-end: markdown--><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://lengrand.fr/content/images/2022/12/image-1.png" class="kg-image" alt="Getting started with OpenAPI Generators : tips and tricks" loading="lazy" width="2000" height="1173" srcset="https://lengrand.fr/content/images/size/w600/2022/12/image-1.png 600w, https://lengrand.fr/content/images/size/w1000/2022/12/image-1.png 1000w, https://lengrand.fr/content/images/size/w1600/2022/12/image-1.png 1600w, https://lengrand.fr/content/images/size/w2400/2022/12/image-1.png 2400w" sizes="(min-width: 720px) 720px"><figcaption>A screenshot of a debug of the model, showing the content of the &quot;allModels&quot; variable</figcaption></figure><p>Once you&apos;re in there, you can dive into the data and find out what you want to do with it. </p><h2 id="extending-the-default-generator">Extending the default generator</h2><p>(Thanks <a href="https://twitter.com/beppecatanese?ref=lengrand.fr">Beppe</a> for the tip!)</p><p>Most generators typically add extra bits of necessary information inside their objects, for example inside the bundle for supporting files : <code><em>bundle</em>.put(&quot;distinctPathParameters&quot;, <em>distinctPathParameters</em>);</code> . The trick is to find <em>where</em> to do this. </p><p>Luckily for us, the smart developers of OpenAPI have created locations to do just that! You&apos;ll want to search for the methods called <code>postProcess*</code> in <code>DefaultCodeGen</code>, those are placeholders that are set at the end of the various <code>generate</code>methods that you can override at your convenience in your custom generator. </p><p>Here is what a completely useless generator could do :</p><!--kg-card-begin: markdown--><pre><code class="language-java">@Override
public Map&lt;String, Object&gt; postProcessSupportingFileData(Map&lt;String, Object&gt; bundle) {
    bundle.put(&quot;bloggingAt1AM&quot;, &quot;isFun&quot;);

    return bundle;
}
</code></pre>
<!--kg-card-end: markdown--><p>Which you could then use inside a `README.mustache` template, for example :</p><!--kg-card-begin: markdown--><pre><code class="language-mustache"># Important announcement

{{bloggingAt1AM}} // Will print &quot;isFun&quot; once ran
</code></pre>
<!--kg-card-end: markdown--><h2 id="generating-custom-lambdas">Generating custom lambdas</h2><p>One of the super powers of mustache is its lambdas, which you can declare in the templates. There is a <a href="https://mustache.github.io/mustache.5.html?ref=lengrand.fr">list available</a> on the mustache website, but the OpenAPI templates define a few more. You can search the code for any class implementing the <code>Mustache.Lambda</code> interface &#x1F60A;.</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://lengrand.fr/content/images/2022/12/image-2.png" class="kg-image" alt="Getting started with OpenAPI Generators : tips and tricks" loading="lazy" width="644" height="802" srcset="https://lengrand.fr/content/images/size/w600/2022/12/image-2.png 600w, https://lengrand.fr/content/images/2022/12/image-2.png 644w"><figcaption>A list of many custom lambdas in the OpenAPI source code</figcaption></figure><p>You can easily create new lambdas as well by implementing that interface yourself. </p><p>Here is a concrete example : Typically, parameters are surrounded with braces when using a generator. For example, for a GET request, with a <code>petId</code> parameter it will come out like this : <code>DELETE http://petstore.swagger.io/v2/pet/{petId}</code>.</p><p>That being said, The <a href="https://www.jetbrains.com/help/idea/http-client-in-product-code-editor.html?ref=lengrand.fr">Jetbrains HTTP Client</a> defines parameter with double braces (!). I need this to have a valid call : <code>DELETE http://petstore.swagger.io/v2/pet/{{petId}}</code>.</p><p>Instead of having to play around a lot with the templating, I decided to create a custom lambda for this in my generator. This is how it looks like : </p><pre><code class="language-java">public class JetbrainsHttpClientClientCodegen extends DefaultCodegen implements CodegenConfig {

    @Override
    protected ImmutableMap.Builder&lt;String, Mustache.Lambda&gt; addMustacheLambdas() {
       return super.addMustacheLambdas()
                .put(&quot;doubleMustache&quot;, new DoubleMustacheLambda());
    }

    public static class DoubleMustacheLambda implements Mustache.Lambda {
        @Override
        public void execute(Template.Fragment fragment, Writer writer) throws IOException {
            String text = fragment.execute();
            writer.write(text
                    .replaceAll(&quot;\\{&quot;, &quot;{{&quot;)
                    .replaceAll(&quot;}&quot;, &quot;}}&quot;)
            );
        }
    }
}</code></pre><p>Two things happen here :</p><ul><li>I create a <code>DoubleMustacheLambda</code> class that implements <code>Mustache.Lambda</code> that &#xA0;itself implements the <code>execute</code> method. The execute method does nothing else than rewriting some of the generated text.</li><li>I override the <code>addMustacheLambdas</code> method, and use it (just like for my data in the previous tip) to insert my custom lambda.</li></ul><p>Once that is done, I can use it inside my <code>api.mustache</code> template! For example : &#xA0;<code>{{#lambda.doubleMustache}}{{path}}{{/lambda.doubleMustache}}</code></p><h2 id="finding-openapi-spec-files-to-test-with">Finding OpenAPI spec files to test with</h2><p>One of my struggles when playing around with OpenAPI Generators is to find files to test stuff with. After a little while, sample files and the <a href="https://petstore3.swagger.io/?ref=lengrand.fr">PetStore</a> don&apos;t cut it any more. I was searching for actual OpenAPI spec from the real world, like for the GitHub API, Heroku, Digital Ocean, .... you name it. </p><p>I asked <a href="https://openapi-generator.slack.com/archives/CLSB0U0R5/p1675289086475889?ref=lengrand.fr">on the official Slack channel</a> and folks were most helpful. They suggested two main things :</p><ul><li><a href="https://github.com/APIs-guru/openapi-directory/tree/main/APIs?ref=lengrand.fr">The OpenAPI directory</a> (which somehow I didn&apos;t know existed!) </li><li><a href="https://www.google.com/search?q=%22api+docs+by+redocly%22+%22REQUEST+BODY+SCHEMA%22&amp;ref=lengrand.fr">A google search</a> to find speccs generated by redocly (which, again, is a tool I didn&apos;t know existed ^^)</li></ul><p>Armed with those two, it&apos;s much easier to go on and try things out. Hope this helps you too!</p><h2 id="a-word-of-conclusion">A word of conclusion</h2><p>I hope those tips will help you hit the ground running with OpenAPI generators. Most of the tips directly come from the open <a href="https://join.slack.com/t/openapi-generator/shared_invite/zt-12jxxd7p2-XUeQM~4pzsU9x~eGLQqX2g?ref=lengrand.fr">Slack channel</a>, the folks there are super useful. The <a href="https://openapi-generator.tech/?ref=lengrand.fr">official documentation</a> is nice, but I honestly found it quite sparse, and once you dive in, a lot of the advice comes down to &quot;have a look at the other generators for inspiration&quot;. I find it logical, because in 99% of the cases, the generator that you need already exists! If it doesn&apos;t though, you&apos;re in for some fun. </p><p>Hopefully I&apos;ll save you a couple hours this way!</p><p>Don&apos;t hesitate to reach out if you have any questions! I&apos;m mostly available on <a href="https://mastodon.online/@jlengrand?ref=lengrand.fr">Mastodon</a> and <a href="https://www.linkedin.com/in/julienlengrand/?ref=lengrand.fr">Linkedin</a> those days &#x1F60A;, though you can still find me on <a href="https://twitter.com/jlengrand?ref=lengrand.fr">Twitter</a>..</p>]]></content:encoded></item><item><title><![CDATA[Things I've learnt about inclusion in Tech]]></title><description><![CDATA[In this post, I'll discuss things I see, and things I try to do to "do my part" (for the lack of a better word) at work and outside of it. I'm not an example, but maybe you can find inspiration yourself (and teach me some more!).]]></description><link>https://lengrand.fr/things-ive-learnt-about-inclusion-in-tech/</link><guid isPermaLink="false">636225b41c11530500c7969f</guid><category><![CDATA[dei]]></category><category><![CDATA[workplace]]></category><category><![CDATA[network]]></category><category><![CDATA[thoughts]]></category><dc:creator><![CDATA[Julien]]></dc:creator><pubDate>Tue, 22 Nov 2022 08:06:42 GMT</pubDate><media:content url="https://images.unsplash.com/photo-1527525443983-6e60c75fff46?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=MnwxMTc3M3wwfDF8c2VhcmNofDl8fHBlb3BsZSUyMHN1Y2Nlc3N8ZW58MHx8fHwxNjY3ODYzNzY1&amp;ixlib=rb-4.0.3&amp;q=80&amp;w=2000" medium="image"/><content:encoded><![CDATA[<img src="https://images.unsplash.com/photo-1527525443983-6e60c75fff46?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=MnwxMTc3M3wwfDF8c2VhcmNofDl8fHBlb3BsZSUyMHN1Y2Nlc3N8ZW58MHx8fHwxNjY3ODYzNzY1&amp;ixlib=rb-4.0.3&amp;q=80&amp;w=2000" alt="Things I&apos;ve learnt about inclusion in Tech"><p><em>This blog&apos;s been on my mind for a while, but I&apos;ve never felt like I was well placed to write about it. I probably am not. Please accept my clumsy words for what they are.</em></p><p>It took a loooong time for me to realize how privileged I am on a daily basis. Being a white male, born in a wealthy country, with a masters degree in a very favored technical field. I&apos;m not ashamed of it, there&apos;s a lot of work behind who I am today. But there&apos;s, let&apos;s be honest also a huge part of genuine randomness / luck and the very large support of a whole family for many years. We all need to be aware of it.</p><p>Interestingly, I was the very first one in the family to study past A level. My parents saved for decades for us to be able to study. I went to school for a while to a place where surgeons, pilots and politicians sent their kids. It&apos;s only years later that I realized why it was hard for me making friends there. It&apos;s funny because even now with both of us making very comfortable salaries we own a cheap car, never went on lavish vacations and still shop in the &quot;no food waste&quot; of the supermarket. </p><p>Anyways, long story short even in a field like Tech where we&apos;re all comfortable compared to many other places, one quickly realises not everyone is born equal. And I&apos;m talking at all levels, whether it&apos;s race, gender, seniority, age, ...</p><p>In this post, I&apos;ll discuss things I see, and things I try to do to &quot;do my part&quot; (for the lack of a better word) at work and outside of it. I&apos;m not an example, but maybe you can find inspiration yourself (and teach me some more!). <em>_Note: It goes without saying that there&apos;s enough material in each of those items to write a whole book about. I try here to bring some ideas to others, and hopefully get some more, nothing else .</em>&#x1F60A;</p><h2 id="it-all-starts-with-awareness">It all starts with awareness</h2><p>You create the reality you&apos;re in. About 6 years ago, I realized that more than 80% of what I was reading / watching and the people I was following on the bird app were (typically older) white males. There&apos;s nothing wrong about it of course, but it&apos;s also far from great. And the thing is, that&apos;s typically self-perpetuating because that&apos;s simply how the algorithms works! </p><p>So I actively started searching for &quot;different&quot; sources. And honestly they&apos;re not hard to find. Start following <a href="https://twitter.com/Kolokodess?ref=lengrand.fr">Kolokodess</a> and you&apos;ll discover a <a href="https://twitter.com/lauragift_?ref=lengrand.fr">whole world</a> of <a href="https://twitter.com/IkegahRuth?ref=lengrand.fr">Nigerian</a> and <a href="https://twitter.com/blackgirlbytes?ref=lengrand.fr">many more</a> tech rockstars (just to name a few). </p><p>Again, the point is not to reason in terms of numbers, but honestly your day <em>is much more interesting </em>if you see content from a various diverse panel of people. It&apos;ll give you ideas, perspectives and motivation in many more forms; as well as make you way more aware of other people&apos;s struggles (even some <a href="https://trishagee.com/2019/03/15/st_what_to_wear/?ref=lengrand.fr">you would have never imagined</a>). </p><p>I&apos;ve seen people do <a href="https://twitter.com/NikkiSiapno/status/1577581571938422784?ref=lengrand.fr">follow Fridays</a>, and they&apos;re great ways to get hear about new great folks! (If you start doing those, try to spotlight some less visible folks as well. I really feel like it&apos;s always the 50 same Java profiles we see at times &#x1F60A;).</p><h2 id="mention-it-when-you-see-it-and-actively-help-with-it">Mention it when you see it, and actively help with it</h2><p>The next step is to mention it when you see it. And let&apos;s be clear, I don&apos;t want to get into the positive discrimination discussions. I have opinions about it but we don&apos;t even need to go there because <strong>there&apos;s definitely enough quality content out there coming from ALL directions without having to lower any kind of bar</strong>. </p><p>The trick though (at least for me) is not to blame an event as it&apos;s happening because it&apos;s obviously too late but to try and be there early to voice it and bring solutions. And it starts locally and with oneself. I try to ask how the lineup looks like when I&apos;m speaking/helping somewhere and I&apos;m happy to propose other folks who I believe would bring a different light to the discussion if I see that we all come from the same types of backgrounds. </p><h3 id="the-win-win-win-situations">The win / win / win situations</h3><p>That&apos;s actually one of the parts I love at my job : helping new voices be heard and get more stories out there! A practical example from my time at ING : We would sponsor many conferences every year and many of those conference came with a &quot;sponsor slot&quot;. Basically a free slot for a speaker. What typically happened is that the folks from marketing were super busy, or simply less on the floor than us and would go for the same 2/3 speakers every time. Every time I heard a cool story on the floor (and check on the person she&apos;s up for talking about it), I&apos;d inform Marketing! </p><p><strong>It&apos;s a win / win / win situation, because ING is happy to have more speakers available, new speakers get an easy way in the circuit and the world gets to hear new stuff! </strong>Our seasoned speaker typically don&apos;t need help to find speaker slots as they already are know in the circuit &#x1F60A;.</p><p>I realise I&apos;m talking about conferences a lot, but the same applies for everything : blog writing, team creation, new projects, ...</p><h2 id="creating-opportunities">Creating opportunities </h2><p>If you&apos;re active in projects, communities, another thing you can help with is to create opportunities for folks, and try to make them less intimidating to reach.</p><p>That&apos;s why I love the shorter Bite Size formats at conference, or the <a href="https://github.com/topics/good-first-issue?ref=lengrand.fr">good-first-issue</a> tag of GitHub. They help make the achievement more accessible for newcomers. Don&apos;t think you can talk about a topic for 45 minutes? Hey, you can speak about it for 15 then right? How about 5? Everyone has 5 minutes of content to say about something! Another trick is to bring a co-speaker with you! I had the chance to present a talk together with <a href="https://twitter.com/apoorva_savant?ref=lengrand.fr">Apoorva</a> last month and it was an absolute blast! As a bonus, we only had 20 minutes of content each to prepare &#x1F60A;. Don&apos;t know much about the project and new to Open-Source? This issue is <strong>reserved for you </strong>and we&apos;ll help you with it as well!</p><p>(Heck to this day even I always start my talks as bite sizes, and then move on to longer formats as I gain confidence about the topic).</p><p>There&apos;s many, many declinations to this idea. Are you a team lead? Organize internal knowledge sharing sessions, it&apos;s less scary to speak to your team first. Then make it cross teams, ... Find you own idea!</p><p>When I joined the redaction commission of <a href="https://nljug.org/category/java-magazine/?ref=lengrand.fr">the Dutch Java Magazine</a>, one of my first proposals was to create a &quot;tweet&quot; format. It&apos;s intimidating to write 4 pages in a printout magazine for anyone. So start with a 280 characters thing to share something you like. You can always write an article about it later! </p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://lengrand.fr/content/images/2022/11/image.png" class="kg-image" alt="Things I&apos;ve learnt about inclusion in Tech" loading="lazy" width="1240" height="1622" srcset="https://lengrand.fr/content/images/size/w600/2022/11/image.png 600w, https://lengrand.fr/content/images/size/w1000/2022/11/image.png 1000w, https://lengrand.fr/content/images/2022/11/image.png 1240w" sizes="(min-width: 720px) 720px"><figcaption>A screenshot of the bite size format of the Dutch Java Magazine #1 2022 (available here : https://nljug.org/java-magazine/java-magazine-1-2022-de-buren-van-java/)</figcaption></figure><p><em>And for the ones in the back : This is absolutely NOT about lowering the bar. This is about simply making things more accessible. And I believe it benefits us all. The tweet format has gotten only good feedback by readers so far, they love the simplicity of it.</em></p><h2 id="keep-mentoring-and-start-sponsoring">Keep mentoring, and start sponsoring</h2><p>Most of the folks I know love to mentor others. Whether it be junior, newer employees or less represented groups. That&apos;s great, and it definitely helps. But an article from <a href="https://blog.pragmaticengineer.com/?ref=lengrand.fr">Gergely</a> lately made me aware on the difference between mentoring and sponsoring and realise I could do more. </p><p>While mentoring focuses on teaching / coaching someone with their skills, be it programming or even soft skills; sponsoring means you are expected to give public and external support to your buddy. The difference is important, <strong>because with sponsorship you&apos;re putting skin in the game</strong> by advocating for your buddy to your management (or any other place). </p><p>It can take many forms, like taking a new risky project, leading an analysis, taking the sponsored keynote spot at a conference... You want to amplify your buddy&apos;s successes (spread them out), boost them and give them access to your network. The effects can be much more empowering than mentoring, seen that way.</p><h2 id="its-a-pipeline-problem">It&apos;s a pipeline problem</h2><p>You&apos;ve probably heard this in one form or other, maybe in several forms even. &quot;Women don&apos;t apply to our jobs&quot;. &quot;Only caucasian males sent an abstract to the conference&quot;, &quot;We can&apos;t find team leads internally&quot;, ...</p><p>I&apos;m not an expert on the topic, <a href="https://www.fastcompany.com/90661349/dear-female-jobseeker-apply-for-the-job-ignore-the-qualifications?ref=lengrand.fr">though it&apos;s clear by now</a> that we don&apos;t have the same behaviours when it applies to job seeking (as well as many other things).</p><p>I won&apos;t dive into the details of writing an inclusive job offer; my main question is whether you personally accept the status quo and get on with it. </p><p>When we organized <a href="https://simplewebconf.com/?ref=lengrand.fr">SimpleWebConf</a> over a year ago, 97% of the 250 abstracts we received for the conferences were from caucasian males. Again, without talking about the quality of the abstract, according to statistics about 80% of the Tech workforce in Europe is male. For an online conference, we could do better! So we asked some groups to relay our CFP and also directly asked folks we knew would do an amazing job to join us. In the end, we ran <a href="https://www.youtube.com/watch?v=kAD7qKwuJFo&amp;t=18281s&amp;ref=lengrand.fr">an online session</a> with 30 folks from 3 continents across many topics like accessibility, sustainability we&apos;re very proud of (dang, that&apos;s back when <a href="https://twitter.com/FredKSchott?ref=lengrand.fr">Fred K. Schott</a> was just starting to introduce <a href="https://astro.build/?ref=lengrand.fr">Astro</a>!).</p><p>And that&apos;s also where I come back to my very first point : The more diverse your network is, the easier it is to promote your content / opportunities in various communities and create something you&apos;re proud of. <em>(And by extension, the more diverse your CFP committee is, the easier it is to have a diverse conference... In hindsight we could have done much better there.)</em></p><p>The same goes on the workplace, I believe. The more diverse your workplace is, the easier it is onwards. At Adyen, we typically try to always include different people from diverse backgrounds (in all senses of the word) during the interview rounds. And again, I believe it&apos;s a win / win situation because it helps get a less biased view of the candidate, but also offer them a better inside view of the company from many different perspectives.</p><h2 id="be-open-about-salary">Be open about salary</h2><p>In the Netherlands, discussing salaries is kinda taboo. And still, I do my best to be open about it (to a point where I&apos;ve literally been shushed by some HR folks in the past). Thing is, keeping secret about salaries favorises only one entity : the company. Sharing salaries makes employees strong, and on good way to reduce discrepancies between folks is for them to be aware of other people &apos;s salary to know if they&apos;re compensated fairly.</p><p>I won&apos;t go about sharing other people &apos;s salaries, of course, nor will I blast mine out there on Twitter ( our salaries in Tech really feel disconnected from many others at times). But I&apos;ll definitely be open about it in conversations.</p><h2 id="respect-others-and-avoid-assumptions">Respect others, and avoid assumptions</h2><p>There&apos;s so much to unpack with this one, and I don&apos;t know how much it really ties into the topic. To me at least, it&apos;s very central to how I try to improve. </p><p>Being French (does it really have to do with this?), I have a very cynical humour and I am very quick to judge people. It has served me well in the past, but it&apos;s also a very big barrier to expand your horizon. </p><p>Cynical humour or edgy jokes might be fine in some context (maybe with close friends, or applied to yourself) but they also typically exclude parts of the population. One good friend one day made me realise that quite literally 80% of my vocabulary for insults had to do with people getting money for sex, or lady parts. Once you see it, it&apos;s hard to unsee. This type of behaviour definitely builds walls faster than bridges and I actively tone them down. </p><p>I&apos;ve screwed up in the past, and I regret it.</p><p>Assuming things is also a great way to exclude people. &quot;Are you from marketing?&quot;, &quot;Where do you <em>originally</em> come from?&quot; : I&apos;m guilty of asking those. As an expat, the first thing I want to talk to with people is food and it&apos;s <em>so</em> hard not to ask where their origins lie to ask them about their food culture. But after having spent days on booths next to colleagues at conferences and heard the sheer amount of times the question pops out, it really made me realise how much of a burden than can be. <a href="https://hollycummins.com/fashion-and-programming-ii/?ref=lengrand.fr">The depth of Holly&apos;s analysis on dress code</a> is compelling, if not saddening! </p><h3 id="giving-space-to-others">Giving space to others</h3><p>Another thing that I genuinely try to improve and still have a long way to go &#xA0;is simply giving space to others. In conversations, in meetings, and many other settings. When talking about things, especially techy stuff, it&apos;s easy to be passionate and monopolise the discussion. And some folks find it much harder than others to raise their voice. </p><p>The meeting is closing, and you realise that damn, your newest colleague who just joined hasn&apos;t said a word about the topic. Well, ask them up. If you see someone not raising his voice at all, give them an opportunity and genuinely ask them their opinion. And ideally, make sure you don&apos;t put them on the spot. You can do that for example by texting them first in online meetings. </p><h3 id="apologising">Apologising</h3><p>On that topic, one thing that I see much too rarely is simple, plain and genuine apologies. We all screw up. How life would be boring without that! Well, we have a saying in French that goes &quot;<a href="https://fr.wiktionary.org/wiki/faute_avou%C3%A9e_%C3%A0_moiti%C3%A9_pardonn%C3%A9e?ref=lengrand.fr#:~:text=Locution%2Dphrase&amp;text=Variante%20de%20faute%20avou%C3%A9e%20est,jug%C3%A9e%20avec%20plus%20de%20cl%C3%A9mence.">a fault confessed is half forgiven</a>&quot;. </p><p><strong>There is no shame in apologising </strong>(though I do know it is a culturally loaded statement)<strong>. In fact, I deeply respect the folks who can recognise their mistakes and apologise. </strong></p><p>So make an apology, either in private to the person you did wrong or publicly, and do better next time. </p><h3 id="were-in-it-together">We&apos;re in it together</h3><p>There&apos;s one last thing I see that I do want to mention, because it pains me. I was raised to be polite to everyone, whoever they are. </p><p>It&apos;s <em>crazy</em> the amount of people who won&apos;t greet the barista, the cleaner, the receptionist or even the bus driver. Personally, it gives me the same feeling as if I saw someone littering. Damn, I wish I could make coffee as good as my barista. I have so much to learn from them. Without the cleaners, there&apos;s <strong>no way </strong>our office could function for more than a couple days. &#xA0;They deserve the same, if not more respect than others! </p><p>So next time you run an Meetup, go the extra mile for the person who has to keep the building open for you and bring them a can and a piece of pizza. Have a chat with them. You wouldn&apos;t be able to be there without them.</p><p></p><p>I knew this article would be waaaaay longer than I expected. There&apos;s much to say about the topic, and still I feel like I have so much more to learn about it. I&apos;m keen on hearing how I can further improve. It might sounds cheesy (and it may well be), but at the end of the day I&apos;d genuinely like all those differences between us to simply not matter. </p><p>Whoever we are, we enrich each other. It&apos;s really exhausting to me to see how polarised we are becoming online <em>and</em> offline. I really wish we simply could coexist and improve day by day. And to go full circle, I once again realise how privileged I am to be able to say that...</p><p>Hit me up on <a href="https://mastodon.online/@jlengrand?ref=lengrand.fr">Mastodon</a> or <a href="https://twitter.com/jlengrand?ref=lengrand.fr">Twitter</a> (for the time being) and let me know what I missed!</p><p>Have a good one, </p>]]></content:encoded></item><item><title><![CDATA[Why I still take notes on paper]]></title><description><![CDATA[Why I still take notes on paper every day. Let's dive a bit into why it works for me, in no particular order.]]></description><link>https://lengrand.fr/why-i-still-take-notes-on-paper/</link><guid isPermaLink="false">62fb67b31c11530500c794e2</guid><category><![CDATA[development]]></category><category><![CDATA[writing]]></category><category><![CDATA[blog]]></category><dc:creator><![CDATA[Julien]]></dc:creator><pubDate>Mon, 22 Aug 2022 07:33:10 GMT</pubDate><media:content url="https://lengrand.fr/content/images/2022/08/IMG_1976.jpg" medium="image"/><content:encoded><![CDATA[<img src="https://lengrand.fr/content/images/2022/08/IMG_1976.jpg" alt="Why I still take notes on paper"><p>It&apos;s a habit I picked up back at school, and it never left me. Every day, in every meeting, I take notes on a paper notebook. You&apos;ll rarely find me with a laptop in a Meeting. And I recommend you try it too. </p><p>Let&apos;s dive a bit into why it works for me, in no particular order.</p><h2 id="politeness">Politeness</h2><p>Have you ever seen someone type on their keyboard in a meeting, or typing on their phone ? What did you think of it?<strong> It might be because I&apos;m getting old, but it always disturbs me</strong>. I never know if that person is writing their grocery shopping list, answering a comment on Facebook or taking notes on what we are discussing about. </p><p>And I&apos;ve seen it happen as well in front of me. Sometimes, it&apos;s really been disturbing to the meeting even, depending on the person types in front of me. The screen is acting as a barrier between me and the person in front of me as well in physical meeting, making it especially awkward (that&apos;s also <a href="https://www.lifehack.org/618331/when-you-choose-your-seat-youre-choosing-your-power?ref=lengrand.fr">why I tend to avoid sitting right in front of someone</a> when I can avoid it by the way).</p><p>Typing in a meeting, or being one&apos;s phone feels impolite to many, even if they won&apos;t tell you. Writing on a piece of paper doesn&apos;t. So I prefer to write! </p><p>One thing though : depending on the context, I actually ask first if it&apos;s ok to take notes. For example when I interview someone, I don&apos;t want them to be startled by me writing down what they are saying &#x1F607;.</p><h2 id="memory">Memory</h2><p>I&apos;ve always been the writer time. Wanna learn something? Write it again and again. So interestingly, I very rarely come back to my notes. </p><p><strong>I don&apos;t write now so I can remember later, I write so I remember now!</strong> &#x1F60A;. I really don&apos;t get the same effect by writing on a keyboard. There is something linking my memory to my hand writing that I can&apos;t explain, but it&apos;s there.</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://lengrand.fr/content/images/2022/08/Screenshot-2022-08-20-at-15.45.50.png" class="kg-image" alt="Why I still take notes on paper" loading="lazy" width="1974" height="1472" srcset="https://lengrand.fr/content/images/size/w600/2022/08/Screenshot-2022-08-20-at-15.45.50.png 600w, https://lengrand.fr/content/images/size/w1000/2022/08/Screenshot-2022-08-20-at-15.45.50.png 1000w, https://lengrand.fr/content/images/size/w1600/2022/08/Screenshot-2022-08-20-at-15.45.50.png 1600w, https://lengrand.fr/content/images/2022/08/Screenshot-2022-08-20-at-15.45.50.png 1974w" sizes="(min-width: 720px) 720px"><figcaption>A bunch of notebooks from the past months. Never opened them again &#x1F62C;</figcaption></figure><h2 id="focus">Focus</h2><p>My mind, just like many of you, races all over the place and I find it super difficult to stay focused in meetings, especially if they are virtual and there are many folks in the room. We live in worlds of constant interruptions and with out laptops and phones around we quite literally have interruption boxes right in our hands all day long. </p><p>Having a notebook and a pen with me helps me stay focused on what is being said instead of having my mind wander off. My hands are busy so I won&apos;t check my socials or my emails, or procrastinate</p><p>On top of this, it&apos;s<a href="https://www.meredith.edu/news/the-surprising-benefits-of-fidgeting-and-doodling/?ref=lengrand.fr"> literally been proven that fidgeting and doodling help with focus</a>, so I definitely use that to my advantage. In meetings, you can find me doodle all over the place on my notebook as things are being said so I can keep focus &#x1F60A;.</p><h2 id="writing-is-typically-faster-than-typing-for-me">Writing is typically faster than typing (for me)</h2><p>Typing is fast, but being used to it, using abbreviations and writing only a few words down to carry ideas I actually write faster than I type. And that becomes especially true on the go, when I want to save things down from my phone. </p><p>I always happen to carry a notebook around in my pocket and that&apos;s the fastest way for me to put ideas down on paper.</p><p> (Well ok for this one, ONE thing actually started competing with my notebooks : voice notes from my watch &#x1F60A;).</p><p>It just feels good</p><p>Seriously though. To me, writing things down feels amazing. I can&apos;t imagine not doing it. Scratching paper, with a nice pen, is one of the nicest feelings I know. Just like reading an old book smelling like old paper in a cozy chair &#x1F60A;. &#xA0;Amazing. </p><p>Writing is getting lost</p><p>I&apos;ll end with this. It&apos;s more of a generic remark but I see that more and more of my friends tend to lose their writing habit. It&apos;s something that they&apos;ve done at school, ut stopped practising for many years now and writing literally feels weird to them.</p><p>I might be getting old fashioned, but to me it&apos;s worrying. I don&apos;t want to lose my writing habit, and I want my kids to see me write and read at home.</p><h2 id="some-limitations">Some limitations</h2><p>Even though I love writing, and my notebooks, things aren&apos;t perfect. For meetings, I&apos;d like to be able to simultaneously write things down, but also have them translated to a document for later sharing and collaboration. </p><p>I haven&apos;t found a good solution so far. Tried an iPad with the <a href="https://paperlike.com/?ref=lengrand.fr">paperlike</a> cover, but it&apos;s still bulky and feels different. I thought about trying the <a href="https://remarkable.com/?ref=lengrand.fr">remarkable</a> a few times, but I love my notebooks too much so I haven&apos;t taken the step to.</p><p></p><p>I&apos;m curious now, what do you think about it? Do you still write? Do you disagree? Do you have tips? Let me know on <a href="https://twitter.com/jlengrand?ref=lengrand.fr">Twitter</a>!</p><p>Have a good one!</p>]]></content:encoded></item><item><title><![CDATA[Kotlin Data Classes, shallow copies and immutability]]></title><description><![CDATA[Are Kotlin data classes immutable? And does the copy method create shallow or deep copies of my class instances? Let's have a look together]]></description><link>https://lengrand.fr/kotlin-data-classes-shallow-copies-and-immutability/</link><guid isPermaLink="false">62e128571c11530500c792dd</guid><category><![CDATA[kotlin]]></category><category><![CDATA[jvm]]></category><category><![CDATA[development]]></category><dc:creator><![CDATA[Julien]]></dc:creator><pubDate>Thu, 28 Jul 2022 10:03:26 GMT</pubDate><media:content url="https://lengrand.fr/content/images/2022/07/Why-Copy-Paste-Does-Not-Work-and-What-to-do-about-it.webp" medium="image"/><content:encoded><![CDATA[<img src="https://lengrand.fr/content/images/2022/07/Why-Copy-Paste-Does-Not-Work-and-What-to-do-about-it.webp" alt="Kotlin Data Classes, shallow copies and immutability"><p><em>TL;DR: The data class <code>copy</code> method in Kotlin creates shallow copies and data classes are NOT immutable data structures by themselves. They become immutable though, if all of their properties are immutable themselves (_val_).</em></p><p><em>Note: You can run all the samples listed here <a href="https://pl.kotl.in/CzsYyKC_l?ref=lengrand.fr">by clicking that link</a></em></p><p>Earlier this week, I gave a Kotlin introduction training to about 50 folks at Adyen. I think most of them appreciated the training and I even got some nice feedback</p><figure class="kg-card kg-embed-card kg-card-hascaption"><blockquote class="twitter-tweet"><p lang="en" dir="ltr">I gave a &quot;introduction to Kotlin&quot; workshop yesterday for about 50 people for the first time. Damn workshops are much more exhausting than I was expecting. <br><br>This feedback made my day though &#x1F970;&#x2764;&#xFE0F;. <a href="https://t.co/jZE8r1UBWJ?ref=lengrand.fr">pic.twitter.com/jZE8r1UBWJ</a></p>&#x2014; Julien Lengrand-Lambert (@jlengrand) <a href="https://twitter.com/jlengrand/status/1551904525358874625?ref_src=twsrc%5Etfw&amp;ref=lengrand.fr">July 26, 2022</a></blockquote>
<script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>
<figcaption>https://twitter.com/jlengrand/status/1551904525358874625</figcaption></figure><p>One fun thing about the training is that most of the audience was coming from a C/C++ background and they asked many questions about references / values, shallow or deep copies and how Kotlin manages memory. Suffice to say I wasn&apos;t ready for it ^^. This blog summarises my findings after the training.</p><h2 id="a-quick-recap-about-data-classes">A quick recap about Data Classes</h2><p>In Kotlin, <a href="https://kotlinlang.org/docs/data-classes.html?ref=lengrand.fr">data classes</a> are specialised structures that are meant hold data (as the name suggests). </p><pre><code class="language-kotlin">data class Address(val number: Int, val street: String, val city: String)

data class User(val name: String, val age: Int, val address: Address)
</code></pre><p>They come with additional goodies compared to normal classes, among which:</p><ul><li>generated <code>hashCode</code> and <code>equals</code> method. (Equals is smart as well, checking if all properties and sub properties have the same value) </li><li>A smart <code>toString</code> method that displays the data content nicely</li></ul><p>Duh, that&apos;s just like Java Records, I hear you say. Yeah, except that data classes also come with the <code>copy</code> method, which is in my opinion give them all their power. With that copy method, you can create a new copy of an existing instance, while modifying some data at the same time. Here is an example:</p><pre><code class="language-kotlin">val bob = User(&quot;Bob&quot;, 42, Address(&quot;12&quot;, &quot;rue des peupliers&quot;, &quot;Paris&quot;))
val anOlderBob = bob.copy(age = 43)</code></pre><p>As I was presenting this to the C++ folks, I heard the same question pop up at three different places at the same time : Is <code>anOlderBob</code> a shallow, or deep copy of <code>bob</code>? </p><p>In other words, if <code>bob</code> changes address now, does <code>anOlderBob</code> get affected?</p><p>Silence in the room.....</p><h2 id="benefits-of-immutable-data-classes">Benefits of Immutable data classes</h2><p>See, I&apos;ve been interested in Functional Programming for a little while, and I know that most of them heavily rely on immutable data structures. One of the main reasons for this is thread safety. If your data cannot be modified any more once it has been written, you are by definition thread safe and you are certain not to have synchronisation issues. I even think that the first person who taught me this was <a href="https://www.youtube.com/watch?v=3jg1AheF4n0&amp;ref=lengrand.fr">the one and only Martin Odersky</a>. </p><p>Thing is, the huge majority of applications out there are in the business of moving data around. And if you cannot modify existing objects, well it also means that you&apos;re gonna have to create many many more objects to compensate for it right?</p><p>Let&apos;s take an example : </p><pre><code class="language-kotlin">data class User(val name: String, val age: Int)

val users = listOf(
        User(&quot;Bob&quot;, 42), 
        User(&quot;Georges&quot;, 12), 
        User(&quot;Emily&quot;, 25), 
        User(&quot;Amy&quot;, 46))
val olderUsers = users.map { it.copy(age = it.age + 1) }

&gt;&gt; [User(name=Bob, age=42), User(name=Georges, age=12), User(name=Emily, age=25), User(name=Amy, age=46)]
&gt;&gt; [User(name=Bob, age=43), User(name=Georges, age=13), User(name=Emily, age=26), User(name=Amy, age=47)]</code></pre><p>Here, we are creating a list of 4 users and then creating a new list with each user older by one year. Because our users are all immutable, we have to create a new copy for each user. </p><p>For more serious applications, the obvious outcome is <strong>that this type of copying </strong><em><strong>has</strong></em><strong> to be shallow </strong>(meaning our new object&apos;s properties link to the memory location of the parent properties), because otherwise the performance hit of creating so many objects would be prohibitive. </p><p>Well, now let&apos;s verify it.</p><h2 id="playing-around-with-data-classes">Playing around with Data Classes</h2><p>Let&apos;s run a few tests to make sure our assumptions are correct (or not). We create <code>FantasyHero</code>, a data class that has mutable and immutable properties, of primitive and more complex types. </p><pre><code class="language-kotlin">enum class WEAPONS{
    AXE, SWORD, WAND, BOW
}

enum class CLASS{
    WIZARD, WARRIOR, PALADIN, THIEF
}

data class Origin(val city: String, var country: String)

data class FantasyHero(
	var name: String, 
    val weapons: MutableList&lt;WEAPONS&gt;, 
    var heroClass: CLASS?, 
    val origin: Origin = Origin(&quot;Utrecht&quot;, &quot;The Netherlands&quot;)
)</code></pre><p>First, let&apos;s check that everything behaves as expected</p><pre><code class="language-kotlin">val gandalf = FantasyHero(
	&quot;Gandalf the Grey&quot;, 
    mutableListOf(WEAPONS.WAND), 
    CLASS.WIZARD
)
val anotherGandalf = FantasyHero(
	&quot;Gandalf the Grey&quot;, 
    mutableListOf(WEAPONS.WAND), 
    CLASS.WIZARD
)
val gandalfCopy = gandalf.copy()


val betterGandalf = gandalf.copy(name=&quot;Gandalf the White&quot;)

println(gandalf)
println(anotherGandalf)
println(betterGandalf)

println(gandalf == anotherGandalf)
println(gandalf == gandalfCopy)
println(gandalf == betterGandalf)
println(&quot;--&quot;)
println(gandalf === anotherGandalf)
println(gandalf === gandalfCopy)
println(gandalf === betterGandalf)

&gt;&gt; FantasyHero(name=gandalf the grey, weapons=[WAND], heroClass=WIZARD, origin=Origin(city=Utrecht, country=The Netherlands))
&gt;&gt; FantasyHero(name=gandalf the grey, weapons=[WAND], heroClass=WIZARD, origin=Origin(city=Utrecht, country=The Netherlands))
&gt;&gt; FantasyHero(name=gandalf the white, weapons=[WAND], heroClass=WIZARD, origin=Origin(city=Utrecht, country=The Netherlands))

&gt;&gt; true
&gt;&gt; true
&gt;&gt; false
&gt;&gt; --
&gt;&gt; false
&gt;&gt; false
&gt;&gt; false</code></pre><p>That all checks out. We created 2 different instances with the same data and they are considered equal (because we check the property values against each other, not the object instances). The straight copy of our instance is equal as well, while the instance where we modified a property isn&apos;t any more. And we also check that all instances of heroes are different. They are <em>not</em> the same object. All good, we can continue</p><h3 id="mutating-complex-properties">Mutating &quot;complex&quot; properties</h3><p>First, let&apos;s give our Wizard an extra weapon. We all know he yields a sword as well after all. Since we expect copies to be shallow copies here, we would expect the list of weapons to be changed in all copies of the hero if we change the original list. And that&apos;s exactly what happens :</p><pre><code class="language-kotlin">gandalf.weapons.add(WEAPONS.SWORD)
println(gandalf.weapons)
println(anotherGandalf.weapons)
println(gandalfCopy.weapons)
println(betterGandalf.weapons)

&gt;&gt; [WAND, SWORD]
&gt;&gt; [WAND]
&gt;&gt; [WAND, SWORD]
&gt;&gt; [WAND, SWORD]

// Same behaviour in case we change one of the later copies btw
anotherGandalf.weapons.remove(WEAPONS.SWORD)
println(gandalf.weapons)
println(anotherGandalf.weapons)
println(gandalfCopy.weapons)
println(betterGandalf.weapons)

&gt;&gt; [WAND]
&gt;&gt; [WAND]
&gt;&gt; [WAND]
&gt;&gt; [WAND]
</code></pre><p>In the same manner, modifying a property that is a data class itself yields the same types of result, all copies are affected : </p><pre><code class="language-kotlin">gandalf.origin.country = &quot;France&quot;
println(gandalf.origin.country)
println(anotherGandalf.origin.country)
println(gandalfCopy.origin.country)
println(betterGandalf.origin.country)

&gt;&gt; France
&gt;&gt; The Netherlands
&gt;&gt; France
&gt;&gt; France</code></pre><h3 id="mutating-a-simple-property">Mutating a simple property</h3><p>So far so good. Let&apos;s change Gandalf&apos;s name!</p><pre><code class="language-kotlin">gandalf.name = &quot;Gandalf the Blue&quot;
println(gandalf.name)
println(anotherGandalf.name)
println(gandalfCopy.name)
println(betterGandalf.name)
    
&gt;&gt; Gandalf the Blue
&gt;&gt; Gandalf the Grey
&gt;&gt; Gandalf the Grey
&gt;&gt; Gandalf the White</code></pre><p>When mutating the <code>String</code> property of our Wizard, we see that the name change isn&apos;t propagated to any of the copies. </p><p>Doing the same test with an <code>Enum</code> leads to a similar result</p><pre><code class="language-kotlin">gandalf.heroClass = CLASS.PALADIN
println(gandalf.heroClass)
println(anotherGandalf.heroClass)
println(gandalfCopy.heroClass)
println(betterGandalf.heroClass)

&gt;&gt; PALADIN
&gt;&gt; WIZARD
&gt;&gt; WIZARD
&gt;&gt; WIZARD</code></pre><p>Now, that had me confused at first. <a href="https://kotlinlang.slack.com/archives/C0B8RC352/p1658928923155649?ref=lengrand.fr">Confused enough to ask on the Kotlin Slack actually</a> (thanks for the help folks!) . Are the copies holding references to the same values or not? Well, it turns out that they are and the explanation goes back to the basics of Kotlin / Java : &#xA0;References are immutable, so modifying them literally leads to referencing a new Object instead. The behaviour has nothing to do with data classes : </p><pre><code class="language-kotlin">var heroClass = CLASS.WARRIOR
val theClass = heroClass
heroClass = CLASS.WIZARD

println(heroClass)
println(theClass)

&gt;&gt; WIZARD
&gt;&gt; WARRIOR</code></pre><p>When copying data classes around, the constructor passes references to all of the properties, and all of those references are immutable.</p><h2 id="conclusion">Conclusion</h2><p>Just like we expected, the data class <code>copy</code> method create shallow copies of instances. We would have saved time <a href="https://kotlinlang.org/docs/data-classes.html?ref=lengrand.fr#copying">just checking the documentation</a> actually: </p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://lengrand.fr/content/images/2022/07/image.png" class="kg-image" alt="Kotlin Data Classes, shallow copies and immutability" loading="lazy" width="1514" height="392" srcset="https://lengrand.fr/content/images/size/w600/2022/07/image.png 600w, https://lengrand.fr/content/images/size/w1000/2022/07/image.png 1000w, https://lengrand.fr/content/images/2022/07/image.png 1514w" sizes="(min-width: 720px) 720px"><figcaption>A screenshot of the documentation about copying data classes (https://kotlinlang.org/docs/data-classes.html#copying)</figcaption></figure><p>Now back to our question : Are data classes thread safe? Well now I realize that there&apos;s been a confusing in my head during the training. <strong>Data classes have little to do with immutability, they are a convenient way to work with objects purely holding data</strong>. However, you <em>can</em> make them immutable by making sure all of their fields are immutable themselves. That&apos;s what I typically do, and you probably should too &#x1F60A;.</p><h2 id="further-reading">Further reading</h2><p>Interestingly, <a href="https://twitter.com/romainbsl/status/1552550074051330048?ref=lengrand.fr">Romain pointed at a KEEP</a> in the Twitter thread which quite literally discusses the topic mentioned here. I&apos;m keen on seeing what&apos;s gonna happen with Value Classes in the future. </p><p><em>Hit me up <a href="https://twitter.com/jlengrand?ref=lengrand.fr">@jlengrand</a>, if you have questions or remarks, I&apos;m always up for learning new things &#x1F60A; </em></p>]]></content:encoded></item><item><title><![CDATA[Easy dependency integration in Kotlin/JS using the "Elm ports" technique]]></title><description><![CDATA[Just starting with Kotlin/JS, I found it hard to use large NPM libraries like Firebase. But my past experiences with Elm and its system of ports helped me find a suitable solution. Let's dive into it!]]></description><link>https://lengrand.fr/using-elm-knowledge-to-dive-into-kotlin-js/</link><guid isPermaLink="false">624c98b81c11530500c790c8</guid><category><![CDATA[elm]]></category><category><![CDATA[kotlin]]></category><category><![CDATA[javascript]]></category><category><![CDATA[development]]></category><dc:creator><![CDATA[Julien]]></dc:creator><pubDate>Tue, 05 Apr 2022 21:43:44 GMT</pubDate><media:content url="https://lengrand.fr/content/images/2022/04/1_Lfia7SouRPONpkZG7uZ3gg.png" medium="image"/><content:encoded><![CDATA[<img src="https://lengrand.fr/content/images/2022/04/1_Lfia7SouRPONpkZG7uZ3gg.png" alt="Easy dependency integration in Kotlin/JS using the &quot;Elm ports&quot; technique"><p><em>TL;DR : Just starting with Kotlin/JS, I found it hard to use large NPM libraries like Firebase. But my past experiences with Elm and its system of ports helped me find a suitable solution. <a href="https://github.com/jlengrand/kotlin-ports-demo?ref=lengrand.fr">The GitHub repo is here</a> </em></p><p><em>TL;DR2 :You can skip the introduction and <a href="https://lengrand.fr/using-elm-knowledge-to-dive-into-kotlin-js#remember-those-good-old-elm-ports">go straight into the implementation here</a>.</em></p><p><em>TL;DR3 : You can test <a href="https://kotlin-ports-demo.netlify.app/?ref=lengrand.fr">the demo here</a></em></p><hr><p>I&apos;m a backend engineer by trade. But those days, pretty much any idea I have to scratch my own itch can be solved using a simple front-end attached to some basic serverless database like Firebase or <a href="https://supabase.com/?ref=lengrand.fr">Supabase</a> (Big Fan here!).</p><p>It&apos;s totally fine, except that it leaves me very little time to play around with my dear friend <a href="https://kotlinlang.org/?ref=lengrand.fr">Kotlin</a> &#x1F62C;. For that reason, and <a href="https://blog.jetbrains.com/kotlin/2021/12/compose-multiplatform-1-0-is-going-live/?ref=lengrand.fr">because Kotlin Multiplatform hit 1.0</a> a couple months back I decided to spend the last weeks diving into Kotlin/Js.</p><p>I won&apos;t spend much time describing it, but in short Kotlin/JS is letting you write Kotlin that gets transpiled into Javascript, and that can run in your frontend (or you backend, for that matters). Now, there are many reasons why people would or would not want to do that. For me, it&apos;s mostly about my experience as a developer. I want to be enjoying myself after a long day of work. And boy I&apos;ve been spoiled by Elm in the past with Developer Experience, so suffice to say that the bar is high &#x1F601;.</p><p>In this blog, I want to dive into a specific part of my experience : using large NPM libraries in my projects. I&apos;ll use <a href="https://www.npmjs.com/package/firebase?ref=lengrand.fr">Firebase</a> as an example. </p><h2 id="the-problem">The problem</h2><p>Let&apos;s say that I want to use Firebase Auth and Firestore in my project. I&apos;ll need access to a couple functions from the library. Namely : </p><pre><code class="language-javascript">// A function to sign up / login
const userCred = await signInWithPopup(auth, provider);

// Another one to log out
await signOut(auth);

// One function to add document
await addDoc(collection(firestore, `users/${uid}/messages`), {
    content : message
});

// one to get the list of messages
const querySnapshot = await getDocs(collection(firestore, `users/${uid}/messages`));
  querySnapshot.forEach((doc) =&gt; {
    console.log(doc.data()
});
    
    
// finally also a method to automatically get updates are new messages are created to avoid having to fetch messages ourselves
const q = query(collection(firestore, `users/${uid}/messages`));
onSnapshot(q, (querySnapshot) =&gt; {
    const messages = [];
    querySnapshot.forEach((doc) =&gt; {
        callback(messages);
    });
});
    
    </code></pre><p>Now, let&apos;s look at the different ways I have to use those from Kotlin/JS.</p><h2 id="using-external-declarations">Using external declarations</h2><p><a href="https://kotlinlang.org/docs/using-packages-from-npm.html?ref=lengrand.fr">External declarations</a> are a way to describe the javascript so that Kotlin can make use of its static typer. The best use case isn&apos;t it. You&apos;re using Javascript, but typed and in Kotlin. It looks like this : </p><pre><code class="language-Kotlin">@JsModule(&quot;is-sorted&quot;)
@JsNonModule
external fun &lt;T&gt; sorted(a: Array&lt;T&gt;): Boolean</code></pre><p>At first sight, that sounds like a great idea. Except it&apos;s more complex than it looks like. The <code>signInWithPopup</code> method needs a provider, and the <code>auth</code> API. We need to basically setup the project. In Javascript, it concise and easy :</p><pre><code class="language-Javascript">const firebaseApp = initializeApp(FIREBASE_CONFIG);
const provider = new GoogleAuthProvider();
const auth = getAuth();
const firestore = getFirestore(firebaseApp);</code></pre><p>Thing is, those declarations contain a lot of complexity, and they&apos;re heavy to describe. Here is what <code>firebaseApp</code> in the console : </p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://lengrand.fr/content/images/2022/04/image.png" class="kg-image" alt="Easy dependency integration in Kotlin/JS using the &quot;Elm ports&quot; technique" loading="lazy" width="1482" height="506" srcset="https://lengrand.fr/content/images/size/w600/2022/04/image.png 600w, https://lengrand.fr/content/images/size/w1000/2022/04/image.png 1000w, https://lengrand.fr/content/images/2022/04/image.png 1482w" sizes="(min-width: 720px) 720px"><figcaption>a console.log of firebaseApp.</figcaption></figure><p>It&apos;s not an impossible job, I just think it&apos;s too much given that we&apos;re literally never gonna use that <code>firebaseApp</code> again except for instantiating what we need.</p><h2 id="using-dynamic-imports">Using dynamic imports</h2><p></p><p>Another option that the <a href="https://kotlinlang.org/docs/dynamic-type.html?ref=lengrand.fr">Kotlin/JS documentation</a> offers is using the <code>dynamic</code> keyword. The keyword basically disables the type checker. </p><p>You could basically decide to do it like this :</p><pre><code class="language-Kotlin">val firebase: dynamic = require(&quot;firebase/app&quot;).default
    
val firebaseConfig: Json = json(
	&quot;key&quot; to &quot;value&quot;
    ...
)
val firebaseApp: dynamic = firebase.initializeApp(firebaseConfig)</code></pre><p>Well, of course it&apos;s possible but we&apos;re then literally removing all the reasons for me to use Kotlin in the first place?! I don&apos;t get any type checking any more, less smart completion, error handling, . . .</p><p>For me, that&apos;s just not an option.</p><h2 id="using-dukat">Using Dukat</h2><p></p><p>I think the folks building Kotlin/JS are very well aware of that problem, and they know that everyone is not going to start creating adapters for every single NPM package on Earth. That&apos;s a massive work in the first place and even more to maintain. Plus, pretty much all of the Javascript ecosystem offers Typescript definitions those days for their libraries, which is pretty much what we want isn&apos;t it? </p><p>Enters <strong><a href="https://github.com/Kotlin/dukat?ref=lengrand.fr">project Dukat</a></strong>, which aims at leveraging those Typescripts definitions and automatically generate Kotlin external declarations from them. A brilliant idea if you ask me. It looks like this : </p><pre><code class="language-bash">$ npm install -g dukat
$ mkdir test; cd test 
$ npm install firebase
$ dukat firebase/app/dist/app/index.d.ts
</code></pre><p>Which generated a bunch of files like this : </p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://lengrand.fr/content/images/2022/04/image-1.png" class="kg-image" alt="Easy dependency integration in Kotlin/JS using the &quot;Elm ports&quot; technique" loading="lazy" width="824" height="488" srcset="https://lengrand.fr/content/images/size/w600/2022/04/image-1.png 600w, https://lengrand.fr/content/images/2022/04/image-1.png 824w" sizes="(min-width: 720px) 720px"><figcaption>a list of files generated by Dukat</figcaption></figure><p>A brilliant idea indeed, but still in <em>very</em> early stage :). So far, none of the libraries that I have tried to convert have really worked and I ended up spending time in generated Kotlin files trying to fix issues, including converted es5 itself. Not fun</p><p>On top of that, <a href="https://youtrack.jetbrains.com/issue/KT-42290?ref=lengrand.fr">the development of Dukat has currently been paused</a> as the new IR compiler is being stabilised. </p><h2 id="using-a-library">Using a library</h2><p></p><p>Of course, there are folks that have tried to solve that issue before and offer solutions. Namely, the <a href="https://github.com/GitLiveApp/firebase-kotlin-sdk?ref=lengrand.fr">firebase-kotlin-sdk</a> mostly solves that issue in my case.</p><p>To be honest, it looks great. I&apos;ve played around with it just a bit and it would have filled my needs and more. It&apos;s also active, and has multiplatform support. </p><p>I&apos;m not sure why I didn&apos;t go that direction. There are two main reasons : I didn&apos;t really want to add &#xA0;a library dependency on top of a library already. We&apos;ll be running in the browser and want to be light remember? It looked like overkill for 4 functions. On top of that, the library introduces additional concepts and I didn&apos;t want to have to dive into additional documentation. Learning Kotlin/JS itself was well enough. </p><h2 id="remember-those-good-old-elm-ports"> &#xA0;Remember those good old Elm ports?</h2><p></p><p>At that point I had been struggling between options for a few days. As I was having coffee with my old friend <a href="https://twitter.com/Swendude?ref=lengrand.fr">Swen</a> he told me &quot;What don&apos;t you just do like we were doing with Elm ports?&quot;. HA! There was the nice option I was searching for the past few days :).</p><h3 id="a-step-back-elm-intro">A step back, Elm intro</h3><p>For those of you that never heard about <a href="https://elm-lang.org/?ref=lengrand.fr">Elm</a> understand what I&apos;m talking about, I have to explain a little. Elm is a<strong> pure functional language for the front-end</strong>. What that means is that if you write something in Elm, you pretty much CANNOT have runtime exceptions. And of course, everything is strongly typed.</p><p>(Seriously though, Elm has been a mind blowing experience for me, try it out).</p><p>Now, that sounds nice, but it doesn&apos;t play nice with Javascript at all. When you write Elm, the runtime makes sure you are safe. But in the Javascript world on the other end, you benefit from all the nice libraries and APIs; even if they can fail. </p><p>To solve that problem <a href="https://lengrand.fr/a-short-introduction-to-ports-and-flags-in-elm/">Elm introduces the concept of ports</a>, which are a way to bridge from the unsafe world of wild Javascript into the nice, clean world of Elm. </p><p>Ports basically allow you to avoid a full rewrite by letting you create strong interface that you define as unbreakable, so that you can interact with your Javascript. </p><h3 id="and-were-back-with-kotlin">And we&apos;re back with Kotlin</h3><p>It might not be very crystal clear just yet, but there is nothing complex I promise. We&apos;ll apply the same concept of ports here in our Kotlin app. What we will do is :</p><ul><li>Define a clear interface of what we need in Javascript using external declarations </li><li>On the Javascript side, create a thin layer to do the heavy lifting for us </li><li>On the Kotlin side, enjoy that safe typed layer, free of anything that we do not require.</li></ul><p>Let&apos;s come back to the Javascript functions we need in Kotlin :</p><pre><code class="language-Javascript">import {FIREBASE_CONFIG} from &quot;./constants&quot;;
import { initializeApp } from &quot;firebase/app&quot;;
import { getAuth, signInWithPopup, GoogleAuthProvider, signOut } from &quot;firebase/auth&quot;;
import { collection, addDoc, getFirestore, getDocs, onSnapshot, query}  from &quot;firebase/firestore&quot;;

const firebaseApp = initializeApp(FIREBASE_CONFIG);
const provider = new GoogleAuthProvider();
const auth = getAuth();
const firestore = getFirestore(firebaseApp);

const userCred = await signInWithPopup(auth, provider);
await signOut(auth);

await addDoc(collection(firestore, `users/${uid}/messages`), {
    content : message
});

const querySnapshot = await getDocs(collection(firestore, `users/${uid}/messages`));
  querySnapshot.forEach((doc) =&gt; {
    console.log(doc.data()
});
    
const q = query(collection(firestore, `users/${uid}/messages`));
onSnapshot(q, (querySnapshot) =&gt; {
    const messages = [];
    querySnapshot.forEach((doc) =&gt; {
        callback(messages);
    });
});
    
    </code></pre><p>On that side, we create our own API layer, with <em>only </em>what we need. The inputs we&apos;ll get, and the output we need in our Kotlin app.</p><pre><code class="language-Javascript">const firebaseApp = initializeApp(FIREBASE_CONFIG);
const provider = new GoogleAuthProvider();
const auth = getAuth();
const firestore = getFirestore(firebaseApp);

export async function logIn(){
    const userCred = await signInWithPopup(auth, provider);

    return {
        accessToken: userCred.user.accessToken,
        email: userCred.user.email,
        uid: userCred.user.uid,
    }
}

export async function logOut(){
    await signOut(auth);
}

export async function saveMessage(uid, message){
    await addDoc(collection(firestore, `users/${uid}/messages`), {
        content : message
    });
}

export async function getMessages(uid){
    let messages = [];

    const querySnapshot = await getDocs(collection(firestore, `users/${uid}/messages`));
    querySnapshot.forEach((doc) =&gt; {
        messages.push({
            id: doc.id,
            content: doc.data().content
        })
    });

    return messages;
}

export async function syncMessages(uid, callback){
    const q = query(collection(firestore, `users/${uid}/messages`));
    onSnapshot(q, (querySnapshot) =&gt; {
        const messages = [];
        querySnapshot.forEach((doc) =&gt; {
            messages.push({
                id: doc.id,
                content: doc.data().content
            })
        });

        callback(messages);
    });
}</code></pre><p>And on the Kotlin side, we now can declare our own thin API layer</p><pre><code class="language-Kotlin">external interface AppUser{
    val email: String
    val uid: String
    val accessToken: String
}

external interface AppMessage{
    val id: String
    val content: String
}

@JsModule(&quot;@jlengrand/firebase-ports&quot;)
@JsNonModule
external object FirebasePorts{
    fun logIn() : Promise&lt;AppUser&gt;
    fun logOut()

    fun saveMessage(uid: String, message: String)
    fun getMessages(uid: String) : Promise&lt;Array&lt;AppMessage&gt;&gt;

    fun syncMessages(uid:String, callback: (Array&lt;AppMessage&gt;?) -&gt; Unit)
}

// And we can use those in my app directly like this
Button(attrs = {
    onClick {
        error = null
        FirebasePorts.getMessages(user!!.uid)
            .then { messages = it }
            .catch { error = it.message }
    }
}) {
    Text(&quot;Retrieve messages manually!&quot;)
}</code></pre><p>And that&apos;s it really, there is nothing more. By writing 50 lines of Javascript that I can easily test, I lifted the need for the whole interoperability with the Firebase library in my Kotlin app.</p><p>Even better, my Kotlin does not even contain any reference to Firebase any more &#x1F92F;. I could just as well change the implementation on the Javascript side to using Supabase and it would work just as nicely. <strong>Firebase has become an artifact of implementation.</strong></p><h3 id="conclusion">Conclusion</h3><p>It&apos;s been a long intro, so I&apos;ll write some words of conclusion &#x1F603;.</p><p>Kotlin/JS is still in its infancy, but I like the power of having access to my favorite backend language to write front-end as well. Let&apos;s see what the future holds. </p><p>Interacting with the Javascript ecosystem is still a tough cookie though. There are many options, all with their own drawbacks. I believe that the &quot;Elm Ports method&quot; has some great stuff going for it though: </p><ul><li><strong>No compromises on the Kotlin side, with no dynamics</strong></li><li><strong>No need to dive into the library&apos;s interface to create the Kotlin external declarations.</strong></li><li><strong>A clean interface, that can even be used as a layer of separation of concern</strong></li><li><strong>No need for an extra library, you are in control of the whole lot </strong></li></ul><p>Of course, it will be way less interesting if you want to build multiplatform modules, or if you use the capabilities of the library extensively.</p><p>You can find an example implementation using the code described above on<strong> <a href="https://github.com/jlengrand/kotlin-ports-demo?ref=lengrand.fr">that GitHub repository</a></strong>.</p><p>I hope you liked the post, let me know what you think, it&apos;s the very beginning of my Kotlin/JS journey &#x1F389;.</p><p></p><p>Some references : </p><ul><li>The image of this post comes from <a href="https://proandroiddev.com/taming-state-in-android-with-elm-architecture-and-kotlin-part-2-c709f75f7596?ref=lengrand.fr">here</a> </li><li>Stack Overflow question, <a href="https://stackoverflow.com/questions/69629323/uncaught-referenceerror-firebase-is-not-defined-in-kotlin-js-project-with-dukat?ref=lengrand.fr">using Dukat with Firebase</a></li><li><a href="https://kanawish.com/posts/201020-kotlin-js/?ref=lengrand.fr">Someone else using Kotlin/JS with Firebase</a></li><li><a href="https://github.com/jlengrand/elm-firebase?ref=lengrand.fr">The very same demo, also from me, but in Elm</a></li></ul>]]></content:encoded></item></channel></rss>