Jekyll2023-03-10T08:04:32+00:00https://code.lulalala.com/feed.xmlAll talk but no codeTranslating XML/Epub using DeepL & ChatGPT2023-03-07T14:00:00+00:002023-03-07T14:00:00+00:00https://code.lulalala.com/2023/xml-epub-translator<p>I developed two ruby gems last year:</p>
<ul>
<li><a href="https://gitlab.com/lulalala/natsukantou">natsukantou</a> for XML translation</li>
<li><a href="https://gitlab.com/lulalala/epub-translator">epub-translator</a> for Epub translation</li>
</ul>
<p>Originally they started out as one simple script to call DeepL API. However gradually my use cases grew, and the script became very unmaintainable, with lots of duplicated code and similar method names. I needed something flexible, so I restarted from scratch.</p>
<p>My goal was to make it <strong>very</strong> customizable to the user:</p>
<ul>
<li>Swappable components:
<ul>
<li>translation engine</li>
<li>plugin, e.g. glossary substitution</li>
</ul>
</li>
<li>Configuration of components at different level:
<ul>
<li>Static config which is reusable</li>
<li>Overridable at call time</li>
</ul>
</li>
<li>User friendly</li>
</ul>
<p>A typical UNIX console program can be be configurable by providing many flags, but I wonder how to represent swappable components with those flags.</p>
<p>What can be friendlier than command line flags? The middleware pattern came into mind. Rails developers should be familiar with this due to Rack, basically it allows stacking of components, which can be filters or different translation engines. The ruby gem <code class="highlighter-rouge">middleware</code> provides exactly this framework, so my configuration file ends up looking like this:</p>
<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="no">Middleware</span><span class="o">::</span><span class="no">Builder</span><span class="p">.</span><span class="nf">new</span> <span class="k">do</span>
<span class="n">use</span> <span class="no">Natsukantou</span><span class="o">::</span><span class="no">Glossary</span><span class="p">,</span> <span class="ss">filepath: </span><span class="s1">'file.tsv'</span>
<span class="n">use</span> <span class="no">Natsukantou</span><span class="o">::</span><span class="no">OtherPlugin</span><span class="p">,</span> <span class="ss">foo: </span><span class="s1">'bar'</span>
<span class="n">use</span> <span class="no">Natsukantou</span><span class="o">::</span><span class="no">DeepL</span><span class="p">,</span> <span class="ss">auth_key: </span><span class="s2">"123"</span><span class="p">,</span> <span class="ss">host: </span><span class="s1">'http://example.com'</span>
<span class="k">end</span>
</code></pre></div></div>
<p>The XML DOM will be accessible in an hash called <code class="highlighter-rouge">env</code>, which will be passed through each stack. The “Glossary” would substitute terms in the DOM, then passing <code class="highlighter-rouge">env</code> down to “OtherPlugin”, which would do its own thing and finally passing <code class="highlighter-rouge">env</code> to “DeepL” for translation. All the static configurations such as API keys can be specified on each stack.</p>
<p>The benefit is that I could persist this configuration as a <code class="highlighter-rouge">.rb</code> file, then reuse it everytime I want to translate. It also can be used from other Ruby programs directly.</p>
<h3 id="plugins-in-action">Plugins in action</h3>
<p>I want to showcase one available plugin: <code class="highlighter-rouge">HandleRubyMarkup</code>. No, this “Ruby” is not referring to the Ruby programming language, but instead the HTML <a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Element/ruby">Ruby Markup</a> often used in annotating Japanese text. It is often desirable to remove this in XMLs before translation, because each individual characters in a term can be tokenized and marked individually by separate XML tags, and DeepL is not smart enough to handle that. This plugin flattens the text and removes the annotation.</p>
<h3 id="wizard-yard">Wizard Yard</h3>
<p>The middleware configuration file is flexible, but would still be a big ask for someone who does not know ruby. Can we improve it further?</p>
<p>I decided to create a wizard which would create this config file. A few ruby gems offers this, and I decided to use <a href="https://github.com/piotrmurach/tty-prompt">tty-prompt</a>. Ideally the wizard asks the user to select desired components, and then for each of them input the parameters.</p>
<p>At first, I thought I could just access Ruby’s method object and use its parameters, e.g. <code class="highlighter-rouge">method(:initialize).parameters</code>. However soon I realized that this is not enough. A good wizard needs to provide explanatory text, and tell the user what type of input is expected. The Ruby method parameter declaration do not encode these information. So where should I write those?</p>
<p>RBS type signature could be one possibility, but YARD comments is the most comprehensive. They are easier to write, and also offers API to parse YARD comments (though not as convenient as I had wished). In the end the <a href="https://gitlab.com/lulalala/natsukantou/blob/main/lib/natsukantou/setup/config_prompt.rb">parser</a> can obtain parameter name, type, description, and whether they are optional, mandatory or with default value. This means if anyone is to implement a plugin or translation engine, they must write Yard comments for the wizard to work.</p>
<p>Finally, I used erb template for generating the middleware configuration file.</p>
<h3 id="xml-and-epub">XML and Epub</h3>
<p>Originally I choose <a href="https://gitlab.com/yorickpeterse/oga">Oga</a> for XML process, thinking it is a better candidate to run on Windows than Nokogiri. This turned out to not matter because the <a href="https://gitlab.com/KitaitiMakoto/epub-maker">epub-maker</a> gem I used relies on Nokogiri (but its partner <a href="https://gitlab.com/KitaitiMakoto/epub-parser">epub-parser</a> is gem agnostic). Anyways I did <a href="https://gitlab.com/yorickpeterse/oga/-/issues/206">encounter & fix a bug</a> on node manipulation, contribution FTW.</p>
<h3 id="how-to-use">How to use</h3>
<p>To run it as a command:</p>
<div class="highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ epub-translator [EPUB_FILE]
</code></pre></div></div>
<p>The wizard would trigger and guide you through setting up a translator configuration.</p>
<p><img src="/assets/2023-03-07-xml-epub-translator/eg_wizard_config.png" alt="Wizard" /></p>
<p>Then it asks you how and what to translate. Enter the language codes (e.g. “en”),
and select chapters to translate (by default all are selected).</p>
<p><img src="/assets/2023-03-07-xml-epub-translator/eg_wizard_translate.png" alt="Wizard" /></p>
<p>During the process, the wizard will ask you whether you want to save the configuration for later reuse. If you choose yes, the config will be saved as <code class="highlighter-rouge">translator_config.rb</code> file.</p>
<p>You will be able to use this config later by using the <code class="highlighter-rouge">-c</code> flag:</p>
<div class="highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ epub-translator [EPUB_FILE] -c translator_config.rb
</code></pre></div></div>
<h3 id="interlace">Interlace</h3>
<p><img src="/assets/2023-03-07-xml-epub-translator/eg_interlace.png" alt="Wizard" style="float: right; width: 300px; margin-left: 1em" /></p>
<p>Interlacing translations is a very useful feature, especially for proof checking the machine translation. I wrote an <a href="https://gitlab.com/lulalala/natsukantou/blob/main/lib/monkey_patch/oga_document.rb#L36-36"><code class="highlighter-rouge">interlace</code> method on <code class="highlighter-rouge">Oga::XML::Document</code></a>, which loops through nodes from two documents, adding nodes from one document to the other. During this I also set the HTML lang attribute. This is so the CSS to style different languages differently.</p>
<p>I made it into a separate command, to adhere to the UNIX philosophy. Theoretically, we could give it three files and ask it to interlace them together. I wonder if there is such a need.</p>
<h3 id="chatgpt-and-the-future">ChatGPT and the future</h3>
<p>ChatGPT is the hype, but I didn’t think much about it because what I just assumed it could not handle XML, and I was wrong. Last week, I discovered that the following prompt works:</p>
<blockquote>
<p>Translate the XML <code class="highlighter-rouge">#{text}</code> from #{env.lang_from.code} to #{env.lang_to.code}, maintaining XML structure.</p>
</blockquote>
<p>I actually don’t know how well it works in different cases, but FOMO! I quickly implemented the prompt. Thanks to the existing architecture, writing this only took half a day.</p>
<p>One problem which I still can’t resolve is to make the glossary plugin work. My current glossary plugin works by replacing terms from the source language to the target language, and then wrapping it with a <skip> tag. DeepL can then be configured to skip those. ChatGPT however, is very inconsistent. Sometimes my modified request would work on the first try, [sometime I need to ask it again](https://twitter.com/lulalala_it/status/1633115456138534914). If you are a prompt guru and knows how to resolve this, please enlighten me.</skip></p>
<p>I also haven’t tackled the “overridable config at call time” yet. Currently configuration are static.</p>
<p>All in all, currently DeepL does everything ChatGPT does better, and offers more functionalities. Treat ChatGPT as a toy for now.</p>
<p>Anyways, thanks for reading, and I wish you will find <code class="highlighter-rouge">epub-translator</code> and <code class="highlighter-rouge">natsukantou</code> useful. Happy translating.</p>I developed two ruby gems last year:Rails 6.1’s ActiveModel Errors 介面大翻新2020-06-19T10:13:00+00:002020-06-19T10:13:00+00:00https://code.lulalala.com/2020/active-model-errors.zh<p><strong><em>The English version of this post is available <a href="0531-1013.html">here</a></em></strong></p>
<p>Rails 6.1 預計今年會推出,其中包含了 ActiveModel Errors 的大改變。我想來解釋一下改變的原因,以及開發者該怎樣做準備。</p>
<h2 id="每個錯誤都被包成一個物件-">每個錯誤都被包成一個物件 <img src="/assets/2020-05-31-active-model-errors/illus1-1.jpg" alt="" style="float: right; width: 300px; margin-left: 1em" /></h2>
<p>當 model attribute 有錯誤資料時,呼叫 <code class="highlighter-rouge">valid?</code> 會在 <code class="highlighter-rouge">errors.messages</code> 這個 hash 中產生每個 attribute 相對應的錯誤訊息。不過在 Rails 5.0 開始,又新增了另一份叫做 <code class="highlighter-rouge">details</code> 的 hash,讓開發者還能取得更詳細的錯誤資訊。</p>
<p><img src="/assets/2020-05-31-active-model-errors/illus1-3.jpg" alt="" style="float: right; width: 300px; margin-left: 1em" /></p>
<p>這兩個hash應該要同步一對一,但是要把messages中某訊息對應到details是一件很瑣碎的工作。我們必須記得array index後才能在另一邊取得資料。這兩個 hash 也會在某些特定狀況變得不一致。有學過物件導向的大家應該都能了解資料不封裝的話,資料一致性就容易被破壞。所以在 Rails 6.1 ,我們把單一錯誤的所有資訊都封裝在一個物件中。</p>
<p><strong>現在<code class="highlighter-rouge">book.errors</code>其實是一陣列的<a href="https://edgeapi.rubyonrails.org/classes/ActiveModel/Error.html"><code class="highlighter-rouge">Error</code></a>物件</strong>.</p>
<p><img src="/assets/2020-05-31-active-model-errors/illus1-4.jpg" alt="" style="float: right; width: 300px; margin-left: 1em" /></p>
<p>大改變通常就會伴隨著deprecation或是需要開發者修復的breaking change,以下我會把最重要的改變列出來:</p>
<h2 id="如何更新">如何更新?</h2>
<h3 id="message-跟-details"><code class="highlighter-rouge">message</code> 跟 <code class="highlighter-rouge">details</code></h3>
<p>原本 <code class="highlighter-rouge">messages</code>, <code class="highlighter-rouge">full_messages</code> and <code class="highlighter-rouge">details</code> 這些方法都是一次提供所有錯誤資訊。現在新的 <code class="highlighter-rouge">Error</code> 物件只提供關於自己的資料:</p>
<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">e</span> <span class="o">=</span> <span class="no">ActiveModel</span><span class="o">::</span><span class="no">Error</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="n">book</span><span class="p">,</span> <span class="ss">:title</span><span class="p">,</span> <span class="ss">:too_long</span><span class="p">,</span> <span class="ss">limit: </span><span class="mi">1000</span><span class="p">)</span>
<span class="n">e</span><span class="p">.</span><span class="nf">message</span> <span class="c1"># 'is too long (maximum is 1000 characters)'</span>
<span class="n">e</span><span class="p">.</span><span class="nf">full_message</span> <span class="c1"># 'Title is too long (maximum is 1000 characters)'</span>
<span class="n">e</span><span class="p">.</span><span class="nf">detail</span> <span class="c1"># { limit: 1000, error: :too_long }</span>
</code></pre></div></div>
<h3 id="enumeration-方法">Enumeration 方法</h3>
<p>原本 <code class="highlighter-rouge">errors</code> 運作類似 hash,比如說我們可以:</p>
<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">book</span><span class="p">.</span><span class="nf">errors</span><span class="p">.</span><span class="nf">each</span> <span class="k">do</span> <span class="o">|</span><span class="n">attribute</span><span class="p">,</span> <span class="n">error_message</span><span class="o">|</span>
<span class="nb">puts</span> <span class="n">attribute</span>
<span class="nb">puts</span> <span class="n">error_message</span>
<span class="k">end</span>
</code></pre></div></div>
<p>為了相容性,在 Rails 6.1 中,上面的程式會出現 deprecation 警告。不過,要是 block 參數不是兩個而是只有一個時,列舉(enumeration)的方法就會 yield 出 Error 物件:</p>
<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">book</span><span class="p">.</span><span class="nf">errors</span><span class="p">.</span><span class="nf">each</span> <span class="k">do</span> <span class="o">|</span><span class="n">error</span><span class="o">|</span>
<span class="nb">puts</span> <span class="n">error</span><span class="p">.</span><span class="nf">attribute</span>
<span class="nb">puts</span> <span class="n">error</span><span class="p">.</span><span class="nf">message</span>
<span class="k">end</span>
</code></pre></div></div>
<p>有些列舉方法是沒有 block 的,如 <code class="highlighter-rouge">first</code> ,這些方法以前會回傳只有一個陣列,現在則是會回傳 <code class="highlighter-rouge">Error</code> 物件。這是breaking change,要是你的網頁有用到的話,麻煩更改一下。</p>
<h3 id="避免直接更改-hash">避免直接更改 hash</h3>
<p>以往,你可以直接插入新的錯誤訊息到 hash 中:</p>
<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">book</span><span class="p">.</span><span class="nf">errors</span><span class="p">[</span><span class="ss">:title</span><span class="p">]</span> <span class="o"><<</span> <span class="s1">'is not interesting enough.'</span>
<span class="n">book</span><span class="p">.</span><span class="nf">errors</span><span class="p">[</span><span class="ss">:title</span><span class="p">].</span><span class="nf">clear</span>
</code></pre></div></div>
<p>現在這些操作會給你 deprecation 警告,因為在未來,這些 hash 是每次呼叫時動態產生的,而不再是資料存放的源頭。要操作的話,請使用 <code class="highlighter-rouge">add</code> 方法或其他 <a href="https://ruby-doc.org/core-2.7.1/Array.html">Array</a> 方法:</p>
<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">book</span><span class="p">.</span><span class="nf">errors</span><span class="p">.</span><span class="nf">add</span><span class="p">(</span><span class="ss">:title</span><span class="p">,</span> <span class="s1">'is not interesting enough.'</span><span class="p">)</span>
<span class="n">book</span><span class="p">.</span><span class="nf">errors</span><span class="p">.</span><span class="nf">delete_all</span> <span class="p">{</span> <span class="o">|</span><span class="n">error</span><span class="o">|</span> <span class="n">error</span><span class="p">.</span><span class="nf">type</span> <span class="o">==</span> <span class="ss">:too_long</span> <span class="p">}</span>
</code></pre></div></div>
<p>以下的資料更改未來會沒有作用:</p>
<ul>
<li><code class="highlighter-rouge">errors</code> 本身類似 hash 的操作 (e.g. <code class="highlighter-rouge">errors[:foo] = 'bar'</code>)</li>
<li><code class="highlighter-rouge">errors#messages</code> 回傳的 hash (e.g. <code class="highlighter-rouge">errors.messages[:foo] = 'bar'</code>)</li>
<li><code class="highlighter-rouge">errors#details</code> 回傳的 hash (e.g. <code class="highlighter-rouge">errors.details[:foo].clear</code>)</li>
</ul>
<h3 id="消除類似hash的介面">消除類似hash的介面</h3>
<p>因為我們要把errors變成陣列,所以類hash的介面之後也會被移除,像是:</p>
<ul>
<li><code class="highlighter-rouge">errors#slice!</code></li>
<li><code class="highlighter-rouge">errors#values</code></li>
<li><code class="highlighter-rouge">errors#keys</code></li>
</ul>
<h3 id="其他">其他</h3>
<ul>
<li><code class="highlighter-rouge">errors#to_xml</code> 會被移除</li>
<li><code class="highlighter-rouge">errors#to_h</code> 會被移除,但是可以用<code class="highlighter-rouge">errors#to_hash</code>替代。</li>
</ul>
<h2 id="新東西">新東西</h2>
<p>新的<code class="highlighter-rouge">where</code>方法能用來尋找特定的錯誤。要注意的是它跟你熟悉的 ActiveRecord#where 有一點不同,我們可以依照以下的參數做篩選:</p>
<ul>
<li>model attribute (Symbol, 必須)</li>
<li>error type (Symbol, 非必須)</li>
<li>options (hash, 非必須)</li>
</ul>
<p>篩選是依照有提供的參數:</p>
<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">book</span><span class="p">.</span><span class="nf">errors</span><span class="p">.</span><span class="nf">where</span><span class="p">(</span><span class="ss">:name</span><span class="p">)</span> <span class="c1"># => 所有跟 name 相關的錯誤</span>
<span class="n">book</span><span class="p">.</span><span class="nf">errors</span><span class="p">.</span><span class="nf">where</span><span class="p">(</span><span class="ss">:name</span><span class="p">,</span> <span class="ss">:too_short</span><span class="p">)</span> <span class="c1"># => 所有 name 錯誤中是 too_short 類的錯誤</span>
<span class="n">book</span><span class="p">.</span><span class="nf">errors</span><span class="p">.</span><span class="nf">where</span><span class="p">(</span><span class="ss">:name</span><span class="p">,</span> <span class="ss">:too_short</span><span class="p">,</span> <span class="ss">minimum: </span><span class="mi">2</span><span class="p">)</span> <span class="c1"># => 所有 name 錯誤中是 too_short 類的錯誤,而且最低要求是 2</span>
</code></pre></div></div>
<p>新的<code class="highlighter-rouge">import</code> 方法是用來匯入並包裝其他地方的錯誤。這對association或是巢狀service object很有幫助,未來應該能開啟新的應用方向。原本的<code class="highlighter-rouge">merge!</code>已經使用<code class="highlighter-rouge">import</code>了。</p>
<p><code class="highlighter-rouge">delete</code> 方法以往只能刪除所有跟某 attribute 相關的錯誤。以後它也能做出更細緻的篩選,比如說只刪除 name 錯誤中 too_short 錯誤。</p>
<h1 id="結論">結論</h1>
<p>若你有興趣,可以閱讀原始的 <a href="https://github.com/rails/rails/pull/32313">pull request</a>。你也可以閱讀<a href="https://edgeapi.rubyonrails.org/classes/ActiveModel/Errors.html">官方API文件</a>。</p>
<p>Rails 6.1 還沒正式發表,這篇又是照我一年前的記憶寫的,所以有些錯誤。在此先說聲抱歉。</p>
<p>寫這篇是希望大家在 6.1 塵埃落定之前提出建議,以便作出修正或是減少升版號的痛苦。所以歡迎大家提供自己的想法。感謝~</p>The English version of this post is available hereRails 6.1’s ActiveModel Errors Revamp2020-05-31T10:13:00+00:002020-05-31T10:13:00+00:00https://code.lulalala.com/2020/active-model-errors<p>The Rails 6.1 will probably be released this year, and with it comes the major changes in ActiveModel Errors. I want to explain the rationale behind the change, and how we can prepare for the upgrade.</p>
<h2 id="wrap-each-error-as-an-object">Wrap each error as an object</h2>
<p>When our model object contains invalid data, the <code class="highlighter-rouge">valid?</code> call would fill up the errors information. Historically this behaved like a hash which maps attribute to error messages. Later in Rails 5.0, a separate <code class="highlighter-rouge">details</code> hash was added for accessing additional information.</p>
<p><img src="/assets/2020-05-31-active-model-errors/illus1-5.jpg" alt="before" /></p>
<p>The two hashes are supposed to be a one-to-one mapping, but finding one message’s corresponding details is actually a chore. You need to get the index of the message, then use that to access the second hash. The two hashes can also get out of sync in a few edge cases. Keeping internal state consistent is more difficult than one would imagine,</p>
<p>For this reason, in 6.1, we wrapped the relevant error information together as an <code class="highlighter-rouge">Error</code> object. <strong>Now under the hood of <code class="highlighter-rouge">book.errors</code> is an array of <a href="https://edgeapi.rubyonrails.org/classes/ActiveModel/Error.html"><code class="highlighter-rouge">Error</code></a> objects</strong>.</p>
<p><img src="/assets/2020-05-31-active-model-errors/illus1-6.jpg" alt="Rails 6.1" /></p>
<p>As with all big changes, there will be deprecations and breaking changes which requires updates. I will list the most important things below:</p>
<h2 id="how-to-upgrade">How to upgrade?</h2>
<h3 id="message-and-details">Message and details</h3>
<p>Instead of accessing <code class="highlighter-rouge">messages</code>, <code class="highlighter-rouge">full_messages</code> and <code class="highlighter-rouge">details</code>, which covers all errors, each individual <code class="highlighter-rouge">Error</code> object knows about its own information:</p>
<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">e</span> <span class="o">=</span> <span class="no">ActiveModel</span><span class="o">::</span><span class="no">Error</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="n">model</span><span class="p">,</span> <span class="ss">:title</span><span class="p">,</span> <span class="ss">:too_long</span><span class="p">,</span> <span class="ss">count: </span><span class="mi">1000</span><span class="p">)</span>
<span class="n">e</span><span class="p">.</span><span class="nf">message</span> <span class="c1"># 'is too long (maximum is 1000 characters)'</span>
<span class="n">e</span><span class="p">.</span><span class="nf">full_message</span> <span class="c1"># 'Title is too long (maximum is 1000 characters)'</span>
<span class="n">e</span><span class="p">.</span><span class="nf">detail</span> <span class="c1"># { count: 1000, error: :too_long }</span>
</code></pre></div></div>
<h3 id="enumeration-methods">Enumeration methods</h3>
<p>Previously, <code class="highlighter-rouge">errors</code> behaves like a hash and we do this:</p>
<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">book</span><span class="p">.</span><span class="nf">errors</span><span class="p">.</span><span class="nf">each</span> <span class="k">do</span> <span class="o">|</span><span class="n">attribute</span><span class="p">,</span> <span class="n">error_message</span><span class="o">|</span>
<span class="nb">puts</span> <span class="n">attribute</span>
<span class="nb">puts</span> <span class="n">error_message</span>
<span class="k">end</span>
</code></pre></div></div>
<p>For compatibility, this would result in deprecation message Rails 6.1. What you can do is to use the single arity enumerator:</p>
<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">book</span><span class="p">.</span><span class="nf">errors</span><span class="p">.</span><span class="nf">each</span> <span class="k">do</span> <span class="o">|</span><span class="n">error</span><span class="o">|</span>
<span class="nb">puts</span> <span class="n">error</span><span class="p">.</span><span class="nf">attribute</span>
<span class="nb">puts</span> <span class="n">error</span><span class="p">.</span><span class="nf">message</span>
<span class="k">end</span>
</code></pre></div></div>
<p>As shown above, if the block takes only one parameter, it would return the <code class="highlighter-rouge">Error</code> objects directly. So when you call <code class="highlighter-rouge">first</code>, those would also return the <code class="highlighter-rouge">Error</code> object.</p>
<h3 id="avoid-modifying-the-hashes-directly">Avoid modifying the hashes directly</h3>
<p>In the past you can add new errors by appending messages directly on to the hash:</p>
<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">book</span><span class="p">.</span><span class="nf">errors</span><span class="p">[</span><span class="ss">:title</span><span class="p">]</span> <span class="o"><<</span> <span class="s1">'is not interesting enough.'</span>
<span class="n">book</span><span class="p">.</span><span class="nf">errors</span><span class="p">[</span><span class="ss">:title</span><span class="p">].</span><span class="nf">clear</span>
</code></pre></div></div>
<p>This would now also raise a deprecation warning, because it is no longer the source of truth, but generated from the Error objects when called. Instead always use the <code class="highlighter-rouge">add</code> interface, or enumeration methods:</p>
<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">book</span><span class="p">.</span><span class="nf">errors</span><span class="p">.</span><span class="nf">add</span><span class="p">(</span><span class="ss">:title</span><span class="p">,</span> <span class="s1">'is not interesting enough.'</span><span class="p">)</span>
<span class="n">book</span><span class="p">.</span><span class="nf">errors</span><span class="p">.</span><span class="nf">delete_all</span> <span class="p">{</span> <span class="o">|</span><span class="n">error</span><span class="o">|</span> <span class="n">error</span><span class="p">.</span><span class="nf">type</span> <span class="o">==</span> <span class="ss">:too_long</span> <span class="p">}</span>
</code></pre></div></div>
<p>Manipulating the following will have no effect:</p>
<ul>
<li><code class="highlighter-rouge">errors</code> itself as a hash (e.g. <code class="highlighter-rouge">errors[:foo] = 'bar'</code>)</li>
<li>the hash returned by <code class="highlighter-rouge">errors#messages</code> (e.g. <code class="highlighter-rouge">errors.messages[:foo] = 'bar'</code>)</li>
<li>the hash returned by <code class="highlighter-rouge">errors#details</code> (e.g. <code class="highlighter-rouge">errors.details[:foo].clear</code>)</li>
</ul>
<h3 id="removal-of-hash-like-interface">Removal of hash-like interface</h3>
<p>As we move towards the array like data representation, several hash-like interfaces will be deprecated and removed. These include:</p>
<ul>
<li><code class="highlighter-rouge">errors#slice!</code></li>
<li><code class="highlighter-rouge">errors#values</code></li>
<li><code class="highlighter-rouge">errors#keys</code></li>
</ul>
<h3 id="misc">Misc</h3>
<ul>
<li><code class="highlighter-rouge">errors#to_xml</code> will be removed.</li>
<li><code class="highlighter-rouge">errors#to_h</code> will be removed, and can be replaced with <code class="highlighter-rouge">errors#to_hash</code>.</li>
</ul>
<h2 id="new-things">New things</h2>
<p>To help filtering the errors, a new <code class="highlighter-rouge">where</code> method is provided. Its method signature is different to ActiveRecord query method though. You can filter by:</p>
<ul>
<li>attribute name (required)</li>
<li>error type (optional)</li>
<li>options (optional)</li>
</ul>
<p>Only supplied params will be matched.</p>
<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">book</span><span class="p">.</span><span class="nf">errors</span><span class="p">.</span><span class="nf">where</span><span class="p">(</span><span class="ss">:name</span><span class="p">)</span> <span class="c1"># => all errors related to name attribute.</span>
<span class="n">book</span><span class="p">.</span><span class="nf">errors</span><span class="p">.</span><span class="nf">where</span><span class="p">(</span><span class="ss">:name</span><span class="p">,</span> <span class="ss">:too_short</span><span class="p">)</span> <span class="c1"># => all name attribute errors of being too short</span>
<span class="n">book</span><span class="p">.</span><span class="nf">errors</span><span class="p">.</span><span class="nf">where</span><span class="p">(</span><span class="ss">:name</span><span class="p">,</span> <span class="ss">:too_short</span><span class="p">,</span> <span class="ss">minimum: </span><span class="mi">2</span><span class="p">)</span> <span class="c1"># => all name attribute errors of being too short and minimum being 2</span>
</code></pre></div></div>
<p>The new <code class="highlighter-rouge">import</code> method (which <code class="highlighter-rouge">merge!</code> uses) allows errors to be nested. This is especially helpful for association validation or <code class="highlighter-rouge">ActiveInteraction</code>, where deep nesting can occur. Nested error opens up possibility to access more information in such cases.</p>
<p><code class="highlighter-rouge">delete</code> method now accepts more granular filters, so you can delete specific type of error within an attribute.</p>
<p>There are also one additional benefit: now we have a dedicated class for this, it will be easier to monkey patch, and for gem authors to write new extensions.</p>
<h1 id="conclusion">Conclusion</h1>
<p>If you are interseted, you can check out the original <a href="https://github.com/rails/rails/pull/32313">pull request</a>. You can also read the <a href="https://edgeapi.rubyonrails.org/classes/ActiveModel/Errors.html">official doc</a> for more information.</p>
<p>As the code is still changing constantly, and me forgetting things here and there, there are probably some errors in this post. I’ll keep this updated, but apology in advance.</p>
<p>If you have some suggestion please feel free to leave a comment below or open an issue on Rails repository. Thanks!</p>The Rails 6.1 will probably be released this year, and with it comes the major changes in ActiveModel Errors. I want to explain the rationale behind the change, and how we can prepare for the upgrade.Settei - 又一個讀取設定的 Gem2018-03-07T15:18:00+00:002018-03-07T15:18:00+00:00https://code.lulalala.com/2018/settei-gem-zh<p>五年前,本部落格第一篇文章就是在講<a href="http://lulalala.logdown.com/posts/91804-rails-config-settings">使用 Settingslogic 作設定</a>。經過了漫長的歲月,終於想到了怎樣還能作的更好,最後作出了一個新的 gem 叫做 Settei 。這是一個使用 yaml,但是又能符合 12-factor app 的設定方式。</p>
<p>https://github.com/lulalala/settei</p>
<h2 id="緣起">緣起</h2>
<p>12-factor app 是一套讓部屬更容易的規則。其中第三點指到,要把設定跟程式分開,並把設定存在環境(變數)中。</p>
<p>但是使用環境變數有很多缺點,要是你的程式有30項大大小小的設定,光是命名變數名稱就會很麻煩:</p>
<div class="highlighter-rouge"><div class="highlight"><pre class="highlight"><code># 傳統使用 ENV 就得寫一長串超累:
BOARD_PAGINATION_PER_PAGE=5
BOARD_PAGINATION_MAX_PAGE=10
BOARD_REPLY_OMIT_CONDITION_N_RECENT_ONLY=5
BOARD_REPLY_OMIT_CONDITION_AVOID_ONLY_N_HIDDEN=2
</code></pre></div></div>
<p>使用YAML 就簡單很多</p>
<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">board</span><span class="pi">:</span>
<span class="na">pagination</span><span class="pi">:</span>
<span class="na">per_page</span><span class="pi">:</span> <span class="m">5</span>
<span class="na">max_page</span><span class="pi">:</span> <span class="m">10</span>
<span class="na">reply_omit_condition</span><span class="pi">:</span>
<span class="na">n_recent_only</span><span class="pi">:</span> <span class="m">5</span>
<span class="na">avoid_only_n_hidden</span><span class="pi">:</span> <span class="m">2</span>
</code></pre></div></div>
<p>但是要怎樣結合 YAML 的優勢跟 ENV VAR 的優勢呢?</p>
<p>我的想法是:把 YAML 給 serialize 成一串文字,就能當 env var 傳到遠端了。</p>
<p>本機開發跟遠端部屬的兩個流程如下:</p>
<p><img src="http://user-image.logdown.io/user/291/blog/291/post/6683287/IC9iAV9QTjS6IyuUw9wn_settei.png" alt="settei.png" /></p>
<h2 id="安裝">安裝</h2>
<p>用 Gemfile 安裝以後:</p>
<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">gem</span> <span class="s1">'settei'</span>
</code></pre></div></div>
<p>在 rails 專案下執行以下 rake task 繼續安裝:</p>
<div class="highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ rake settei:install:rails
</code></pre></div></div>
<h2 id="使用方法">使用方法</h2>
<p>要是 <code class="highlighter-rouge">config/environments/default.yml</code> 內容是這樣的話:</p>
<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">the_answer_to_life_the_universe_and_everything</span><span class="pi">:</span> <span class="m">42</span>
<span class="na">google</span><span class="pi">:</span>
<span class="na">api</span><span class="pi">:</span> <span class="s">foo</span>
</code></pre></div></div>
<p>就能這樣取得設定</p>
<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="no">Setting</span><span class="p">.</span><span class="nf">dig</span><span class="p">(</span><span class="ss">:the_answer_to_life_the_universe_and_everything</span><span class="p">)</span>
<span class="no">Setting</span><span class="p">.</span><span class="nf">dig</span><span class="p">(</span><span class="ss">:google</span><span class="p">,</span> <span class="ss">:api</span><span class="p">)</span>
</code></pre></div></div>
<h2 id="部署">部署</h2>
<p>要是你是使用 capistrano 或是 mina 的話,應該自動會有效。我塞了一個設定,所以deploy時,你的production.yml會直接變成Env var隨著遠端的 Rails server 啟動。於是遠端的 server 也就拿到了設定。</p>
<p>要是你是使用 heroku 的話,使用 <code class="highlighter-rouge">rake settei:heroku:config:set app=[app_name]</code> 來把 production.yml 上傳到指定的app中。</p>
<p>更詳盡的使用方法請閱讀 github,也歡迎指教~~</p>五年前,本部落格第一篇文章就是在講使用 Settingslogic 作設定。經過了漫長的歲月,終於想到了怎樣還能作的更好,最後作出了一個新的 gem 叫做 Settei 。這是一個使用 yaml,但是又能符合 12-factor app 的設定方式。Gem development inside Rails app2018-02-28T14:27:00+00:002018-02-28T14:27:00+00:00https://code.lulalala.com/2018/gem-development-inside-rails-app<p>Often during app development, it’s a good idea to extract some functionality into a gem. The simple way to do this is to open a new git repository, do a <code class="highlighter-rouge">bundle gem foobar</code>, publish it, install said gem inside Rails app, use it and test some more.</p>
<p>How about updates? We have to change the gem, guessing how it can be used inside Rails. Then release a version, install it inside your app, and finally do some testing. This is a lot of friction. This can be especially bad if your gem is closely coupled with the app, or gets updated a lot.</p>
<p>How about this?</p>
<ol>
<li>Create an empty gem (e.g. <code class="highlighter-rouge">bundle gem foobar</code>) (without doing any release)</li>
<li>Push it onto Github.</li>
<li>Put the gem in your Rails app as a submodule: <code class="highlighter-rouge">git submodule add https://github.com/foo/foobar.git vendor/foobar</code></li>
<li>Install the empty gem in your app’s Gemfile: <code class="highlighter-rouge">gem 'settei', path:'./vendor/foobar'</code></li>
<li>…</li>
<li>Profit!</li>
</ol>
<p>Now develop the gem entirely inside app’s submodule. It’s probably possible to autoload it (though I don’t recommend it). This will also work in production too.</p>Often during app development, it’s a good idea to extract some functionality into a gem. The simple way to do this is to open a new git repository, do a bundle gem foobar, publish it, install said gem inside Rails app, use it and test some more.Rails’ many default_url_options2018-02-14T08:26:00+00:002018-02-14T08:26:00+00:00https://code.lulalala.com/2018/rails-many-default-url-options<p>I have read so many different ways to set default_url_options. But at least in Rails 5.1.4, only some of them worked. The thing is, often one works for console but not for controller, or the opposite happens:</p>
<div class="highlighter-rouge"><div class="highlight"><pre class="highlight"><code># in development.rb
config.action_controller.default_url_options({:protocol => 'https'})
config.action_controller.default_url_options(:protocol => 'https')
# Does not work
# in development.rb, outside config block
Rails.application.routes.default_url_options[:protocol] = 'https'
# Does not work, but works under console
# in routes.rb
Rails.application.routes.draw do
default_url_options protocol: :https
# Does not work, but works under console
# in ApplicationController
def default_url_options(options={})
{ secure: true }
end
# Does not work
# in ApplicationController
def default_url_options
{ protocol: :https }
end
# Works in browser, but does not work under console
# in development.rb
config.action_controller.default_url_options= {:protocol => 'https'}
# Works in browser, but does not work under console
</code></pre></div></div>
<p>This means we probably want to set the options at two different places for it to work always. I think this caused many stackoverflow questions, and deserve to have a Rails repo issue.</p>I have read so many different ways to set default_url_options. But at least in Rails 5.1.4, only some of them worked. The thing is, often one works for console but not for controller, or the opposite happens:Pundit and controller based authorization2018-01-18T08:46:00+00:002018-01-18T08:46:00+00:00https://code.lulalala.com/2018/4879384<p>If we have an Order class, pundit gem will figure out to use the OrderPolicy for authorization. But what if we have multiple domains using the same item?</p>
<p>For example an online auction site, there will be a <code class="highlighter-rouge">Seller::OrdersController</code> and a <code class="highlighter-rouge">Buyer::OrdersController</code>. They will manage the two sides of the same order. The idea is that, buyer should be allowed to only update via <code class="highlighter-rouge">Buyer::OrdersController</code> but not <code class="highlighter-rouge">Seller::OrdersController</code>. And vice versa for seller. Obviously we would want to have two sets of policies.</p>
<p>However in Pundit namespaced policy require us to call <code class="highlighter-rouge">authorze [:seller, item]</code>, in order for it to use the <code class="highlighter-rouge">Seller::ItemPolicy</code>. This can become repetitive.</p>
<p>For me, there is less room for error if we have a 1:1 relationship between controller and policy, and a controller can be assumed to use the same policy. So I patched <code class="highlighter-rouge">authorize</code> call so policy can be set on the controller level.</p>
<h2 id="solution">Solution</h2>
<p>Put the following under <code class="highlighter-rouge">ApplicationController</code> as private methods.</p>
<div class="highlighter-rouge"><div class="highlight"><pre class="highlight"><code> class_attribute :pundit_policy_class
def self.set_pundit_policy_class(klass)
self.pundit_policy_class = klass
end
def authorize(record, query: nil, policy_class: nil)
query ||= params[:action].to_s + "?"
@_pundit_policy_authorized = true
if policy_class
policy = policy_class.new(pundit_user, record)
elsif self.pundit_policy_class
policy = self.pundit_policy_class.new(pundit_user, record)
else
policy = policy(record)
end
unless policy.public_send(query)
raise NotAuthorizedError, query: query, record: record, policy: policy
end
record
end
</code></pre></div></div>
<p>In your controller, you can then do this to set policy:</p>
<div class="highlighter-rouge"><div class="highlight"><pre class="highlight"><code>class Seller::OrdersController < ApplicationController
set_pundit_policy_class Seller::OrderPolicy
</code></pre></div></div>
<p>This means all actions under this controller will use <code class="highlighter-rouge">Seller::OrderPolicy</code> by default when you call <code class="highlighter-rouge">authorize</code>.</p>
<p>If we want to override this, we can also pass in <code class="highlighter-rouge">policy_class</code> in <code class="highlighter-rouge">authorize</code>:</p>
<div class="highlighter-rouge"><div class="highlight"><pre class="highlight"><code> authorize @order, policy_class: FooPolicy
</code></pre></div></div>
<p>Note that I made some changes to <code class="highlighter-rouge">authorize</code>’s method signature: <code class="highlighter-rouge">query</code> is a keyword argument now. It also returns the record object (as planned for its 1.2 release).</p>If we have an Order class, pundit gem will figure out to use the OrderPolicy for authorization. But what if we have multiple domains using the same item?《深入理解運算原理》的翻譯慘不忍睹2018-01-03T12:51:00+00:002018-01-03T12:51:00+00:00https://code.lulalala.com/2018/chinese-translation-of-understand-computation<p>《Understand Computation》這本書原本我很期待的,聽 <a href="https://devchat.tv/ruby-rogues/263-rr-programmer-education-and-skill-development-with-tom-stuart">Ruby Rouge</a> 的介紹,似乎可以讓讀者理解許多有趣的計算機科學原理,像是正規表示式等等。原本都快要去買英文版了,想不到12月初碁峰資訊代理的中文版竟然上市了,於是我就很高興的買了一本回家看。</p>
<p>結果越讀越不對勁,很多地方不大理解,於是去找了英文版來對照,才發現翻譯實在不行,大概每四到五頁就會出現巨大的錯誤。沒有對照就看不懂。這邊列出最明顯的四個例子:</p>
<h2 id="漏了一個-no-意思差很多">漏了一個 No 意思差很多</h2>
<p>原文中這句話被這樣翻譯:</p>
<blockquote>
<p>That all works as expected, but it would be nice if we could support conditional state-
ments with no « else » clause, like « if (x) { y = 1 } »</p>
</blockquote>
<blockquote>
<p>所有工作都一如預期,但我們若能以 else 子句支援條件陳述式,就像if (x) { y = 1 },那就更棒了。(40頁)</p>
</blockquote>
<p>少了「no」結果意思變反了。這個問題在 31 頁也出現過。</p>
<h2 id="例2">例2</h2>
<blockquote>
<p>小步語意使得我們必須從諸如 3 的不可約運算式辨識出像是 1 + 2 的可化簡運算式。</p>
</blockquote>
<p>當初看到這句的時候愣了一下,為何能從前者辨識出後者,所以去找了原文:</p>
<blockquote>
<p>With small-step semantics we had to distinguish reducible expressions like « 1 + 2 » from irreducible expressions like « 3 »</p>
</blockquote>
<p>才知道意思只是兩者參雜的情況下分辨是哪一種而已。要翻的話應該是這樣:</p>
<blockquote>
<p>在小步語意我們必須分辨可化簡運算式(如 1 + 2 )與不可化簡運算式(如 3 )</p>
</blockquote>
<h2 id="很棘手">很棘手</h2>
<p>看你能不能找到這段錯誤的地方:</p>
<blockquote>
<p>當我們在大型程式呼叫 #reduce 的時候,如果訊息往下經過抽象語法樹,然後一直到它抵達準備好化簡的程式碼片段,可能就會造成棘手的巢狀 #reduce 呼叫。</p>
</blockquote>
<p>應該很難,但是比對原文以後:</p>
<blockquote>
<p>when we call #reduce on a large program, that might cause a handful of nested #reduce calls as the message travels down the abstract syntax tree until it reaches the piece of code that is ready to reduce.</p>
</blockquote>
<p>我們就會發覺正面的句子被翻譯成負面的了。「handful」被譯為「棘手的」,其實作者想表達這只會造成「一些」呼叫而已。</p>
<h2 id="58-59-頁編輯的失誤連續-combo">58 59 頁編輯的失誤連續 Combo</h2>
<blockquote>
<p>我們在前面看到,操作語意是藉由設計語言的直譯器來解釋語言的意義。相較之下,操作語意語言對語言的翻譯就像編譯器。(第58頁)</p>
</blockquote>
<p>是不是不懂為何要「相較」同一個東西?嗯,因為第二句話寫錯了,應該是對「指稱語意(denotational semantics)」</p>
<p>接下來也有點不懂:</p>
<blockquote>
<p>這些語意風格無一必能說明如何有效實做語言的直譯器或編譯器…(第58頁)</p>
</blockquote>
<p><strong>無一必能</strong> 是什麼呢?翻了一下英文翻譯</p>
<blockquote>
<p>None of these styles of semantics necessarily says anything about how to efficiently implement an interpreter or compiler for a language, but…</p>
</blockquote>
<p>喔,原來多加了一個「必」字。</p>
<blockquote>
<p>指稱語意的優點是操作語意更為抽象(第59頁)</p>
</blockquote>
<p>這次則是漏了一個「比」字。</p>
<p>最後是一個理解錯誤:</p>
<blockquote>
<p>指稱只能如其含意;尤其若指稱語言具有某些操作含意,那麼指稱語意就只能讓我們更接近實際執行的程式…(59頁)</p>
</blockquote>
<blockquote>
<p>A denotation is only as good as its meaning; in particular, a denotational semantics only gets us closer to being able to actually execute a program if the denotation language has some operational meaning</p>
</blockquote>
<p>「只能」放錯位置了。意思應該是「指稱語意只有在指稱用語言本身有操作含意時,才讓我們更接近實際執行程式」</p>
<h2 id="結論">結論</h2>
<p>怕被認為雞蛋挑骨頭,所以比較小的問題放在這裡:<a href="https://goo.gl/VVWzMb">https://goo.gl/VVWzMb</a></p>
<p>上次買了《Ruby 物件導向設計實踐-敏捷入門》,一樣也是有一些小問題,但是因為是代理中國出版社翻譯的書,所以想說反應問題也沒用。</p>
<p>這次的翻譯是<a href="http://www.eslite.com/Search_BW.aspx?query=%e8%b3%b4%e6%a6%ae%e6%a8%9e">賴榮樞</a>,之前已經翻譯了十本以上的資訊書,應該算是很有經驗的譯者,所以我原本比較有信心。可是這麼多問題似乎代表編輯似乎沒有在做任何事情,糟蹋這本好書實在讓我很失望。</p>《Understand Computation》這本書原本我很期待的,聽 Ruby Rouge 的介紹,似乎可以讓讀者理解許多有趣的計算機科學原理,像是正規表示式等等。原本都快要去買英文版了,想不到12月初碁峰資訊代理的中文版竟然上市了,於是我就很高興的買了一本回家看。AdequateErrors - Overcoming limitation of Rails model errors API2017-12-11T05:20:00+00:002017-12-11T05:20:00+00:00https://code.lulalala.com/2017/adequate-errors<p>Over the years I encountered many issues related to <code class="highlighter-rouge">ActiveModel::Errors</code> API. After looking at the Rails source, I realized the original design was the root cause. <code class="highlighter-rouge">errors</code> was originally just a hash of array of <code class="highlighter-rouge">String</code>, which worked for simple requirements, but not for more complex ones.</p>
<p>In April I started collecting use cases, and study Rails source. Last month I finally put my hands on implementing a solution: a gem to apply object-oriented design principles to make each error an object, and provide new set of APIs to access these objects. I call it <a href="https://github.com/lulalala/adequate_errors"><strong>AdequateErrors</strong></a>.</p>
<p>AdequateErrors can be accessed by calling <code class="highlighter-rouge">model.errors.adequate</code>. It co-exists with existng Rails API, so nothing will break. But what issues does it solve? Let me list them one by one:</p>
<h1 id="query-on-errors-using-where">Query on errors using <code class="highlighter-rouge">where</code></h1>
<p>Imagine we need to access the <code class="highlighter-rouge">empty</code> error on <strong>any</strong> attributes:</p>
<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">model</span><span class="p">.</span><span class="nf">errors</span><span class="p">.</span><span class="nf">details</span><span class="p">.</span><span class="nf">each</span> <span class="p">{</span><span class="o">|</span><span class="n">attribute</span><span class="p">,</span> <span class="n">errors</span><span class="o">|</span>
<span class="n">errors</span><span class="p">.</span><span class="nf">find</span> <span class="p">{</span><span class="o">|</span><span class="n">h</span><span class="o">|</span>
<span class="n">h</span><span class="p">[</span><span class="ss">:error</span><span class="p">]</span> <span class="o">==</span> <span class="ss">:empty</span>
<span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>
<p>AdequateErrors provides a <code class="highlighter-rouge">where</code> method. Now we can stop using loops, and write complex queries:</p>
<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">model</span><span class="p">.</span><span class="nf">errors</span><span class="p">.</span><span class="nf">adequate</span><span class="p">.</span><span class="nf">where</span><span class="p">(</span><span class="ss">type: :empty</span><span class="p">)</span>
<span class="n">model</span><span class="p">.</span><span class="nf">errors</span><span class="p">.</span><span class="nf">adequate</span><span class="p">.</span><span class="nf">where</span><span class="p">(</span><span class="ss">attribute: :title</span><span class="p">,</span> <span class="ss">type: :empty</span><span class="p">)</span>
<span class="n">model</span><span class="p">.</span><span class="nf">errors</span><span class="p">.</span><span class="nf">adequate</span><span class="p">.</span><span class="nf">where</span><span class="p">(</span><span class="ss">attribute: :title</span><span class="p">,</span> <span class="ss">type: :empty</span><span class="p">,</span> <span class="ss">count: </span><span class="mi">3</span><span class="p">)</span>
</code></pre></div></div>
<p>This returns an array of Error objects. Simple.</p>
<h1 id="access-both-the-message-and-details-of-one-particular-error">Access both the message and details of one particular error</h1>
<p>If one attribute has two <code class="highlighter-rouge">foo_error</code> and one <code class="highlighter-rouge">bar_error</code>, e.g.:</p>
<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># model.errors.details</span>
<span class="p">{</span><span class="ss">:name</span><span class="o">=></span><span class="p">[{</span><span class="ss">error: :foo_error</span><span class="p">,</span> <span class="ss">count: </span><span class="mi">1</span><span class="p">},</span> <span class="p">{</span><span class="ss">error: :bar_error</span><span class="p">},</span> <span class="p">{</span><span class="ss">error: :foo_error</span><span class="p">,</span> <span class="ss">count: </span><span class="mi">3</span><span class="p">}]}</span>
</code></pre></div></div>
<p>How do you access the message on the third particular error? With the current implementation, we have to resort to using array indexes:</p>
<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">model</span><span class="p">.</span><span class="nf">errors</span><span class="p">.</span><span class="nf">messages</span><span class="p">[</span><span class="ss">:name</span><span class="p">][</span><span class="mi">2</span><span class="p">]</span>
</code></pre></div></div>
<p>Or we can call <code class="highlighter-rouge">generate_message</code> to recreate a message from the details, but that’s also tedious.</p>
<p>With AdequateErrors, we won’t have this problem. Error is represented as an object, message and details are its attributes, so accessing those are straightforward:</p>
<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">e</span> <span class="o">=</span> <span class="n">model</span><span class="p">.</span><span class="nf">errors</span><span class="p">.</span><span class="nf">adequate</span><span class="p">.</span><span class="nf">where</span><span class="p">(</span><span class="ss">attribute: :title</span><span class="p">,</span> <span class="ss">type: :foo_error</span><span class="p">).</span><span class="nf">first</span>
<span class="n">e</span><span class="p">.</span><span class="nf">message</span> <span class="c1"># full message</span>
<span class="n">e</span><span class="p">.</span><span class="nf">options</span> <span class="c1"># similar to details, where meta informations such as `:count` is stored.</span>
</code></pre></div></div>
<h1 id="lazily-evaluating-message-for-internationalization">Lazily evaluating message for internationalization</h1>
<p><a href="https://github.com/morgoth">@morgoth</a> mentioned this issue that when you’re adding error, it’s translated right away.</p>
<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># actual:</span>
<span class="no">I18n</span><span class="p">.</span><span class="nf">with_locale</span><span class="p">(</span><span class="ss">:pl</span><span class="p">)</span> <span class="p">{</span> <span class="n">user</span><span class="p">.</span><span class="nf">error</span><span class="p">.</span><span class="nf">full_messages</span> <span class="p">}</span> <span class="c1"># => outputs EN errors always</span>
<span class="c1"># expecting:</span>
<span class="no">I18n</span><span class="p">.</span><span class="nf">with_locale</span><span class="p">(</span><span class="ss">:pl</span><span class="p">)</span> <span class="p">{</span> <span class="n">user</span><span class="p">.</span><span class="nf">error</span><span class="p">.</span><span class="nf">full_messages</span> <span class="p">}</span> <span class="c1"># => outputs PL errors</span>
<span class="no">I18n</span><span class="p">.</span><span class="nf">with_locale</span><span class="p">(</span><span class="ss">:pt</span><span class="p">)</span> <span class="p">{</span> <span class="n">user</span><span class="p">.</span><span class="nf">error</span><span class="p">.</span><span class="nf">full_messages</span> <span class="p">}</span> <span class="c1"># => outputs PT errors</span>
</code></pre></div></div>
<p>Taking this into consideration, AdequateErrors lazily evaluates messages only when <code class="highlighter-rouge">message</code> is called.</p>
<h1 id="error-message-attribute-prefix">Error message attribute prefix</h1>
<p>Not all error messages start with the attribute name, but Rails forces you to do this. People have developed <a href="https://github.com/jeremydurham/custom-err-msg">hacks</a> to bypass this. Others simply assigned errors to <code class="highlighter-rouge">:base</code> instead of the actual attribute. This is ugly.</p>
<p>Here is AdequateErrors’ solution. It has its own namespace in the locale file, and instead of the global default format <code class="highlighter-rouge">"%{attribute} %{message}"</code>, the prefix is moved into each individual entries:</p>
<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">en</span><span class="pi">:</span>
<span class="na">adequate_errors</span><span class="pi">:</span>
<span class="na">messages</span><span class="pi">:</span>
<span class="na">invalid</span><span class="pi">:</span> <span class="s2">"</span><span class="s">%{attribute}</span><span class="nv"> </span><span class="s">is</span><span class="nv"> </span><span class="s">invalid"</span>
<span class="na">inclusion</span><span class="pi">:</span> <span class="s2">"</span><span class="s">%{attribute}</span><span class="nv"> </span><span class="s">is</span><span class="nv"> </span><span class="s">not</span><span class="nv"> </span><span class="s">included</span><span class="nv"> </span><span class="s">in</span><span class="nv"> </span><span class="s">the</span><span class="nv"> </span><span class="s">list"</span>
<span class="na">exclusion</span><span class="pi">:</span> <span class="s2">"</span><span class="s">%{attribute}</span><span class="nv"> </span><span class="s">is</span><span class="nv"> </span><span class="s">reserved"</span>
</code></pre></div></div>
<p>All built-in error types have been converted into this. If one wishes to have prefix-less error, simply have its entry in locale file without the <code class="highlighter-rouge">%{attribute}</code>.</p>
<h1 id="just-less-error-prune-code">Just less error prune code</h1>
<p>I remember when I first learned about Object-Oriented design principle in uni, there was this example of payroll system. In the system, one array stores account name and another array stores account number. Whenever we need to delete an account, we need to manipulate both arrays. Further more, if we need to add a new attribute, we need to add a third array. It is very clear that objectifying this system can make it simpler and less error-prone.</p>
<p>This is what the current Rails errors implementation looks like:</p>
<div class="highlighter-rouge"><div class="highlight"><pre class="highlight"><code> def copy!(other) # :nodoc:
@messages = other.messages.dup
@details = other.details.dup
end
def clear
messages.clear
details.clear
end
def delete(key)
attribute = key.to_sym
details.delete(attribute)
messages.delete(attribute)
end
</code></pre></div></div>
<p>This being similar to the case I mentioned above, really can benefit from an object-oriented approach.</p>
<h1 id="conclusion">Conclusion</h1>
<p>If you are a long-time Rails developer, chances are you have met similar issue before, please try this gem. If you have other usecases that you wish to improve on, I would like to know and see if it can be added into the gem. Happy hacking!</p>
<p><img src="http://user-image.logdown.io/user/291/blog/291/post/2909828/NoJZ5XqSQ0qVeVIyerNy_adequate.png" alt="adequate.png" /></p>Over the years I encountered many issues related to ActiveModel::Errors API. After looking at the Rails source, I realized the original design was the root cause. errors was originally just a hash of array of String, which worked for simple requirements, but not for more complex ones.Magics that Decorator/Presenter Gems Do to Make Your Type Less2017-11-22T14:53:00+00:002017-11-22T14:53:00+00:00https://code.lulalala.com/2017/rails-decorator-presenter<p>Some of you Rails developers probably have used a ‘decorator’ or ‘presenter’ library. These libraries aim to bridge between Rails model and view layers. If I am to define it, a <em>presenter</em> allows developers to group helper methods related to a model to be under a namespace related to that model, instead of the current global space.</p>
<p>But would you believe it? There are actually a dozen or more decorator/presenter gems out there. Why do we reinvent the wheels? The first reason is that there is really a demand, because keeping large amount of helper methods under the same namespace is just unrealistic. The second reason is that, these gem owners have different views on this philosophical question: should the interface be implicit (things are done for the user under the hood) or explicit (user has to type more).</p>
<p>As a fun exercise I will compare 6 of these gems and explain how the general concept works. I have not used some of the gems, but only read the readme and some of the code, so if there are mistakes please let me know.</p>
<p>*Disclaimer, I am the owner of LulalalaPresenter gem. And if you have never used a presenter/decorator before, read this <a href="http://lulalala.logdown.com/posts/308472-lulalala-presenter-for-rails">post</a> to know why it is useful.</p>
<p>The following table represents the spectrum of these gems. On the left end, we see gems favoring implicity more. On the right end, gems are more explicit in nature.</p>
<table style="font-size:0.8em">
<tr>
<th><br /></th>
<th>Active<br />Decorator</th>
<th>Draper</th>
<th>Oprah</th>
<th>Display-case</th>
<th>Lulalala<br />Presenter</th>
<th>RailsCast<br /></th>
</tr>
<tr>
<td></td>
<td><a href="https://github.com/amatsuda/active_decorator" target="_blank">link</a></td>
<td><a href="https://github.com/drapergem/draper" target="_blank">link</a></td>
<td><a href="https://github.com/endofunky/oprah" target="_blank">link</a></td>
<td><a href="https://github.com/objects-on-rails/display-case" target="_blank">link</a></td>
<td><a href="https://github.com/lulalala/lulalala_presenter" target="_blank">link</a></td>
<td><a href="http://railscasts.com/episodes/287-presenters-from-scratch" target="_blank">link</a></td>
</tr>
<tr>
<td>Decorate (Quack like a model)</td>
<td>Y</td>
<td>optional</td>
<td>Y</td>
<td>Y</td>
<td></td>
<td></td>
</tr>
<tr>
<td>Decorates Association</td>
<td>Y</td>
<td>optional</td>
<td>optional</td>
<td></td>
<td></td>
<td></td>
</tr>
<tr>
<td>Directly call helper method within decorator</td>
<td>Y</td>
<td>optional</td>
<td></td>
<td></td>
<td></td>
<td></td>
</tr>
<tr>
<td>Globally accessible view context</td>
<td>Y</td>
<td>Y</td>
<td></td>
<td></td>
<td>Y</td>
<td></td>
</tr>
<tr>
<td>Automatically decorates</td>
<td>Y</td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
</tr>
<tr>
<td>Presenter/Model mapping</td>
<td>1:1</td>
<td>N:1</td>
<td>N:1</td>
<td>N:1</td>
<td>1:1</td>
<td>1:1</td>
</tr>
<tr>
<td>Lines of code</td>
<td>281</td>
<td>718</td>
<td>130</td>
<td>375</td>
<td>110</td>
<td>38</td>
</tr>
<tr>
<td>Lines of test</td>
<td>473</td>
<td>3037</td>
<td>354</td>
<td>1556</td>
<td>294</td>
<td>n/a</td>
</tr>
</table>
<p>(I find many more gems after writing this. A complete list can be found <a href="https://docs.google.com/spreadsheets/d/1BVGC9ULsiv1uMOVW2buctXrfOHKpab63TXh_ssax3nA/edit?usp=sharing">here</a>. If you want to add your gem to it, let me know~)</p>
<h2 id="i-hate-magic-camp">“I hate magic” camp</h2>
<p>The conservative rubyists prefer to avoid magic. They don’t like to override too much things, and they write PORO instead of meta-programming.</p>
<p>The most simple example can be seen from Ryan Bate’s RailsCast: <a href="http://railscasts.com/episodes/287-presenters-from-scratch">“Presenters from Scratch”</a>. In the video he explains step by step how to make a simple presenter.</p>
<p>The only meta-programming used is how it infers the presenter class from the model. All other interfaces are simple object passing method calls. Only 38 lines are required to achieve this.</p>
<p>Since by default it does not act like a model, Ryan calls it a <strong>presenter</strong> instead of decorator. You can still delegate calls to model if you wish, but the readme indicates that presenter object should not be mixed up with model object.</p>
<p>I’ll talk about LulalalaPresenter later after ActiveDecorator, because it is its fork.</p>
<h2 id="decorator">Decorator</h2>
<div style="float:right;width: 300px;margin-left: 20px">
<img src="http://user-image.logdown.io/user/291/blog/291/post/3766261/684SJISbSOBtgSsNFQ3s_B2017-12-04%2022_16_57-Edit_%20helpers.png" title="" style="width:100%" />
</div>
<p>According to <a href="https://www.amazon.com/Design-Patterns-Ruby-Russ-Olsen/dp/0321490452">Design Patterns in Ruby</a>, the decorator pattern is a wrapper that “supports the same core interface, but adds its own twist on that interface.” In this case we add view related functionality around the model. A decorator can act as if it is the model, which means less view changes are required.</p>
<p>ActiveDecorator, oprah and display-case all position themselves as decorators. Draper on the other hand gives the user freedom to choose if the wrapper should become a decorator or not.</p>
<h2 id="one-association-further">One association further</h2>
<p>Associations are part of the ActiveModel interface too, and some gems can decorate associations for you to save some key strokes (<a href="https://github.com/amatsuda/active_decorator/pull/68/files">example</a>). This is no small task, as there are multiple ways to trigger associations. It is therefore more possible to break across Rails versions.</p>
<h2 id="directly-call-helper-methods-within-decorator">Directly call helper methods within decorator</h2>
<p>Normally for a model decorator to call helper methods within it, it needs to call via <code class="highlighter-rouge">view_context</code> (often alias as <code class="highlighter-rouge">h</code>), e.g. <code class="highlighter-rouge">h.url_for()</code>. Both ActiveDecorator and Draper offers a way to save key strokes so you can call <code class="highlighter-rouge">url_for</code> directly. This is done by a simple trick: if the model does not support a method, we retry it on view context again (<a href="https://github.com/drapergem/draper/blob/master/lib/draper/lazy_helpers.rb">example</a>).</p>
<p>The implications are: 1. this is a wee bit slower because <code class="highlighter-rouge">method_missing</code> is utilized. 2. if same method name exists on model and view_context, model’s method takes precedence. This is usually not a problem.</p>
<h2 id="globally-accessible-view_context">Globally accessible view_context</h2>
<p>Draper, ActiveDecorator and LulalalaPresenter all keeps the view_context in a globally accessible place (<a href="https://github.com/drapergem/draper/blob/master/lib/draper/view_context.rb#L39">example</a>). This is done for two reasons:</p>
<ol>
<li>
<p>Give an OO-esque feel to the decoration method:<br />
Draper decorates by calling <code class="highlighter-rouge">model.decorate</code>
LulalalaPresenter presents by calling <code class="highlighter-rouge">model.presenter</code>
The design saves you from passing view_context, otherwise one will need to do <code class="highlighter-rouge">model.decorate(view_context)</code> all the time.</p>
</li>
<li>
<p>To allow automatic decoration (see below).</p>
</li>
</ol>
<h2 id="the-holy-grail-of-implicity">The Holy Grail of Implicity</h2>
<p>We have reached the end of implicity. To automatically decorate things, ActiveDecorator hooks into the render call, and decorates instance variables when applicable (<a href="https://github.com/amatsuda/active_decorator/blob/master/lib/active_decorator/monkey/abstract_controller/rendering.rb">example</a>).</p>
<p>The gem walked the extra miles for you, so you don’t have to do anything beside writing the decorator class. In some way, this feels like Rails philosophy, where we just write the controller/model/view, and things will just hook up perfectly without you knowing what’s happening under the hood.</p>
<h2 id="my-own-presenter-and-conclusion">My Own Presenter and Conclusion</h2>
<div style="float:right;width: 300px;margin-left: 20px">
<img src="http://user-image.logdown.io/user/291/blog/291/post/3766261/nSU4JtAvRG7EiGAjhygr_B2017-12-04%2022_10_04-Edit_%20helpers.png" title="" style="width:100%" />
</div>
<p>As you can see from the table, most are decorators. They behave like models, delegating calls to the inside. However if we treat presenter as a separate object, we can reduce a lot of the delegation complexities.</p>
<p>I was looking for a presenter which does not involve decorating ActiveModel, but I couldn’t find one. Ryan’s solution was the closest I could find. So I thought I can make my own.</p>
<p>I personally hate typing parenthesis, because my left little finger aches when holding <code class="highlighter-rouge">shift</code> key. Instead of <code class="highlighter-rouge">present(model).foo</code>, I prefer <code class="highlighter-rouge">model.present.foo</code>. If we are to do this, how can presenter get hold of view_context object? We can pass it in everytime like this <code class="highlighter-rouge">model.present.foo(vc)</code>, but that’s definitely too much too type. In the end, I found out ActiveDecorator’s globally accessible view_context, so I used it to make my own. Hopefully this does not offend any one, I feel guilty but I want to please my little finger.</p>
<p>Do we need presenters/decorators pattern? Some may argue this is an offense to the MVC architecture, and some may think it introduces extra complexity. Again we will probably never get a consensus on this, but I use it because it made coding Rails more pleasant. So if you have large number of helper methods in your codebase, I recommend you to just pick a gem and try it out :)</p>Some of you Rails developers probably have used a ‘decorator’ or ‘presenter’ library. These libraries aim to bridge between Rails model and view layers. If I am to define it, a presenter allows developers to group helper methods related to a model to be under a namespace related to that model, instead of the current global space.