Of course you can use dry-schema for that purpose, but i do think the framework should enforce that for the whole ecosystem to follow.
Of course you can use dry-schema for that purpose, but i do think the framework should enforce that for the whole ecosystem to follow.
We're missing the 'form' part. Validations should be done against forms, not models. Default forms can easily be derived from models, which is nice for simple apps, but this is really something that's lacking in rails.
Permitted params exists because there is no form (dry-schema for example). There's ActiveModel, but it's not the same, and it's not ad-hoc / bound to the view/controller layer of things.
The enforcement of allowed params (and type casting from strings) definitely seemed like it was in the right place per-action in the control layer though, so we kept that from the forms even though we implemented it in the controllers.
I wonder if we just didn't understand the idea though? Or maybe if the original implementers didn't implement it right?
Schema layer purpose is to enforce invariant at the boundary of HTTP.
class ApplicationForm
extend Portrayal
include ActiveModel::Model
class << self
def from_params(params)
new(**filter_params(params))
end
def filter_params(params)
params
.require(model_name.param_key)
.permit(*portrayal.keywords)
.to_hash
.transform_keys(&:to_sym)
end
end
end
class MyForm < ApplicationForm
keyword :first_name
keyword :last_name
validates :first_name, :last_name, presence: true
end
Can use it in a controller action like form = MyForm.from_params(params)
if form.valid?…
# the usual
One of the good things about this approach is that you can also use this object in form_for/form_with in your views.It's a bit dated now, but, I did a short presentation for my local ruby meetup and the slides are here [1]
1. https://slides.com/patrickdavey/rails-5-2-attributes-api#/25
attribute :my_time_at, :datetime, default: -> { Time.now }
attribute :persisted, :boolean, default: false
What I like about the types is that it will automatically coerce the messy user data automatically (this is what AR is doing under the hood anyway right?), you can just be more intentional about it.Anyway, glad you're happy with portrayal :) I definitely agree with you on the "freeze, read-onlyness" etc. being good things to have!
It wasn't lack of defaults, it was handling of defaults. Portrayal does a couple of smart things like evaluating defaults in a specific order in the correct context (while still only evaluating once), such that this becomes possible:
keyword :name
keyword :greeting, default: proc { "Hello, #{name}" }
I like being able to do this. (This also works gracefully with subclassing.)The coercion argument is good, but I'm not a fan of doing it implicitly. (This was the real counter-argument I should've made). I prefer to have a single place where input is entirely processed, such as a `from_params(params)`-style constructor. In that constructor you could either coerce, or do anything else with the input prior to passing it through to `.new`.
typed_params {
param :data, type: :hash do
param :first_name, type: :string, optional: true
param :last_name, type: :string, optional: true
param :email, type: :string, format: { with: /@/ }
param :password, type: :string, length: { minimum: 8 }
with if: -> { current_bearer&.admin? } do
param :metadata, type: :hash, allow_blank: true, optional: true
param :role, type: :string, inclusion: { in: %w[admin user] }, optional: true,
transform: -> (_, role) { [:role_attributes, { role: }] }
param :permissions, type: :array, optional: true, if: -> { current_account.ent? } do
items type: :string
end
end
end
}
def create
# ... controller code here
end
Each param schema has to be defined by source, e.g. request body vs query string. And params aren't intermixed (including routing params).