DEV Community

Steve Alex
Steve Alex

Posted on • Edited on

Ruby/Rails Hash.to_s. Why?, Where is String.to_h?

For years I've been using a serialized column in a model to store semi-constant setting/preferences. I've used various schemes to update, change, add or delete these preference. Updating values is fairly simple, just use a form that defines the key/value settings[some_field] value. Adding and deleting something is a little more involved and usually result in a re-deploy (since you had to add code to take care of the change!).

I've been playing with a new app that will never get deployed. I just wanted to see how difficult it would be to replicate Point-Of-Sale(POS) system. A POS is just a high-end cash register. I was more or less in charge of a VFW bar and we had a POS, until Covid forced us to close. We had about 8 months left on our contract, so we had to pay about $200 a month for doing nothing.

After a few weeks playing with it, using all the Rails goodies (Turbo Frames/Streams), it was not that difficult to create a rudimentary POS. It tablet friendly in that 95% is button clicks. I then adding features that dealt with taxes, discounts and other stuff.

  class Business < ApplicationRecord
    # where preferences/settings are stored

  class Department < ApplicationRecord
    has_many :items
    belongs_to :business
    # puts like items under it. Thing like Liquor, Food
    # that may have different tax-rates

  class Item < ApplicationRecord
    belongs_to :department
    has_many :ticket_items
    # inventory, price 

  class Employee < ApplicationRecord
    has_many :tickets
    has_many :tills
    # who using the pos

  class Ticket < ApplicationRecord
    belongs_to :employee
    has_many :ticket_items
    # a new sale

  class TicketItem < ApplicationRecord
    belongs_to :ticket
    belongs_to :item
    # what items are on the ticket  (quantity and Price/tax 
    # at time of sale (think Happy Hour)

  class Till < ApplicationRecord
    belongs_to :employee
    # balances sales with payment
Enter fullscreen mode Exit fullscreen mode

Enough of the POS and getting back to my Title/Question and settings/preferences. My current settings are :

{"item_tax"=>"included",                                                                                           
 "taxes_used"=>["sales", "county", "federal", "city", "liquor"],                                                   
 "employee_discount"=>{"percent"=>0.2, "round"=>0.25},                                                             
 "discounts_used"=>{"time_discount"=>{"percent"=>0.1, "round"=>0.25}, "special_discount"=>{"percent"=>0.2, "round"=>0.25}}}
Enter fullscreen mode Exit fullscreen mode
  • item_tax: Defines if the item price includes that tax or it's a separate line

  • taxes_used: Is defined the the Department. It just sums them, but can be separated by a Taxable class that also defines what the tax rates are based on ticket date. Tax rates do change, but very seldom.

  • employee_discount: Is a switch on ticket that if the customer is an employee, they get 20% off

  • discounts_used: Defines the different discounts. time-discount: takes care of changing happy hour prices it my get_current_price method.

I was just managing the settings in the console when I discovered that the scaffold view had a settings field. I just changed it to a text-area.

The cover image is pretty view of my settings form field. I said, what the heck and changed something and submitted. Low in behold, they changed! - but the serialized hash was now just a string - and everything that depended on the hash broke.

I'm sure Rails uses some form of Hash.to_s to populate the field. I can't think where else it would be used. That gets to my Why? question. Can anyone think of other reasons to use Hash.to_s other than to view it.

Since my settings: as serialized serialize :settings, JSON as JSON, what is stored in the DB is JSON but returned as a Ruby Hash.

"{\"item_tax\"=>\"included\", \"taxes_used\"=>[\"sales\", \"county\",
\"federal\", \"city\", \"liquor\"],
\"employee_discount\"=>{\"percent\"=>0.2, \"round\"=>0.25},
\"discounts_used\"=>{\"time_discount\"=>{\"percent\"=>0.1, \"round\"=>0.25},
\"special_discount\"=>{\"percent\"=>0.2, \"round\"=>0.25}}}"
Enter fullscreen mode Exit fullscreen mode

If I convert the hash to json (j = b.settings.to_json) I get:

"{\"item_tax\":\"included\",\"taxes_used\":[\"sales\",\"county\",
\"federal\",\"city\",\"liquor\"],
\"employee_discount\":{\"percent\":0.2,\"round\":0.25},
\"discounts_used\":{\"time_discount\":{\"percent\":0.1,\"round\":0.25},
\"special_discount\":{\"percent\":0.2,\"round\":0.25}}}"
Enter fullscreen mode Exit fullscreen mode

See the difference??? Hash.to string basically give Ruby like version of a JSON like string. All that is different is the hash assignment operators. JSON uses :, Ruby used =>

So I throw together a monkey patch (or could of just used a method) String.json_to_h

# Edit, forgot about array's
# /lib/core_extensins/json_to_h.rb
# convert JSOM to a Hash
module CoreExtensions
  module String
    def json_to_h
      if valid?
        JSON.parse(self.gsub('=>', ':'))
      end
    end

    def valid?
      default = true
      open_hash = self[0] == "{" && self[-1] == "}"
      open_arry = self[0] == "[" && self[-1] == "]"
      open_match = (self.count('{') + count('[')) 
      close_match = (self.count('}') + count(']'))
      return(default && (open_hash || open_arry)  && (open_match == close_match))
     end
  end
end

Enter fullscreen mode Exit fullscreen mode

I added a few sanity checks but it will error in the controller if not valid. I just had the change the scaffold controller a little:

def update
  # convert setting to hash and upate latter
  settings = business_params['settings'].json_to_h
  # settings will be nil if not a valid hash
  business_params.delete('settings')
  respond_to do |format|
    if settings.present?
      @business.attributes = business_params
      @business.settings = settings
      @business.save
      ...
Enter fullscreen mode Exit fullscreen mode

So I answered my second question Where is String.to_h

Don't think I'd use it except in development, but it was an interesting trip.

Top comments (0)