# Example of value objects using Ruby's Data class

Last week, I wrote an article about [how to create value objects in Ruby - the idiomatic way](https://allaboutcoding.ghinda.com/how-to-create-value-objects-in-ruby-the-idiomatic-way). This week, I will share some real examples of using the data object to show some real examples.

## Remove boilerplate constructor code

If you are defining classes and expose the initializer parameters as getters and you plan to make them immutable, then I think you just found the most common case for using the Data class:

Instead of this:

```ruby
class Link
  attr_reader :url, :source

  def initialize(url:, source:)
    @url = url
    @source = source
  end
end
```

I write this:

```ruby
class Link < Data.define(:url, :source)
end
```

You can of course also write the simple form, but I do recommend the the previous way with inheritance (will followup in another article about this):

```ruby
Link = Data.define(:url, :source)
```

## **When calling an external API**

When calling an external API that returns JSON, I like to implement a method that returns a `Response` object.

Here I define a Response object with two computed properties:

* parsed\_body
    
* success
    

```ruby
class Response < Data.define(:body, :status, :headers)
  HTTP_SUCCESS_STATUS_CODES = (200..299)

  def success?    = HTTP_SUCCESS_STATUS_CODES.include?(status)
  def parsed_body = JSON.parse(body, symbolize_names: true)
  def failed?     = !success?
end
```

Mind you that you cannot memoize using instance variables inside a Data class due to immutability. If you try something like this you will get `FrozenError`

```ruby
class Response < Data.define(:body, :status, :headers)
  def parsed_body 
    @parsed_body ||= JSON.parse(body, symbolize_names: true)
  end
end

r = Response.new(body: "{}", status: 200, headers: {})
r.parsed_body
# can't modify frozen Response: #<data Response body="{}", status=200, headers={}> (FrozenError)
```

A more full example might look like this using [httparty](https://github.com/jnunemaker/httparty) gem

```ruby
require 'httparty' 
require 'json'

class Response < Data.define(:body, :status, :headers)
  HTTP_SUCCESS_STATUS_CODES = (200..299)

  def success?    = HTTP_SUCCESS_STATUS_CODES.include?(status)
  def parsed_body = JSON.parse(body, symbolize_names: true)
  def failed?     = !success?
end

def get(url, query: {})
  response = HTTParty.get(url, query)
  Response.new(body: response.body, status: response.code, headers: response.headers)
end

response = get(
  'https://bsky.social/xrpc/' \
  'com.atproto.identity.resolveHandle?handle=lucianghinda.com')
puts response.parsed_body[:did] # did:plc:1362asasdah213212
puts response.success? # true
```

From this example you can for example expand it to add a RateLimit object:

```ruby
require 'httparty' 
require 'json'

class RateLimit < Data.define(:limit, :remaining, :reset)
end

class Response < Data.define(:body, :status, :headers, :rate_limit)
  HTTP_SUCCESS_STATUS_CODES = (200..299)

  def success?    = HTTP_SUCCESS_STATUS_CODES.include?(status)
  def parsed_body = JSON.parse(body, symbolize_names: true)
  def failed?     = !success?
end

def get(url, query: {})
  response = HTTParty.get(url, query)

  rate_limit = RateLimit.new(
    limit: response.headers['ratelimit-limit'].to_i,
    remaining: response.headers['ratelimit-remaining'].to_i,
    reset: response.headers['ratelimit-reset'].to_i
  )
  
  Response.new(
    body: response.body,
    status: response.code,
    headers: response.headers,
    rate_limit: rate_limit
  )
end
```

You can even add a constructor to RateLimit for example:

```ruby
class RateLimit < Data.define(:limit, :remaining, :reset)
  def self.from_headers(headers)
    limit = headers['ratelimit-limit'].to_i
    remaining = headers['ratelimit-remaining'].to_i
    reset = headers['ratelimit-reset'].to_i

    new(limit: limit, remaining: remaining, reset: reset)
  end
end
```

## **Global or public list of objects from your domain**

Here is an example I found in [TheOdinProject](https://github.com/TheOdinProject/theodinproject):

```ruby
# Source: https://github.com/TheOdinProject/theodinproject/app/models/flag.rb

class Flag < ApplicationRecord
  Reason = Data.define(:name, :description, :value)

  REASONS = [
    { name: :broken, description: 'Link does not work', value: 10 },
    { name: :insecure, description: 'Link is not secure or safe', value: 20 },
    { name: :spam, description: 'Spam or misleading', value: 30 },
    { name: :inappropriate, description: 'Inappropriate imagery or language', value: 40 },
    { name: :other, description: 'Other', value: 50 }
  ].map { |reason| Reason.new(**reason) }
end
```

Furthe on you will see they are using `REASONS` in a Rails enum:

```ruby
# Source: https://github.com/TheOdinProject/theodinproject/app/models/flag.rb

class Flag < ApplicationRecord
   enum reason: REASONS.each_with_object({}) { |reason, hash| hash[reason.name] = reason.value }
end
```

And then in the view as a list of choices:

```erb
<% Flag::REASONS.each do |reason| %>
  <div class="relative flex items-center">
    <div class="absolute flex h-6 items-center">
      <%= form.radio_button(
        :reason, 
        reason.name, 
        data: { test_id: "flag-reason-#{reason.name}"}, 
        class: 'h-4 w-4 border-gray-300 dark:border-gray-500 dark:bg-gray-700/50' # ...
      %>
    </div>

    <div class="pl-7 text-sm leading-6">
      <%= form.label(
        :reason, 
        reason.description, 
        value: reason.name, 
        class: 'block text-sm font-medium text-gray-700 dark:text-gray-200 dark:text-gray-200' 
      %>
    </div>
  </div>
<% end %>
```

---

If you like this article:

👐 Interested in learning how to improve your developer testing skills? Join my live online workshop about [**goodenoughtesting.com**](http://goodenoughtesting.com/) **\- to learn test design techniques for writing effective tests**

👉 Join my [**Short Ruby Newsletter**](https://newsletter.shortruby.com/) for weekly Ruby updates from the community

🤝 Let's connect on [**Bluesky**](https://bsky.app/profile/lucianghinda.com), [**Ruby.social**](http://ruby.social/)**,** [**Linkedin**](https://linkedin.com/in/lucianghinda)**,** [**Twitter**](https://x.com/lucianghinda) **where I post mostly about Ruby and Ruby on Rails.**

🎥 Follow me on [**my YouTube channel**](https://www.youtube.com/@shortruby) for short videos about Ruby/Rails
