无法获取深层嵌套的哈希值

时间:2016-09-30 08:36:14

标签: ruby hash

我有这个rake任务,它使用rest-client从这个API中获取一些凌乱的JSON,然后使用hashie使代码更漂亮。

很遗憾,我无法获取其中一个深度嵌套的值productGroup。如果工作正常,则应输出:category => "Jeans"或类似内容。请参阅底部的JSON。

这不起作用:

mash.deep_fetch(:fields, 0).deep_locate(-> (key, value, object) { value.include?("product_group") }) { "ERROR: category" }

示例输出:

% rake get_products
{:category=>nil, :name=>"Luxurous Jumpsuit", :image=>"http://nlyscandinavia.scene7.com/is/image/nlyscandinavia/productLarge/129579-0012.jpg", :price=>"599", :description=>"Lorem ipsum dolor"}

mash示例:

#<Hashie::Mash brand="Dr Denim" categories=[#<Hashie::Mash name="Kvinne > KLÆR > Jeans > Slim">] description="Lorem ipsum dolor." fields=[#<Hashie::Mash name="sale" value="false">, #<Hashie::Mash name="product_id_original" value="226693-7698">, #<Hashie::Mash name="gender" value="Kvinne">, #<Hashie::Mash name="artNumber" value="226693-7698">, #<Hashie::Mash name="productGroup" value="Jeans">, #<Hashie::Mash name="productStyle" value="Slim">, #<Hashie::Mash name="extraImageProductSmall" value="http://nlyscandinavia.scene7.com/is/image/nlyscandinavia/cart_thumb/226693-7698.jpg">, #<Hashie::Mash name="productClass" value="Klær">, #<Hashie::Mash name="extraImageProductLarge" value="http://nlyscandinavia.scene7.com/is/image/nlyscandinavia/productLarge/226693-7698.jpg">, #<Hashie::Mash name="sizes" value="W24/L32,W25/L32,W26/L32,W27/L32,W28/L32,W29/L32,W30/L32,W31/L32,W32/L32,W26/L30,W27/L30,W28/L30,W29/L30,W24/L30,W25/L30,W32/L30,W31/L30,W30/L30">, #<Hashie::Mash name="color" value="Mid Blue">] identifiers=#<Hashie::Mash sku="226693-7698"> language="no" name="Regina Jeans" offers=[#<Hashie::Mash feed_id=10086 id="2820760a-c5b2-494a-b5dd-ab713f796cb9" in_stock=1 modified=1474947357838 price_history=[#<Hashie::Mash date=1474949513421 price=#<Hashie::Mash currency="NOK" value="599">>] product_url="http://pdt.tradedoubler.com/click?a1234" program_logo="http://hst.tradedoubler.com/file/17833/2014-logos/200X200.png" program_name="Nelly NO" source_product_id="226693-7698">] product_image=#<Hashie::Mash url="http://nlyscandinavia.scene7.com/is/image/nlyscandinavia/productLarge/226693-7698.jpg">>

get_products.rake:

# encoding: utf-8

# Gets messy JSON from other store via REST client and cleans it up with Hashie

require "rest_client"
require "hashie"

Product = Struct.new(:category, :name, :image, :price, :description)

module ProductsFromOtherStore
  CATEGORIES = [
    "festkjoler",
    "jakker",
    "jeans",
    "jumpsuit",
    "vesker"
  ]

  def self.fetch
    CATEGORIES.map do |category|
      Tradedoubler.fetch category
    end
  end

  # Prettify, ie. `fooBar` => `foo_bar`

  def self.prettify(x)
    case x
    when Hash
      x.map { |key, value| [key.underscore, prettify(value)] }.to_h
    when Array
      x.map { |value| prettify(value) }
    else
      x
    end
  end
end

class ProductsFromOtherStore::Tradedoubler
  KEY = "FE34B1309AB749F1578AEE87D9D74535513F6B54"

  # Products to fetch from API

  LIMIT = 2

  def self.fetch category
    new(category).filtered_products.take(LIMIT)
  rescue RestClient::RequestTimeout => e
    Array.new
  end

  def initialize category
    @category = category

    # API doesn't support gender or category searches, so do some filtering based on available JSON fields

    @filters = Array.new

    define_filter { |mash|
      mash.fields.any? { |field|
        field.name == "gender" && field.value.downcase == "kvinne"
      }
    }

    define_filter { |mash|
      mash.categories.any? { |category|
        category.name.underscore.include? @category
      }
    }
  end

  def define_filter(&filter)
    @filters << filter
  end

  def filtered_products
    filtered_mashes.map { |mash|
      # puts mash

      Product.new(
        # mash.deep_fetch(:fields, 0).find { |field| field[:name] == "product_group" }[:value],
        mash.deep_fetch(:fields, 0).deep_locate(-> (key, value, object) { value.include?("product_group") }) { "ERROR: category" },
        mash.deep_fetch(:name) { "ERROR: name" },
        mash.deep_fetch(:product_image, :url) { "ERROR: image URL" },
        mash.deep_fetch(:offers, 0, :price_history, 0, :price, :value) { "ERROR: price" },
        mash.deep_fetch(:description) { "ERROR: description" }
      )
    }
  end

private
  def request
    response = RestClient::Request.execute(
      :method => :get,
      :url => "http://api.tradedoubler.com/1.0/products.json;q=#{ URI.encode(@category) };limit=#{ LIMIT }?token=#{ KEY }",
      :timeout => 0.4
    )
  end

  def hashes
    ProductsFromOtherStore.prettify(JSON.parse(request)["products"])
  end

  def mashes
    hashes.map { |hash| Hashie::Mash.new(hash) }.each do |mash|
      mash.extend Hashie::Extensions::DeepFetch
      mash.extend Hashie::Extensions::DeepLocate
    end
  end

  def filtered_mashes
    mashes.select { |mash| mash_matches_filter? mash }
  end

  def mash_matches_filter? mash

    # `.all?` requires all filters to match, `.any?` requires only one

    @filters.all? { |filter| filter.call mash }
  end
end

# All that for this

task :get_products => :environment do
  @all_products_from_all_categories = ProductsFromOtherStore.fetch

  @all_products_from_all_categories.each do |products|
    products.each do |product|
      puts product.to_h
    end
  end
end

我们通过rest-client获得的混乱JSON:

{
    "productHeader": {
        "totalHits": 367
    },
    "products": [{
        "name": "501 CT Jeans For Women",
        "productImage": {
            "url": "http://nlyscandinavia.scene7.com/is/image/nlyscandinavia/productLarge/441576-1056.jpg"
        },
        "language": "no",
        "description": "Jeans fra Levi's. Noe kortere nederst, fem lommer. Normal høyde på midjen, med hemper i linningen og knappegylfen. Dekorative slitte partier foran og nederst på benet.<br />Laget av 100% bomull.",
        "brand": "Levis",
        "identifiers": {
            "sku": "441576-1056"
        },
        "fields": [{
            "name": "sale",
            "value": "false"
        }, {
            "name": "sizes",
            "value": "W24/L32,W25/L32,W26/L32,W27/L32,W28/L32,W29/L32,W30/L32,W31/L32,W25/L34,W26/L34,W27/L34,W28/L34,W29/L34,W30/L34"
        }, {
            "name": "productStyle",
            "value": "Straight"
        }, {
            "name": "gender",
            "value": "Kvinne"
        }, {
            "name": "product_id_original",
            "value": "441576-1056"
        }, {
            "name": "productGroup",
            "value": "Jeans"
        }, {
            "name": "extraImageProductLarge",
            "value": "http://nlyscandinavia.scene7.com/is/image/nlyscandinavia/productLarge/441576-1056.jpg"
        }, {
            "name": "extraImageProductSmall",
            "value": "http://nlyscandinavia.scene7.com/is/image/nlyscandinavia/cart_thumb/441576-1056.jpg"
        }, {
            "name": "artNumber",
            "value": "441576-1056"
        }, {
            "name": "productClass",
            "value": "Klær"
        }, {
            "name": "color",
            "value": "Indigo"
        }],
        "offers": [{
            "feedId": 10086,
            "productUrl": "http://pdt.tradedoubler.com/click?a(2402331)p(80279)product(57d37b9ce4b085c06c38c96b)ttid(3)url(http%3A%2F%2Fnelly.com%2Fno%2Fkl%C3%A6r-til-kvinner%2Fkl%C3%A6r%2Fjeans%2Flevis-441%2F501-ct-jeans-for-women-441576-1056%2F)",
            "priceHistory": [{
                "price": {
                    "value": "1195",
                    "currency": "NOK"
                },
                "date": 1473477532181
            }],
            "modified": 1473477532181,
            "inStock": 1,
            "sourceProductId": "441576-1056",
            "programLogo": "http://hst.tradedoubler.com/file/17833/2014-logos/200X200.png",
            "programName": "Nelly NO",
            "id": "57d37b9ce4b085c06c38c96b"
        }],
        "categories": [{
            "name": "Kvinne > KLÆR > Jeans > Straight"
        }]
    }, {
        "name": "501 CT Jeans For Women",
        "productImage": {
            "url": "http://nlyscandinavia.scene7.com/is/image/nlyscandinavia/productLarge/441576-6581.jpg"
        },
        "language": "no",
        "description": "Jeans fra Levi's. Noe kortere nederst, fem lommer. Normal høyde på midjen, med hemper i linningen og knappegylfen. Dekorative slitte partier foran og nederst på benet.<br />Laget av 100% bomull.",
        "brand": "Levis",
        "identifiers": {
            "sku": "441576-6581"
        },
        "fields": [{
            "name": "sale",
            "value": "false"
        }, {
            "name": "artNumber",
            "value": "441576-6581"
        }, {
            "name": "productStyle",
            "value": "Straight"
        }, {
            "name": "gender",
            "value": "Kvinne"
        }, {
            "name": "extraImageProductLarge",
            "value": "http://nlyscandinavia.scene7.com/is/image/nlyscandinavia/productLarge/441576-6581.jpg"
        }, {
            "name": "extraImageProductSmall",
            "value": "http://nlyscandinavia.scene7.com/is/image/nlyscandinavia/cart_thumb/441576-6581.jpg"
        }, {
            "name": "productGroup",
            "value": "Jeans"
        }, {
            "name": "product_id_original",
            "value": "441576-6581"
        }, {
            "name": "productClass",
            "value": "Klær"
        }, {
            "name": "color",
            "value": "Desert"
        }, {
            "name": "sizes",
            "value": "W24/L32,W25/L32,W26/L32,W27/L32,W28/L32,W29/L32,W30/L32,W31/L32,W25/L34,W26/L34,W27/L34,W28/L34,W29/L34,W30/L34,W31/L34"
        }],
        "offers": [{
            "feedId": 10086,
            "productUrl": "http://pdt.tradedoubler.com/click?a(2402331)p(80279)product(57b3cafbe4b06cf59bc254bf)ttid(3)url(http%3A%2F%2Fnelly.com%2Fno%2Fkl%C3%A6r-til-kvinner%2Fkl%C3%A6r%2Fjeans%2Flevis-441%2F501-ct-jeans-for-women-441576-6581%2F)",
            "priceHistory": [{
                "price": {
                    "value": "1195",
                    "currency": "NOK"
                },
                "date": 1471400699283
            }],
            "modified": 1471400699283,
            "inStock": 1,
            "sourceProductId": "441576-6581",
            "programLogo": "http://hst.tradedoubler.com/file/17833/2014-logos/200X200.png",
            "programName": "Nelly NO",
            "id": "57b3cafbe4b06cf59bc254bf"
        }],
        "categories": [{
            "name": "Kvinne > KLÆR > Jeans > Straight"
        }]
    }]
}

1 个答案:

答案 0 :(得分:1)

您的代码示例中发生了很多事情。我试图拆分并重组它。它与您的代码不同,但我认为它应该让您开始,也许您可​​以在有更具体的问题时回来。

请注意,我没有使用hashie,我认为在一些地方访问一些深度嵌套的哈希结构并不能证明在项目中添加新库是合理的。

问题/想法/提示:

  • 价格是整数还是浮动?
  • JSON是否一致(所有元素一直存在?)
  • 您使用的是Ruby 2.3吗?然后查看Hash#dig
  • 你为什么要美化JSON键?当你构建Product个对象时,对我来说没有意义吗?
  • 除非存在性能问题,否则我会首先将所有产品转换为Ruby对象,然后进行过滤。更简单,更易读。

<强>代码

产品(与您的相同)

Product = Struct.new(:category, :name, :image, :price, :description)

JsonProductBuilder 将解析后的JSON转换为Product Objects。

class JsonProductBuilder
  def initialize(json)
    @json = json
  end

  def call
    json.fetch('products', []).map do |item|
      Product.new(
        extract_category(item),
        item['name'],
        item.fetch('productImage', {})['url'],
        extract_price(item),
        item['description']
      )
    end
  end

  private

  attr_reader :json

  def extract_category(item)
    field = item['fields'].find do |field|
      field['name'] == 'productGroup'
    end
    field['value'] if field
  end

  def extract_price(item)
    offer = item['offers'].first
    history = offer['priceHistory'].first
    value = history['price']['value']
    Integer(value) # Or use Float?
  end
end

CategoryFilter 会返回有限的产品子集。您可以轻松添加其他过滤器并进行组合。也许您可能希望调查lazy以提高性能。

class CategoryFilter
  def initialize(products, *categories)
    @products = products
    @categories = categories
  end

  def call
    products.select do |product|
      categories.include?(product.category)
    end
  end

  private

  attr_reader :products, :categories
end

像这样使用:

limit = 10
categories = ['laptop', 'something']
params = {
  q: categories.join(','),
   limit: limit,
 }

 paramsString = params.map do |key, value|
   "#{key}=#{value}"
 end.join(';')

 response = RestClient.get(
   "http://api.tradedoubler.com/1.0/products.json;#{paramsString}?token=#{token}"
  )

json = JSON.parse(response)
products = JsonProductBuilder.new(json).call
puts products.size

products = CategoryFilter.new(products, 'Klær', 'Sko', 'Jeans').call
puts products.size

products.each do |product|
  puts product.to_h
end