Module: AgentCode::HasValidation

Extended by:
ActiveSupport::Concern
Included in:
AgentCodeModel
Defined in:
lib/agentcode/concerns/has_validation.rb

Overview

Format validation concern for models.

This concern runs ActiveModel validations on request data before it reaches the database. Field permissions (which fields each role can write) are controlled by the policy, not the model.

Also provides cross-tenant FK validation: any belongs_to FK in the submitted data is checked to ensure the referenced record belongs to the current organization (directly or via FK chain).

Usage: class Post < ApplicationRecord include AgentCode::HasValidation

# Standard Rails validations for type/format (use allow_nil: true)
validates :title, length: { maximum: 255 }, allow_nil: true
validates :status, inclusion: { in: %w[draft published] }, allow_nil: true
end

Field permissions are defined on the policy: class PostPolicy < AgentCode::ResourcePolicy def permitted_attributes_for_create(user) has_role?(user, 'admin') ? ['*'] : ['title', 'content'] end end

Instance Method Summary collapse

Instance Method Details

#validate_for_action(params, permitted_fields:, organization: nil) ⇒ Hash

Validate data for a given action. Filters to only permitted fields, then runs ActiveModel validations and cross-tenant FK validation.

Parameters:

  • params (Hash)

    The request data

  • permitted_fields (Array<String>)

    Fields the user is allowed to set (['*'] for all)

  • organization (Object, nil) (defaults to: nil)

    Current organization for FK scoping (optional)

Returns:

  • (Hash)

    { valid: Boolean, errors: Hash, validated: Hash }



40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
# File 'lib/agentcode/concerns/has_validation.rb', line 40

def validate_for_action(params, permitted_fields:, organization: nil)
  # Filter to only permitted fields
  if permitted_fields == ['*']
    filtered = params.each_with_object({}) { |(k, v), h| h[k.to_s] = v }
  else
    permitted = permitted_fields.map(&:to_s)
    filtered = params.each_with_object({}) do |(k, v), h|
      h[k.to_s] = v if permitted.include?(k.to_s)
    end
  end

  # Remove organization_id from validated data — managed by framework
  filtered.delete("organization_id") if organization

  # Run ActiveModel validations on a temp instance
  temp = self.class.new
  safe_attrs = filtered.select { |k, _| temp.respond_to?("#{k}=") }
  temp.assign_attributes(safe_attrs)

  errors = {}
  unless temp.valid?
    temp.errors.each do |error|
      field_name = error.attribute.to_s
      if filtered.key?(field_name)
        errors[field_name] ||= []
        errors[field_name] << error.message
      end
    end
  end

  # Cross-tenant FK validation
  if organization
    fk_errors = validate_foreign_keys_for_organization(filtered, organization)
    errors.merge!(fk_errors)
  end

  if errors.any?
    { valid: false, errors: errors, validated: {} }
  else
    { valid: true, errors: {}, validated: filtered }
  end
end