¿Cómo implementa la validación en rutas NO CRUD? Por ejemplo, hay una ruta para buscar recipes . Acepta parámetros de term (para búsqueda), sort , page , etc.
¿Cuál es la mejor manera de hacer esto en Rails?
La solución más sencilla sería validar todas estas cosas usando el montón de expresiones if-else en el controlador, pero estoy buscando algo más limpio y más fácil para la extensión y el mantenimiento.
terminé con lo siguiente
class RequestValidation attr_reader :validators attr_reader :errors def initialize @validators = [] @errors = [] end def add_validator(validator) @validators << validator end def validate @validators.each do |validator| validator.validate @errors += validator.errors end end def valid? validate errors.empty? end endEjemplo de clase de validador
class SortValidator < Validator VALID_SORT_FIELD = [:created_at, :rate, :total_time] VALID_SORT_DIRECTIONS = [:asc, :desc] def validate return unless param valid = true begin sort = param.to_unsafe_h if sort.size == 1 k = sort.keys[0].to_sym v = sort.values[0].to_sym valid = VALID_SORT_FIELD.include?(k) && VALID_SORT_DIRECTIONS.include?(v) else valid = false end rescue => ex valid = false end @errors << "Sort parameter invalid" unless valid end def is_valid? !errors.empty? end end Entonces, RequestValidation es como una clase de constructor/contenedor para todos los validadores. Y luego, en el controlador:
validation = RequestValidation.new validation.add_validator(RecipesValidators::SortValidator.new(params[:sort])) validation.validate if validation.errors.empty?De esta manera, cuando se requiere una nueva validación, solo necesitamos crear una nueva clase de Validador y agregarla para solicitar la validación.
Dado que me tomé una pausa de Rails durante varios años, existen dudas sobre si este código es limpio y ordenado y de acuerdo con los principios de Ruby.
¿O hay un mejor camino?
Gracias por adelantado
Hoy en día iría poniendo algún contrato y una estructura fuertemente tipada entre el controlador y la base de datos.
Algo como:
# obtain an instance of RecipeSearchParamsContract def contract @contract ||= SomeValidationContract.new end # obtain an instance of a coercing struct def input MyStruct.new(params) end def query MyQuery.new end def your_action if contract.call(params).success? render query.call(input) end end Entonces estás separando completamente todas las preocupaciones. Buenas librerías para basar tu código son dry-validator y dry-struct , con las cuales puedes generar un contrato de la siguiente manera:
class SearchParamsContract < Dry::Validation::Contract params do optional(:sort).hash(max_size?: 1) do optional(:rate).value(included_in?: %w[asc desc]) optional(:created_at).value(included_in?: %w[asc desc]) optional(:total_time).value(included_in?: %w[asc desc]) end optional(:page).value(:integer) optional(:term).filled(:string) end endUsando esto, la estructura forzada y validada está disponible en el resultado mismo, por lo que puede encadenar todo y usar la coincidencia de patrones estructurales para obtener un código increíble y muy rubí (sin rieles extra)
def search case contract.call(params) in Success(input) @recipes = query.call(input) else render :error end endLa solución aquí nunca fue realmente un validador personalizado; en su lugar, debe crear una clase como un modelo o un objeto de formulario para encapsular las validaciones y los datos. El hecho de que su acción no persista no significa que no pueda usar un modelo para representar datos y lógica empresarial.
class RecipeSearch include ActiveModel::Model include ActiveModel::Attributes attribute :query, :string attribute :sort_by, :string # ... validates :query, length: { minimum: 5 } validates :sort_by, in: ['newest', 'quickest', 'cheapest'] # ... # @todo apply filters to Recipe and return collection def perform # ... end endEsto le permite agregar validaciones y lógica. Y pasarlo a formularios:
<%= form_with(model: local_assigns(:recipe_seach) || RecipeSearch.new) do |f| %> # ... <% end %>También es un error pensar que la búsqueda no puede ser parte de CRUD. Después de todo, es solo lectura con filtros aplicados. Si realmente necesita una vista separada de su índice normal, entonces cree una acción de búsqueda separada, pero no es realmente una necesidad.
class RecipesController def index @recipe_search = RecipeSearch.new(search_params) @recipes = @recipe_search.perform end private def search_params params.fetch(:recipe_search, {}) .permit(:query, :sort_by) end end