<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title> File: README — Documentation by YARD 0.9.36 </title> <link rel="stylesheet" href="css/style.css" type="text/css" /> <link rel="stylesheet" href="css/common.css" type="text/css" /> <script type="text/javascript"> pathId = "README"; relpath = ''; </script> <script type="text/javascript" charset="utf-8" src="js/jquery.js"></script> <script type="text/javascript" charset="utf-8" src="js/app.js"></script> </head> <body> <div class="nav_wrap"> <iframe id="nav" src="file_list.html?1"></iframe> <div id="resizer"></div> </div> <div id="main" tabindex="-1"> <div id="header"> <div id="menu"> <a href="_index.html">Index</a> » <span class="title">File: README</span> </div> <div id="search"> <a class="full_list_link" id="class_list_link" href="class_list.html"> <svg width="24" height="24"> <rect x="0" y="4" width="24" height="4" rx="1" ry="1"></rect> <rect x="0" y="12" width="24" height="4" rx="1" ry="1"></rect> <rect x="0" y="20" width="24" height="4" rx="1" ry="1"></rect> </svg> </a> </div> <div class="clear"></div> </div> <div id="content"><div id='filecontents'> <p><img src=“logo.svg” height=250 alt=“Anchormodel logo”/></p> <h1 id="label-Introducing+Anchormodel">Introducing Anchormodel</h1> <p>This gem provides a simple but powerful alternative to <a href="https://api.rubyonrails.org/v7.0/classes/ActiveRecord/Enum.html">Rails Enums</a>. In contrast to regular Enums, Anchormodels can hold application logic, making them ideal for tying code to database objects.</p> <h1 id="label-Use+case">Use case</h1> <p>Typically, a Rails application consists of three kinds of state:</p> <ul><li> <p>The code, which we can consider static within a given version. Code can reference to other code, e.g. <code>node.parent = other_node</code>.</p> </li><li> <p>The database contents, which can fluctuate within the bounds of the DB schema. Data can reference to other data, ideally via foreign keys.</p> </li><li> <p>A mix of the two, where code needs to be specifically tailored for some kind of data. A prominent example of such a mix would for instance be user roles: roles must be hardcoded in the code because security logic is tied to them. However, as users are assigned to roles in the database, roles also need to be persisted in the database. This is where Anchormodel comes into play.</p> </li></ul> <h2 id="label-Alternatives+coviering+the+same+use+case">Alternatives coviering the same use case</h2> <h3 id="label-Enum">Enum</h3> <p>In Rails and other frameworks, the third point in the listing above is typically achieved via Enums. Enums map either Integers or Strings to constants and therefore provide a link between the DB and the application code.</p> <p>However, at least in Rails, Enums provide very limited customization options. They are basic values that can be used in if-statements. Anchormodels however are regular classes and can easily be extended.</p> <h3 id="label-ActiveEnum">ActiveEnum</h3> <p>The gem (<a href="https://github.com/adzap/active_enum">ActiveEnum)</a> allows to create Enum-like classes that can be extended. However it only supports Integer keys. I find this unsatisfactory, as debugging with tools like <code>psql</code> or <code>mysql</code> is made unnecessarily hard when you only see numbers. Keys for enums should be meaningful, making you immediately understand what they stand for.</p> <p>This is why Anchormodel is strictly relying on String keys corresponding to the entries of an Anchormodel.</p> <h1 id="label-Installation">Installation</h1> <ol><li> <p>Add gem to Gemfile: <code>gem 'anchormodel'</code></p> </li><li> <p>In <code>application_record.rb</code>, add in the class body: <code>include Anchormodel::ModelMixin</code></p> </li></ol> <h1 id="label-Generator">Generator</h1> <p>For convenience, Anchormodel provides a Rails generator:</p> <p><code>rails generate anchormodel Role</code></p> <p>This will create <code>app/anchormodels/role.rb</code>.</p> <h1 id="label-Basic+example">Basic example</h1> <p><code>app/anchormodels/role.rb</code>:</p> <pre class="code ruby"><code class="ruby"><span class='kw'>class</span> <span class='const'>Role</span> <span class='op'><</span> <span class='const'><span class='object_link'><a href="Anchormodel.html" title="Anchormodel (class)">Anchormodel</a></span></span> <span class='comment'># Make <, > etc. based on <=> operator whic hwe will define below </span> <span class='id identifier rubyid_include'>include</span> <span class='const'>Comparable</span> <span class='comment'># Expose the attribute privilege_level </span> <span class='id identifier rubyid_attr_reader'>attr_reader</span> <span class='symbol'>:privilege_level</span> <span class='comment'># Define <=> to make user roles comparable based on the privilege level </span> <span class='kw'>def</span> <span class='op'><=></span><span class='lparen'>(</span><span class='id identifier rubyid_other'>other</span><span class='rparen'>)</span> <span class='ivar'>@privilege_level</span> <span class='op'><=></span> <span class='id identifier rubyid_other'>other</span><span class='period'>.</span><span class='id identifier rubyid_privilege_level'>privilege_level</span> <span class='kw'>end</span> <span class='comment'># Declare all available roles </span> <span class='id identifier rubyid_new'>new</span> <span class='symbol'>:guest</span><span class='comma'>,</span> <span class='label'>privilege_level:</span> <span class='int'>0</span> <span class='id identifier rubyid_new'>new</span> <span class='symbol'>:manager</span><span class='comma'>,</span> <span class='label'>privilege_level:</span> <span class='int'>1</span> <span class='id identifier rubyid_new'>new</span> <span class='symbol'>:admin</span><span class='comma'>,</span> <span class='label'>privilege_level:</span> <span class='int'>2</span> <span class='kw'>end</span> </code></pre> <p><code>app/models/user.rb</code></p> <pre class="code ruby"><code class="ruby"><span class='comment'># The DB table `users` must have a String column `users.role` </span><span class='kw'>class</span> <span class='const'>User</span> <span class='op'><</span> <span class='const'>ApplicationRecord</span> <span class='comment'># If `users.role` has an `NOT NULL` constraint, use: </span> <span class='id identifier rubyid_belongs_to_anchormodel'>belongs_to_anchormodel</span> <span class='symbol'>:role</span> <span class='comment'># If `users.role` can be `NULL`, use the following instead: </span> <span class='id identifier rubyid_belongs_to_anchormodel'>belongs_to_anchormodel</span> <span class='symbol'>:role</span><span class='comma'>,</span> <span class='label'>optional:</span> <span class='kw'>true</span> <span class='kw'>end</span> </code></pre> <p>You may now use the following methods:</p> <pre class="code ruby"><code class="ruby"><span class='comment'># Retrieve all user roles: </span><span class='const'>Role</span><span class='period'>.</span><span class='id identifier rubyid_all'>all</span> <span class='comment'># Retrieve a specific role from the String and find its privilege level </span><span class='const'>Role</span><span class='period'>.</span><span class='id identifier rubyid_find'>find</span><span class='lparen'>(</span><span class='symbol'>:guest</span><span class='rparen'>)</span><span class='period'>.</span><span class='id identifier rubyid_privilege_level'>privilege_level</span> <span class='comment'># Implement a Rails helper that makes sure users can only edit other users that have a lower privilege level than themselves </span><span class='kw'>def</span> <span class='id identifier rubyid_user_can_edit?'>user_can_edit?</span><span class='lparen'>(</span><span class='id identifier rubyid_this_user'>this_user</span><span class='comma'>,</span> <span class='id identifier rubyid_other_user'>other_user</span><span class='rparen'>)</span> <span class='id identifier rubyid_this_user'>this_user</span><span class='period'>.</span><span class='id identifier rubyid_role'>role</span><span class='period'>.</span><span class='id identifier rubyid_privilege_level'>privilege_level</span> <span class='op'>></span> <span class='id identifier rubyid_other_user'>other_user</span><span class='period'>.</span><span class='id identifier rubyid_role'>role</span><span class='period'>.</span><span class='id identifier rubyid_privilege_level'>privilege_level</span> <span class='kw'>end</span> <span class='comment'># Pretty print a user's role, e.g. using the Rails FastGettext gem: </span><span class='id identifier rubyid_puts'>puts</span><span class='lparen'>(</span><span class='tstring'><span class='tstring_beg'>"</span><span class='tstring_content'>User </span><span class='embexpr_beg'>#{</span><span class='ivar'>@user</span><span class='period'>.</span><span class='id identifier rubyid_name'>name</span><span class='embexpr_end'>}</span><span class='tstring_content'> has role </span><span class='embexpr_beg'>#{</span><span class='ivar'>@user</span><span class='period'>.</span><span class='id identifier rubyid_role'>role</span><span class='period'>.</span><span class='id identifier rubyid_label'>label</span><span class='embexpr_end'>}</span><span class='tstring_end'>"</span></span><span class='rparen'>)</span> <span class='comment'># Check whether @user has role admin </span><span class='ivar'>@user</span><span class='period'>.</span><span class='id identifier rubyid_role'>role</span><span class='period'>.</span><span class='id identifier rubyid_admin?'>admin?</span> <span class='comment'># true if and only if the role is admin (false otherwise) </span></code></pre> <h1 id="label-Rails+Enum+style+model+methods">Rails Enum style model methods</h1> <p>By default, Anchormodel adds three kinds of methods for each key to the model:</p> <ul><li> <p>a reader (getter)</p> </li><li> <p>a writer (setter)</p> </li><li> <p>a Rails scope</p> </li></ul> <p>For instance:</p> <pre class="code ruby"><code class="ruby"><span class='kw'>class</span> <span class='const'>User</span> <span class='op'><</span> <span class='const'>ApplicationRecord</span> <span class='id identifier rubyid_belongs_to_anchormodel'>belongs_to_anchormodel</span> <span class='symbol'>:role</span> <span class='comment'># where Role has keys :guest, :manager and :admin </span> <span class='id identifier rubyid_belongs_to_anchormodel'>belongs_to_anchormodel</span> <span class='symbol'>:shape</span> <span class='comment'># where Shape has keys :circle and :rectangle </span><span class='kw'>end</span> <span class='comment'># User now implements the following methods, given that @user is retrieved as follows: </span><span class='ivar'>@user</span> <span class='op'>=</span> <span class='const'>User</span><span class='period'>.</span><span class='id identifier rubyid_first'>first</span> <span class='comment'># for example </span> <span class='comment'># Readers </span><span class='ivar'>@user</span><span class='period'>.</span><span class='id identifier rubyid_guest?'>guest?</span> <span class='comment'># same as @user.role.guest? </span><span class='ivar'>@user</span><span class='period'>.</span><span class='id identifier rubyid_manager?'>manager?</span> <span class='ivar'>@user</span><span class='period'>.</span><span class='id identifier rubyid_admin?'>admin?</span> <span class='ivar'>@user</span><span class='period'>.</span><span class='id identifier rubyid_rectangle?'>rectangle?</span> <span class='comment'># same as @user.shape.rectangle? </span><span class='ivar'>@user</span><span class='period'>.</span><span class='id identifier rubyid_circle?'>circle?</span> <span class='comment'># Writers </span><span class='ivar'>@user</span><span class='period'>.</span><span class='id identifier rubyid_guest!'>guest!</span> <span class='comment'># same as @user.role = Role.find(:guest) </span><span class='ivar'>@user</span><span class='period'>.</span><span class='id identifier rubyid_manager!'>manager!</span> <span class='ivar'>@user</span><span class='period'>.</span><span class='id identifier rubyid_admin!'>admin!</span> <span class='ivar'>@user</span><span class='period'>.</span><span class='id identifier rubyid_rectangle!'>rectangle!</span> <span class='comment'># same as @user.shape = Shape.find(:rectangle) </span><span class='ivar'>@user</span><span class='period'>.</span><span class='id identifier rubyid_circle!'>circle!</span> <span class='comment'># Scopes </span><span class='const'>User</span><span class='period'>.</span><span class='id identifier rubyid_guest'>guest</span> <span class='comment'># same as User.where(role: 'guest') </span><span class='const'>User</span><span class='period'>.</span><span class='id identifier rubyid_manager'>manager</span> <span class='const'>User</span><span class='period'>.</span><span class='id identifier rubyid_admin'>admin</span> <span class='const'>User</span><span class='period'>.</span><span class='id identifier rubyid_rectangle'>rectangle</span> <span class='comment'># same as User.where(shape: 'rectangle') </span><span class='const'>User</span><span class='period'>.</span><span class='id identifier rubyid_circle'>circle</span> </code></pre> <p>This behavior is similar as the one from Rails Enums. If you want to disable it, use:</p> <pre class="code ruby"><code class="ruby"><span class='kw'>class</span> <span class='const'>User</span> <span class='op'><</span> <span class='const'>ApplicationRecord</span> <span class='id identifier rubyid_belongs_to_anchormodel'>belongs_to_anchormodel</span> <span class='symbol'>:role</span><span class='comma'>,</span> <span class='label'>model_readers:</span> <span class='kw'>false</span><span class='comma'>,</span> <span class='label'>model_writers:</span> <span class='kw'>false</span><span class='comma'>,</span> <span class='label'>model_scopes:</span> <span class='kw'>false</span> <span class='comment'># or, equivalent, to disable all at once: </span> <span class='id identifier rubyid_belongs_to_anchormodel'>belongs_to_anchormodel</span> <span class='symbol'>:role</span><span class='comma'>,</span> <span class='label'>model_methods:</span> <span class='kw'>false</span> <span class='kw'>end</span> </code></pre> <h1 id="label-Calling+a+column+differently+than+the+Anchormodel">Calling a column differently than the Anchormodel</h1> <p>If your column name (and the model’s attribute) is called differently than the Anchormodel, you may give the Anchormodel’s class as the second argument. For example:</p> <pre class="code ruby"><code class="ruby"><span class='comment'># app/anchormodels/color.rb </span><span class='kw'>class</span> <span class='const'>Color</span> <span class='op'><</span> <span class='const'><span class='object_link'><a href="Anchormodel.html" title="Anchormodel (class)">Anchormodel</a></span></span> <span class='id identifier rubyid_new'>new</span> <span class='symbol'>:green</span> <span class='id identifier rubyid_new'>new</span> <span class='symbol'>:red</span> <span class='kw'>end</span> <span class='comment'># app/models/user.rb </span><span class='kw'>class</span> <span class='const'>User</span> <span class='op'><</span> <span class='const'>ApplicationRecord</span> <span class='id identifier rubyid_belongs_to_anchormodel'>belongs_to_anchormodel</span> <span class='symbol'>:favorite_color</span><span class='comma'>,</span> <span class='const'>Color</span> <span class='kw'>end</span> </code></pre> <h2 id="label-Having+multiple+attributes+to+the+same+Anchormodel">Having multiple attributes to the same Anchormodel</h2> <p>If you want to have multiple attributes in the same model pointing to the same Anchormodel, you need to disable <code>model_methods</code> for at least one of them (otherwise the model methods will clash in your model class):</p> <pre class="code ruby"><code class="ruby"><span class='comment'># app/models/user.rb </span> <span class='id identifier rubyid_belongs_to_anchormodel'>belongs_to_anchormodel</span> <span class='symbol'>:role</span> <span class='id identifier rubyid_belongs_to_anchormodel'>belongs_to_anchormodel</span> <span class='symbol'>:secondary_role</span><span class='comma'>,</span> <span class='const'>Role</span><span class='comma'>,</span> <span class='label'>model_methods:</span> <span class='kw'>false</span> </code></pre> </div></div> <div id="footer"> Generated on Tue Apr 23 17:20:43 2024 by <a href="https://yardoc.org" title="Yay! A Ruby Documentation Tool" target="_parent">yard</a> 0.9.36 (ruby-3.2.2). </div> </div> </body> </html>