Get IPAddr instance from its json representation

2019-08-14 22:40发布

问题:

How can I get back an IPAddr instance from its json representation?

Context:

I'm working on a Rails application that uses the draftsman gem. Essentially, when one makes a change to a record in the database and that ruby model has_drafts, instead of making the change to the record right away, a draft of the changes and the resulting record is saved in the drafts table until one publish!es that draft. Then the changes are made to the actual record. Pretty neat :)

The drafts table holds the object that would result from allowing the changes to happen in a json column in a postgres table.

I'm also using Devise and my user model uses the trackable module, which includes a current_sign_in_ip as well as a last_sign_in_ip of column type inet.

Problem:

inet type columns in my user table, as well as the rest of the User object, are being stored in the drafts table created by the draftsman gem in a single json column called object. reifying the object from its draft does not succeed in this case because of the following reason: IPAddr instances have a json representation that the IPAddr class is incapable of perceiving. For example, IPAddr instances in ruby respond to to_json, which gives me something that looks like this: "\"family\":2,\"addr\":2130706433,\"mask_addr\":4294967295}". These instances also respond to to_s and give me something like this: "127.0.0.1". Unfortunately, the IPAddr class doesn't seem to know how to bring itself back from json from what I can tell:

[277, 286] in /home/jake/.rvm/gems/ruby-2.2.1/bundler/gems/draftsman-1bd3c797d540/lib/draftsman/model.rb
   277:               data = {
   278:                 :item      => self,
   279:                 :whodunnit => Draftsman.whodunnit,
   280:                 :object    => object_attrs_for_draft_record
   281:               }
=> 282:               data = merge_metadata_for_draft(data)
   283: 
   284:               # If there's already a draft, update it.
   285:               if send(self.class.draft_association_name).present?
   286:                 data[:object_changes] = changes_for_draftsman if track_object_changes_for_draft?

(byebug) data[:object]['current_sign_in_ip']
#<IPAddr: IPv4:127.0.0.1/255.255.255.255>
(byebug) data[:object]['current_sign_in_ip'].to_json
"{\"family\":2,\"addr\":2130706433,\"mask_addr\":4294967295}"

(byebug) IPAddr.new(data[:object]['current_sign_in_ip'].to_s)
#<IPAddr: IPv4:127.0.0.1/255.255.255.255>
(byebug) IPAddr.new(data[:object]['current_sign_in_ip'].to_json)
*** IPAddr::InvalidAddressError Exception: invalid address

nil
(byebug) 

I figured posting this question to StackOverflow would get some attention to this issue, but I also submitted a github issue on the Draftsman github page if any other information is needed.

回答1:

Not sure if this helps, but if you read my comment below d_ethier's answer you can see that IPAddr doesn't really know anything about JSON.

However you can monkey patch it to understand JSON like so:

require 'active_model'
require 'active_support'
require 'active_support/core_ext'
require 'ipaddr'

class IPAddr
  include ActiveModel::Serializers::JSON

  attr_accessor :addr, :mask_addr, :family

  def attributes=(hash)
    hash.each do |key, value|
      send("#{key}=", value)
    end
  end

  def attributes
    instance_values
  end
end   

p i = IPAddr.new('127.0.0.1')
p j = i.to_json
p IPAddr.new.from_json(j)

Output:

#<IPAddr: IPv4:127.0.0.1/255.255.255.255>
"{\"family\":2,\"addr\":2130706433,\"mask_addr\":4294967295}"
#<IPAddr: IPv4:127.0.0.1/255.255.255.255>

Source:
http://apidock.com/rails/ActiveModel/Serializers/JSON/from_json



回答2:

I suspect the issue is something else.

IPAddr.new(data[:object]['current_sign_in_ip'].to_s) is not altering the contents of data[:object]['current_sign_in_ip'], so there shouldn't be a reason to "convert" it back.

In fact, data[:object]['current_sign_in_ip'] is already an IPAddr object. Wrapping it in IPAddr.new is redundant.

I suspect you're trying to do something else and this is just your description, so without seeing your code, it's hard to help.

If this is the case, you could work around it by creating a variable for the original IPAddr instance and use that later on.

original_ipaddr = data[:object]['current_sign_in_ip']
... rest of your code
IPAddr.new(data[:object]['current_sign_in_ip'].to_json)
IPAddr.new(original_ipaddr.to_s)