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.bean.openapi3;
18  
19  import static org.apache.juneau.TestUtils.*;
20  import static org.apache.juneau.bean.openapi3.OpenApiBuilder.*;
21  import static org.junit.jupiter.api.Assertions.*;
22  
23  import java.net.*;
24  import java.util.*;
25  
26  import org.apache.juneau.*;
27  import org.junit.jupiter.api.*;
28  
29  /**
30   * Testcase for {@link SchemaInfo}.
31   */
32  class SchemaInfo_Test extends TestBase {
33  
34  	@Nested class A_basicTests extends TestBase {
35  
36  		private static final BeanTester<SchemaInfo> TESTER =
37  			testBean(
38  				bean()
39  					.setDefault("a")
40  					.setDeprecated(true)
41  					.setDescription("b")
42  					.setEnum(list("c1", "c2"))
43  					.setExample("d")
44  					.setExclusiveMaximum(true)
45  					.setExclusiveMinimum(true)
46  					.setFormat("e")
47  					.setItems(items("f"))
48  					.setMaxItems(1)
49  					.setMaxLength(2)
50  					.setMaxProperties(3)
51  					.setMaximum(4)
52  					.setMinItems(5)
53  					.setMinLength(6)
54  					.setMinProperties(7)
55  					.setMinimum(8)
56  					.setMultipleOf(9)
57  					.setNullable(true)
58  					.setPattern("g")
59  					.setReadOnly(true)
60  					.setRef("h")
61  					.setRequired(list("i"))
62  					.setTitle("j")
63  					.setType("k")
64  					.setUniqueItems(true)
65  					.setWriteOnly(true)
66  			)
67  			.props("default,deprecated,description,enum,example,exclusiveMaximum,exclusiveMinimum,format,items{type},maxItems,maxLength,maxProperties,maximum,minItems,minLength,minProperties,minimum,multipleOf,nullable,pattern,readOnly,ref,required,title,type,uniqueItems,writeOnly")
68  			.vals("a,true,b,[c1,c2],d,true,true,e,{f},1,2,3,4,5,6,7,8,9,true,g,true,h,[i],j,k,true,true")
69  			.json("{'$ref':'h','default':'a',deprecated:true,description:'b','enum':['c1','c2'],example:'d',exclusiveMaximum:true,exclusiveMinimum:true,format:'e',items:{type:'f'},maxItems:1,maxLength:2,maxProperties:3,maximum:4,minItems:5,minLength:6,minProperties:7,minimum:8,multipleOf:9,nullable:true,pattern:'g',readOnly:true,required:['i'],title:'j',type:'k',uniqueItems:true,writeOnly:true}")
70  			.string("{'$ref':'h','default':'a','deprecated':true,'description':'b','enum':['c1','c2'],'example':'d','exclusiveMaximum':true,'exclusiveMinimum':true,'format':'e','items':{'type':'f'},'maxItems':1,'maxLength':2,'maxProperties':3,'maximum':4,'minItems':5,'minLength':6,'minProperties':7,'minimum':8,'multipleOf':9,'nullable':true,'pattern':'g','readOnly':true,'required':['i'],'title':'j','type':'k','uniqueItems':true,'writeOnly':true}".replace('\'','"'))
71  		;
72  
73  		@Test void a01_gettersAndSetters() {
74  			TESTER.assertGettersAndSetters();
75  		}
76  
77  		@Test void a02_copy() {
78  			TESTER.assertCopy();
79  		}
80  
81  		@Test void a03_toJson() {
82  			TESTER.assertToJson();
83  		}
84  
85  		@Test void a04_fromJson() {
86  			TESTER.assertFromJson();
87  		}
88  
89  		@Test void a05_roundTrip() {
90  			TESTER.assertRoundTrip();
91  		}
92  
93  		@Test void a06_toString() {
94  			TESTER.assertToString();
95  		}
96  
97  		@Test void a07_keySet() {
98  			assertList(TESTER.bean().keySet(), "$ref", "default", "deprecated", "description", "enum", "example", "exclusiveMaximum", "exclusiveMinimum", "format", "items", "maxItems", "maxLength", "maxProperties", "maximum", "minItems", "minLength", "minProperties", "minimum", "multipleOf", "nullable", "pattern", "readOnly", "required", "title", "type", "uniqueItems", "writeOnly");
99  		}
100 
101 		@Test void a08_nullParameters() {
102 			var x = bean();
103 			assertThrows(IllegalArgumentException.class, () -> x.get(null, String.class));
104 			assertThrows(IllegalArgumentException.class, () -> x.set(null, "value"));
105 		}
106 
107 		@Test void a08b_getSetRef() {
108 			// Test get/set with "$ref" property to cover switch branches
109 			var x = bean();
110 			x.set("$ref", "#/components/schemas/MySchema");
111 			assertEquals("#/components/schemas/MySchema", x.get("$ref", String.class));
112 			assertEquals("#/components/schemas/MySchema", x.getRef());
113 		}
114 
115 		@Test void a09_addMethods() {
116 			assertBean(
117 				bean()
118 					.addEnum("a1", "a2")
119 					.addRequired("b1", "b2")
120 					.addAllOf(schemaInfo("c1"), schemaInfo("c2"))
121 					.addAnyOf(schemaInfo("d1"), schemaInfo("d2"))
122 					.addOneOf(schemaInfo("e1"), schemaInfo("e2")),
123 				"enum,required,allOf{#{type}},anyOf{#{type}},oneOf{#{type}}",
124 				"[a1,a2],[b1,b2],{[{c1},{c2}]},{[{d1},{d2}]},{[{e1},{e2}]}"
125 			);
126 		}
127 
128 		@Test void a10_asMap() {
129 			assertBean(
130 				bean()
131 					.setType("a")
132 					.set("x1", "x1a")
133 					.asMap(),
134 				"type,x1",
135 				"a,x1a"
136 			);
137 		}
138 
139 		@Test void a11_extraKeys() {
140 			var x = bean().set("x1", "x1a").set("x2", "x2a");
141 			assertList(x.extraKeys(), "x1", "x2");
142 			assertEmpty(bean().extraKeys());
143 		}
144 
145 		@Test void a12_strictMode() {
146 			assertThrows(RuntimeException.class, () -> bean().strict().set("foo", "bar"));
147 			assertDoesNotThrow(() -> bean().set("foo", "bar"));
148 
149 			assertFalse(bean().isStrict());
150 			assertTrue(bean().strict().isStrict());
151 			assertFalse(bean().strict(false).isStrict());
152 		}
153 		
154 		@Test void a13_collectionSetters() {
155 			var x = bean()
156 				.setEnum(list("a1", "a2"))
157 				.setRequired(list("b1", "b2"));
158 
159 			assertBean(x,
160 				"enum,required",
161 				"[a1,a2],[b1,b2]"
162 			);
163 		}
164 	}
165 
166 	@Nested class B_emptyTests extends TestBase {
167 
168 		private static final BeanTester<SchemaInfo> TESTER =
169 			testBean(bean())
170 			.props("type,format,title,description,default,multipleOf,maximum,exclusiveMaximum,minimum,exclusiveMinimum,maxLength,minLength,pattern,maxItems,minItems,uniqueItems,maxProperties,minProperties,required,enum,items,example,nullable,readOnly,writeOnly,deprecated,ref")
171 			.vals("<null>,<null>,<null>,<null>,<null>,<null>,<null>,<null>,<null>,<null>,<null>,<null>,<null>,<null>,<null>,<null>,<null>,<null>,<null>,<null>,<null>,<null>,<null>,<null>,<null>,<null>,<null>")
172 			.json("{}")
173 			.string("{}")
174 		;
175 
176 		@Test void b01_gettersAndSetters() {
177 			TESTER.assertGettersAndSetters();
178 		}
179 
180 		@Test void b02_copy() {
181 			TESTER.assertCopy();
182 		}
183 
184 		@Test void b03_toJson() {
185 			TESTER.assertToJson();
186 		}
187 
188 		@Test void b04_fromJson() {
189 			TESTER.assertFromJson();
190 		}
191 
192 		@Test void b05_roundTrip() {
193 			TESTER.assertRoundTrip();
194 		}
195 
196 		@Test void b06_toString() {
197 			TESTER.assertToString();
198 		}
199 
200 		@Test void b07_keySet() {
201 			assertEmpty(TESTER.bean().keySet());
202 		}
203 	}
204 
205 	@Nested class C_extraProperties extends TestBase {
206 		private static final BeanTester<SchemaInfo> TESTER =
207 			testBean(
208 				bean()
209 					.set("additionalItems", schemaInfo("a"))
210 					.set("additionalProperties", schemaInfo("b"))
211 					.set("allOf", list(schemaInfo("c1"), schemaInfo("c2")))
212 					.set("anyOf", list(schemaInfo("d1"), schemaInfo("d2")))
213 					.set("default", "e")
214 					.set("description", "f")
215 					.set("discriminator", discriminator("g"))
216 					.set("enum", list("h1", "h2"))
217 					.set("example", "i")
218 					.set("exclusiveMaximum", true)
219 					.set("exclusiveMinimum", true)
220 					.set("externalDocs", externalDocumentation().setUrl(URI.create("j")))
221 					.set("format", "k")
222 					.set("items", schemaInfo("l"))
223 					.set("maxItems", 1)
224 					.set("maxLength", 2)
225 					.set("maxProperties", 3)
226 					.set("maximum", 4)
227 					.set("minItems", 5)
228 					.set("minLength", 6)
229 					.set("minProperties", 7)
230 					.set("minimum", 8)
231 					.set("multipleOf", 9)
232 					.set("not", schemaInfo("m"))
233 					.set("nullable", true)
234 					.set("oneOf", list(schemaInfo("n1"), schemaInfo("n2")))
235 					.set("pattern", "o")
236 					.set("properties", map("p1", schemaInfo("p2")))
237 					.set("readOnly", true)
238 					.set("required", list("q1", "q2"))
239 					.set("title", "r")
240 					.set("type", "s")
241 					.set("uniqueItems", true)
242 					.set("writeOnly", true)
243 					.set("xml", xml().setName("t"))
244 					.set("x1", "x1a")
245 					.set("x2", null)
246 			)
247 			.props("additionalItems{type},additionalProperties{type},allOf{#{type}},anyOf{#{type}},default,description,discriminator{propertyName},enum{#{toString}},example,exclusiveMaximum,exclusiveMinimum,externalDocs{url},format,items{type},maxItems,maxLength,maxProperties,maximum,minItems,minLength,minProperties,minimum,multipleOf,not{type},nullable,oneOf{#{type}},pattern,properties{p1{type}},readOnly,required{#{toString}},title,type,uniqueItems,writeOnly,xml{name},x1,x2")
248 			.vals("{a},{b},{[{c1},{c2}]},{[{d1},{d2}]},e,f,{g},{[{h1},{h2}]},i,true,true,{j},k,{l},1,2,3,4,5,6,7,8,9,{m},true,{[{n1},{n2}]},o,{{p2}},true,{[{q1},{q2}]},r,s,true,true,{t},x1a,<null>")
249 			.json("{additionalItems:{type:'a'},additionalProperties:{type:'b'},allOf:[{type:'c1'},{type:'c2'}],anyOf:[{type:'d1'},{type:'d2'}],'default':'e',description:'f',discriminator:{propertyName:'g'},'enum':['h1','h2'],example:'i',exclusiveMaximum:true,exclusiveMinimum:true,externalDocs:{url:'j'},format:'k',items:{type:'l'},maxItems:1,maxLength:2,maxProperties:3,maximum:4,minItems:5,minLength:6,minProperties:7,minimum:8,multipleOf:9,not:{type:'m'},nullable:true,oneOf:[{type:'n1'},{type:'n2'}],pattern:'o',properties:{p1:{type:'p2'}},readOnly:true,required:['q1','q2'],title:'r',type:'s',uniqueItems:true,writeOnly:true,x1:'x1a',xml:{name:'t'}}")
250 			.string("{'additionalItems':{'type':'a'},'additionalProperties':{'type':'b'},'allOf':[{'type':'c1'},{'type':'c2'}],'anyOf':[{'type':'d1'},{'type':'d2'}],'default':'e','description':'f','discriminator':{'propertyName':'g'},'enum':['h1','h2'],'example':'i','exclusiveMaximum':true,'exclusiveMinimum':true,'externalDocs':{'url':'j'},'format':'k','items':{'type':'l'},'maxItems':1,'maxLength':2,'maxProperties':3,'maximum':4,'minItems':5,'minLength':6,'minProperties':7,'minimum':8,'multipleOf':9,'not':{'type':'m'},'nullable':true,'oneOf':[{'type':'n1'},{'type':'n2'}],'pattern':'o','properties':{'p1':{'type':'p2'}},'readOnly':true,'required':['q1','q2'],'title':'r','type':'s','uniqueItems':true,'writeOnly':true,'x1':'x1a','xml':{'name':'t'}}".replace('\'', '"'))
251 		;
252 
253 		@Test void c01_gettersAndSetters() {
254 			TESTER.assertGettersAndSetters();
255 		}
256 
257 		@Test void c02_copy() {
258 			TESTER.assertCopy();
259 		}
260 
261 		@Test void c03_toJson() {
262 			TESTER.assertToJson();
263 		}
264 
265 		@Test void c04_fromJson() {
266 			TESTER.assertFromJson();
267 		}
268 
269 		@Test void c05_roundTrip() {
270 			TESTER.assertRoundTrip();
271 		}
272 
273 		@Test void c06_toString() {
274 			TESTER.assertToString();
275 		}
276 
277 		@Test void c07_keySet() {
278 			assertList(TESTER.bean().keySet(), "additionalItems", "additionalProperties", "allOf", "anyOf", "default", "description", "discriminator", "enum", "example", "exclusiveMaximum", "exclusiveMinimum", "externalDocs", "format", "items", "maxItems", "maxLength", "maxProperties", "maximum", "minItems", "minLength", "minProperties", "minimum", "multipleOf", "not", "nullable", "oneOf", "pattern", "properties", "readOnly", "required", "title", "type", "uniqueItems", "writeOnly", "x1", "x2", "xml");
279 		}
280 
281 		@Test void c08_get() {
282 			assertMapped(
283 				TESTER.bean(), (obj,prop) -> obj.get(prop, Object.class),
284 				"additionalItems{type},additionalProperties{type},allOf{#{type}},anyOf{#{type}},default,description,discriminator{propertyName},enum{#{toString}},example,exclusiveMaximum,exclusiveMinimum,externalDocs{url},format,items{type},maxItems,maxLength,maxProperties,maximum,minItems,minLength,minProperties,minimum,multipleOf,not{type},nullable,oneOf{#{type}},pattern,properties{p1{type}},readOnly,required{#{toString}},title,type,uniqueItems,writeOnly,xml{name},x1,x2",
285 				"{a},{b},{[{c1},{c2}]},{[{d1},{d2}]},e,f,{g},{[{h1},{h2}]},i,true,true,{j},k,{l},1,2,3,4,5,6,7,8,9,{m},true,{[{n1},{n2}]},o,{{p2}},true,{[{q1},{q2}]},r,s,true,true,{t},x1a,<null>"
286 			);
287 		}
288 
289 		@Test void c09_getTypes() {
290 			assertMapped(
291 				TESTER.bean(), (obj,prop) -> simpleClassNameOf(obj.get(prop, Object.class)),
292 				"additionalItems,additionalProperties,allOf,anyOf,default,description,discriminator,enum,example,exclusiveMaximum,exclusiveMinimum,externalDocs,format,items,maxItems,maxLength,maxProperties,maximum,minItems,minLength,minProperties,minimum,multipleOf,not,nullable,oneOf,pattern,properties,readOnly,required,title,type,uniqueItems,writeOnly,xml,x1,x2",
293 				"SchemaInfo,SchemaInfo,ArrayList,ArrayList,String,String,Discriminator,ArrayList,String,Boolean,Boolean,ExternalDocumentation,String,Items,Integer,Integer,Integer,Integer,Integer,Integer,Integer,Integer,Integer,SchemaInfo,Boolean,ArrayList,String,LinkedHashMap,Boolean,ArrayList,String,String,Boolean,Boolean,Xml,String,<null>"
294 			);
295 		}
296 
297 		@Test void c10_nullPropertyValue() {
298 			assertThrows(IllegalArgumentException.class, ()->bean().get(null));
299 			assertThrows(IllegalArgumentException.class, ()->bean().get(null, String.class));
300 			assertThrows(IllegalArgumentException.class, ()->bean().set(null, "a"));
301 		}
302 	}
303 
304 	@Nested class D_refs extends TestBase {
305 
306 		@Test void d01_resolveRefs_basic() {
307 			var openApi = openApi()
308 				.setComponents(components().setSchemas(Map.of(
309 					"Pet", schemaInfo().setType("object").setTitle("Pet")
310 				)));
311 
312 			assertBean(
313 				schemaInfo().setRef("#/components/schemas/Pet").resolveRefs(openApi, new ArrayDeque<>(), 10),
314 				"type,title",
315 				"object,Pet"
316 			);
317 		}
318 
319 		@Test void d02_resolveRefs_nested() {
320 			var openApi = openApi()
321 				.setComponents(components().setSchemas(Map.of(
322 					"Pet", schemaInfo().setType("object").setTitle("Pet"),
323 					"Pets", schemaInfo().setType("array").setItems(items().setRef("#/components/schemas/Pet"))
324 				)));
325 
326 			assertBean(
327 				schemaInfo().setRef("#/components/schemas/Pets").resolveRefs(openApi, new ArrayDeque<>(), 10),
328 				"type,items{type,title}",
329 				"array,{object,Pet}"
330 			);
331 		}
332 
333 		@Test void d03_resolveRefs_maxDepth() {
334 			var openApi = openApi()
335 				.setComponents(components().setSchemas(Map.of(
336 					"Pet", schemaInfo().setType("object").setTitle("Pet"),
337 					"Pets", schemaInfo().setType("array").setItems(items().setRef("#/components/schemas/Pet"))
338 				)));
339 
340 			assertBean(
341 				schemaInfo().setRef("#/components/schemas/Pets").resolveRefs(openApi, new ArrayDeque<>(), 1),
342 				"type,items{ref}",
343 				"array,{#/components/schemas/Pet}"
344 			);
345 		}
346 
347 		@Test void d04_resolveRefs_circular() {
348 			var openApi = openApi()
349 				.setComponents(components().setSchemas(Map.of(
350 					"A", schemaInfo().setType("object").setTitle("A").setProperties(Map.of("b", schemaInfo().setRef("#/components/schemas/B"))),
351 					"B", schemaInfo().setType("object").setTitle("B").setProperties(Map.of("a", schemaInfo().setRef("#/components/schemas/A")))
352 				)));
353 
354 			assertBean(
355 				schemaInfo().setRef("#/components/schemas/A").resolveRefs(openApi, new ArrayDeque<>(), 10),
356 				"type,title,properties{b{type,title,properties{a{ref}}}}",
357 				"object,A,{{object,B,{{#/components/schemas/A}}}}"
358 			);
359 		}
360 
361 		@Test void d05_resolveRefs() {
362 			// Test resolveRefs when both ref and allOf are null (covers the missing branch)
363 			var openApi = openApi()
364 				.setComponents(components().setSchemas(map("MySchema", schemaInfo().setType("object").setTitle("My Schema"))));
365 
366 			var schema = bean()
367 				.setType("string")
368 				.setDescription("Test schema");
369 
370 			var result = schema.resolveRefs(openApi, new ArrayDeque<>(), 10);
371 
372 			// Should return the same object unchanged
373 			assertSame(schema, result);
374 			assertEquals("string", result.getType());
375 			assertEquals("Test schema", result.getDescription());
376 		}
377 
378 		@Test void d06_resolveRefs_maxDepthDirect() {
379 			// Test max depth directly (covers the refStack.size() >= maxDepth branch)
380 			var openApi = openApi()
381 				.setComponents(components().setSchemas(map("MySchema", schemaInfo().setType("string"))));
382 
383 			var refStack = new ArrayDeque<String>();
384 			refStack.add("dummy1");
385 			refStack.add("dummy2");
386 			refStack.add("dummy3");
387 			
388 			var schema = schemaInfo().setRef("#/components/schemas/MySchema");
389 			var result = schema.resolveRefs(openApi, refStack, 3);
390 
391 			// Should return the original object without resolving
392 			assertSame(schema, result);
393 			assertEquals("#/components/schemas/MySchema", result.getRef());
394 		}
395 
396 		@Test void d07_resolveRefs_additionalProperties() {
397 			// Test resolveRefs with additionalProperties to cover that branch
398 			var openApi = openApi()
399 				.setComponents(components().setSchemas(map(
400 					"MyAdditional", schemaInfo().setType("string").setDescription("Additional property schema")
401 				)));
402 
403 			var schema = schemaInfo()
404 				.setType("object")
405 				.setAdditionalProperties(schemaInfo().setRef("#/components/schemas/MyAdditional"));
406 			var result = schema.resolveRefs(openApi, new ArrayDeque<>(), 10);
407 
408 			// additionalProperties should have its ref resolved
409 			assertSame(schema, result);
410 			assertNotNull(result.getAdditionalProperties());
411 			assertEquals("string", result.getAdditionalProperties().getType());
412 			assertNull(result.getAdditionalProperties().getRef());
413 		}
414 	}
415 
416 	//---------------------------------------------------------------------------------------------
417 	// Helper methods
418 	//---------------------------------------------------------------------------------------------
419 
420 	private static SchemaInfo bean() {
421 		return schemaInfo();
422 	}
423 }