【问题标题】:Best way to iterate through a JSON object to create a ruby object遍历 JSON 对象以创建 ruby​​ 对象的最佳方法
【发布时间】:2017-08-02 09:26:24
【问题描述】:

我收到了一个 JSON 对象形式的 FullContact API 响应,其中包含一些包含一组联系人详细信息的嵌套数组。我想在我的 Rails 应用程序控制器中创建一个对象,该对象包含响应中的任何信息。我正在尝试使用如下代码执行此操作(请注意,我使用的 gem 允许使用点符号访问对象):

@automatic_profile = AutomaticProfile.new(
  profile_id: @profile.id,
  first_name: @intel.contact_info.full_name,
  email: @profile.email,
  gender: @intel.demographics.gender,
  city: @intel.demographics.location_deduced.city.name,
  skype: @intel.contact_info.chats.select { |slot| slot.client == "skype" }[0].handle),
  organization_1: @intel.organizations[0].name if @intel.organizations,
  # other similar lines for other organizations
  twitter: (@intel.social_profiles.select { |slot| slot.type_name == "Twitter" }[0].url if @intel.social_profiles),
  twitter_followers: (@intel.social_profiles.select { |slot| slot.type_name == "Twitter" }[0].followers.to_i) if @intel.social_profiles,
  twitter_following: (@intel.social_profiles.select { |slot| slot.type_name == "Twitter" }[0].following.to_i if @intel.social_profiles),
  # other similar lines for other social profiles
)

这段代码有两个问题:

  1. Json 对象并不总是具有填充某些哈希键所需的所有信息,因此例如在调用不存在的数组中的索引时会引发异常。

我尝试在每一行中添加一个 if 语句,如下所示:

twitter: (@intel.social_profiles.select { |slot| slot.type_name == "Twitter" }[0].url if @intel.social_profiles),

但它不是 DRY,我对括号的使用感到很困惑,以至于我提出了额外的例外。

  1. 为了给我的键设置正确的值,我使用 slot 方法来查找我正在寻找的特定数据。这似乎也很冗长,不太实用。

您能否就使用嵌套数组响应的大 Json 数据创建对象的最佳实践提出建议,并就如何解决这种特殊情况提出建议?

【问题讨论】:

    标签: ruby-on-rails arrays json ruby


    【解决方案1】:

    您可以使用.first.try 的组合。 (以及.dig,如果您使用的是 ruby​​ 2.3)以避免在访问它们时出现异常。

    .try 如果找不到它只会返回 nil。例如:

    { a: 2 }.try(:b) # returns nil 
    

    .dig 类似于.try,但它可以有多个层次,因此这对于深度嵌套的层次可能很有用。

    [['a'], ['a','b']].dig(0, 1) # first element, then second element - nil
    [['a'], ['a','b']].dig(1, 1) # second, then second again - 'b'
    
    { a: [1, 2] }.dig(:a, 0) # 1
    { a: [1, 2] }.dig(:a, 2) # nil
    
    foo = OpenStruct.new
    foo.bar = "foobar"
    { b: foo }.dig(:b, :bar) # 'foobar'
    
    @intel.dig(:contact_info, :full_name)
    @intel.dig(:organizations, :first, :name)
    

    对于最后一部分,你也可以这样重构:

    def twitter_profile
      return unless @intel.social_profiles.present?
      @intel.social_profiles.find { |slot| slot.type_name == "Twitter" }
    end
    
    twitter: twitter_profile.try(:url),
    twitter_followers: twitter_profile.try(:followers).to_i,
    twitter_followings: twitter_profile.try(:followings).to_i,
    

    twitter_profile 可以是控制器中的私有方法。如果您发现您开始拥有太多这些,您可以拥有一个用于创建配置文件的服务对象。

    class ProfileCreatorService
      def initialize(intel)
        @intel = intel
      end
    
      def perform
        AutomaticProfile.new(...)
      end
    
      private 
    
      def twitter_profile
        return unless @intel.social_profiles.present?
        @intel.social_profiles.find { |slot| slot.type_name == "Twitter" }
      end
    
      ..
    end
    

    【讨论】:

    • 非常感谢何。看起来这是一个非常明智的方法。让我试一试,然后回到你身边。尽管您建议在哪里设置该方法 twitter_profile ,但还有一件事。我很困惑,因为没有模型 Intel 并且它不可能是 Profile 模型的实例方法,因为我还没有创建对象。您能提供进一步的建议吗?
    • 不应该是类似于下面在 application_record.rb 中添加的代码 `def twitter_profile return 除非 self.social_profiles self.social_profiles.select { |slot| slot.type_name == "Twitter" }.first end`
    • return unless @intel.social_profiles.present? 会更好(如果social_profiles 是一个空数组,那么它将始终为真)。此外,执行[].select { |el| el.some_cond? }.first 可以更改为[].find { |el| el.some_cond? },因此将在第一次真实发生时停止,并且不执行每个数组元素的块。
    • @andrew 它可能只是控制器中的私有方法。或者,如果它变得太多,您可以创建一个服务对象,将所有这些创建逻辑隐藏在控制器之外。所以你可能喜欢 ProfileCreator.new(@intel).perform (这会返回一个配置文件)
    • @MrYoshiji,何。感谢您的非常有用的建议。现在说得通了
    猜你喜欢
    • 1970-01-01
    • 2016-03-30
    • 2013-01-22
    • 2015-01-11
    相关资源
    最近更新 更多