Class: Rhino::Blueprint::BlueprintValidator

Inherits:
Object
  • Object
show all
Defined in:
lib/rhino/blueprint/blueprint_validator.rb

Overview

Validates parsed blueprint data structures. Port of rhino-server BlueprintValidator.php / rhino-adonis-server blueprint_validator.ts.

Constant Summary collapse

VALID_COLUMN_TYPES =
%w[
  string text integer bigInteger boolean date datetime
  timestamp decimal float json uuid foreignId
].freeze
VALID_ACTIONS =
%w[
  index show store update destroy trashed restore forceDelete
].freeze

Instance Method Summary collapse

Instance Method Details

#route_key_warnings(options, columns) ⇒ Object

Warn when the route_key column is not marked unique — non-unique route keys make member-endpoint lookups ambiguous.



205
206
207
208
209
210
211
212
213
214
215
216
# File 'lib/rhino/blueprint/blueprint_validator.rb', line 205

def route_key_warnings(options, columns)
  warnings = []
  route_key = options[:route_key]
  return warnings unless route_key

  column = columns.find { |c| c[:name] == route_key.to_s }
  if column && !column[:unique]
    warnings << "route_key column '#{route_key}' is not marked unique — member lookups may be ambiguous"
  end

  warnings
end

#validate_columns(columns) ⇒ Array<String>

Validate columns.

Parameters:

  • columns (Array<Hash>)

Returns:

  • (Array<String>)

    errors



81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
# File 'lib/rhino/blueprint/blueprint_validator.rb', line 81

def validate_columns(columns)
  errors = []
  seen = Set.new

  columns.each do |col|
    if col[:name].nil? || col[:name].strip.empty?
      errors << "Column name is required"
      next
    end

    if seen.include?(col[:name])
      errors << "Duplicate column name '#{col[:name]}'"
    end
    seen.add(col[:name])

    unless VALID_COLUMN_TYPES.include?(col[:type])
      errors << "Invalid column type '#{col[:type]}' for column '#{col[:name]}'"
    end

    if col[:type] == "foreignId" && col[:foreign_model].nil?
      errors << "Column '#{col[:name]}' is foreignId but missing 'foreign_model'"
    end
  end

  errors
end

#validate_model(blueprint, valid_roles = {}) ⇒ Hash

Validate a full model blueprint.

Parameters:

  • blueprint (Hash)
  • valid_roles (Hash) (defaults to: {})

    optional role definitions for cross-reference

Returns:

  • (Hash)

    { valid:, errors:, warnings: }



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
# File 'lib/rhino/blueprint/blueprint_validator.rb', line 47

def validate_model(blueprint, valid_roles = {})
  errors = []
  warnings = []

  # Model name
  if blueprint[:model].nil? || blueprint[:model].strip.empty?
    errors << "Model name is required"
  elsif !blueprint[:model].match?(/\A[A-Z][a-zA-Z0-9]*\z/)
    errors << "Invalid model name '#{blueprint[:model]}' — must be PascalCase (match /^[A-Z][a-zA-Z0-9]*$/)"
  end

  # Columns
  errors.concat(validate_columns(blueprint[:columns]))

  # Permissions
  column_names = blueprint[:columns].map { |c| c[:name] }
  perm_result = validate_permissions(blueprint[:permissions], valid_roles, column_names)
  errors.concat(perm_result[:errors])
  warnings.concat(perm_result[:warnings])

  # Options
  errors.concat(validate_options(blueprint[:options], column_names))
  warnings.concat(route_key_warnings(blueprint[:options], blueprint[:columns]))

  # Relationships
  errors.concat(validate_relationships(blueprint[:relationships]))

  { valid: errors.empty?, errors: errors, warnings: warnings }
end

#validate_options(options, column_names = nil) ⇒ Object

Validate options. When column_names is provided, also validates that a configured route_key references a declared column (or 'id').



162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
# File 'lib/rhino/blueprint/blueprint_validator.rb', line 162

def validate_options(options, column_names = nil)
  errors = []

  if options[:except_actions]
    options[:except_actions].each do |action|
      unless VALID_ACTIONS.include?(action)
        errors << "Invalid action '#{action}' in except_actions"
      end
    end
  end

  route_key = options[:route_key]
  if route_key && column_names && !(["id"] + column_names).include?(route_key.to_s)
    errors << "route_key '#{route_key}' does not match any declared column"
  end

  errors
end

#validate_permissions(permissions, valid_roles, column_names) ⇒ Hash

Validate permissions.

Returns:

  • (Hash)

    { errors:, warnings: }



111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
# File 'lib/rhino/blueprint/blueprint_validator.rb', line 111

def validate_permissions(permissions, valid_roles, column_names)
  errors = []
  warnings = []
  has_roles = !valid_roles.empty?

  permissions.each do |role, perm|
    # Check role exists
    if has_roles && !valid_roles.key?(role)
      errors << "Unknown role '#{role}' in permissions"
    end

    # Check actions
    perm[:actions].each do |action|
      unless VALID_ACTIONS.include?(action)
        errors << "Invalid action '#{action}' for role '#{role}'"
      end
    end

    # Check field references
    all_column_names = ["id"] + column_names
    check_field_references(perm[:show_fields], all_column_names, role, "show_fields", warnings)
    check_field_references(perm[:create_fields], all_column_names, role, "create_fields", warnings)
    check_field_references(perm[:update_fields], all_column_names, role, "update_fields", warnings)

    # Warn on conflicts
    if perm[:hidden_fields].any? && perm[:show_fields].any?
      perm[:hidden_fields].each do |field|
        if perm[:show_fields].include?(field)
          warnings << "Role '#{role}': field '#{field}' is in both show_fields and hidden_fields"
        end
      end
    end

    # Warn on create_fields without store action
    if perm[:create_fields].any? && !perm[:create_fields].include?("*") &&
       perm[:create_fields].any? { |f| f != "*" } && !perm[:actions].include?("store")
      warnings << "Role '#{role}': has create_fields but no 'store' action"
    end

    # Warn on update_fields without update action
    if perm[:update_fields].any? && !perm[:update_fields].include?("*") &&
       perm[:update_fields].any? { |f| f != "*" } && !perm[:actions].include?("update")
      warnings << "Role '#{role}': has update_fields but no 'update' action"
    end
  end

  { errors: errors, warnings: warnings }
end

#validate_relationships(relationships) ⇒ Object

Validate relationships.



182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
# File 'lib/rhino/blueprint/blueprint_validator.rb', line 182

def validate_relationships(relationships)
  errors = []
  valid_types = %w[belongsTo hasMany hasOne belongsToMany]

  relationships.each do |rel|
    rel = rel.transform_keys(&:to_s) if rel.is_a?(Hash)

    if rel["type"].nil?
      errors << "Relationship is missing type"
    elsif !valid_types.include?(rel["type"])
      errors << "Invalid relationship type '#{rel["type"]}'"
    end

    if rel["model"].nil?
      errors << "Relationship is missing model"
    end
  end

  errors
end

#validate_roles(roles) ⇒ Hash

Validate role definitions.

Parameters:

  • roles (Hash<String, Hash>)

Returns:

  • (Hash)

    { valid:, errors: }



21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
# File 'lib/rhino/blueprint/blueprint_validator.rb', line 21

def validate_roles(roles)
  errors = []

  if roles.empty?
    errors << "At least one role is required"
    return { valid: false, errors: errors }
  end

  roles.each do |slug, role|
    unless slug.match?(/\A[a-z][a-z0-9_]*\z/)
      errors << "Invalid role slug '#{slug}' — must match /^[a-z][a-z0-9_]*$/"
    end

    if role[:name].nil? || role[:name].strip.empty?
      errors << "Role '#{slug}' must have a non-empty name"
    end
  end

  { valid: errors.empty?, errors: errors }
end