SmartKv

Build Status Coverage Status Gem Version

Best practice of writing options or configurations by strictly allowing and requiring keys.

It doesn't have to be options or configurations. You can use it for strict request body or other use cases too.

Background

Have you ever used ruby options like this:

# this example is for rails
d = DateTime.now
e = d.change(hour: 1, minute: 5)

and then move on with your life.. until you realize that the code doesn't behave as you expected it to behave.
But why? Everything looks fine, right? Yes, it does look fine.. but it's not fine.

So, what's the problem?
The problem was the option key :minute was not recognized.
Eh? :confused:
Why didn't it tell me if it was not recognized?

I wish that too.
But Hash has a default value of nil if key is not found (same thing applies to OpenStruct). So, it will fail gracefully even if an option is actually required, unless the developer uses #fetch instead of #[]. Even if the developer uses #fetch for required keys, it doesn't actually check if there are foreign keys input. Also, most developers won't bother checking each options' key made by the users of the library or method.

If only the source of the DateTime#change method starts like this:

# this class can be defined on its own file
class ChangeOptions < SmartKv
  optional :nsec, :usec, :year, :month, :day, :hour, :min, :sec, :offset, :start
end

class DateTime
...
  def change(options)
    options = ChangeOptions.new(options)
    ...
  end
end

So, when you do this d.change(hour: 1, minute: 5), it will yell:

NotImplementedError: unrecognized key(s): `:minute' in ChangeOptions

Well, this is better. But, how do you know all the right options?
Type: ChangeOptions.optional_keys and ChangeOptions.required_keys.

More Usage Example

class Config < SmartKv
  required :some_key, :second_key
  optional :an_option
end

Config.new({some_key: "val"})

This will complain that you're not using the :second key. If you add another key that is not recognized, it will complain too. If there is a key that you don't always use but want it to be recognized, mark it as optional.

Inheritable

class ChildConfig < Config
  required :first_key
end

ChildConfig.new({first_key: "val", second_key: "val 2"})

This will also complain that you're not using the :some_key.

Directly callable

Whatever given as input is callable directly.

c = Config.new({some_key: "val", second_key: "val 2"})
c[:some_key]

c2 = Config.new(OpenStruct.new({some_key: "val", second_key: "val 2"}))
c2.second_key

Override callable object

You can change the callable object to any class that accepts hash as input of its new class method.

class Convertable < SmartKv
  required :abcd
  callable_as OpenStruct
end

c = Convertable.new({abcd: 123})
c.abcd #=> 123

Not using it for options or configs?

You can choose not to use it for options or configs. Maybe for strict request body keys?

class PostBody < SmartKv
  required :app_key, :secret_key
end
.
.
request.set_form_data(PostBody.new({app_key: "abc", secret_key: "def"}))

Installation

Add this line to your application's Gemfile:

gem 'smart_kv'

And then execute:

$ bundle

Coming Soon

  • [X] Convertable from hash (as input) to OpenStruct (the resulting object) or another object and vice versa
  • [X] Suggests corrections for unrecognized keys using DidYouMean
  • [ ] Support nested/deep key value object as input
  • [ ] Make some nested keys from the same parent key required and some others optional
  • [ ] Accept config file (e.g. json, yaml, etc.) or file path as input

Contributing

Bug reports and pull requests are welcome on GitHub at https://github.com/styd/smart_kv. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the Contributor Covenant code of conduct.

License

The gem is available as open source under the terms of the MIT License.

Code of Conduct

Everyone interacting in the SmartKv project’s codebases, issue trackers, chat rooms and mailing lists is expected to follow the code of conduct.