Rails OrderedOptions vs. Ruby's OpenStruct

I am trying to understand the difference between OrderedOptions class provided by ActiveSupport and Ruby’s OpenStruct class.

What’s the difference between the two? When would you use one over the other?

OrderedOptions

require "active_support/ordered_options"

config = ActiveSupport::OrderedOptions.new

# set the values
config.api_key = "my-api-key"
config.api_secret = "my-api-secret"

# access the values
config.api_key  # => 'my-api-key'
config.api_secret # => 'my-api-secret'

OpenStruct

require 'ostruct'

config = OpenStruct.new

# set the values
config.api_key = "my-api-key"
config.api_secret = "my-api-secret"

# access the values
config.api_key  # => 'my-api-key'
config.api_secret # => 'my-api-secret'

Thanks!

My comment may not age well but OpenStruct occasionally being slower compared to alternatives. It may not be true anymore or not true for specific ruby versions but still these uncertainties would be the reason why I would prefer an alternative

https://docs.rubocop.org/rubocop-performance/cops_performance.html#performanceopenstruct

1 Like

OrderedOption can be accessed like a hash, and you can use symbols or strings:

config = ActiveSupport::OrderedOptions.new
config.api_key = "my-api-key"

config["api_key"] # => "my-api-key"
config[:api_key] # => "my-api-key"
2 Likes

The use-cases you are showing are similar, and while both support [] like a hash, OrderedOptions has these additional features:

  • is_a?(Hash) returns true, so it can be used with anything expecting a hash. Because of this it also supports the Hash constructor that returns a default when a key isn’t found

  • you can access required options with a bang and raise an exception:

    config = OrderedOptions.new
    config.api_key = "foo"
    
    puts config.api_secret! # raises since it was never set
    

I think the point of OrderedOptions is to make a somewhat safer hash to store options or configuration. To be opinionated for a moment, I don’t think it’s any better than OpenStruct for that purpose and if you want to model some configuration, you would be better served by making a class that has defined properties for all the config options and can validate itself. You could that with plain ole Ruby:

class MyConfig
  attr_reader :api_key, :api_secret
  def initialize(api_key:, api_secret:)
    @api_key    = api_key
    @api_secret = api_secret
    raise ArgumentError, "api_key is required" if @api_key.nil?
    raise ArgumentError, "api_secret is required" if @api_secret.nil?
  end
end

config = MyConfig.new(
  api_key: ENV["API_KEY"],
  api_secret: ENV["API_SECRET"]) # or whatever

That’s a bit more code for sure, but you are unlikely to modify this code frequently and the tradeoff of getting explicit errors when required stuff is missing will be more of a benefit over time than saving a few lines of code up front.

1 Like

Thanks for the detailed answer @davetron5000, much appreciated!