Today I Learned

8 posts by wojtekwrona

Interval column in Rails 6.1 and AS' Duration#parts

Rails 6.1 introduces support for PostgreSQL interval datatype by mapping it to the ActiveSupport::Duration:

add_column :triggers, :time_offset, :interval
> Trigger.new(time_offset: 1.day + 2.hours + 3.minutes + 4.seconds)
=> #<Trigger:0x00007fbe18c9af08
=> #  id: nil,
=> #  time_offset: 1 day, 2 hours, 3 minutes, and 4 seconds
=> #>

Under the hood, it serializes duration to iso8601:

> (1.year + 2.days + 3.hours + 4.minutes + 5.seconds).iso8601
=> "P1Y2DT3H4M5S"
> ActiveSupport::Duration.parse("P1Y2DT3H4M5S")
=> 1 year, 2 days, 3 hours, 4 minutes, and 5 seconds

Today I learned, that iso8601 does not support mixing “weeks” part with any other date parts:

> 1.week.iso8601
=> "P1W"
> (1.week + 2.days).iso8601
=> "P9D"

Because of that, if you try to save a duration with weeks and other date parts, those weeks will be converted to days:

> trigger.time_offset = 1.week + 2.days + 3.hours
=> 1 week, 2 days, and 3 hours
> trigger.time_offset.parts
=> {:weeks=>1, :days=>2, :hours=>3}
> trigger.save
=> true
> trigger.time_offset.parts
=> {:days=>9, :hours=>3}

While saving just weeks, or any duration without weeks, will give you consistent results:

> trigger.time_offset = 5.weeks
=> 5 weeks
> trigger.save
=> true
> trigger.time_offset
=> 5 weeks
> trigger.time_offset = 500.days
=> 500 days
> trigger.save
=> true
> trigger.time_offset
=> 500 days

Form objects with Active Admin

In this example, I’m using form object’s implementation from Selleo/pattern, but the solution shouldn’t differ much for other popular implementations.

If you want to use a form object class with the Active Admin, there are a few things you need to take care of:

  • Overwrite controller’s build_new_resource and find_resource methods.
    ActiveAdmin uses build_new_resource to build a new resource for new and create actions, and find_resource for retrieving resource in edit, update and show.
    We don’t want to use our form object in show, so we check action_name in find_new_resource.
contoller do
  def build_new_resource
    OrderForm.new(super)
  end
  
  def find_resource
    return OrderForm.new(super) if action_name.in?(%w[edit update])
    super
  end
end
  • Permit all params
    We don’t want to duplicate our form’s responsibility with Rails’ Strong Parameters.
controller do
  before_action -> { params.permit! }
end
  • Either overwrite the default form, or extend it with the names of your attributes, as it won’t be able to infer them automatically from the form object’s class.
form do |f|
  f.semantic_errors
  f.inputs(*OrderForm.attributes_names)
  f.actions
end

Repeating all of that for every page where you want to use a form object can get tedious, so here’s a dynamic concern which you can use to include those changes to your page quickly:

module UsesFormObject
  module_function

  def with_class(form_object_class)
    Module.new do
      define_singleton_method(:included) do |activeadmin_dsl|
        activeadmin_dsl.instance_eval do
          form do |f|
            f.semantic_errors
            f.inputs(*form_object_class.attributes_names)
            f.actions
          end

          controller do
            before_action -> { params.permit! }

            private

            define_method(:build_new_resource) do
              form_object_class.new(super())
            end

            define_method(:find_resource) do
              return form_object_class.new(super()) if action_name.in?(%w[edit update])

              super()
            end
          end
        end
      end
    end
  end
end

You can use it like that:

ActiveAdmin.register Order do
  include UsesFormObject.with_class(OrderForm)
end

with_options in Rails

Active Support in Rails adds a with_option method to the Object (i.e., it’s available everywhere).

It yields a proxy object, which adds given options to each method call:

def log(arg, **kwargs)
  puts(arg: arg, kwargs: kwargs)
end

> with_options a: 123 do |with_a|
>   with_a.log(:called_with_a)
>   log(:called_without_a)
>   with_a.log(:called_with_a_and_b, b: 456)
>   with_a.log(:called_with_overwritten_a, a: 789)
> end
{:arg=>:called_with_a, :kwargs=>{:a=>123}}
{:arg=>:called_without_a, :kwargs=>{}}
{:arg=>:called_with_a_and_b, :kwargs=>{:a=>123, :b=>456}}
{:arg=>:called_with_overwritten_a, :kwargs=>{:a=>789}}

You can use it to group multiple similar calls or to avoid repetition.
E.g., if you need to call I18n multiple times in a place that does not support inferring the scope:

I18n.with_options(scope: 'active_admin.orders.edit.details') do |i18n|
  panel i18n.t('header', order_number: order_number) do
    ...
  end
end

That way, each call to i18n.t will have the correct scope option without you having to repeat it over and over.

Another use case is to group conditional ActiveModel validations:

class EventForm < ApplicationRecord
  with_options if: :teams_enabled? do |teams|
    teams.validates :team_max, presence: true
    teams.validates :team_descripion, length: { maximum: 500 }
  end
end

System wide vim-like tab switching in Mac Os

Mac Os allows you to natively map any menu bar command to a keybind of your choice. We can use it to map switching between tabs to ergonomic vim-like cmd+j/k instead of ctrl+tab/ctrl+shift+tab system-wide.

Start by locating tab switching commands in the menu bar: Chrome menu bar command on Mac Os The mapping works by matching the exact command’s name, so you need to note them carefully.
For switching tabs, in my experience, every application uses the “Select Next/Previous Tab” command, except for Chrome, which uses “Select next/previous tab” (i.e., lowercase), and Safari, which uses “Show Next/Previous Tab” ¯ \_(ツ)_/¯

Next, go to the System Preferences > Keyboard > Shortcuts > App Shortcuts, click on the plus sign, and enter the name of the command which you noted previously: Adding a keybing

After adding all the keybinds, you will end up with something like that: Fully configured keybinds

From now on, you should be able to use a way more ergonomic cmd+j/k to switch between tabs in most applications - and if a particular application uses a different system in place of the tabs, you know how to map it to the same keybind so that you can make better use of your muscle memory.

Nested assignments inside array assignment in Ruby

In Ruby, an assignment returns its value.

> a = 1
=> 1

You can slightly abuse it to nest variable assignments inside an array assignment:

a = [
  b = 1,
  c = 2
]

> a
=> [1, 2]
> b
=> 1
> c
=> 2

You could even go a step further to nest variable assignments inside array assignments nested inside yet another array assignment - although it seems to be losing readability at this point:

a = [
  *b = [
    c = 1,
    d = 2
  ],
  *e = [
    f = 3,
    g = 4
  ]
]

> a
=> [1, 2, 3, 4]
> b
=> [1, 2]
> c
=> 1

This may be useful in tests, where you have multiple variables, which you need to use both individually and collectively, and you want to have descriptive variable names:

triggers = [
  trigger_with_conditions_fulfilled = create(:trigger),
  trigger_with_conditions_not_fulfilled = create(:trigger)
]

allow(trigger_with_conditions_fulfilled).to receive(:conditions_fulfilled?).and_return(true)
allow(trigger_with_conditions_not_fulfilled).to receive(:conditions_fulfilled?).and_return(false)

ExecuteTriggers.call(triggers)

Dependent types in Ruby with dry-rb

Dry-rb supports dependent types (i.e., it allows you to match types based on the value):

class HasA < Dry::Struct
  attribute :a, Types::Symbol
end

class HasB < Dry::Struct
  attribute :b, Types::Symbol
end

class ShortText < Dry::Struct
  attribute :text, Types::String.constrained(max_size: 3)
end

class LongText < Dry::Struct
  attribute :text, Types::String.constrained(min_size: 4)
end

class OurStruct < Dry::Struct
  attribute :a_or_b, Types::Array.of(A | B)
  attribute :texts, Types::Array.of(ShortText | LongText)
end

> struct = OurStruct.new(
>   a_or_b: [{ a: :abc } , { b: :def }],
>   texts: [{ text: 'abc' } , { text: 'defgh' }],
> )
=> #<OurStruct
=> #  a_or_b=[#<A a=:abc>, #<B b=:def>]
=> #  texts=[#<ShortText text="abc">, #<LongText text="defgh">]
=> #>

You can use it to, e.g., parse data and then attach specific behavior to it:

module Types
  EventData = Types::Hash.schema(name: Types::String, value: Types::Integer)
end

class GroupCondition < Dry::Struct
  attribute :event, Types::Value(:group_formed)
  attribute :data, Types::EventData

  def applicable?
    Group.exists?(name: data.fetch(:name)) && data.fetch(:value) >= 123
  end
end

class InvitationCondition < Dry::Struct
  attribute :event, Types::Value(:invitation_sent)
  attribute :data, Types::EventData

  def applicable?
    Invitation.exists?(value: data.fetch(:value))
  end
end

class ConditionsSet < Dry::Struct
  attribute :conditions, Types::Array.of(GroupCondition | InvitationCondition)
end

> set = ConditionsSet.new(
>   conditions: [
>     { event: :invitation_sent, data: { name: 'abc', value: 123 } },
>     { event: :group_formed, data: { name: 'def', value: 456 } }
>   ]
> )
=> #<ConditionsSet
=> #  conditions=[
=> #    <InvitationCondition event=:invitation_sent data={:name=>"abc", :value=>123}>,
=> #    <GroupCondition event=:group_formed data={:name=>"def", :value=>456}>
=> #  ]
=> #>
> set.conditions.all?(&:applicable?)
=> true

How to fix Elasticsearch 'FORBIDDEN/12/index read-only'

By default, Elasticsearch installed with homebrew on Mac OS goes into read-only mode when you have less than 5% of free disk space. If you see errors similar to this:

Elasticsearch::Transport::Transport::Errors::Forbidden:
  [403] {"error":{"root_cause":[{"type":"cluster_block_exception","reason":"blocked by: [FORBIDDEN/12/index read-only / allow delete (api)];"}],"type":"cluster_block_exception","reason":"blocked by: [FORBIDDEN/12/index read-only / allow delete (api)];"},"status":403}

Or in /usr/local/var/log/elasticsearch.log you can see logs similar to:

flood stage disk watermark [95%] exceeded on [nCxquc7PTxKvs6hLkfonvg][nCxquc7][/usr/local/var/lib/elasticsearch/nodes/0] free: 15.3gb[4.1%], all indices on this node will be marked read-only

Then you can fix it by running the following commands:

curl -XPUT -H "Content-Type: application/json" http://localhost:9200/_cluster/settings -d '{ "transient": { "cluster.routing.allocation.disk.threshold_enabled": false } }'
curl -XPUT -H "Content-Type: application/json" http://localhost:9200/_all/_settings -d '{"index.blocks.read_only_allow_delete": null}'

How to get XPath of Capybara's query

Have you ever found yourself in a situation, where you were trying to do something like e.g. click_link 'Approve' and Capybara was not able to find that element on the page despite the fact that it’s quite clearly visible, and you were asking yourself “what the heck is it searching for then?”. Or maybe your find(sth) is failing and you think it’s a bug in the Capybara 😱
Worry no more! Now you can easily check generated XPath used by Capybara*. In most cases, find(*args, **options) translates to:

Capybara::Queries::SelectorQuery.new(*args, session_options: current_scope.session_options, **options).xpath

E.g. to see XPath for click_on 'Approve':

Capybara::Queries::SelectorQuery.new(:link_or_button, 'Approve', session_options: current_scope.session_options).xpath

And XPath for find('tbody > tr > td:nth-child(2)'):

Capybara::Queries::SelectorQuery.new('tbody > tr > td:nth-child(2)', session_options: current_scope.session_options).xpath

Then you can copy that XPath to the Chrome’s console and test it with $x('xpath').

* Presented solution doesn’t work with some types of more complicated queries, e.g. find('a', text: 'APPROVED') actually uses CSS selector instead of the XPath, and then filter results using Capybara::Result. You can check type of the selector used using .selector.format on your selector query.