Excerpt

Last week, I wrote an article about 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:
```plain text
class Link
attr_reader :url, :source
def initialize(url:, source:)
@url = url
@source = source
end
end
```
I write this:
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):
## When calling an external API
When calling an external API that returns JS

Last week, I wrote an article about 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:
```plain text
class Link
attr_reader :url, :source
def initialize(url:, source:)
@url = url
@source = source
end
end
```
I write this:
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):
## 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
```plain text
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
```plain text
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 gem
```plain text
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:
```plain text
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:
```plain text
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
```
Here is an example I found in TheOdinProject:
```plain text
# 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:
And then in the view as a list of choices:
```plain text
<% 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 - to learn test design techniques for writing effective tests
👉 Join my Short Ruby Newsletter for weekly Ruby updates from the community