5 ms·
RoR is nice, but it lacks one of the most important abstraction layer: The schema layer for HTTP. Currently the default is to mix model params with http params.
by revskill 4y ago
RoR is nice, but it lacks one of the most important abstraction layer: The schema layer for HTTP. Currently the default is to mix model params with http params. It's bad default.
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.
- hakunin 4y agoI've had great success building form objects with portrayal (https://github.com/scottscheapflights/portrayal https://github.com/scottscheapflights/portrayal) and ActiveModel. You can put validations into them, and auto-filter the params by making a `from_params(params)`-type constructor. In it you can call require/permit with `portrayal.keywords`. You're right that it'd be nice to have this out of the box, but this is just a few lines of glue code to setup. I'm in the process of making a new app template that does that.
- revskill 4y agoThis gem is just for construction, no validation ? Schema layer purpose is to enforce invariant at the boundary of HTTP.
- hakunin 4y agoThe gem is just for declaring struct keywords (slightly more featureful than vanilla structs). Validation is left up to ActiveModel::Model. I would do a setup like this: 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.
- hakunin 4y agoThat said, there are similar objects one can make without using the "form" moniker, for API submissions for example. Granted, I also wish a solution was built-in.
- deleted 4y ago[deleted]
- patrickdavey 4y agoHave you looked at ActiveModel::Attributes ? That coupled with ActiveModel::Model makes for simple form objects quite nicely. Also, you can write your _own_ ActiveModel::Type so you can require that a particular attribute is a particular (business) model. I've been finding it a very nice way to write form objects. 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 https://slides.com/patrickdavey/rails-5-2-attributes-api#/25
- hakunin 4y agoThanks for the suggestion. I actually explored switching to Attributes for a new Rails app, but 3 reasons made me come back to portrayal gem: 1) attributes don't support defaults. 2) I prefer not to set types on form object attributes, since they are supposed to deal with messy user input. Instead I lean on validations. And 3) I like read-onlyness, freeze, equality, duplication, and introspection features of portrayal gem.
- patrickdavey 4y agoAttributes do support defaults. You can absolutely do: 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!
- deleted 4y ago[deleted]
- jbverschoor 4y agoYeah, the whole permitted params is actually is pretty shitty workaround against something else. 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.
- revskill 4y agoIt's the main reason for bad legacy of many of Rails applications in the wild (noone wants to maintain the model code inside the view/controller). And it's HARD to understand why ? Omasake is bad omasake when you mix "unrelated concerns" together.
- canadianfella 4y ago[dead]
- deleted 4y ago[deleted]
- rubyfan 4y agoSounds like you want MVVM vs. the default MVC pattern Rails ships. I think there are a few options out there which are bolt on for Rails but nothing out of the box.
- bvirb 4y agoI inherited a project where all the validations were in the forms and it meant that you couldn't reliably use the model layer without the forms layer (you'd end up saving invalid data). We eventually moved the validations to the model layer and dropped the forms abstraction. It didn't feel like the right separation of concerns when we had to pretend data was moving through forms to reliably interact directly with the model layer. 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?
- Spivak 4y agoYeah, I'm pretty sure every Rails shop has invented some flavor of APIObjectSerializer based on JSON-API, REST, or whatever because doing it ad-hoc will make you pull your hair out.
- ezekg 4y agoI also wasn't a fan of how Rails handles params, so I ended up writing my own lib. And I'm actually working on open sourcing it and extracting into a gem. 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).
- revskill 4y agoWe want a generic interface: Think Rack for params parsing/serializing, then you could easily parse any params into your own data structure. Stringly types are out of scope.
- ezekg 4y agoWhy do you need something so generic? Can you share a use case?
- fknorangesite 4y agoI'm very ambivalent about Grape, but this very much reminds me of something I do really like about it: the param validation. https://github.com/ruby-grape/grape#parameters https://github.com/ruby-grape/grape#parameters
- ezekg 4y agoYep! I was very inspired by Grape’s param system.