View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one or more
3    * contributor license agreements.  See the NOTICE file distributed with
4    * this work for additional information regarding copyright ownership.
5    * The ASF licenses this file to You under the Apache License, Version 2.0
6    * (the "License"); you may not use this file except in compliance with
7    * the License.  You may obtain a copy of the License at
8    *
9    *      http://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the License for the specific language governing permissions and
15   * limitations under the License.
16   */
17  package org.apache.juneau.httppart;
18  
19  import static org.apache.juneau.TestUtils.*;
20  import static org.apache.juneau.commons.utils.CollectionUtils.*;
21  import static org.apache.juneau.commons.utils.StringUtils.*;
22  import static org.apache.juneau.junit.bct.BctAssertions.*;
23  import static org.junit.jupiter.api.Assertions.*;
24  
25  import java.util.*;
26  
27  import org.apache.juneau.*;
28  import org.apache.juneau.annotation.*;
29  import org.apache.juneau.commons.reflect.*;
30  import org.apache.juneau.http.annotation.*;
31  import org.junit.jupiter.api.*;
32  
33  class HttpPartSchema_Path_Test extends TestBase {
34  
35  	//-----------------------------------------------------------------------------------------------------------------
36  	// Basic test
37  	//-----------------------------------------------------------------------------------------------------------------
38  	@Test void a01_basic() {
39  		assertDoesNotThrow(()->HttpPartSchema.create().build());
40  	}
41  
42  	//-----------------------------------------------------------------------------------------------------------------
43  	// @Path
44  	//-----------------------------------------------------------------------------------------------------------------
45  
46  	@Path("x")
47  	public static class A01 {}
48  
49  	@Test void a01_value() {
50  		var s = HttpPartSchema.create().applyAll(Path.class, A01.class).build();
51  		assertEquals("x", s.getName());
52  	}
53  
54  	@Path("x")
55  	@Schema(
56  		t="number",
57  		f="int32",
58  		cf="csv",
59  		max="1",
60  		min="2",
61  		mo="3",
62  		p="4",
63  		maxl=1,
64  		minl=2,
65  		emax=true,
66  		emin=true,
67  		d={"b1","b2"},
68  		items=@Items($ref="d1"),
69  		e="e1,e2,e3"
70  	)
71  	public static class A02 {}
72  
73  	@Test void a02_basic_onClass() {
74  		assertBean(
75  			HttpPartSchema.create().applyAll(Path.class, A02.class).noValidate().build(),
76  			"name,type,format,collectionFormat,maximum,minimum,multipleOf,pattern,maxLength,minLength,exclusiveMaximum,exclusiveMinimum,enum",
77  			"x,NUMBER,INT32,CSV,1,2,3,4,1,2,true,true,[e1,e2,e3]"
78  		);
79  	}
80  
81  	public static class A03 {
82  		public void a(
83  				@Path("x")
84  				@Schema(
85  					t="number",
86  					f="int32",
87  					cf="csv",
88  					max="1",
89  					min="2",
90  					mo="3",
91  					p="4",
92  					maxl=1,
93  					minl=2,
94  					emax=true,
95  					emin=true,
96  					d={"b1","b2"},
97  					items=@Items($ref="d1"),
98  					e="e1,e2,e3"
99  				)
100 				String x
101 			) {
102 			/* no-op */
103 		}
104 	}
105 
106 	@Test void a03_basic_onParameter() throws Exception {
107 		var mpi = MethodInfo.of(A03.class.getMethod("a", String.class)).getParameter(0);
108 		assertBean(
109 			HttpPartSchema.create().applyAll(Path.class, mpi).noValidate().build(),
110 			"name,type,format,collectionFormat,maximum,minimum,multipleOf,pattern,maxLength,minLength,exclusiveMaximum,exclusiveMinimum,enum",
111 			"x,NUMBER,INT32,CSV,1,2,3,4,1,2,true,true,[e1,e2,e3]"
112 		);
113 	}
114 
115 	public static class A04 {
116 		public void a(
117 				@Path("y")
118 				@Schema(
119 					t="integer",
120 					f="int64",
121 					cf="ssv",
122 					max="5",
123 					min="6",
124 					mo="7",
125 					p="8",
126 					maxl=5,
127 					minl=6,
128 					emax=false,
129 					emin=false,
130 					d={"b3","b3"},
131 					items=@Items($ref="d2"),
132 					e="e4,e5,e6"
133 				)
134 				A01 x
135 			) {
136 			/* no-op */
137 		}
138 	}
139 
140 	@Test void a04_basic_onParameterAndClass() throws Exception {
141 		var mpi = MethodInfo.of(A04.class.getMethod("a", A01.class)).getParameter(0);
142 		assertBean(
143 			HttpPartSchema.create().applyAll(Path.class, mpi).noValidate().build(),
144 			"name,type,format,collectionFormat,maximum,minimum,multipleOf,pattern,maxLength,minLength,exclusiveMaximum,exclusiveMinimum,enum",
145 			"y,INTEGER,INT64,SSV,5,6,7,8,5,6,false,false,[e4,e5,e6]"
146 		);
147 	}
148 
149 	@Path("x")
150 	@Schema(
151 		items=@Items(
152 			t="number",
153 			f="int32",
154 			cf="csv",
155 			max="1",
156 			min="2",
157 			mo="3",
158 			p="4",
159 			maxl=1,
160 			minl=2,
161 			maxi=3,
162 			mini=4,
163 			emax=true,
164 			emin=true,
165 			ui=true,
166 			df={"c1","c2"},
167 			e="e1,e2",
168 			items=@SubItems(
169 				t="integer",
170 				f="int64",
171 				cf="ssv",
172 				max="5",
173 				min="6",
174 				mo="7",
175 				p="8",
176 				maxl=5,
177 				minl=6,
178 				maxi=7,
179 				mini=8,
180 				emax=false,
181 				emin=false,
182 				ui=false,
183 				df={"c3","c4"},
184 				e="e3,e4",
185 				items={
186 					"type:'string',",
187 					"format:'float',",
188 					"collectionFormat:'tsv',",
189 					"maximum:'9',",
190 					"minimum:'10',",
191 					"multipleOf:'11',",
192 					"pattern:'12',",
193 					"maxLength:9,",
194 					"minLength:10,",
195 					"maxItems:11,",
196 					"minItems:12,",
197 					"exclusiveMaximum:true,",
198 					"exclusiveMinimum:true,",
199 					"uniqueItems:true,",
200 					"default:'c5\\nc6',",
201 					"enum:['e5','e6'],",
202 					"items:{",
203 						"type:'array',",
204 						"format:'double',",
205 						"collectionFormat:'pipes',",
206 						"maximum:'13',",
207 						"minimum:'14',",
208 						"multipleOf:'15',",
209 						"pattern:'16',",
210 						"maxLength:13,",
211 						"minLength:14,",
212 						"maxItems:15,",
213 						"minItems:16,",
214 						"exclusiveMaximum:false,",
215 						"exclusiveMinimum:false,",
216 						"uniqueItems:false,",
217 						"default:'c7\\nc8',",
218 						"enum:['e7','e8']",
219 					"}"
220 				}
221 			)
222 		)
223 	)
224 	public static class A05 {}
225 
226 	@Test void a05_basic_nestedItems_onClass() {
227 		var s = HttpPartSchema.create().applyAll(Path.class, A05.class).noValidate().build();
228 		assertEquals("x", s.getName());
229 
230 		var items = s.getItems();
231 		assertBean(
232 			items,
233 			"type,format,collectionFormat,maximum,minimum,multipleOf,pattern,maxLength,minLength,maxItems,minItems,exclusiveMaximum,exclusiveMinimum,uniqueItems,enum,default",
234 			"NUMBER,INT32,CSV,1,2,3,4,1,2,3,4,true,true,true,[e1,e2],c1\nc2"
235 		);
236 
237 		items = items.getItems();
238 		assertBean(
239 			items,
240 			"type,format,collectionFormat,maximum,minimum,multipleOf,pattern,maxLength,minLength,maxItems,minItems,exclusiveMaximum,exclusiveMinimum,uniqueItems,enum,default",
241 			"INTEGER,INT64,SSV,5,6,7,8,5,6,7,8,false,false,false,[e3,e4],c3\nc4"
242 		);
243 
244 		items = items.getItems();
245 		assertBean(
246 			items,
247 			"type,format,collectionFormat,maximum,minimum,multipleOf,pattern,maxLength,minLength,maxItems,minItems,exclusiveMaximum,exclusiveMinimum,uniqueItems,enum,default",
248 			"STRING,FLOAT,TSV,9,10,11,12,9,10,11,12,true,true,true,[e5,e6],c5\nc6"
249 		);
250 
251 		items = items.getItems();
252 		assertBean(
253 			items,
254 			"type,format,collectionFormat,maximum,minimum,multipleOf,pattern,maxLength,minLength,maxItems,minItems,exclusiveMaximum,exclusiveMinimum,uniqueItems,enum,default",
255 			"ARRAY,DOUBLE,PIPES,13,14,15,16,13,14,15,16,false,false,false,[e7,e8],c7\nc8"
256 		);
257 	}
258 
259 	//-----------------------------------------------------------------------------------------------------------------
260 	// String input validations.
261 	//-----------------------------------------------------------------------------------------------------------------
262 
263 	@Path @Schema(p="x.*")
264 	public static class B02a {}
265 
266 	@Test void b02a_pattern() throws Exception {
267 		var s = HttpPartSchema.create().applyAll(Path.class, B02a.class).build();
268 		s.validateInput("x");
269 		s.validateInput("xx");
270 		assertThrowsWithMessage(SchemaValidationException.class, "Value does not match expected pattern.  Must match pattern: x.*", ()->s.validateInput("y"));
271 		assertThrowsWithMessage(SchemaValidationException.class, "Value does not match expected pattern.  Must match pattern: x.*", ()->s.validateInput("yx"));
272 		// Empty values are never allowed.
273 		assertThrowsWithMessage(SchemaValidationException.class, "Empty value not allowed.", ()->s.validateInput(""));
274 	}
275 
276 	@Path @Schema(minl=2, maxl=3)
277 	public static class B03a {}
278 
279 	@Test void b03a_length() throws Exception {
280 		var s = HttpPartSchema.create().applyAll(Path.class, B03a.class).build();
281 		s.validateInput("12");
282 		s.validateInput("123");
283 		s.validateInput(null);
284 		assertThrowsWithMessage(SchemaValidationException.class, "Minimum length of value not met.", ()->s.validateInput("1"));
285 		assertThrowsWithMessage(SchemaValidationException.class, "Maximum length of value exceeded.", ()->s.validateInput("1234"));
286 	}
287 
288 	@Path
289 	@Schema(
290 		items=@Items(
291 			minl=2, maxl=3,
292 			items=@SubItems(
293 				minl=3, maxl=4,
294 				items={
295 					"minLength:4,maxLength:5,",
296 					"items:{minLength:5,maxLength:6}"
297 				}
298 			)
299 		)
300 	)
301 	public static class B03b {}
302 
303 	@Test void b03b_length_items() throws Exception {
304 		var s = HttpPartSchema.create().applyAll(Path.class, B03b.class).build();
305 
306 		s.getItems().validateInput("12");
307 		s.getItems().getItems().validateInput("123");
308 		s.getItems().getItems().getItems().validateInput("1234");
309 		s.getItems().getItems().getItems().getItems().validateInput("12345");
310 
311 		s.getItems().validateInput("123");
312 		s.getItems().getItems().validateInput("1234");
313 		s.getItems().getItems().getItems().validateInput("12345");
314 		s.getItems().getItems().getItems().getItems().validateInput("123456");
315 
316 		s.getItems().validateInput(null);
317 		s.getItems().getItems().validateInput(null);
318 		s.getItems().getItems().getItems().validateInput(null);
319 		s.getItems().getItems().getItems().getItems().validateInput(null);
320 
321 		assertThrowsWithMessage(SchemaValidationException.class, "Minimum length of value not met.", ()->s.getItems().validateInput("1"));
322 		assertThrowsWithMessage(SchemaValidationException.class, "Minimum length of value not met.", ()->s.getItems().getItems().validateInput("12"));
323 		assertThrowsWithMessage(SchemaValidationException.class, "Minimum length of value not met.", ()->s.getItems().getItems().getItems().validateInput("123"));
324 		assertThrowsWithMessage(SchemaValidationException.class, "Minimum length of value not met.", ()->s.getItems().getItems().getItems().getItems().validateInput("1234"));
325 
326 		assertThrowsWithMessage(SchemaValidationException.class, "Maximum length of value exceeded.", ()->s.getItems().validateInput("1234"));
327 		assertThrowsWithMessage(SchemaValidationException.class, "Maximum length of value exceeded.", ()->s.getItems().getItems().validateInput("12345"));
328 		assertThrowsWithMessage(SchemaValidationException.class, "Maximum length of value exceeded.", ()->s.getItems().getItems().getItems().validateInput("123456"));
329 		assertThrowsWithMessage(SchemaValidationException.class, "Maximum length of value exceeded.", ()->s.getItems().getItems().getItems().getItems().validateInput("1234567"));
330 	}
331 
332 	@Path @Schema(e="X,Y")
333 	public static class B04a {}
334 
335 	@Test void b04a_enum() throws Exception {
336 		var s = HttpPartSchema.create().applyAll(Path.class, B04a.class).build();
337 		s.validateInput("X");
338 		s.validateInput("Y");
339 		s.validateInput(null);
340 		assertThrowsWithMessage(SchemaValidationException.class, "Value does not match one of the expected values.  Must be one of the following:  X, Y", ()->s.validateInput("Z"));
341 	}
342 
343 	@Path @Schema(e=" X , Y ")
344 	public static class B04b {}
345 
346 	@Test void b04b_enum() throws Exception {
347 		var s = HttpPartSchema.create().applyAll(Path.class, B04b.class).build();
348 		s.validateInput("X");
349 		s.validateInput("Y");
350 		s.validateInput(null);
351 		assertThrowsWithMessage(SchemaValidationException.class, "Value does not match one of the expected values.  Must be one of the following:  X, Y", ()->s.validateInput("Z"));
352 	}
353 
354 	@Path @Schema(e="X,Y")
355 	public static class B04c {}
356 
357 	@Test void b04c_enum_json() throws Exception {
358 		var s = HttpPartSchema.create().applyAll(Path.class, B04c.class).build();
359 		s.validateInput("X");
360 		s.validateInput("Y");
361 		s.validateInput(null);
362 		assertThrowsWithMessage(SchemaValidationException.class, "Value does not match one of the expected values.  Must be one of the following:  X, Y", ()->s.validateInput("Z"));
363 	}
364 
365 	@Path
366 	@Schema(
367 		items=@Items(
368 			e="W",
369 			items=@SubItems(
370 				e="X",
371 				items={
372 					"enum:['Y'],",
373 					"items:{enum:['Z']}"
374 				}
375 			)
376 		)
377 	)
378 	public static class B04d {}
379 
380 	@Test void b04d_enum_items() throws Exception {
381 		var s = HttpPartSchema.create().applyAll(Path.class, B04d.class).build();
382 
383 		s.getItems().validateInput("W");
384 		s.getItems().getItems().validateInput("X");
385 		s.getItems().getItems().getItems().validateInput("Y");
386 		s.getItems().getItems().getItems().getItems().validateInput("Z");
387 
388 		assertThrowsWithMessage(SchemaValidationException.class, "Value does not match one of the expected values.  Must be one of the following:  W", ()->s.getItems().validateInput("V"));
389 		assertThrowsWithMessage(SchemaValidationException.class, "Value does not match one of the expected values.  Must be one of the following:  X", ()->s.getItems().getItems().validateInput("V"));
390 		assertThrowsWithMessage(SchemaValidationException.class, "Value does not match one of the expected values.  Must be one of the following:  Y", ()->s.getItems().getItems().getItems().validateInput("V"));
391 		assertThrowsWithMessage(SchemaValidationException.class, "Value does not match one of the expected values.  Must be one of the following:  Z", ()->s.getItems().getItems().getItems().getItems().validateInput("V"));
392 	}
393 
394 	//-----------------------------------------------------------------------------------------------------------------
395 	// Numeric validations
396 	//-----------------------------------------------------------------------------------------------------------------
397 
398 	@Path @Schema(min="10", max="100")
399 	public static class C01a {}
400 
401 	@Test void c01a_minmax_ints() throws Exception {
402 		var s = HttpPartSchema.create().applyAll(Path.class, C01a.class).build();
403 		s.validateOutput(10, BeanContext.DEFAULT);
404 		s.validateOutput(100, BeanContext.DEFAULT);
405 		s.validateOutput(null, BeanContext.DEFAULT);
406 		assertThrowsWithMessage(SchemaValidationException.class, "Minimum value not met.", ()->s.validateOutput(9, BeanContext.DEFAULT));
407 		assertThrowsWithMessage(SchemaValidationException.class, "Maximum value exceeded.", ()->s.validateOutput(101, BeanContext.DEFAULT));
408 	}
409 
410 	@Path
411 	@Schema(
412 		items=@Items(
413 			min="10", max="100",
414 			items=@SubItems(
415 				min="100", max="1000",
416 				items={
417 					"minimum:1000,maximum:10000,",
418 					"items:{minimum:10000,maximum:100000}"
419 				}
420 			)
421 		)
422 	)
423 	public static class C01b {}
424 
425 	@Test void c01b_minmax_ints_items() throws Exception {
426 		var s = HttpPartSchema.create().applyAll(Path.class, C01b.class).build();
427 
428 		s.getItems().validateOutput(10, BeanContext.DEFAULT);
429 		s.getItems().getItems().validateOutput(100, BeanContext.DEFAULT);
430 		s.getItems().getItems().getItems().validateOutput(1000, BeanContext.DEFAULT);
431 		s.getItems().getItems().getItems().getItems().validateOutput(10000, BeanContext.DEFAULT);
432 
433 		s.getItems().validateOutput(100, BeanContext.DEFAULT);
434 		s.getItems().getItems().validateOutput(1000, BeanContext.DEFAULT);
435 		s.getItems().getItems().getItems().validateOutput(10000, BeanContext.DEFAULT);
436 		s.getItems().getItems().getItems().getItems().validateOutput(100000, BeanContext.DEFAULT);
437 
438 		assertThrowsWithMessage(SchemaValidationException.class, "Minimum value not met.", ()->s.getItems().validateOutput(9, BeanContext.DEFAULT));
439 		assertThrowsWithMessage(SchemaValidationException.class, "Minimum value not met.", ()->s.getItems().getItems().validateOutput(99, BeanContext.DEFAULT));
440 		assertThrowsWithMessage(SchemaValidationException.class, "Minimum value not met.", ()->s.getItems().getItems().getItems().validateOutput(999, BeanContext.DEFAULT));
441 		assertThrowsWithMessage(SchemaValidationException.class, "Minimum value not met.", ()->s.getItems().getItems().getItems().getItems().validateOutput(9999, BeanContext.DEFAULT));
442 
443 		assertThrowsWithMessage(SchemaValidationException.class, "Maximum value exceeded.", ()->s.getItems().validateOutput(101, BeanContext.DEFAULT));
444 		assertThrowsWithMessage(SchemaValidationException.class, "Maximum value exceeded.", ()->s.getItems().getItems().validateOutput(1001, BeanContext.DEFAULT));
445 		assertThrowsWithMessage(SchemaValidationException.class, "Maximum value exceeded.", ()->s.getItems().getItems().getItems().validateOutput(10001, BeanContext.DEFAULT));
446 		assertThrowsWithMessage(SchemaValidationException.class, "Maximum value exceeded.", ()->s.getItems().getItems().getItems().getItems().validateOutput(100001, BeanContext.DEFAULT));
447 	}
448 
449 	@Path
450 	@Schema(min="10", max="100", emin=true, emax=true)
451 	public static class C02a {}
452 
453 	@Test void c02a_minmax_exclusive() throws Exception {
454 		var s = HttpPartSchema.create().applyAll(Path.class, C02a.class).build();
455 		s.validateOutput(11, BeanContext.DEFAULT);
456 		s.validateOutput(99, BeanContext.DEFAULT);
457 		s.validateOutput(null, BeanContext.DEFAULT);
458 		assertThrowsWithMessage(SchemaValidationException.class, "Minimum value not met.", ()->s.validateOutput(10, BeanContext.DEFAULT));
459 		assertThrowsWithMessage(SchemaValidationException.class, "Maximum value exceeded.", ()->s.validateOutput(100, BeanContext.DEFAULT));
460 	}
461 
462 	@Path
463 	@Schema(
464 		items=@Items(
465 			min="10", max="100", emin=true, emax=true,
466 			items=@SubItems(
467 				min="100", max="1000", emin=true, emax=true,
468 				items={
469 					"minimum:1000,maximum:10000,exclusiveMinimum:true,exclusiveMaximum:true,",
470 					"items:{minimum:10000,maximum:100000,exclusiveMinimum:true,exclusiveMaximum:true}"
471 				}
472 			)
473 		)
474 	)
475 	public static class C02b {}
476 
477 	@Test void c02b_minmax_exclusive_items() throws Exception {
478 		var s = HttpPartSchema.create().applyAll(Path.class, C02b.class).build();
479 
480 		s.getItems().validateOutput(11, BeanContext.DEFAULT);
481 		s.getItems().getItems().validateOutput(101, BeanContext.DEFAULT);
482 		s.getItems().getItems().getItems().validateOutput(1001, BeanContext.DEFAULT);
483 		s.getItems().getItems().getItems().getItems().validateOutput(10001, BeanContext.DEFAULT);
484 
485 		s.getItems().validateOutput(99, BeanContext.DEFAULT);
486 		s.getItems().getItems().validateOutput(999, BeanContext.DEFAULT);
487 		s.getItems().getItems().getItems().validateOutput(9999, BeanContext.DEFAULT);
488 		s.getItems().getItems().getItems().getItems().validateOutput(99999, BeanContext.DEFAULT);
489 
490 		assertThrowsWithMessage(SchemaValidationException.class, "Minimum value not met.", ()->s.getItems().validateOutput(10, BeanContext.DEFAULT));
491 		assertThrowsWithMessage(SchemaValidationException.class, "Minimum value not met.", ()->s.getItems().getItems().validateOutput(100, BeanContext.DEFAULT));
492 		assertThrowsWithMessage(SchemaValidationException.class, "Minimum value not met.", ()->s.getItems().getItems().getItems().validateOutput(1000, BeanContext.DEFAULT));
493 		assertThrowsWithMessage(SchemaValidationException.class, "Minimum value not met.", ()->s.getItems().getItems().getItems().getItems().validateOutput(10000, BeanContext.DEFAULT));
494 
495 		assertThrowsWithMessage(SchemaValidationException.class, "Maximum value exceeded.", ()->s.getItems().validateOutput(100, BeanContext.DEFAULT));
496 		assertThrowsWithMessage(SchemaValidationException.class, "Maximum value exceeded.", ()->s.getItems().getItems().validateOutput(1000, BeanContext.DEFAULT));
497 		assertThrowsWithMessage(SchemaValidationException.class, "Maximum value exceeded.", ()->s.getItems().getItems().getItems().validateOutput(10000, BeanContext.DEFAULT));
498 		assertThrowsWithMessage(SchemaValidationException.class, "Maximum value exceeded.", ()->s.getItems().getItems().getItems().getItems().validateOutput(100000, BeanContext.DEFAULT));
499 	}
500 
501 	@Path
502 	@Schema(min="10.1", max="100.1")
503 	public static class C03a {}
504 
505 	@Test void c03_minmax_floats() throws Exception {
506 		var s = HttpPartSchema.create().applyAll(Path.class, C03a.class).build();
507 		s.validateOutput(10.1f, BeanContext.DEFAULT);
508 		s.validateOutput(100.1f, BeanContext.DEFAULT);
509 		s.validateOutput(null, BeanContext.DEFAULT);
510 		assertThrowsWithMessage(SchemaValidationException.class, "Minimum value not met.", ()->s.validateOutput(10f, BeanContext.DEFAULT));
511 		assertThrowsWithMessage(SchemaValidationException.class, "Maximum value exceeded.", ()->s.validateOutput(100.2f, BeanContext.DEFAULT));
512 	}
513 
514 	@Path
515 	@Schema(
516 		items=@Items(
517 			min="10.1", max="100.1",
518 			items=@SubItems(
519 				min="100.1", max="1000.1",
520 				items={
521 					"minimum:1000.1,maximum:10000.1,",
522 					"items:{minimum:10000.1,maximum:100000.1}"
523 				}
524 			)
525 		)
526 	)
527 	public static class C03b {}
528 
529 	@Test void c03b_minmax_floats_items() throws Exception {
530 		var s = HttpPartSchema.create().applyAll(Path.class, C03b.class).build();
531 
532 		s.getItems().validateOutput(10.1f, BeanContext.DEFAULT);
533 		s.getItems().getItems().validateOutput(100.1f, BeanContext.DEFAULT);
534 		s.getItems().getItems().getItems().validateOutput(1000.1f, BeanContext.DEFAULT);
535 		s.getItems().getItems().getItems().getItems().validateOutput(10000.1f, BeanContext.DEFAULT);
536 
537 		s.getItems().validateOutput(100.1f, BeanContext.DEFAULT);
538 		s.getItems().getItems().validateOutput(1000.1f, BeanContext.DEFAULT);
539 		s.getItems().getItems().getItems().validateOutput(10000.1f, BeanContext.DEFAULT);
540 		s.getItems().getItems().getItems().getItems().validateOutput(100000.1f, BeanContext.DEFAULT);
541 
542 		assertThrowsWithMessage(SchemaValidationException.class, "Minimum value not met.", ()->s.getItems().validateOutput(10f, BeanContext.DEFAULT));
543 		assertThrowsWithMessage(SchemaValidationException.class, "Minimum value not met.", ()->s.getItems().getItems().validateOutput(100f, BeanContext.DEFAULT));
544 		assertThrowsWithMessage(SchemaValidationException.class, "Minimum value not met.", ()->s.getItems().getItems().getItems().validateOutput(1000f, BeanContext.DEFAULT));
545 		assertThrowsWithMessage(SchemaValidationException.class, "Minimum value not met.", ()->s.getItems().getItems().getItems().getItems().validateOutput(10000f, BeanContext.DEFAULT));
546 
547 		assertThrowsWithMessage(SchemaValidationException.class, "Maximum value exceeded.", ()->s.getItems().validateOutput(100.2f, BeanContext.DEFAULT));
548 		assertThrowsWithMessage(SchemaValidationException.class, "Maximum value exceeded.", ()->s.getItems().getItems().validateOutput(1000.2f, BeanContext.DEFAULT));
549 		assertThrowsWithMessage(SchemaValidationException.class, "Maximum value exceeded.", ()->s.getItems().getItems().getItems().validateOutput(10000.2f, BeanContext.DEFAULT));
550 		assertThrowsWithMessage(SchemaValidationException.class, "Maximum value exceeded.", ()->s.getItems().getItems().getItems().getItems().validateOutput(100000.2f, BeanContext.DEFAULT));
551 	}
552 
553 	@Path
554 	@Schema(min="10.1", max="100.1", emin=true, emax=true)
555 	public static class C04a {}
556 
557 	@Test void c04a_minmax_floats_exclusive() throws Exception {
558 		var s = HttpPartSchema.create().applyAll(Path.class, C04a.class).build();
559 		s.validateOutput(10.2f, BeanContext.DEFAULT);
560 		s.validateOutput(100f, BeanContext.DEFAULT);
561 		s.validateOutput(null, BeanContext.DEFAULT);
562 		assertThrowsWithMessage(SchemaValidationException.class, "Minimum value not met.", ()->s.validateOutput(10.1f, BeanContext.DEFAULT));
563 		assertThrowsWithMessage(SchemaValidationException.class, "Maximum value exceeded.", ()->s.validateOutput(100.1f, BeanContext.DEFAULT));
564 	}
565 
566 	@Path
567 	@Schema(
568 		items=@Items(
569 			min="10.1", max="100.1", emin=true, emax=true,
570 			items=@SubItems(
571 				min="100.1", max="1000.1", emin=true, emax=true,
572 				items={
573 					"minimum:1000.1,maximum:10000.1,exclusiveMinimum:true,exclusiveMaximum:true,",
574 					"items:{minimum:10000.1,maximum:100000.1,exclusiveMinimum:true,exclusiveMaximum:true}"
575 				}
576 			)
577 		)
578 	)
579 	public static class C04b {}
580 
581 	@Test void c04b_minmax_floats_exclusive_items() throws Exception {
582 		var s = HttpPartSchema.create().applyAll(Path.class, C04b.class).build();
583 
584 		s.getItems().validateOutput(10.2f, BeanContext.DEFAULT);
585 		s.getItems().getItems().validateOutput(100.2f, BeanContext.DEFAULT);
586 		s.getItems().getItems().getItems().validateOutput(1000.2f, BeanContext.DEFAULT);
587 		s.getItems().getItems().getItems().getItems().validateOutput(10000.2f, BeanContext.DEFAULT);
588 
589 		s.getItems().validateOutput(100f, BeanContext.DEFAULT);
590 		s.getItems().getItems().validateOutput(1000f, BeanContext.DEFAULT);
591 		s.getItems().getItems().getItems().validateOutput(10000f, BeanContext.DEFAULT);
592 		s.getItems().getItems().getItems().getItems().validateOutput(100000f, BeanContext.DEFAULT);
593 
594 		assertThrowsWithMessage(SchemaValidationException.class, "Minimum value not met.", ()->s.getItems().validateOutput(10.1f, BeanContext.DEFAULT));
595 		assertThrowsWithMessage(SchemaValidationException.class, "Minimum value not met.", ()->s.getItems().getItems().validateOutput(100.1f, BeanContext.DEFAULT));
596 		assertThrowsWithMessage(SchemaValidationException.class, "Minimum value not met.", ()->s.getItems().getItems().getItems().validateOutput(1000.1f, BeanContext.DEFAULT));
597 		assertThrowsWithMessage(SchemaValidationException.class, "Minimum value not met.", ()->s.getItems().getItems().getItems().getItems().validateOutput(10000.1f, BeanContext.DEFAULT));
598 
599 		assertThrowsWithMessage(SchemaValidationException.class, "Maximum value exceeded.", ()->s.getItems().validateOutput(100.1f, BeanContext.DEFAULT));
600 		assertThrowsWithMessage(SchemaValidationException.class, "Maximum value exceeded.", ()->s.getItems().getItems().validateOutput(1000.1f, BeanContext.DEFAULT));
601 		assertThrowsWithMessage(SchemaValidationException.class, "Maximum value exceeded.", ()->s.getItems().getItems().getItems().validateOutput(10000.1f, BeanContext.DEFAULT));
602 		assertThrowsWithMessage(SchemaValidationException.class, "Maximum value exceeded.", ()->s.getItems().getItems().getItems().getItems().validateOutput(100000.1f, BeanContext.DEFAULT));
603 	}
604 
605 	@Path
606 	@Schema(mo="10")
607 	public static class C05a {}
608 
609 	@Test void c05a_multipleOf() throws Exception {
610 		var s = HttpPartSchema.create().applyAll(Path.class, C05a.class).build();
611 		s.validateOutput(0, BeanContext.DEFAULT);
612 		s.validateOutput(10, BeanContext.DEFAULT);
613 		s.validateOutput(20, BeanContext.DEFAULT);
614 		s.validateOutput(10f, BeanContext.DEFAULT);
615 		s.validateOutput(20f, BeanContext.DEFAULT);
616 		s.validateOutput(null, BeanContext.DEFAULT);
617 		assertThrowsWithMessage(SchemaValidationException.class, "Multiple-of not met.", ()->s.validateOutput(11, BeanContext.DEFAULT));
618 	}
619 
620 	@Path
621 	@Schema(
622 		items=@Items(
623 			mo="10",
624 			items=@SubItems(
625 				mo="100",
626 				items={
627 					"multipleOf:1000,",
628 					"items:{multipleOf:10000}"
629 				}
630 			)
631 		)
632 	)
633 	public static class C05b {}
634 
635 	@Test void c05b_multipleOf_items() throws Exception {
636 		var s = HttpPartSchema.create().applyAll(Path.class, C05b.class).build();
637 
638 		s.getItems().validateOutput(0, BeanContext.DEFAULT);
639 		s.getItems().getItems().validateOutput(0, BeanContext.DEFAULT);
640 		s.getItems().getItems().getItems().validateOutput(0, BeanContext.DEFAULT);
641 		s.getItems().getItems().getItems().getItems().validateOutput(0, BeanContext.DEFAULT);
642 
643 		s.getItems().validateOutput(10, BeanContext.DEFAULT);
644 		s.getItems().getItems().validateOutput(100, BeanContext.DEFAULT);
645 		s.getItems().getItems().getItems().validateOutput(1000, BeanContext.DEFAULT);
646 		s.getItems().getItems().getItems().getItems().validateOutput(10000, BeanContext.DEFAULT);
647 
648 		s.getItems().validateOutput(20, BeanContext.DEFAULT);
649 		s.getItems().getItems().validateOutput(200, BeanContext.DEFAULT);
650 		s.getItems().getItems().getItems().validateOutput(2000, BeanContext.DEFAULT);
651 		s.getItems().getItems().getItems().getItems().validateOutput(20000, BeanContext.DEFAULT);
652 
653 		s.getItems().validateOutput(10f, BeanContext.DEFAULT);
654 		s.getItems().getItems().validateOutput(100f, BeanContext.DEFAULT);
655 		s.getItems().getItems().getItems().validateOutput(1000f, BeanContext.DEFAULT);
656 		s.getItems().getItems().getItems().getItems().validateOutput(10000f, BeanContext.DEFAULT);
657 
658 		s.getItems().validateOutput(20f, BeanContext.DEFAULT);
659 		s.getItems().getItems().validateOutput(200f, BeanContext.DEFAULT);
660 		s.getItems().getItems().getItems().validateOutput(2000f, BeanContext.DEFAULT);
661 		s.getItems().getItems().getItems().getItems().validateOutput(20000f, BeanContext.DEFAULT);
662 
663 		assertThrowsWithMessage(SchemaValidationException.class, "Multiple-of not met.", ()->s.getItems().validateOutput(11, BeanContext.DEFAULT));
664 		assertThrowsWithMessage(SchemaValidationException.class, "Multiple-of not met.", ()->s.getItems().getItems().validateOutput(101, BeanContext.DEFAULT));
665 		assertThrowsWithMessage(SchemaValidationException.class, "Multiple-of not met.", ()->s.getItems().getItems().getItems().validateOutput(1001, BeanContext.DEFAULT));
666 		assertThrowsWithMessage(SchemaValidationException.class, "Multiple-of not met.", ()->s.getItems().getItems().getItems().getItems().validateOutput(10001, BeanContext.DEFAULT));
667 	}
668 
669 	@Path
670 	@Schema(mo="10.1")
671 	public static class C06a {}
672 
673 	@Test void c06a_multipleOf_floats() throws Exception {
674 		var s = HttpPartSchema.create().applyAll(Path.class, C06a.class).build();
675 		s.validateOutput(0, BeanContext.DEFAULT);
676 		s.validateOutput(10.1f, BeanContext.DEFAULT);
677 		s.validateOutput(20.2f, BeanContext.DEFAULT);
678 		s.validateOutput(null, BeanContext.DEFAULT);
679 		assertThrowsWithMessage(SchemaValidationException.class, "Multiple-of not met.", ()->s.validateOutput(10.2f, BeanContext.DEFAULT));
680 	}
681 
682 	@Path
683 	@Schema(
684 		items=@Items(
685 			mo="10.1",
686 			items=@SubItems(
687 				mo="100.1",
688 				items={
689 					"multipleOf:1000.1,",
690 					"items:{multipleOf:10000.1}"
691 				}
692 			)
693 		)
694 	)
695 	public static class C06b {}
696 
697 	@Test void c06b_multipleOf_floats_items() throws Exception {
698 		var s = HttpPartSchema.create().applyAll(Path.class, C06b.class).build();
699 
700 		s.getItems().validateOutput(0, BeanContext.DEFAULT);
701 		s.getItems().getItems().validateOutput(0, BeanContext.DEFAULT);
702 		s.getItems().getItems().getItems().validateOutput(0, BeanContext.DEFAULT);
703 		s.getItems().getItems().getItems().getItems().validateOutput(0, BeanContext.DEFAULT);
704 
705 		s.getItems().validateOutput(10.1f, BeanContext.DEFAULT);
706 		s.getItems().getItems().validateOutput(100.1f, BeanContext.DEFAULT);
707 		s.getItems().getItems().getItems().validateOutput(1000.1f, BeanContext.DEFAULT);
708 		s.getItems().getItems().getItems().getItems().validateOutput(10000.1f, BeanContext.DEFAULT);
709 
710 		s.getItems().validateOutput(20.2f, BeanContext.DEFAULT);
711 		s.getItems().getItems().validateOutput(200.2f, BeanContext.DEFAULT);
712 		s.getItems().getItems().getItems().validateOutput(2000.2f, BeanContext.DEFAULT);
713 		s.getItems().getItems().getItems().getItems().validateOutput(20000.2f, BeanContext.DEFAULT);
714 
715 		assertThrowsWithMessage(SchemaValidationException.class, "Multiple-of not met.", ()->s.getItems().validateOutput(10.2f, BeanContext.DEFAULT));
716 		assertThrowsWithMessage(SchemaValidationException.class, "Multiple-of not met.", ()->s.getItems().getItems().validateOutput(100.2f, BeanContext.DEFAULT));
717 		assertThrowsWithMessage(SchemaValidationException.class, "Multiple-of not met.", ()->s.getItems().getItems().getItems().validateOutput(1000.2f, BeanContext.DEFAULT));
718 		assertThrowsWithMessage(SchemaValidationException.class, "Multiple-of not met.", ()->s.getItems().getItems().getItems().getItems().validateOutput(10000.2f, BeanContext.DEFAULT));
719 	}
720 
721 	//-----------------------------------------------------------------------------------------------------------------
722 	// Collections/Array validations
723 	//-----------------------------------------------------------------------------------------------------------------
724 
725 	@Path
726 	@Schema(
727 		items=@Items(
728 			uniqueItems=true,
729 			items=@SubItems(
730 				uniqueItems=true,
731 				items={
732 					"uniqueItems:true,",
733 					"items:{uniqueItems:true}"
734 				}
735 			)
736 		)
737 
738 	)
739 	public static class D01 {}
740 
741 	@Test void d01a_uniqueItems_arrays() throws Exception {
742 		var s = HttpPartSchema.create().applyAll(Path.class, D01.class).build();
743 
744 		var good = split("a,b");
745 		var bad = split("a,a");
746 
747 		s.getItems().validateOutput(good, BeanContext.DEFAULT);
748 		s.getItems().getItems().validateOutput(good, BeanContext.DEFAULT);
749 		s.getItems().getItems().getItems().validateOutput(good, BeanContext.DEFAULT);
750 		s.getItems().getItems().getItems().getItems().validateOutput(good, BeanContext.DEFAULT);
751 		s.getItems().validateOutput(null, BeanContext.DEFAULT);
752 
753 		assertThrowsWithMessage(SchemaValidationException.class, "Duplicate items not allowed.", ()->s.getItems().validateOutput(bad, BeanContext.DEFAULT));
754 		assertThrowsWithMessage(SchemaValidationException.class, "Duplicate items not allowed.", ()->s.getItems().getItems().validateOutput(bad, BeanContext.DEFAULT));
755 		assertThrowsWithMessage(SchemaValidationException.class, "Duplicate items not allowed.", ()->s.getItems().getItems().getItems().validateOutput(bad, BeanContext.DEFAULT));
756 		assertThrowsWithMessage(SchemaValidationException.class, "Duplicate items not allowed.", ()->s.getItems().getItems().getItems().getItems().validateOutput(bad, BeanContext.DEFAULT));
757 	}
758 
759 	@Test void d01b_uniqueItems_collections() throws Exception {
760 		var s = HttpPartSchema.create().applyAll(Path.class, D01.class).build();
761 
762 		List<String>
763 			good = l("a","b"),
764 			bad = l("a","a");
765 
766 		s.getItems().validateOutput(good, BeanContext.DEFAULT);
767 		s.getItems().getItems().validateOutput(good, BeanContext.DEFAULT);
768 		s.getItems().getItems().getItems().validateOutput(good, BeanContext.DEFAULT);
769 		s.getItems().getItems().getItems().getItems().validateOutput(good, BeanContext.DEFAULT);
770 		s.getItems().validateOutput(null, BeanContext.DEFAULT);
771 
772 		assertThrowsWithMessage(SchemaValidationException.class, "Duplicate items not allowed.", ()->s.getItems().validateOutput(bad, BeanContext.DEFAULT));
773 		assertThrowsWithMessage(SchemaValidationException.class, "Duplicate items not allowed.", ()->s.getItems().getItems().validateOutput(bad, BeanContext.DEFAULT));
774 		assertThrowsWithMessage(SchemaValidationException.class, "Duplicate items not allowed.", ()->s.getItems().getItems().getItems().validateOutput(bad, BeanContext.DEFAULT));
775 		assertThrowsWithMessage(SchemaValidationException.class, "Duplicate items not allowed.", ()->s.getItems().getItems().getItems().getItems().validateOutput(bad, BeanContext.DEFAULT));
776 	}
777 
778 	@Path
779 	@Schema(
780 		items=@Items(
781 			minItems=1, maxItems=2,
782 			items=@SubItems(
783 				minItems=2, maxItems=3,
784 				items={
785 					"minItems:3,maxItems:4,",
786 					"items:{minItems:4,maxItems:5}"
787 				}
788 			)
789 		)
790 
791 	)
792 	public static class D02 {}
793 
794 	@Test void d02a_minMaxItems_arrays() throws Exception {
795 		var s = HttpPartSchema.create().applyAll(Path.class, D02.class).build();
796 
797 		s.getItems().validateOutput(split("1"), BeanContext.DEFAULT);
798 		s.getItems().getItems().validateOutput(split("1,2"), BeanContext.DEFAULT);
799 		s.getItems().getItems().getItems().validateOutput(split("1,2,3"), BeanContext.DEFAULT);
800 		s.getItems().getItems().getItems().getItems().validateOutput(split("1,2,3,4"), BeanContext.DEFAULT);
801 
802 		s.getItems().validateOutput(split("1,2"), BeanContext.DEFAULT);
803 		s.getItems().getItems().validateOutput(split("1,2,3"), BeanContext.DEFAULT);
804 		s.getItems().getItems().getItems().validateOutput(split("1,2,3,4"), BeanContext.DEFAULT);
805 		s.getItems().getItems().getItems().getItems().validateOutput(split("1,2,3,4,5"), BeanContext.DEFAULT);
806 
807 		assertThrowsWithMessage(SchemaValidationException.class, "Minimum number of items not met.", ()->s.getItems().validateOutput(new String[0], BeanContext.DEFAULT));
808 		assertThrowsWithMessage(SchemaValidationException.class, "Minimum number of items not met.", ()->s.getItems().getItems().validateOutput(split("1"), BeanContext.DEFAULT));
809 		assertThrowsWithMessage(SchemaValidationException.class, "Minimum number of items not met.", ()->s.getItems().getItems().getItems().validateOutput(split("1,2"), BeanContext.DEFAULT));
810 		assertThrowsWithMessage(SchemaValidationException.class, "Minimum number of items not met.", ()->s.getItems().getItems().getItems().getItems().validateOutput(split("1,2,3"), BeanContext.DEFAULT));
811 
812 		assertThrowsWithMessage(SchemaValidationException.class, "Maximum number of items exceeded.", ()->s.getItems().validateOutput(split("1,2,3"), BeanContext.DEFAULT));
813 		assertThrowsWithMessage(SchemaValidationException.class, "Maximum number of items exceeded.", ()->s.getItems().getItems().validateOutput(split("1,2,3,4"), BeanContext.DEFAULT));
814 		assertThrowsWithMessage(SchemaValidationException.class, "Maximum number of items exceeded.", ()->s.getItems().getItems().getItems().validateOutput(split("1,2,3,4,5"), BeanContext.DEFAULT));
815 		assertThrowsWithMessage(SchemaValidationException.class, "Maximum number of items exceeded.", ()->s.getItems().getItems().getItems().getItems().validateOutput(split("1,2,3,4,5,6"), BeanContext.DEFAULT));
816 	}
817 
818 	//-----------------------------------------------------------------------------------------------------------------
819 	// @Path with default values - required flag behavior
820 	//-----------------------------------------------------------------------------------------------------------------
821 
822 	@Path(name="x")
823 	public static class E01 {}
824 
825 	@Test void e01_required_noDefault() {
826 		var s = HttpPartSchema.create().applyAll(Path.class, E01.class).build();
827 		// Path parameters without default values should be required
828 		assertTrue(s.isRequired());
829 	}
830 
831 	@Path(name="x", def="defaultValue")
832 	public static class E02 {}
833 
834 	@Test void e02_notRequired_withDefault() {
835 		var s = HttpPartSchema.create().applyAll(Path.class, E02.class).build();
836 		// Path parameters with default values should NOT be required
837 		assertFalse(s.isRequired());
838 		assertEquals("defaultValue", s.getDefault());
839 	}
840 
841 	@Path(name="x", def="")
842 	public static class E03 {}
843 
844 	@Test void e03_notRequired_withEmptyDefault() {
845 		var s = HttpPartSchema.create().applyAll(Path.class, E03.class).build();
846 		// Path parameters with empty string default should NOT be required
847 		assertFalse(s.isRequired());
848 		assertEquals("", s.getDefault());
849 	}
850 
851 	public static class E04 {
852 		public void a(
853 				@Path(name = "logger", def = "") String loggerName,
854 				@Path(name = "level", def = "INFO") String levelName
855 			) {
856 			/* no-op */
857 		}
858 	}
859 
860 	@Test void e04_multiplePathParamsWithDefaults() throws Exception {
861 		var loggerParam = MethodInfo.of(E04.class.getMethod("a", String.class, String.class)).getParameter(0);
862 		var levelParam = MethodInfo.of(E04.class.getMethod("a", String.class, String.class)).getParameter(1);
863 
864 		var loggerSchema = HttpPartSchema.create().applyAll(Path.class, loggerParam).build();
865 		var levelSchema = HttpPartSchema.create().applyAll(Path.class, levelParam).build();
866 
867 		// Both should not be required since they have defaults
868 		assertFalse(loggerSchema.isRequired());
869 		assertFalse(levelSchema.isRequired());
870 
871 		// Verify defaults are set correctly
872 		assertEquals("", loggerSchema.getDefault());
873 		assertEquals("INFO", levelSchema.getDefault());
874 
875 		// Verify names are set correctly
876 		assertEquals("logger", loggerSchema.getName());
877 		assertEquals("level", levelSchema.getName());
878 
879 		// This should not throw the regression error:
880 		// "Cannot specify a default value on a required value."
881 		assertDoesNotThrow(() -> loggerSchema.validateInput("test"));
882 		assertDoesNotThrow(() -> levelSchema.validateInput("DEBUG"));
883 	}
884 
885 	@Path(value="/remainder")
886 	public static class E05 {}
887 
888 	@Test void e05_pathRemainder_notRequired() {
889 		var s = HttpPartSchema.create().applyAll(Path.class, E05.class).build();
890 		// Path remainders (starting with '/') should never be required
891 		assertFalse(s.isRequired());
892 	}
893 }