jsonb_accessor icon indicating copy to clipboard operation
jsonb_accessor copied to clipboard

Prefix / Suffix feature

Open mmarusyk opened this issue 10 months ago • 4 comments

This PR introduces an improvement for utilizing prefix and suffix options for attribute names in jsonb_accessor.

Why?

In some cases, it suffices to store certain information about orders in jsonb format when it's only needed for historical purposes or for generating invoices, etc.

Results

How it currently looks in the Order model:

class Order < ApplicationRecord
  jsonb_accessor :contact_details,
                 contact_details_email: [:string, { store_key: :email }],
                 contact_details_last_name: [:string, { store_key: :last_name }],
                 contact_details_first_name: [:string, { store_key: :first_name }],
                 contact_details_phone_number: [:string, { store_key: :phone_number }]

  jsonb_accessor :billing_details,
                 billing_details_email: [:string, { store_key: :email }],
                 billing_details_last_name: [:string, { store_key: :last_name }],
                 billing_details_first_name: [:string, { store_key: :first_name }],
                 billing_details_phone_number: [:string, { store_key: :phone_number }],
                 billing_details_full_address_line: [:string, { store_key: :full_address_line }]

  jsonb_accessor :shipping_details,
                 shipping_details_full_address_line: [:string, { store_key: :full_address_line }]
end

How it will look after implementing prefixes:

class Order < ApplicationRecord
  jsonb_accessor :contact_details,
                 email: [:string, { prefix: true }],
                 last_name: [:string, { prefix: true }],
                 first_name: [:string, { prefix: true }],
                 phone_number: [:string, { prefix: true }]

  jsonb_accessor :billing_details,
                 email: [:string, { prefix: true }],
                 last_name: [:string, { prefix: true }],
                 first_name: [:string, { prefix: true }],
                 phone_number: [:string, { prefix: true }],
                 full_address_line: [:string, { prefix: true }]

  jsonb_accessor :shipping_details,
                 full_address_line: [:string, { prefix: true }]
end

In rails console:

order.billing_details #=> {"email"=>"[email protected]", "last_name"=>"Mosciski", "first_name"=>"Chi", "phone_number"=>"475912955432", "full_address_line"=>"Suite 897 932 Nitzsche Shoal, Tammiborough, MI 90349-4217"}
order.billing_details_first_name #=> "Chi"
order.contact_details_first_name #=> "Marquita"
order.contact_details #=> {"email"=>"[email protected]", "last_name"=>"Wunsch", "first_name"=>"Marquita", "phone_number"=>"278587369859"}
order.update(contact_details_first_name: "Mike", billing_details_first_name: "John") #=> true
order.reload.billing_details #=> {"email"=>"[email protected]", "last_name"=>"Mosciski", "first_name"=>"John", "phone_number"=>"475912955432", "full_address_line"=>"Suite 897 932 Nitzsche Shoal, Tammiborough, MI 90349-4217"}
order.reload.contact_details #=> {"email"=>"[email protected]", "last_name"=>"Wunsch", "first_name"=>"Mike", "phone_number"=>"278587369859"}
order.contact_details_last_name="Marsk" #=> "Marsk"
order.contact_details_last_name #=> "Marsk"
order.contact_details #=> {"email"=>"[email protected]", "last_name"=>"Marsk", "first_name"=>"Mike", "phone_number"=>"278587369859"}
irb(main):012:0> order.save #=> true
irb(main):013:0> Order.contact_details_where(last_name: "Marsk") #=>
[#<Order:0x00007f7a0a85fc88
  id: 2,
  contact_details: {"email"=>"[email protected]", "last_name"=>"Marsk", "first_name"=>"Mike", "phone_number"=>"278587369859"},
  billing_details: {"email"=>"[email protected]", "last_name"=>"Mosciski", "first_name"=>"John", "phone_number"=>"475912955432", "full_address_line"=>"Suite 897 932 Nitzsche Shoal, Tammiborough, MI 90349-4217"},
  shipping_details: {"full_address_line"=>"38266 Durgan Motorway, Kulasmouth, NC 46602-3316"},
  contact_details_email: "[email protected]",
  contact_details_last_name: "Marsk",
  contact_details_first_name: "Mike",
  contact_details_phone_number: "278587369859",
  billing_details_email: "[email protected]",
  billing_details_last_name: "Mosciski",
  billing_details_first_name: "John",
  billing_details_phone_number: "475912955432",
  billing_details_full_address_line: "Suite 897 932 Nitzsche Shoal, Tammiborough, MI 90349-4217",
  shipping_details_full_address_line: "38266 Durgan Motorway, Kulasmouth, NC 46602-3316">]
irb(main):014:0> Order.contact_details_where(last_name: "Random") #=> []

Also, it works fine with store key:

class Order < ApplicationRecord
  jsonb_accessor :contact_details,
                 email: [:string, { prefix: true, store_key: :e }], # Added store key
                 last_name: [:string, { prefix: true }],
                 first_name: [:string, { prefix: true }],
                 phone_number: [:string, { prefix: true }]

  jsonb_accessor :billing_details,
                 email: [:string, { prefix: true }],
                 last_name: [:string, { prefix: true }],
                 first_name: [:string, { prefix: true }],
                 phone_number: [:string, { prefix: true }],
                 full_address_line: [:string, { prefix: true }]

  jsonb_accessor :shipping_details,
                 full_address_line: [:string, { prefix: true }]
end
order.contact_details #=> {"e"=>"[email protected]", "last_name"=>"Kshlerin", "first_name"=>"Jessie", "phone_number"=>"567805903539"}
irb(main):021:0> order.contact_details_email="[email protected]" #=> "[email protected]"
order.contact_details_email #=> "[email protected]"
order.contact_details #=> {"e"=>"[email protected]", "last_name"=>"Kshlerin", "first_name"=>"Jessie", "phone_number"=>"567805903539"}
order.save #=> true
order.reload.contact_details #=> {"e"=>"[email protected]", "last_name"=>"Kshlerin", "first_name"=>"Jessie", "phone_number"=>"567805903539"}

For quering you can use name attribute without prefix/suffix:

Order.find(7).contact_details => {"e"=>"[email protected]"}
Order.contact_details_where(email: "[email protected]") # =>
[#<Order:0x00007f96bdd7a158
  id: 7,
  contact_details: {"e"=>"[email protected]"},
  billing_details: nil,
  shipping_details: nil,
  created_at: Sun, 07 Apr 2024 11:15:37.782726000 UTC +00:00,
  updated_at: Sun, 07 Apr 2024 11:15:37.782726000 UTC +00:00,
  contact_details_email: "[email protected]",
  contact_details_last_name: nil,
  contact_details_first_name: nil,
  contact_details_phone_number: nil,
  billing_details_email: nil,
  billing_details_last_name: nil,
  billing_details_first_name: nil,
  billing_details_phone_number: nil,
  billing_details_full_address_line: nil,
  shipping_details_full_address_line: nil>]

I'll be happy if we have this improvement in this gem. Thank you!

fixes #173

Previous conversation is in this pull request: https://github.com/madeintandem/jsonb_accessor/pull/177

mmarusyk avatar May 06 '25 18:05 mmarusyk

I'm not sure why CI is broken, don't think it's related to your change. I'll try to clean up the CI tomorrow :crossed_fingers:

ashkulz avatar May 07 '25 02:05 ashkulz

I still need to get some additional permissions from @darcygarrett so I can't merge #183 right now, hopefully I should get them soon :crossed_fingers:

ashkulz avatar May 08 '25 05:05 ashkulz

@mmarusyk I like the idea, but having to specify prefix: true for every field doesn't strike me as good. I'd rather have something like

class Order < ApplicationRecord
  jsonb_accessor :contact_details, prefix: true,
                 email: :string,
                 last_name: :string,
                 first_name: :string,
                 phone_number: :string

  jsonb_accessor :billing_details, prefix: true,
                 email: :string,
                 last_name: :string,
                 first_name: :string,
                 phone_number: :string,
                 full_address_line: :string

  jsonb_accessor :shipping_details,
                 full_address_line: :string
end

Since the signature for the macro is def jsonb_accessor(jsonb_attribute, field_types) I think we can add keyword arguments there (especially since we're targeting Ruby 3.2+), but I'm not sure how well it'll play with the implicit Hash being passed for the field_types. Or do you have any other suggestion how we'd be able to DRY it up further?

ashkulz avatar May 13 '25 12:05 ashkulz

@ashkulz Thanks for the feedback.

I agree that repeating prefix: true for every accessor isn't ideal. When I have time, I'll investigate ways to improve the macro, possibly by supporting keyword arguments.

mmarusyk avatar May 17 '25 09:05 mmarusyk