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`.