Ruby/Rails Hash.to_s. Why?, Where is String.to_h?
Steve Alex
Posted on February 11, 2023
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
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}}}
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}}}"
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}}}"
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
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
...
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.
Posted on February 11, 2023
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.