kube_schema 1.12.0 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/lib/kube/errors.rb CHANGED
@@ -3,734 +3,749 @@
3
3
  require "yaml"
4
4
 
5
5
  module Kube
6
- # Base error for all Kube gems. Catch this to handle any Kube error.
7
- class Error < StandardError; end
8
-
9
- # Raised when a schema version is unknown.
10
- class UnknownVersionError < Error; end
11
-
12
- # Raised when a version string is formatted incorrectly (e.g. "v1.33.6").
13
- class IncorrectVersionFormat < Error; end
14
-
15
- # Raised by Resource#valid! when data fails schema validation.
16
- #
17
- # Produces detailed, human-readable error messages that identify the exact
18
- # key path and offending value, followed by the resource YAML with error
19
- # lines highlighted in red and missing keys injected inline.
20
- #
21
- # Schema validation failed for Deployment "web":
22
- # - spec.selector is required but missing
23
- # - spec.replicas = "not_a_number" — expected integer, got String
24
- #
25
- # ---
26
- # apiVersion: apps/v1
27
- # kind: Deployment
28
- # spec:
29
- # selector: # MISSING — required
30
- # replicas: "nah" # expected integer, got String
31
- #
32
- class ValidationError < Error
33
- attr_reader :errors
34
-
35
- RED = "\033[31m"
36
- YELLOW = "\033[33m"
37
- RESET = "\033[0m"
38
-
39
- BOLD = "\033[1m"
40
- UNDERLINE = "\033[4m"
41
-
42
- # @param errors [Array<Hash>] raw JSONSchemer error hashes
43
- # @param kind [String, nil] Kubernetes resource kind (e.g. "Deployment")
44
- # @param name [String, nil] resource metadata.name (e.g. "web")
45
- # @param manifest [Hash, nil] the full resource data that failed validation
46
- def initialize(errors, kind: nil, name: nil, manifest: nil)
47
- @errors = errors
48
- @kind = kind
49
- @name = name
50
- @manifest = manifest
51
- super(build_message)
52
- end
53
-
54
- # Ruby 3.2+ calls detailed_message for uncaught exceptions.
55
- # We override it so the message ends at column 0, preventing
56
- # Ruby from indenting the backtrace to match YAML indentation.
57
- def detailed_message(highlight: false, **)
58
- out = "\n#{BOLD}#{UNDERLINE}#{RED}#{header_line}#{RESET}\n\n"
59
- out += error_lines.join("\n")
60
- if @manifest
61
- out += "\n\n#{annotate_manifest(@manifest, @errors)}"
62
- end
63
- out += "\n\n"
64
- out
65
- end
66
-
67
- private
68
-
69
- def header_line
70
- header = "Schema validation failed"
71
- header += " for #{@kind}" if @kind
72
- header += " #{@name.inspect}" if @name
73
- header
74
- end
75
-
76
- def error_lines
77
- @errors.flat_map { |e| format_error(e) }
6
+ module Errors
7
+ # Base error for all Kube gems.
8
+ class Error < StandardError; end
9
+
10
+ # Raised when a schema version is unknown.
11
+ class UnknownVersionError < Error; end
12
+
13
+ # Raised when a version string is formatted incorrectly (e.g. "v1.33.6").
14
+ class IncorrectVersionFormat < Error; end
15
+
16
+ class MissingSchemaError < Error; end
17
+
18
+ # Raised by Resource#valid! when schema validation fails.
19
+ #
20
+ # @example
21
+ # Schema validation failed for Deployment "web":
22
+ # - spec.selector is required but missing
23
+ # - spec.replicas = "not_a_number" — expected integer, got String
24
+ # ---
25
+ # apiVersion: apps/v1
26
+ # kind: Deployment
27
+ # spec:
28
+ # selector: # MISSING — required
29
+ # replicas: "nah" # expected integer, got String
30
+ class ValidationError < Error
31
+ attr_reader :errors
32
+
33
+ RED = "\033[31m"
34
+ YELLOW = "\033[33m"
35
+ RESET = "\033[0m"
36
+
37
+ BOLD = "\033[1m"
38
+ UNDERLINE = "\033[4m"
39
+
40
+ # @param errors [Array<Hash>] raw JSONSchemer error hashes
41
+ # @param kind [String, nil] Kubernetes resource kind (e.g. "Deployment")
42
+ # @param name [String, nil] resource metadata.name (e.g. "web")
43
+ # @param manifest [Hash, nil] the full resource data that failed validation
44
+ def initialize(errors, kind: nil, name: nil, manifest: nil)
45
+ @errors = errors
46
+ @kind = kind
47
+ @name = name
48
+ @manifest = manifest
49
+ super(build_message)
78
50
  end
79
51
 
80
- # Plain-text message for programmatic access (e.g. rescue => e; e.message)
81
- def build_message
82
- msg = "#{header_line}\n#{error_lines.join("\n")}"
52
+ # Keeps the backtrace from indenting past column 0.
53
+ # @returns [String] the message, unwrapped for the backtrace.
54
+ def detailed_message(highlight: false, **)
55
+ out = "\n#{BOLD}#{UNDERLINE}#{RED}#{header_line}#{RESET}\n\n"
56
+ out += error_lines.join("\n")
83
57
  if @manifest
84
- msg += "\n\n#{annotate_manifest(@manifest, @errors)}"
58
+ out += "\n\n#{annotate_manifest(@manifest, @errors)}"
85
59
  end
86
- msg
60
+ out += "\n\n"
61
+ out
87
62
  end
88
63
 
89
- # ── Error line formatting ────────────────────────────────────────
90
-
91
- # Format a single JSONSchemer error hash into one or more human-readable lines.
92
- def format_error(error)
93
- path = pointer_to_dot(error["data_pointer"])
94
- type = error["type"]
95
- data = error["data"]
96
-
97
- case type
98
- when "required"
99
- format_required(error, path)
100
- when "string", "integer", "number", "boolean", "array", "object", "null"
101
- "❌ #{path} = #{truncate(data.inspect)} — expected #{type}, got #{data.class}"
102
- when "minimum"
103
- limit = error.dig("schema", "minimum")
104
- "❌ #{path} = #{truncate(data.inspect)} — must be >= #{limit}"
105
- when "maximum"
106
- limit = error.dig("schema", "maximum")
107
- "❌ #{path} = #{truncate(data.inspect)} — must be <= #{limit}"
108
- when "exclusiveMinimum"
109
- limit = error.dig("schema", "exclusiveMinimum")
110
- "❌ #{path} = #{truncate(data.inspect)} — must be > #{limit}"
111
- when "exclusiveMaximum"
112
- limit = error.dig("schema", "exclusiveMaximum")
113
- "❌ #{path} = #{truncate(data.inspect)} — must be < #{limit}"
114
- when "minLength"
115
- limit = error.dig("schema", "minLength")
116
- "❌ #{path} = #{truncate(data.inspect)} — length must be >= #{limit}"
117
- when "maxLength"
118
- limit = error.dig("schema", "maxLength")
119
- "❌ #{path} = #{truncate(data.inspect)} — length must be <= #{limit}"
120
- when "minItems"
121
- limit = error.dig("schema", "minItems")
122
- "❌ #{path} — array must have >= #{limit} items, got #{data.is_a?(Array) ? data.length : data.inspect}"
123
- when "maxItems"
124
- limit = error.dig("schema", "maxItems")
125
- "❌ #{path} — array must have <= #{limit} items, got #{data.is_a?(Array) ? data.length : data.inspect}"
126
- when "enum"
127
- allowed = error.dig("schema", "enum")
128
- "❌ #{path} = #{truncate(data.inspect)} — must be one of: #{truncate(allowed.inspect)}"
129
- when "const"
130
- expected = error.dig("schema", "const")
131
- "❌ #{path} = #{truncate(data.inspect)} — must be #{truncate(expected.inspect)}"
132
- when "pattern"
133
- pattern = error.dig("schema", "pattern")
134
- "❌ #{path} = #{truncate(data.inspect)} — does not match pattern: #{pattern}"
135
- when "format"
136
- fmt = error.dig("schema", "format")
137
- "❌ #{path} = #{truncate(data.inspect)} — invalid #{fmt} format"
138
- when "multipleOf"
139
- factor = error.dig("schema", "multipleOf")
140
- "❌ #{path} = #{truncate(data.inspect)} — must be a multiple of #{factor}"
141
- when "uniqueItems"
142
- "❌ #{path} — array items must be unique"
143
- when "additionalProperties"
144
- "❌ #{path}: #{error["error"] || "has additional properties that are not allowed"}"
145
- else
146
- msg = error["error"] || type
147
- "❌ #{path}: #{msg}"
148
- end
149
- end
64
+ private
150
65
 
151
- # Format a `required` error, expanding each missing key onto its own line.
152
- def format_required(error, path)
153
- missing = error.dig("details", "missing_keys") || []
154
- if missing.empty?
155
- return "❌ #{path}: #{error["error"] || "required"}"
66
+ def header_line
67
+ header = "Schema validation failed"
68
+ if @kind
69
+ header += " for #{@kind}"
70
+ end
71
+ if @name
72
+ header += " #{@name.inspect}"
73
+ end
74
+ header
156
75
  end
157
76
 
158
- missing.map do |key|
159
- "❌ #{join_path(path, key)} is required but missing"
77
+ def error_lines
78
+ @errors.flat_map { |e| format_error(e) }
160
79
  end
161
- end
162
80
 
163
- # Short inline description for annotating YAML lines.
164
- def format_error_short(error)
165
- type = error["type"]
166
- data = error["data"]
167
-
168
- case type
169
- when "string", "integer", "number", "boolean", "array", "object", "null"
170
- "expected #{type}, got #{data.class}"
171
- when "minimum"
172
- "must be >= #{error.dig("schema", "minimum")}"
173
- when "maximum"
174
- "must be <= #{error.dig("schema", "maximum")}"
175
- when "exclusiveMinimum"
176
- "must be > #{error.dig("schema", "exclusiveMinimum")}"
177
- when "exclusiveMaximum"
178
- "must be < #{error.dig("schema", "exclusiveMaximum")}"
179
- when "minLength"
180
- "length must be >= #{error.dig("schema", "minLength")}"
181
- when "maxLength"
182
- "length must be <= #{error.dig("schema", "maxLength")}"
183
- when "minItems"
184
- "array must have >= #{error.dig("schema", "minItems")} items"
185
- when "maxItems"
186
- "array must have <= #{error.dig("schema", "maxItems")} items"
187
- when "enum"
188
- "must be one of: #{truncate(error.dig("schema", "enum").inspect, max: 40)}"
189
- when "pattern"
190
- "does not match pattern: #{error.dig("schema", "pattern")}"
191
- when "format"
192
- "invalid #{error.dig("schema", "format")} format"
193
- when "multipleOf"
194
- "must be a multiple of #{error.dig("schema", "multipleOf")}"
195
- when "uniqueItems"
196
- "array items must be unique"
197
- else
198
- error["error"] || type
81
+ # Plain-text message for rescue => e; e.message.
82
+ # @returns [String] the header and formatted error lines.
83
+ def build_message
84
+ msg = "#{header_line}\n#{error_lines.join("\n")}"
85
+ if @manifest
86
+ msg += "\n\n#{annotate_manifest(@manifest, @errors)}"
87
+ end
88
+ msg
89
+ end
90
+
91
+ # ── Error line formatting ────────────────────────────────────────
92
+
93
+ # Format one JSONSchemer error into readable lines.
94
+ # @parameter error [Hash] a raw JSONSchemer error hash.
95
+ # @returns [String, Array(String)] one or more readable lines.
96
+ def format_error(error)
97
+ path = pointer_to_dot(error["data_pointer"])
98
+ type = error["type"]
99
+ data = error["data"]
100
+
101
+ case type
102
+ when "required"
103
+ format_required(error, path)
104
+ when "string", "integer", "number", "boolean", "array", "object", "null"
105
+ "❌ #{path} = #{truncate(data.inspect)} — expected #{type}, got #{data.class}"
106
+ when "minimum"
107
+ limit = error.dig("schema", "minimum")
108
+ "❌ #{path} = #{truncate(data.inspect)} — must be >= #{limit}"
109
+ when "maximum"
110
+ limit = error.dig("schema", "maximum")
111
+ "❌ #{path} = #{truncate(data.inspect)} — must be <= #{limit}"
112
+ when "exclusiveMinimum"
113
+ limit = error.dig("schema", "exclusiveMinimum")
114
+ "❌ #{path} = #{truncate(data.inspect)} — must be > #{limit}"
115
+ when "exclusiveMaximum"
116
+ limit = error.dig("schema", "exclusiveMaximum")
117
+ "❌ #{path} = #{truncate(data.inspect)} — must be < #{limit}"
118
+ when "minLength"
119
+ limit = error.dig("schema", "minLength")
120
+ "❌ #{path} = #{truncate(data.inspect)} — length must be >= #{limit}"
121
+ when "maxLength"
122
+ limit = error.dig("schema", "maxLength")
123
+ "❌ #{path} = #{truncate(data.inspect)} — length must be <= #{limit}"
124
+ when "minItems"
125
+ limit = error.dig("schema", "minItems")
126
+ "❌ #{path} — array must have >= #{limit} items, got #{data.is_a?(Array) ? data.length : data.inspect}"
127
+ when "maxItems"
128
+ limit = error.dig("schema", "maxItems")
129
+ "❌ #{path} — array must have <= #{limit} items, got #{data.is_a?(Array) ? data.length : data.inspect}"
130
+ when "enum"
131
+ allowed = error.dig("schema", "enum")
132
+ "❌ #{path} = #{truncate(data.inspect)} — must be one of: #{truncate(allowed.inspect)}"
133
+ when "const"
134
+ expected = error.dig("schema", "const")
135
+ "❌ #{path} = #{truncate(data.inspect)} — must be #{truncate(expected.inspect)}"
136
+ when "pattern"
137
+ pattern = error.dig("schema", "pattern")
138
+ "❌ #{path} = #{truncate(data.inspect)} — does not match pattern: #{pattern}"
139
+ when "format"
140
+ fmt = error.dig("schema", "format")
141
+ "❌ #{path} = #{truncate(data.inspect)} — invalid #{fmt} format"
142
+ when "multipleOf"
143
+ factor = error.dig("schema", "multipleOf")
144
+ "❌ #{path} = #{truncate(data.inspect)} — must be a multiple of #{factor}"
145
+ when "uniqueItems"
146
+ "❌ #{path} — array items must be unique"
147
+ when "additionalProperties"
148
+ "❌ #{path}: #{error["error"] || "has additional properties that are not allowed"}"
149
+ else
150
+ msg = error["error"] || type
151
+ "❌ #{path}: #{msg}"
152
+ end
199
153
  end
200
- end
201
154
 
202
- # ── Annotated YAML rendering ────────────────────────────────────
203
-
204
- # Render the manifest as YAML with error lines in red and
205
- # missing required keys injected as red comment lines.
206
- def annotate_manifest(manifest, errors)
207
- # Build two maps from the error list:
208
- # error_at["/spec/replicas"] => "expected integer, got String"
209
- # missing_at["/spec"] => ["selector", "template"]
210
- error_at = {}
211
- missing_at = {}
212
-
213
- errors.each do |e|
214
- ptr = e["data_pointer"]
215
- if e["type"] == "required"
216
- keys = e.dig("details", "missing_keys") || []
217
- (missing_at[ptr] ||= []).concat(keys)
155
+ # Format a required error, one line per missing key.
156
+ # @parameter error [Hash] the required-type JSONSchemer error.
157
+ # @parameter path [String] the dot-notation path to the error.
158
+ # @returns [String, Array(String)] one line per missing key.
159
+ def format_required(error, path)
160
+ missing = error.dig("details", "missing_keys") || []
161
+ if missing.empty?
162
+ "❌ #{path}: #{error["error"] || "required"}"
218
163
  else
219
- error_at[ptr] = format_error_short(e)
164
+ missing.map do |key|
165
+ "❌ #{join_path(path, key)} is required but missing"
166
+ end
220
167
  end
221
168
  end
222
169
 
223
- yaml_lines = manifest.to_yaml.lines
224
- path_stack = [] # [[segment, indent_level], ...]
225
- array_indices = {} # indent_level -> current 0-based index
226
- result = []
227
-
228
- yaml_lines.each do |line|
229
- raw = line.chomp
170
+ # Short inline description for annotating YAML lines.
171
+ # @parameter error [Hash] a raw JSONSchemer error hash.
172
+ # @returns [String] a short description of the violation.
173
+ def format_error_short(error)
174
+ type = error["type"]
175
+ data = error["data"]
176
+
177
+ case type
178
+ when "string", "integer", "number", "boolean", "array", "object", "null"
179
+ "expected #{type}, got #{data.class}"
180
+ when "minimum"
181
+ "must be >= #{error.dig("schema", "minimum")}"
182
+ when "maximum"
183
+ "must be <= #{error.dig("schema", "maximum")}"
184
+ when "exclusiveMinimum"
185
+ "must be > #{error.dig("schema", "exclusiveMinimum")}"
186
+ when "exclusiveMaximum"
187
+ "must be < #{error.dig("schema", "exclusiveMaximum")}"
188
+ when "minLength"
189
+ "length must be >= #{error.dig("schema", "minLength")}"
190
+ when "maxLength"
191
+ "length must be <= #{error.dig("schema", "maxLength")}"
192
+ when "minItems"
193
+ "array must have >= #{error.dig("schema", "minItems")} items"
194
+ when "maxItems"
195
+ "array must have <= #{error.dig("schema", "maxItems")} items"
196
+ when "enum"
197
+ "must be one of: #{truncate(error.dig("schema", "enum").inspect, max: 40)}"
198
+ when "pattern"
199
+ "does not match pattern: #{error.dig("schema", "pattern")}"
200
+ when "format"
201
+ "invalid #{error.dig("schema", "format")} format"
202
+ when "multipleOf"
203
+ "must be a multiple of #{error.dig("schema", "multipleOf")}"
204
+ when "uniqueItems"
205
+ "array items must be unique"
206
+ else
207
+ error["error"] || type
208
+ end
209
+ end
230
210
 
231
- if raw.match?(/\A---\s*$/)
232
- result << raw
233
- next
211
+ # ── Annotated YAML rendering ────────────────────────────────────
212
+
213
+ # Render the manifest as annotated, colorized YAML.
214
+ # @parameter manifest [Hash] the resource data that failed validation.
215
+ # @parameter errors [Array(Hash)] the raw JSONSchemer error hashes.
216
+ # @returns [String] the YAML with errors highlighted inline.
217
+ def annotate_manifest(manifest, errors)
218
+ # Maps each error to its formatted message.
219
+ # @example
220
+ # error_at["/spec/replicas"] => "expected integer, got String"
221
+ # missing_at["/spec"] => ["selector", "template"]
222
+ error_at = {}
223
+ missing_at = {}
224
+
225
+ errors.each do |e|
226
+ ptr = e["data_pointer"]
227
+ if e["type"] == "required"
228
+ keys = e.dig("details", "missing_keys") || []
229
+ (missing_at[ptr] ||= []).concat(keys)
230
+ else
231
+ error_at[ptr] = format_error_short(e)
232
+ end
234
233
  end
235
234
 
236
- indent = line[/\A\s*/].length
237
- is_array_item = raw.match?(/\A\s*-\s/)
235
+ yaml_lines = manifest.to_yaml.lines
236
+ path_stack = [] # Each entry is [segment, indent_level].
237
+ array_indices = {} # Current 0-based index per indent level.
238
+ result = []
238
239
 
239
- if is_array_item
240
- # Array items sit at the same indent as their parent key.
241
- # Pop anything strictly deeper, but keep the parent.
242
- path_stack.pop while path_stack.any? && path_stack.last[1] > indent
243
- array_indices.delete_if { |level, _| level > indent }
240
+ yaml_lines.each do |line|
241
+ raw = line.chomp
244
242
 
245
- idx = array_indices[indent] || 0
246
- array_indices[indent] = idx + 1
243
+ if raw.match?(/\A---\s*$/)
244
+ result << raw
245
+ next
246
+ end
247
+
248
+ indent = line[/\A\s*/].length
249
+ is_array_item = raw.match?(/\A\s*-\s/)
250
+
251
+ if is_array_item
252
+ # Array items sit at the same indent as their parent key.
253
+ # Pop anything strictly deeper, but keep the parent.
254
+ path_stack.pop while path_stack.any? && path_stack.last[1] > indent
255
+ array_indices.delete_if { |level, _| level > indent }
256
+
257
+ idx = array_indices[indent] || 0
258
+ array_indices[indent] = idx + 1
259
+
260
+ # Push the index one level deeper than the "- " prefix
261
+ path_stack.push([idx.to_s, indent + 1])
262
+
263
+ # Inline key on the array item: "- name: app"
264
+ if (m = raw.match(/\A\s*-\s+(\w[\w.-]*):/))
265
+ path_stack.push([m[1], indent + 2])
266
+ end
267
+ else
268
+ # Regular key — pop entries at >= this indent
269
+ path_stack.pop while path_stack.any? && path_stack.last[1] >= indent
270
+ array_indices.delete_if { |level, _| level >= indent }
271
+
272
+ if (m = raw.match(/\A\s*(\w[\w.-]*):/))
273
+ path_stack.push([m[1], indent])
274
+ end
275
+ end
247
276
 
248
- # Push the index one level deeper than the "- " prefix
249
- path_stack.push([idx.to_s, indent + 1])
277
+ pointer = "/" + path_stack.map(&:first).join("/")
250
278
 
251
- # Inline key on the array item: "- name: app"
252
- if (m = raw.match(/\A\s*-\s+(\w[\w.-]*):/))
253
- path_stack.push([m[1], indent + 2])
279
+ if error_at.key?(pointer)
280
+ result << "#{RED}#{raw}#{RESET} #{YELLOW}# #{error_at[pointer]}#{RESET}"
281
+ else
282
+ result << raw
254
283
  end
255
- else
256
- # Regular key — pop entries at >= this indent
257
- path_stack.pop while path_stack.any? && path_stack.last[1] >= indent
258
- array_indices.delete_if { |level, _| level >= indent }
259
284
 
260
- if (m = raw.match(/\A\s*(\w[\w.-]*):/))
261
- path_stack.push([m[1], indent])
285
+ # Inject missing required keys as red lines right after their parent
286
+ if missing_at.key?(pointer)
287
+ child_indent = indent + 2
288
+ missing_at[pointer].each do |mk|
289
+ result << "#{RED}#{" " * child_indent}#{mk}:#{RESET} #{YELLOW}# MISSING — required#{RESET}"
290
+ end
262
291
  end
263
292
  end
264
293
 
265
- pointer = "/" + path_stack.map(&:first).join("/")
294
+ result.join("\n")
295
+ end
296
+
297
+ # ── Helpers ──────────────────────────────────────────────────────
266
298
 
267
- if error_at.key?(pointer)
268
- result << "#{RED}#{raw}#{RESET} #{YELLOW}# #{error_at[pointer]}#{RESET}"
299
+ def pointer_to_dot(pointer)
300
+ if pointer.nil? || pointer.empty?
301
+ "root"
269
302
  else
270
- result << raw
303
+ pointer.delete_prefix("/").gsub("/", ".")
271
304
  end
305
+ end
272
306
 
273
- # Inject missing required keys as red lines right after their parent
274
- if missing_at.key?(pointer)
275
- child_indent = indent + 2
276
- missing_at[pointer].each do |mk|
277
- result << "#{RED}#{" " * child_indent}#{mk}:#{RESET} #{YELLOW}# MISSING — required#{RESET}"
278
- end
279
- end
307
+ def join_path(base, key)
308
+ base == "root" ? key.to_s : "#{base}.#{key}"
280
309
  end
281
310
 
282
- result.join("\n")
283
- end
311
+ def truncate(str, max: 60)
312
+ str.length > max ? "#{str[0...max]}..." : str
313
+ end
314
+ end
284
315
 
285
- # ── Helpers ──────────────────────────────────────────────────────
316
+ # Raised when a schema download fails.
317
+ class DownloadError < Error
318
+ attr_reader :url, :status_code
286
319
 
287
- def pointer_to_dot(pointer)
288
- return "root" if pointer.nil? || pointer.empty?
289
- pointer.delete_prefix("/").gsub("/", ".")
320
+ def initialize(message = nil, url: nil, status_code: nil)
321
+ @url = url
322
+ @status_code = status_code
323
+ super(message || "Failed to download schema: #{@url} (HTTP #{@status_code})")
290
324
  end
325
+ end
326
+ end
291
327
 
292
- def join_path(base, key)
293
- base == "root" ? key.to_s : "#{base}.#{key}"
294
- end
328
+ # Flat aliases for backward compatibility with pre-nesting code.
329
+ # Local/ConstantMatchesPath requires nesting under Kube::Errors now.
330
+ Error = Errors::Error
331
+ UnknownVersionError = Errors::UnknownVersionError
332
+ IncorrectVersionFormat = Errors::IncorrectVersionFormat
333
+ MissingSchemaError = Errors::MissingSchemaError
334
+ ValidationError = Errors::ValidationError
335
+ DownloadError = Errors::DownloadError
336
+ end
295
337
 
296
- def truncate(str, max: 60)
297
- str.length > max ? "#{str[0...max]}..." : str
298
- end
299
- end
300
338
 
301
- # Raised when a schema download fails.
302
- class DownloadError < Error
303
- attr_reader :url, :status_code
339
+ __END__
304
340
 
305
- def initialize(message = nil, url: nil, status_code: nil)
306
- @url = url
307
- @status_code = status_code
308
- super(message || "Failed to download schema: #{@url} (HTTP #{@status_code})")
309
- end
310
- end
311
- end
341
+ require "kube/schema"
312
342
 
313
- if __FILE__ == $0
314
- require "bundler/setup"
315
- require "rspec/autorun"
316
- require "kube/schema"
317
-
318
- RSpec.describe Kube::ValidationError do
319
- # Helper to build a minimal JSONSchemer-style error hash.
320
- def error_hash(overrides = {})
321
- {
322
- "data" => nil,
323
- "data_pointer" => "",
324
- "schema" => {},
325
- "schema_pointer" => "",
326
- "root_schema" => {},
327
- "type" => "unknown",
328
- "error" => "something went wrong"
329
- }.merge(overrides)
330
- end
331
-
332
- describe "#message" do
333
- context "header" do
334
- it "includes kind and name when both are provided" do
335
- err = described_class.new(
336
- [error_hash],
337
- kind: "Deployment",
338
- name: "web"
339
- )
340
- expect(err.message).to include('Schema validation failed for Deployment "web"')
341
- end
343
+ validation_error = Kube::ValidationError
342
344
 
343
- it "includes kind without name" do
344
- err = described_class.new(
345
- [error_hash],
346
- kind: "Service"
347
- )
348
- expect(err.message).to include("Schema validation failed for Service")
349
- end
345
+ # Helper to build a minimal JSONSchemer-style error hash.
346
+ error_hash = lambda { |overrides = {}|
347
+ {
348
+ "data" => nil,
349
+ "data_pointer" => "",
350
+ "schema" => {},
351
+ "schema_pointer" => "",
352
+ "root_schema" => {},
353
+ "type" => "unknown",
354
+ "error" => "something went wrong"
355
+ }.merge(overrides)
356
+ }
350
357
 
351
- it "shows a generic header when no kind or name" do
352
- err = described_class.new([error_hash])
353
- expect(err.message).to include("Schema validation failed")
354
- end
355
- end
358
+ describe "Kube::ValidationError" do
359
+ describe "#message header" do
360
+ it "includes kind and name when both are provided" do
361
+ err = validation_error.new([error_hash.()], kind: "Deployment", name: "web")
362
+ err.message.should.include 'Schema validation failed for Deployment "web"'
363
+ end
356
364
 
357
- context "type errors" do
358
- %w[string integer number boolean array object null].each do |type|
359
- it "formats #{type} type mismatch with the actual value" do
360
- err = described_class.new([
361
- error_hash(
362
- "data_pointer" => "/spec/replicas",
363
- "type" => type,
364
- "data" => "bad_value"
365
- )
366
- ])
367
- expect(err.message).to include("spec.replicas = \"bad_value\" — expected #{type}, got String")
368
- end
369
- end
365
+ it "includes kind without name" do
366
+ err = validation_error.new([error_hash.()], kind: "Service")
367
+ err.message.should.include "Schema validation failed for Service"
368
+ end
370
369
 
371
- it "shows integer value for string type error" do
372
- err = described_class.new([
373
- error_hash(
374
- "data_pointer" => "/spec/template/image",
375
- "type" => "string",
376
- "data" => 2343
377
- )
378
- ])
379
- expect(err.message).to include("spec.template.image = 2343 — expected string, got Integer")
380
- end
370
+ it "shows a generic header when no kind or name" do
371
+ validation_error.new([error_hash.()]).message.should.include "Schema validation failed"
372
+ end
373
+ end
374
+
375
+ describe "#message type errors" do
376
+ %w[string integer number boolean array object null].each do |type|
377
+ it "formats #{type} type mismatch with the actual value" do
378
+ err = validation_error.new([
379
+ error_hash.(
380
+ "data_pointer" => "/spec/replicas",
381
+ "type" => type,
382
+ "data" => "bad_value"
383
+ )
384
+ ])
385
+ err.message.should.include "spec.replicas = \"bad_value\" — expected #{type}, got String"
381
386
  end
387
+ end
382
388
 
383
- context "required errors" do
384
- it "expands each missing key onto its own line" do
385
- err = described_class.new([
386
- error_hash(
387
- "data_pointer" => "/spec",
388
- "type" => "required",
389
- "details" => { "missing_keys" => %w[selector template] }
390
- )
391
- ])
392
- expect(err.message).to include("spec.selector is required but missing")
393
- expect(err.message).to include("spec.template is required but missing")
394
- end
389
+ it "shows integer value for string type error" do
390
+ err = validation_error.new([
391
+ error_hash.(
392
+ "data_pointer" => "/spec/template/image",
393
+ "type" => "string",
394
+ "data" => 2343
395
+ )
396
+ ])
397
+ err.message.should.include "spec.template.image = 2343 — expected string, got Integer"
398
+ end
399
+ end
395
400
 
396
- it "handles required at root level" do
397
- err = described_class.new([
398
- error_hash(
399
- "data_pointer" => "",
400
- "type" => "required",
401
- "details" => { "missing_keys" => ["spec"] }
402
- )
403
- ])
404
- expect(err.message).to include("spec is required but missing")
405
- end
401
+ describe "#message required errors" do
402
+ it "expands each missing key onto its own line" do
403
+ err = validation_error.new([
404
+ error_hash.(
405
+ "data_pointer" => "/spec",
406
+ "type" => "required",
407
+ "details" => { "missing_keys" => %w[selector template] }
408
+ )
409
+ ])
410
+ err.message.should.include "spec.selector is required but missing"
411
+ err.message.should.include "spec.template is required but missing"
412
+ end
406
413
 
407
- it "falls back gracefully if missing_keys is absent" do
408
- err = described_class.new([
409
- error_hash(
410
- "data_pointer" => "/spec",
411
- "type" => "required",
412
- "details" => {}
413
- )
414
- ])
415
- # Should not raise, should produce some message
416
- expect(err.message).to include("spec")
417
- end
418
- end
414
+ it "handles required at root level" do
415
+ err = validation_error.new([
416
+ error_hash.(
417
+ "data_pointer" => "",
418
+ "type" => "required",
419
+ "details" => { "missing_keys" => ["spec"] }
420
+ )
421
+ ])
422
+ err.message.should.include "spec is required but missing"
423
+ end
419
424
 
420
- context "numeric constraint errors" do
421
- it "formats minimum violations" do
422
- err = described_class.new([
423
- error_hash(
424
- "data_pointer" => "/spec/replicas",
425
- "type" => "minimum",
426
- "data" => -1,
427
- "schema" => { "minimum" => 0 }
428
- )
429
- ])
430
- expect(err.message).to include("spec.replicas = -1 — must be >= 0")
431
- end
425
+ it "falls back gracefully if missing_keys is absent" do
426
+ err = validation_error.new([
427
+ error_hash.(
428
+ "data_pointer" => "/spec",
429
+ "type" => "required",
430
+ "details" => {}
431
+ )
432
+ ])
433
+ # Should not raise, should produce some message
434
+ err.message.should.include "spec"
435
+ end
436
+ end
432
437
 
433
- it "formats maximum violations" do
434
- err = described_class.new([
435
- error_hash(
436
- "data_pointer" => "/spec/replicas",
437
- "type" => "maximum",
438
- "data" => 999,
439
- "schema" => { "maximum" => 100 }
440
- )
441
- ])
442
- expect(err.message).to include("spec.replicas = 999 — must be <= 100")
443
- end
438
+ describe "#message numeric constraint errors" do
439
+ it "formats minimum violations" do
440
+ err = validation_error.new([
441
+ error_hash.(
442
+ "data_pointer" => "/spec/replicas",
443
+ "type" => "minimum",
444
+ "data" => -1,
445
+ "schema" => { "minimum" => 0 }
446
+ )
447
+ ])
448
+ err.message.should.include "spec.replicas = -1 — must be >= 0"
449
+ end
444
450
 
445
- it "formats exclusiveMinimum violations" do
446
- err = described_class.new([
447
- error_hash(
448
- "data_pointer" => "/spec/value",
449
- "type" => "exclusiveMinimum",
450
- "data" => 0,
451
- "schema" => { "exclusiveMinimum" => 0 }
452
- )
453
- ])
454
- expect(err.message).to include("spec.value = 0 — must be > 0")
455
- end
451
+ it "formats maximum violations" do
452
+ err = validation_error.new([
453
+ error_hash.(
454
+ "data_pointer" => "/spec/replicas",
455
+ "type" => "maximum",
456
+ "data" => 999,
457
+ "schema" => { "maximum" => 100 }
458
+ )
459
+ ])
460
+ err.message.should.include "spec.replicas = 999 — must be <= 100"
461
+ end
456
462
 
457
- it "formats exclusiveMaximum violations" do
458
- err = described_class.new([
459
- error_hash(
460
- "data_pointer" => "/spec/value",
461
- "type" => "exclusiveMaximum",
462
- "data" => 100,
463
- "schema" => { "exclusiveMaximum" => 100 }
464
- )
465
- ])
466
- expect(err.message).to include("spec.value = 100 — must be < 100")
467
- end
463
+ it "formats exclusiveMinimum violations" do
464
+ err = validation_error.new([
465
+ error_hash.(
466
+ "data_pointer" => "/spec/value",
467
+ "type" => "exclusiveMinimum",
468
+ "data" => 0,
469
+ "schema" => { "exclusiveMinimum" => 0 }
470
+ )
471
+ ])
472
+ err.message.should.include "spec.value = 0 — must be > 0"
473
+ end
468
474
 
469
- it "formats multipleOf violations" do
470
- err = described_class.new([
471
- error_hash(
472
- "data_pointer" => "/spec/count",
473
- "type" => "multipleOf",
474
- "data" => 7,
475
- "schema" => { "multipleOf" => 3 }
476
- )
477
- ])
478
- expect(err.message).to include("spec.count = 7 — must be a multiple of 3")
479
- end
480
- end
475
+ it "formats exclusiveMaximum violations" do
476
+ err = validation_error.new([
477
+ error_hash.(
478
+ "data_pointer" => "/spec/value",
479
+ "type" => "exclusiveMaximum",
480
+ "data" => 100,
481
+ "schema" => { "exclusiveMaximum" => 100 }
482
+ )
483
+ ])
484
+ err.message.should.include "spec.value = 100 — must be < 100"
485
+ end
481
486
 
482
- context "string constraint errors" do
483
- it "formats pattern violations" do
484
- err = described_class.new([
485
- error_hash(
486
- "data_pointer" => "/metadata/name",
487
- "type" => "pattern",
488
- "data" => "INVALID",
489
- "schema" => { "pattern" => "^[a-z0-9-]+$" }
490
- )
491
- ])
492
- expect(err.message).to include('metadata.name = "INVALID" — does not match pattern: ^[a-z0-9-]+$')
493
- end
487
+ it "formats multipleOf violations" do
488
+ err = validation_error.new([
489
+ error_hash.(
490
+ "data_pointer" => "/spec/count",
491
+ "type" => "multipleOf",
492
+ "data" => 7,
493
+ "schema" => { "multipleOf" => 3 }
494
+ )
495
+ ])
496
+ err.message.should.include "spec.count = 7 — must be a multiple of 3"
497
+ end
498
+ end
494
499
 
495
- it "formats minLength violations" do
496
- err = described_class.new([
497
- error_hash(
498
- "data_pointer" => "/metadata/name",
499
- "type" => "minLength",
500
- "data" => "",
501
- "schema" => { "minLength" => 1 }
502
- )
503
- ])
504
- expect(err.message).to include('metadata.name = "" — length must be >= 1')
505
- end
500
+ describe "#message string constraint errors" do
501
+ it "formats pattern violations" do
502
+ err = validation_error.new([
503
+ error_hash.(
504
+ "data_pointer" => "/metadata/name",
505
+ "type" => "pattern",
506
+ "data" => "INVALID",
507
+ "schema" => { "pattern" => "^[a-z0-9-]+$" }
508
+ )
509
+ ])
510
+ err.message.should.include 'metadata.name = "INVALID" — does not match pattern: ^[a-z0-9-]+$'
511
+ end
506
512
 
507
- it "formats maxLength violations" do
508
- err = described_class.new([
509
- error_hash(
510
- "data_pointer" => "/metadata/name",
511
- "type" => "maxLength",
512
- "data" => "a" * 300,
513
- "schema" => { "maxLength" => 253 }
514
- )
515
- ])
516
- expect(err.message).to include("metadata.name =")
517
- expect(err.message).to include("— length must be <= 253")
518
- end
519
- end
513
+ it "formats minLength violations" do
514
+ err = validation_error.new([
515
+ error_hash.(
516
+ "data_pointer" => "/metadata/name",
517
+ "type" => "minLength",
518
+ "data" => "",
519
+ "schema" => { "minLength" => 1 }
520
+ )
521
+ ])
522
+ err.message.should.include 'metadata.name = "" — length must be >= 1'
523
+ end
520
524
 
521
- context "enum errors" do
522
- it "shows allowed values" do
523
- err = described_class.new([
524
- error_hash(
525
- "data_pointer" => "/spec/type",
526
- "type" => "enum",
527
- "data" => "InvalidType",
528
- "schema" => { "enum" => %w[ClusterIP NodePort LoadBalancer] }
529
- )
530
- ])
531
- expect(err.message).to include('"InvalidType" — must be one of:')
532
- expect(err.message).to include("ClusterIP")
533
- end
534
- end
525
+ it "formats maxLength violations" do
526
+ err = validation_error.new([
527
+ error_hash.(
528
+ "data_pointer" => "/metadata/name",
529
+ "type" => "maxLength",
530
+ "data" => "a" * 300,
531
+ "schema" => { "maxLength" => 253 }
532
+ )
533
+ ])
534
+ err.message.should.include "metadata.name ="
535
+ err.message.should.include "— length must be <= 253"
536
+ end
537
+ end
535
538
 
536
- context "format errors" do
537
- it "shows the expected format" do
538
- err = described_class.new([
539
- error_hash(
540
- "data_pointer" => "/spec/startTime",
541
- "type" => "format",
542
- "data" => "not-a-date",
543
- "schema" => { "format" => "date-time" }
544
- )
545
- ])
546
- expect(err.message).to include('spec.startTime = "not-a-date" — invalid date-time format')
547
- end
548
- end
539
+ describe "#message enum errors" do
540
+ it "shows allowed values" do
541
+ err = validation_error.new([
542
+ error_hash.(
543
+ "data_pointer" => "/spec/type",
544
+ "type" => "enum",
545
+ "data" => "InvalidType",
546
+ "schema" => { "enum" => %w[ClusterIP NodePort LoadBalancer] }
547
+ )
548
+ ])
549
+ err.message.should.include '"InvalidType" — must be one of:'
550
+ err.message.should.include "ClusterIP"
551
+ end
552
+ end
549
553
 
550
- context "array constraint errors" do
551
- it "formats minItems violations" do
552
- err = described_class.new([
553
- error_hash(
554
- "data_pointer" => "/spec/containers",
555
- "type" => "minItems",
556
- "data" => [],
557
- "schema" => { "minItems" => 1 }
558
- )
559
- ])
560
- expect(err.message).to include("spec.containers — array must have >= 1 items, got 0")
561
- end
554
+ describe "#message format errors" do
555
+ it "shows the expected format" do
556
+ err = validation_error.new([
557
+ error_hash.(
558
+ "data_pointer" => "/spec/startTime",
559
+ "type" => "format",
560
+ "data" => "not-a-date",
561
+ "schema" => { "format" => "date-time" }
562
+ )
563
+ ])
564
+ err.message.should.include 'spec.startTime = "not-a-date" — invalid date-time format'
565
+ end
566
+ end
562
567
 
563
- it "formats uniqueItems violations" do
564
- err = described_class.new([
565
- error_hash(
566
- "data_pointer" => "/spec/ports",
567
- "type" => "uniqueItems",
568
- "data" => [80, 80]
569
- )
570
- ])
571
- expect(err.message).to include("spec.ports — array items must be unique")
572
- end
573
- end
568
+ describe "#message array constraint errors" do
569
+ it "formats minItems violations" do
570
+ err = validation_error.new([
571
+ error_hash.(
572
+ "data_pointer" => "/spec/containers",
573
+ "type" => "minItems",
574
+ "data" => [],
575
+ "schema" => { "minItems" => 1 }
576
+ )
577
+ ])
578
+ err.message.should.include "spec.containers — array must have >= 1 items, got 0"
579
+ end
574
580
 
575
- context "unknown error types" do
576
- it "falls back to JSONSchemer's error message" do
577
- err = described_class.new([
578
- error_hash(
579
- "data_pointer" => "/spec/something",
580
- "type" => "oneOf",
581
- "error" => "value at `/spec/something` does not match exactly one schema"
582
- )
583
- ])
584
- expect(err.message).to include("spec.something: value at `/spec/something` does not match exactly one schema")
585
- end
581
+ it "formats uniqueItems violations" do
582
+ err = validation_error.new([
583
+ error_hash.(
584
+ "data_pointer" => "/spec/ports",
585
+ "type" => "uniqueItems",
586
+ "data" => [80, 80]
587
+ )
588
+ ])
589
+ err.message.should.include "spec.ports — array items must be unique"
590
+ end
591
+ end
586
592
 
587
- it "falls back to the type name when no error message exists" do
588
- err = described_class.new([
589
- error_hash(
590
- "data_pointer" => "/spec/x",
591
- "type" => "customKeyword",
592
- "error" => nil
593
- )
594
- ])
595
- expect(err.message).to include("spec.x: customKeyword")
596
- end
597
- end
593
+ describe "#message unknown error types" do
594
+ it "falls back to JSONSchemer's error message" do
595
+ err = validation_error.new([
596
+ error_hash.(
597
+ "data_pointer" => "/spec/something",
598
+ "type" => "oneOf",
599
+ "error" => "value at `/spec/something` does not match exactly one schema"
600
+ )
601
+ ])
602
+ err.message.should.include "spec.something: value at `/spec/something` does not match exactly one schema"
603
+ end
598
604
 
599
- context "value truncation" do
600
- it "truncates long values" do
601
- long_value = "a" * 200
602
- err = described_class.new([
603
- error_hash(
604
- "data_pointer" => "/spec/data",
605
- "type" => "integer",
606
- "data" => long_value
607
- )
608
- ])
609
- expect(err.message).to include("...")
610
- expect(err.message.length).to be < 500
611
- end
612
- end
605
+ it "falls back to the type name when no error message exists" do
606
+ err = validation_error.new([
607
+ error_hash.(
608
+ "data_pointer" => "/spec/x",
609
+ "type" => "customKeyword",
610
+ "error" => nil
611
+ )
612
+ ])
613
+ err.message.should.include "spec.x: customKeyword"
614
+ end
615
+ end
613
616
 
614
- context "dot-notation path conversion" do
615
- it "converts JSON pointers to dot notation" do
616
- err = described_class.new([
617
- error_hash(
618
- "data_pointer" => "/spec/template/spec/containers/0/image",
619
- "type" => "string",
620
- "data" => 123
621
- )
622
- ])
623
- expect(err.message).to include("spec.template.spec.containers.0.image = 123")
624
- end
617
+ describe "#message value truncation" do
618
+ it "truncates long values" do
619
+ err = validation_error.new([
620
+ error_hash.(
621
+ "data_pointer" => "/spec/data",
622
+ "type" => "integer",
623
+ "data" => "a" * 200
624
+ )
625
+ ])
626
+ err.message.should.include "..."
627
+ err.message.length.should.be < 500
628
+ end
629
+ end
625
630
 
626
- it "uses 'root' for empty pointer" do
627
- err = described_class.new([
628
- error_hash(
629
- "data_pointer" => "",
630
- "type" => "object",
631
- "data" => "not_an_object"
632
- )
633
- ])
634
- expect(err.message).to include("root = ")
635
- end
636
- end
631
+ describe "#message dot-notation path conversion" do
632
+ it "converts JSON pointers to dot notation" do
633
+ err = validation_error.new([
634
+ error_hash.(
635
+ "data_pointer" => "/spec/template/spec/containers/0/image",
636
+ "type" => "string",
637
+ "data" => 123
638
+ )
639
+ ])
640
+ err.message.should.include "spec.template.spec.containers.0.image = 123"
637
641
  end
638
642
 
639
- describe "#errors" do
640
- it "exposes the raw JSONSchemer error hashes" do
641
- raw = [error_hash("type" => "string")]
642
- err = described_class.new(raw)
643
- expect(err.errors).to equal(raw)
644
- end
643
+ it "uses 'root' for empty pointer" do
644
+ err = validation_error.new([
645
+ error_hash.(
646
+ "data_pointer" => "",
647
+ "type" => "object",
648
+ "data" => "not_an_object"
649
+ )
650
+ ])
651
+ err.message.should.include "root = "
645
652
  end
653
+ end
646
654
 
647
- describe "annotated manifest output" do
648
- it "includes the resource YAML when manifest is provided" do
649
- manifest = {
655
+ describe "#errors" do
656
+ it "exposes the raw JSONSchemer error hashes" do
657
+ raw = [error_hash.("type" => "string")]
658
+ validation_error.new(raw).errors.should.be.identical_to raw
659
+ end
660
+ end
661
+
662
+ describe "annotated manifest output" do
663
+ it "includes the resource YAML when manifest is provided" do
664
+ err = validation_error.new(
665
+ [error_hash.("data_pointer" => "/spec/replicas", "type" => "integer", "data" => "bad")],
666
+ kind: "Deployment",
667
+ name: "web",
668
+ manifest: {
650
669
  "apiVersion" => "apps/v1",
651
670
  "kind" => "Deployment",
652
671
  "metadata" => { "name" => "web", "namespace" => "prod" },
653
672
  "spec" => { "replicas" => "bad" }
654
673
  }
655
- err = described_class.new(
656
- [error_hash("data_pointer" => "/spec/replicas", "type" => "integer", "data" => "bad")],
657
- kind: "Deployment",
658
- name: "web",
659
- manifest: manifest
660
- )
661
- expect(err.message).to include("apiVersion: apps/v1")
662
- expect(err.message).to include("kind: Deployment")
663
- expect(err.message).to include("name: web")
664
- expect(err.message).to include("namespace: prod")
665
- expect(err.message).to include("replicas: bad")
666
- end
674
+ )
675
+ err.message.should.include "apiVersion: apps/v1"
676
+ err.message.should.include "kind: Deployment"
677
+ err.message.should.include "name: web"
678
+ err.message.should.include "namespace: prod"
679
+ err.message.should.include "replicas: bad"
680
+ end
667
681
 
668
- it "highlights error lines in red (ANSI)" do
669
- manifest = {
682
+ it "highlights error lines in red (ANSI)" do
683
+ err = validation_error.new(
684
+ [error_hash.("data_pointer" => "/spec/replicas", "type" => "integer", "data" => "bad")],
685
+ manifest: {
670
686
  "apiVersion" => "apps/v1",
671
687
  "kind" => "Deployment",
672
688
  "spec" => { "replicas" => "bad" }
673
689
  }
674
- err = described_class.new(
675
- [error_hash("data_pointer" => "/spec/replicas", "type" => "integer", "data" => "bad")],
676
- manifest: manifest
677
- )
678
- # The replicas line should be wrapped in red ANSI
679
- expect(err.message).to include("\033[31m")
680
- expect(err.message).to match(/\033\[31m\s*replicas: bad\033\[0m/)
681
- end
690
+ )
691
+ # The replicas line should be wrapped in red ANSI
692
+ err.message.should.include "\033[31m"
693
+ err.message.should.match(/\033\[31m\s*replicas: bad\033\[0m/)
694
+ end
682
695
 
683
- it "appends a yellow inline comment on error lines" do
684
- manifest = {
696
+ it "appends a yellow inline comment on error lines" do
697
+ err = validation_error.new(
698
+ [error_hash.("data_pointer" => "/spec/replicas", "type" => "integer", "data" => "bad")],
699
+ manifest: {
685
700
  "apiVersion" => "v1",
686
701
  "kind" => "Pod",
687
702
  "spec" => { "replicas" => "bad" }
688
703
  }
689
- err = described_class.new(
690
- [error_hash("data_pointer" => "/spec/replicas", "type" => "integer", "data" => "bad")],
691
- manifest: manifest
692
- )
693
- expect(err.message).to match(/\033\[33m# expected integer, got String\033\[0m/)
694
- end
704
+ )
705
+ err.message.should.match(/\033\[33m# expected integer, got String\033\[0m/)
706
+ end
695
707
 
696
- it "injects missing required keys as red lines" do
697
- manifest = {
708
+ it "injects missing required keys as red lines" do
709
+ err = validation_error.new(
710
+ [error_hash.(
711
+ "data_pointer" => "/spec",
712
+ "type" => "required",
713
+ "details" => { "missing_keys" => ["selector"] }
714
+ )],
715
+ manifest: {
698
716
  "apiVersion" => "apps/v1",
699
717
  "kind" => "Deployment",
700
718
  "spec" => { "replicas" => 3 }
701
719
  }
702
- err = described_class.new(
703
- [error_hash(
704
- "data_pointer" => "/spec",
705
- "type" => "required",
706
- "details" => { "missing_keys" => ["selector"] }
707
- )],
708
- manifest: manifest
709
- )
710
- expect(err.message).to match(/\033\[31m\s*selector:\033\[0m/)
711
- expect(err.message).to include("MISSING")
712
- end
720
+ )
721
+ err.message.should.match(/\033\[31m\s*selector:\033\[0m/)
722
+ err.message.should.include "MISSING"
723
+ end
713
724
 
714
- it "does not color lines that have no errors" do
715
- manifest = {
725
+ it "does not color lines that have no errors" do
726
+ err = validation_error.new(
727
+ [error_hash.("data_pointer" => "/spec/replicas", "type" => "integer", "data" => "bad")],
728
+ manifest: {
716
729
  "apiVersion" => "apps/v1",
717
730
  "kind" => "Deployment",
718
731
  "metadata" => { "name" => "web" },
719
732
  "spec" => { "replicas" => "bad" }
720
733
  }
721
- err = described_class.new(
722
- [error_hash("data_pointer" => "/spec/replicas", "type" => "integer", "data" => "bad")],
723
- manifest: manifest
724
- )
725
- # The "name: web" line should NOT have red ANSI
726
- err.message.each_line do |line|
727
- next unless line.include?("name: web")
728
- expect(line).not_to include("\033[31m")
729
- end
730
- end
734
+ )
735
+ # The "name: web" line should NOT have red ANSI
736
+ lines = err.message.each_line.select { |line| line.include?("name: web") }
737
+ lines.should.not.be.empty
738
+ lines.each { |line| line.should.not.include "\033[31m" }
739
+ end
731
740
 
732
- it "handles array items with correct pointer mapping" do
733
- manifest = {
741
+ it "handles array items with correct pointer mapping" do
742
+ err = validation_error.new(
743
+ [error_hash.(
744
+ "data_pointer" => "/spec/containers/0/image",
745
+ "type" => "string",
746
+ "data" => 2343
747
+ )],
748
+ manifest: {
734
749
  "apiVersion" => "v1",
735
750
  "kind" => "Pod",
736
751
  "spec" => {
@@ -739,23 +754,15 @@ if __FILE__ == $0
739
754
  ]
740
755
  }
741
756
  }
742
- err = described_class.new(
743
- [error_hash(
744
- "data_pointer" => "/spec/containers/0/image",
745
- "type" => "string",
746
- "data" => 2343
747
- )],
748
- manifest: manifest
749
- )
750
- expect(err.message).to match(/\033\[31m\s*image: 2343\033\[0m/)
751
- end
757
+ )
758
+ err.message.should.match(/\033\[31m\s*image: 2343\033\[0m/)
759
+ end
752
760
 
753
- it "omits the manifest section when manifest is nil" do
754
- err = described_class.new(
755
- [error_hash("data_pointer" => "/spec/x", "type" => "string", "data" => 1)]
756
- )
757
- expect(err.message).not_to include("---")
758
- end
761
+ it "omits the manifest section when manifest is nil" do
762
+ err = validation_error.new(
763
+ [error_hash.("data_pointer" => "/spec/x", "type" => "string", "data" => 1)]
764
+ )
765
+ err.message.should.not.include "---"
759
766
  end
760
767
  end
761
768
  end