Blog Posted March 30, 2014

A Simple Ruby Class Example

Some colleagues asked about basic Ruby examples. The following RemoteConfig class makes an HTTP request and provides an object-oriented interface to XML served at the URL requested. The class serves as simple intro to some common needs and Ruby-oriented language features:

  • dynamic method definition
  • performing GET requests over HTTPS
  • creating a basic, object-oriented interface
  • parsing attribute-heavy XML with XPATH queries
  • testing with Rspec and Webmock

The XML

Assume the following XML is hosted at https://somedomain.com/config.xml:

1
2
3
4
5
6
7
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
  <add key="first_key" value="first key value" />
  <add key="second_key" value="second key value" />
  <add key="third_key" value="third key value" />
  <add key="fourth_key" value="fourth key value" />
</configuration>

The Ruby

A Ruby class providing an interface to the above-cited XML could like like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
require 'net/http'
require 'uri'
require 'nokogiri'

class RemoteConfig
  attr_reader :xml

  # On instantiation, perform an HTTP request to the XML
  # config file, parse it with nokogiri, and define methods
  # through which its values can be accessed:
  def initialize
    @xml = get_and_parse_config

    create_methods
  end

  # A method to store the XML endpoint
  def url
    "https://somedomain.com/config.xml"
  end

  # private methods; not publicly exposed for use
  # in an RemoteConfig instance
  private

  # Perform a GET request to retrieve the remote XML
  # and parse the response with Nokogiri
  def get_and_parse_config
    Nokogiri::XML(get_remote_config)
  end

  # Set up Net::HTTP to perform a GET request against
  # the remote XML URL using HTTPS.
  # Retrieve the response body.
  def get_remote_config
    http = Net::HTTP.new(uri.host, uri.port)
    http.use_ssl = true

    http.request(request).body
  end

  # Perform the HTTP request to the XML file:
  def request
    Net::HTTP::Get.new(uri.request_uri)
  end

  # parse the URL string as a URI
  def uri
    URI.parse(url)
  end

  # Use xpath to fetch the relevant attribute value:
  def fetch_value(value)
    @xml.xpath("//add[@key='#{value}']/@value").text
  end

  # An array of the methods we want a RemoteConfig
  # instance to have:
  def available_methods
    [
      :first_key,
      :second_key,
      :third_key,
      :fourth_key
    ]
  end

  # Rather than repeat our method logic, define the public
  # instance methods on instantantiation of the class:
  def create_methods
    available_methods.each do |method|
      self.class.send(:define_method, method) { fetch_value method.to_s }
    end
  end
end

Usage

1
2
3
4
5
6
7
8
9
10
11
# instantiate an instance of RemoteConfig
config = RemoteConfig.new

config.first_key
# => 'first key value'

config.second_key
# => 'second key value'

config.url
# => 'https://somedomain.com/config.xml'

Testing

I'm assuming you're using Rspec and webmock, and that you have a spec_helper.rb file.

Your spec_helper.rb contains the following:

1
2
3
4
5
6
require 'webmock/rspec'
require 'remote_config'

# Disable real HTTP network requests when
# running our tests:
WebMock.disable_net_connect!

And the spec looks like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
require 'spec_helper'

describe RemoteConfig do

  subject(:remote_config) { described_class.new }

  before :each do

    # Use webmock to stub HTTP requests to return the value we expect:
    stub_request(:get, 'https://somedomain.com/config.xml').to_return(
      :status => 200,
      :body => '<?xml version="1.0" encoding="UTF-8"?>
        <configuration>
          <add key="first_key" value="first key value" />
          <add key="second_key" value="second key value" />
          <add key="third_key" value="third key value" />
          <add key="fourth_key" value="fourth key value" />
        </configuration>'
    )
  end

  # Test its public methods:

  describe "#initialize" do
    it "creates a Nokogiri XML document" do
      expect(remote_config.xml.class).to eq(Nokogiri::XML::Document)
    end
  end

  describe "#first_key" do
    subject { remote_config.first_key }

    it { should eq 'first key value' }
  end

  describe "#second_key" do
    subject { remote_config.second_key }

    it { should eq 'second key value' }
  end

  describe "#third_key" do
    subject { remote_config.third_key }

    it { should eq 'third key value' }
  end

  describe "#url" do
    subject { remote_config.url }

    it { should eq 'https://somdomain.com/config.xml' }
  end
end