Search code examples
ruby-on-railsvalidationformtastic

RoR converting a virtual attribute into two database attributes


I'm currently having trouble finding a nice way to code the following situation:

There is a Model called TcpService, which has two attributes, port_from and port_to, both Integers. It also has a virtual attribute called portrange, which is a String. portrange is the String representation of the attributes port_from and port_to, so portrange = "80 90" should yield port_from = 80, port_to = 90. What I'm trying to do now is using the same Formtastic form for creating AND updating a TcpService-object. The form looks pretty standard (HAML code):

= semantic_form_for @tcp_service do |f|
  = f.inputs do
    = f.input :portrange, as: :string, label: "Portrange"
    -# calls @tcp_service.portrange to determine the shown value

  = f.actions do
    = f.action :submit, label: "Save"

The thing is, I don't know of a non-messy way to make the values I want appear in the form. On new I want the field to be empty, if create failed I want it to show the faulty user input along with an error, else populate port_from and port_to using portrange. On edit I want the String representation of port_from and port_to to appear, if update failed I want it to show the faulty user input along with an error, else populate port_from and port_to using portrange.

The Model looks like this, which seems quite messy to me. Is there a better way of making it achieve what I need?

class TcpService < ActiveRecord::Base
  # port_from, port_to: integer
  attr_accessor :portrange

  validate :portrange_to_ports   # populates `port_from` and `port_to` 
                                 # using `portrange` AND adds errors

  # raises exception if conversion fails
  def self.string_to_ports(string)
    ... # do stuff
    return port_from, port_to
  end

  # returns string representation of ports without touching self
  def ports_to_string
    ... # do stuff
    return string_representation
  end

  # is called every time portrange is set, namely during 'create' and 'update'
  def portrange=(val)
    return if val.nil?
    @portrange = val
    begin
      self.port_from, self.port_to = TcpService.string_to_ports(val)
    # catches conversion errors and makes errors of them
    rescue StandardError => e
      self.errors.add(:portrange, e.to_s())
    end
  end

  # is called every time the form is rendered
  def portrange
    # if record is freshly loaded from DB, this is true
    if self.port_from && self.port_to && @portrange.nil?
      self.ports_to_string()
    else
      @portrange
    end
  end

  private
    # calls 'portrange=(val)' in order to add errors during validation
    def portrange_to_ports
      self.portrange = self.portrange
    end

end

Thanks for reading


Solution

  • In your model

    def portrange
      return "" if self.port_from.nil? || self.port_to.nil?
      "#{self.port_from} #{self.port_to}"
    end
    
    def portrange=(str)
      return false unless str.match /^[0-9]{1,5}\ [0-9]{1,5}/
      self.port_from = str.split(" ").first
      self.port_to = str.split(" ").last
      self.portrange
    end
    

    Using this you should be able tu use the portrange setter and getter in your form.