Skip to content
 
 

Repository files navigation

Scheemer

Scheemer uses Dry::Schema to enable us to write consistent looking structural parameter validation and data accessing on our services.

Usage

Endpoint parameter objects

Scheemer::DSL combines Dry Schema validation with convenient access to the validated parameters. Define the shape that the endpoint actually receives, then choose whether the validated payload is flat or wrapped.

Flat parameters

Use :flat for endpoints whose request parameters are not nested under a resource name:

class IndexParams
  extend Scheemer::DSL

  params_mode :flat

  schema do
    optional(:status).filled(:string)
    optional(:page).filled(:integer)
  end
end

params = IndexParams.new({ status: "open", page: 2 })
params[:status] # => "open"
params.page      # => 2
params.to_h      # => { "status" => "open", "page" => 2 }

When the endpoint only needs the normalized hash, use .call instead of keeping the parameter object:

attributes = IndexParams.call({ status: "open", page: 2 })
# => { "status" => "open", "page" => 2 }

Wrapped parameters

Use :wrapped when the request follows the usual resource convention. The root must be named explicitly, so adding another top-level field cannot change which data is exposed by the object:

class CreateUserParams
  extend Scheemer::DSL

  params_mode :wrapped, root: :user

  schema do
    required(:user).hash do
      required(:emailAddress).filled(:string)
      optional(:displayName).filled(:string)
    end
  end
end

params = CreateUserParams.new(
  { user: { emailAddress: "ada@example.com", displayName: "Ada" } }
)

params.email_address # => "ada@example.com"
params[:displayName] # => "Ada"
params.to_h          # => { "email_address" => "ada@example.com",
                     #      "display_name" => "Ada" }

Classes without params_mode retain the legacy behavior of exposing the first validated top-level value. New classes should prefer an explicit mode.

Access and normalization

Top-level keys can be accessed with methods, strings, or symbols. Snake case and camel case spellings are interchangeable:

params.email_address       # => "ada@example.com"
params[:email_address]     # => "ada@example.com"
params["emailAddress"]     # => "ada@example.com"
params.fetch(:email_address) # => "ada@example.com"
params.key?("emailAddress")  # => true
params.values_at(:email_address, :display_name)
# => ["ada@example.com", "Ada"]

[] returns nil for a missing key. fetch raises KeyError unless a default or block is provided:

params[:timezone]                  # => nil
params.fetch(:timezone, "UTC")     # => "UTC"
params.fetch(:timezone) { "UTC" }  # => "UTC"

Params includes Enumerable, so collection methods operate on the underlying hash or array. It also supports dig, empty?, size, length, to_h, and to_hash:

params.size     # => 2
params.empty?   # => false
params.to_hash  # => same normalized, indifferent-access hash as to_h
params.map(&:to_a)

to_h returns an ActiveSupport::HashWithIndifferentAccess, so string and symbol keys can be used interchangeably. Nested hashes also have indifferent access, although their key spelling is not converted to snake case:

result = params.to_h
result[:email_address] == result["email_address"] # => true
result[:profile]["displayName"]                   # => "Ada"

Key normalization applies only to the top-level Params object. Nested hashes also have indifferent access, but their keys retain the spelling in the validated data:

params.dig(:profile, :displayName) # nested lookup uses the hash's actual key

Defaults and validation context

on_missing fills a value before the validated payload is exposed. This is useful for endpoint defaults:

class SearchParams
  extend Scheemer::DSL

  params_mode :flat

  schema do
    optional(:page).filled(:integer)
    optional(:query).filled(:string)
  end

  on_missing path: "page", fallback_to: 1
end

SearchParams.call({})
# => { "page" => 1 }

Extra constructor data is available to custom validation through validate!:

class UpdateUserParams
  extend Scheemer::DSL

  params_mode :flat

  schema do
    required(:email).filled(:string)
  end

  def validate!(data)
    raise "not allowed" unless data[:current_user].admin?
  end
end

UpdateUserParams.new(
  { email: "ada@example.com" },
  { current_user: current_user }
)

Invalid payloads raise Scheemer::InvalidSchemaError from new and .call.

Standalone modules

Use Scheemer::Params::DSL when key translation is needed without schema validation:

class RawParams
  extend Scheemer::Params::DSL
end

RawParams.new({ someValue: "testing" }).some_value # => "testing"

Use Scheemer::Schema::DSL when only validation is needed:

class UserSchema
  extend Scheemer::Schema::DSL

  schema do
    required(:name).filled(:string)
  end
end

UserSchema.validate!({ name: "Ada" })

Development

See Development.md for setup, test, lint, and Docker commands.

Installation

Install the gem and add to the application's Gemfile by executing:

$ bundle add aion-dk/scheemer

If bundler is not being used to manage dependencies, install the gem by executing:

$ gem install aion-dk/scheemer

License

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

About

Request Parameter enforcement

Resources

Code of conduct

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages