Accessor methods for specific values of a hash in

2019-08-28 00:08发布

问题:

I'm writing an API wrapper in Ruby for the Omeka API. Part of the API is making a class that represents an item on the Omeka site. A GET request to the API returns a JSON object, which I convert to a hash (see below, since it's long). Rather than making users navigate through a long hash, I'm using the Recursive Open Struct gem to create accessor methods. For example:

class OmekaItem

  attr_accessor :data

  def initialize(hash)
    @data = RecursiveOpenStruct.new(hash, :recurse_over_arrays => true)
  end

end

Assuming that I've made an instance of the class called item that lets the user access data like so: item.data.id or item.data.modified.

The most important data is in the element texts array. There are two kinds of element texts: "Dublin Core" and "Item Type Metadata." I'd like to create a separate Open Struct for the data in those fields. This is how I do that:

  class OmekaItem

    attr_accessor :data, :dublin_core, :item_type_metadata

    def initialize(hash)
      @data = RecursiveOpenStruct.new(hash, :recurse_over_arrays => true)

      dublin_core = Hash.new
      item_type_metadata = Hash.new
      @data.element_texts.each do |element_text|
        if element_text.element_set.name == "Dublin Core"
          method_name = element_text.element.name.downcase.gsub(/\s/, '_')
          value       = element_text.text
          dublin_core[method_name] = value
        elsif element_text.element_set.name == "Item Type Metadata"
          method_name = element_text.element.name.downcase.gsub(/\s/, '_')
          value       = element_text.text
          item_type_metadata[method_name] = value
        end
      end

      @dublin_core = RecursiveOpenStruct.new(dublin_core)
      @item_type_metadata = RecursiveOpenStruct.new(item_type_metadata)

    end

  end

Now users can access the Dublin Core metadata with method calls like this: item.dublin_core.title.

So far so good, but this is where I'm stuck. The class needs to implement a to_h method to return a hash in the original format with changed data, so that I can pass it to POST and PUT methods. If someone were to change the data by calling item.data.element_text[1].text = "My new data" then I could easily call the method on the open struct that returns a hash. But if a user changes item.dublin_core.title = "My new title" that data will be separate from the open struct stored in @data. How can I go about making the two places where the data is stored line up?


A typical hash for an item looks like this:

>> pp hash
{"id"=>1,
 "url"=>"http://localhost/omeka-2.1-rc1/api/items/1",
 "public"=>true,
 "featured"=>false,
 "added"=>"2013-07-13T04:47:08+00:00",
 "modified"=>"2013-07-14T19:37:45+00:00",
 "item_type"=>
  {"id"=>10,
   "url"=>"http://localhost/omeka-2.1-rc1/api/item_types/10",
   "name"=>"Lesson Plan",
   "resource"=>"item_types"},
 "collection"=>
  {"id"=>1,
   "url"=>"http://localhost/omeka-2.1-rc1/api/collections/1",
   "resource"=>"collections"},
 "owner"=>
  {"id"=>1,
   "url"=>"http://localhost/omeka-2.1-rc1/api/users/1",
   "resource"=>"users"},
 "files"=>
  {"count"=>0,
   "url"=>"http://localhost/omeka-2.1-rc1/api/files?item=1",
   "resource"=>"files"},
 "tags"=>[],
 "element_texts"=>
  [{"html"=>false,
    "text"=>"Item Title",
    "element_set"=>
     {"id"=>1,
      "url"=>"http://localhost/omeka-2.1-rc1/api/element_sets/1",
      "name"=>"Dublin Core",
      "resource"=>"element_sets"},
    "element"=>
     {"id"=>50,
      "url"=>"http://localhost/omeka-2.1-rc1/api/elements/50",
      "name"=>"Title",
      "resource"=>"elements"}},
   {"html"=>false,
    "text"=>"Item Subject",
    "element_set"=>
     {"id"=>1,
      "url"=>"http://localhost/omeka-2.1-rc1/api/element_sets/1",
      "name"=>"Dublin Core",
      "resource"=>"element_sets"},
    "element"=>
     {"id"=>49,
      "url"=>"http://localhost/omeka-2.1-rc1/api/elements/49",
      "name"=>"Subject",
      "resource"=>"elements"}},
   {"html"=>false,
    "text"=>"Item Contributor",
    "element_set"=>
     {"id"=>1,
      "url"=>"http://localhost/omeka-2.1-rc1/api/element_sets/1",
      "name"=>"Dublin Core",
      "resource"=>"element_sets"},
    "element"=>
     {"id"=>37,
      "url"=>"http://localhost/omeka-2.1-rc1/api/elements/37",
      "name"=>"Contributor",
      "resource"=>"elements"}},
   {"html"=>true,
    "text"=>"Item Description",
    "element_set"=>
     {"id"=>1,
      "url"=>"http://localhost/omeka-2.1-rc1/api/element_sets/1",
      "name"=>"Dublin Core",
      "resource"=>"element_sets"},
    "element"=>
     {"id"=>41,
      "url"=>"http://localhost/omeka-2.1-rc1/api/elements/41",
      "name"=>"Description",
      "resource"=>"elements"}},
   {"html"=>true,
    "text"=>"Item Creator",
    "element_set"=>
     {"id"=>1,
      "url"=>"http://localhost/omeka-2.1-rc1/api/element_sets/1",
      "name"=>"Dublin Core",
      "resource"=>"element_sets"},
    "element"=>
     {"id"=>39,
      "url"=>"http://localhost/omeka-2.1-rc1/api/elements/39",
      "name"=>"Creator",
      "resource"=>"elements"}},
   {"html"=>false,
    "text"=>"Item Source",
    "element_set"=>
     {"id"=>1,
      "url"=>"http://localhost/omeka-2.1-rc1/api/element_sets/1",
      "name"=>"Dublin Core",
      "resource"=>"element_sets"},
    "element"=>
     {"id"=>48,
      "url"=>"http://localhost/omeka-2.1-rc1/api/elements/48",
      "name"=>"Source",
      "resource"=>"elements"}},
   {"html"=>false,
    "text"=>"Item Publisher",
    "element_set"=>
     {"id"=>1,
      "url"=>"http://localhost/omeka-2.1-rc1/api/element_sets/1",
      "name"=>"Dublin Core",
      "resource"=>"element_sets"},
    "element"=>
     {"id"=>45,
      "url"=>"http://localhost/omeka-2.1-rc1/api/elements/45",
      "name"=>"Publisher",
      "resource"=>"elements"}},
   {"html"=>false,
    "text"=>"Item Date",
    "element_set"=>
     {"id"=>1,
      "url"=>"http://localhost/omeka-2.1-rc1/api/element_sets/1",
      "name"=>"Dublin Core",
      "resource"=>"element_sets"},
    "element"=>
     {"id"=>40,
      "url"=>"http://localhost/omeka-2.1-rc1/api/elements/40",
      "name"=>"Date",
      "resource"=>"elements"}},
   {"html"=>false,
    "text"=>"Item Rights",
    "element_set"=>
     {"id"=>1,
      "url"=>"http://localhost/omeka-2.1-rc1/api/element_sets/1",
      "name"=>"Dublin Core",
      "resource"=>"element_sets"},
    "element"=>
     {"id"=>47,
      "url"=>"http://localhost/omeka-2.1-rc1/api/elements/47",
      "name"=>"Rights",
      "resource"=>"elements"}},
   {"html"=>false,
    "text"=>"Item Relation",
    "element_set"=>
     {"id"=>1,
      "url"=>"http://localhost/omeka-2.1-rc1/api/element_sets/1",
      "name"=>"Dublin Core",
      "resource"=>"element_sets"},
    "element"=>
     {"id"=>46,
      "url"=>"http://localhost/omeka-2.1-rc1/api/elements/46",
      "name"=>"Relation",
      "resource"=>"elements"}},
   {"html"=>false,
    "text"=>"Item Format",
    "element_set"=>
     {"id"=>1,
      "url"=>"http://localhost/omeka-2.1-rc1/api/element_sets/1",
      "name"=>"Dublin Core",
      "resource"=>"element_sets"},
    "element"=>
     {"id"=>42,
      "url"=>"http://localhost/omeka-2.1-rc1/api/elements/42",
      "name"=>"Format",
      "resource"=>"elements"}},
   {"html"=>false,
    "text"=>"Item Language",
    "element_set"=>
     {"id"=>1,
      "url"=>"http://localhost/omeka-2.1-rc1/api/element_sets/1",
      "name"=>"Dublin Core",
      "resource"=>"element_sets"},
    "element"=>
     {"id"=>44,
      "url"=>"http://localhost/omeka-2.1-rc1/api/elements/44",
      "name"=>"Language",
      "resource"=>"elements"}},
   {"html"=>false,
    "text"=>"Item Type",
    "element_set"=>
     {"id"=>1,
      "url"=>"http://localhost/omeka-2.1-rc1/api/element_sets/1",
      "name"=>"Dublin Core",
      "resource"=>"element_sets"},
    "element"=>
     {"id"=>51,
      "url"=>"http://localhost/omeka-2.1-rc1/api/elements/51",
      "name"=>"Type",
      "resource"=>"elements"}},
   {"html"=>false,
    "text"=>"Item Identifier",
    "element_set"=>
     {"id"=>1,
      "url"=>"http://localhost/omeka-2.1-rc1/api/element_sets/1",
      "name"=>"Dublin Core",
      "resource"=>"element_sets"},
    "element"=>
     {"id"=>43,
      "url"=>"http://localhost/omeka-2.1-rc1/api/elements/43",
      "name"=>"Identifier",
      "resource"=>"elements"}},
   {"html"=>false,
    "text"=>"Item Coverage",
    "element_set"=>
     {"id"=>1,
      "url"=>"http://localhost/omeka-2.1-rc1/api/element_sets/1",
      "name"=>"Dublin Core",
      "resource"=>"element_sets"},
    "element"=>
     {"id"=>38,
      "url"=>"http://localhost/omeka-2.1-rc1/api/elements/38",
      "name"=>"Coverage",
      "resource"=>"elements"}},
   {"html"=>false,
    "text"=>"Item Type Duration",
    "element_set"=>
     {"id"=>3,
      "url"=>"http://localhost/omeka-2.1-rc1/api/element_sets/3",
      "name"=>"Item Type Metadata",
      "resource"=>"element_sets"},
    "element"=>
     {"id"=>11,
      "url"=>"http://localhost/omeka-2.1-rc1/api/elements/11",
      "name"=>"Duration",
      "resource"=>"elements"}},
   {"html"=>false,
    "text"=>"Item Type Standards",
    "element_set"=>
     {"id"=>3,
      "url"=>"http://localhost/omeka-2.1-rc1/api/element_sets/3",
      "name"=>"Item Type Metadata",
      "resource"=>"element_sets"},
    "element"=>
     {"id"=>24,
      "url"=>"http://localhost/omeka-2.1-rc1/api/elements/24",
      "name"=>"Standards",
      "resource"=>"elements"}},
   {"html"=>false,
    "text"=>"Item Type Objectives",
    "element_set"=>
     {"id"=>3,
      "url"=>"http://localhost/omeka-2.1-rc1/api/element_sets/3",
      "name"=>"Item Type Metadata",
      "resource"=>"element_sets"},
    "element"=>
     {"id"=>25,
      "url"=>"http://localhost/omeka-2.1-rc1/api/elements/25",
      "name"=>"Objectives",
      "resource"=>"elements"}},
   {"html"=>false,
    "text"=>"Item Type Materials",
    "element_set"=>
     {"id"=>3,
      "url"=>"http://localhost/omeka-2.1-rc1/api/element_sets/3",
      "name"=>"Item Type Metadata",
      "resource"=>"element_sets"},
    "element"=>
     {"id"=>26,
      "url"=>"http://localhost/omeka-2.1-rc1/api/elements/26",
      "name"=>"Materials",
      "resource"=>"elements"}},
   {"html"=>false,
    "text"=>"Item Type Lesson Plan Text",
    "element_set"=>
     {"id"=>3,
      "url"=>"http://localhost/omeka-2.1-rc1/api/element_sets/3",
      "name"=>"Item Type Metadata",
      "resource"=>"element_sets"},
    "element"=>
     {"id"=>27,
      "url"=>"http://localhost/omeka-2.1-rc1/api/elements/27",
      "name"=>"Lesson Plan Text",
      "resource"=>"elements"}}],
 "extended_resources"=>[]}

回答1:

This is what I ended up doing. It uses some of Ruby's metaprogramming methods to loop through the element texts, and create methods for each of them. The two things I didn't know that really helped out were these. First, looping through the array with each_with_index lets me have the index of the item in the array, which was necessary for creating the accessor methods. Second, I learned how to pass a value to a proc; see the setter methods.

  class OmekaItem

    attr_accessor :data

    # Parse the data we got from the API into handy methods. All of the data
    # from the JSON returned by the API is available as RecursiveOpenStructs
    # through @data. The Dublin Core and Item Type Metadata fields are also
    # available though special methods of the form dc_title and itm_field.
    #
    # @param  hash [Hash] Uses the hash from OmekaClient::Client::get_hash
    #
    def initialize(hash)
      @data = RecursiveOpenStruct.new(hash, :recurse_over_arrays => true)

      # Step through the element texts separating them into Dublin Core and
      # Item Type Metadata elements. e is the element text hash; i is the
      # index of the element_text in the array of element texts.
      @data.element_texts.each_with_index do |e, i|
        if e.element_set.name == "Dublin Core"
          # Define a reader method that retrieves the data from this element
          # text in @data
          self.class.send(:define_method,
            # The name of the method will have the form "dc_title"
            e.element.name.downcase.gsub(/^/, 'dc_').gsub(/\s/, '_'),
            proc{ @data.element_texts[i].text }
            )
          # Define a setter method that sets the data for this element text in
          # @ data
          self.class.send(:define_method,
            # The name of the method will have the form "dc_title="
            e.element.name.downcase.gsub(/^/, 'dc_').gsub(/\s/, '_').gsub(/$/, '='),
            proc{ |value| @data.element_texts[i].text = value }
            )
        elsif e.element_set.name == "Item Type Metadata"
          # Define a reader method that retrieves the data from this element
          # text in @data
          self.class.send(:define_method,
            # The name of the method will have the form "itm_field"
            e.element.name.downcase.gsub(/^/, 'itm_').gsub(/\s/, '_'),
            proc{ @data.element_texts[i].text }
            )
          # Define a setter method that sets the data for this element text in
          # @ data
          self.class.send(:define_method,
            # The name of the method will have the form "itm_title="
            e.element.name.downcase.gsub(/^/, 'itm_').gsub(/\s/, '_').gsub(/$/, '='),
            proc{ |value| @data.element_texts[i].text = value }
            )
        end
      end

    end

  end