Skip to content

Instantly share code, notes, and snippets.

@krainboltgreene
Last active March 27, 2018 05:51
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save krainboltgreene/4295befff55dc7f3907db5093159dee7 to your computer and use it in GitHub Desktop.
Save krainboltgreene/4295befff55dc7f3907db5093159dee7 to your computer and use it in GitHub Desktop.

smart_params

  • Build
  • Downloads
  • Version

Work smart, not strong. This gem gives developers an easy to understand and easy to maintain schema for request parameters. Meant as a drop-in replacement for strong_params.

Using

So lets say you have a complex set of incoming data, say a JSON:API-specification compliant payload that contains the data to create an account on your server. Of course, your e-commerce platform is pretty flexible; You don't stop users from creating an account just because they don't have an email or password. So let's see how this would play out:

class CreateAccountSchema
  include SmartParams

  need :data, type: ["Hash", "Array"], strict: true do
    allow :id, type: "String", coerce: true
    need :type, type: "String", strict: true
    allow :attributes do
      allow :email, type: "String", strict: true
      allow :username, type: "String", strict: true
      allow :name, type: "String", strict: true
      allow :password, type: "String", strict: true, default: -> { SecureRandom.hex(32) }
    end
  end
  allow :meta, type: "Hash"
  allow :included, type: "Array", of: {type: "Hash", strict: true}
end

And now using that schema in the controller:

class AccountsController < ApplicationController
  def create
    schema = CreateAccountSchema.new(params)
    # parameters will be a StrongParams::Dataset, which will respond to the various fields you defined

    # Here we're pulling out the id and data properties defined above
    record = Account.create({id: schema.data.id, **schema.data.attributes})

    redirect_to account_url(record)
  end
end

Okay, so lets look at some scenarios.

First, lets try an empty payload:

CreateAccountSchema.new({})

# raise SmartParams::Error::MissingProperty, name: ["data"], was: nil

Great, we've told SmartParams we need data and it enforced this! The exception class knows the "key chain" path to the property that was missing and the value that was given. Lets experiment with that:

CreateAccountSchema.new({data: ""})

# raise SmartParams::Error::InvalidPropertyType, name: ["data"], was: "", type: String, wanted: ["Hash", "Array"]

Sweet, we can definitely catch this and give the client a meaningful error!

Okay, so to show off a good payload I'm going to do two things: Examine the properties and turn it to a Hash.

Lets see a minimum viable account according to our schema:

schema = CreateAccountSchema.new({
  data: {
    type: "accounts"
  }
})

schema.data.type
# "accounts"

schema.to_json
# {
#   "data" => {
#     "type" => "accounts",
#     "attributes" => {
#       "password" => "1a6c3ffa4e96ad1660cb819f52a3393d924ac20073e84a9a6943a721d49bab38"
#     }
#   }
# }

Wait, what happened here? Well we told SmartParams that we're going to want a default password, so it delivered!

Why not strong_params?

Okay, so sure strong_params exists and it's definitely better than attr_accessible (if you remember that mess), but it often leaves you with code like this:

https://github.com/diaspora/diaspora/blob/develop/app/controllers/users_controller.rb#L140-L158 or https://github.com/discourse/discourse/blob/master/app/controllers/posts_controller.rb#L592-L677 or https://github.com/gitlabhq/gitlabhq/blob/42725ea96c7c2804d8a08130de529aceb87129d1/app/controllers/projects/clusters_controller.rb#L87-L109

None of this is very maintainable and it's definitely not easy to teach. So my solution is to follow the wake of other libraries: Define a maintainable interface that can be easily tested and easily integrated. It doesn't require wholesale adoption nor is it hard to remove.

Installing

Add this line to your application's Gemfile:

gem "smart_params", "1.0.0"

And then execute:

$ bundle

Or install it yourself with:

$ gem install smart_params

Contributing

  1. Read the Code of Conduct
  2. Fork it
  3. Create your feature branch (git checkout -b my-new-feature)
  4. Commit your changes (git commit -am 'Add some feature')
  5. Push to the branch (git push origin my-new-feature)
  6. Create new Pull Request
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment