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.jsonschema.annotation;
18  
19  import static org.apache.juneau.TestUtils.*;
20  import static org.apache.juneau.junit.bct.BctAssertions.*;
21  import static org.junit.jupiter.api.Assertions.*;
22  
23  import org.apache.juneau.*;
24  import org.apache.juneau.annotation.*;
25  import org.junit.jupiter.api.*;
26  
27  class SchemaAnnotation_Test extends TestBase {
28  
29  	private static final String CNAME = SchemaAnnotation_Test.class.getName();
30  
31  	private static class X1 {}
32  
33  	//------------------------------------------------------------------------------------------------------------------
34  	// Basic tests
35  	//------------------------------------------------------------------------------------------------------------------
36  
37  	Schema a1 = SchemaAnnotation.create()
38  		.default_("a")
39  		.enum_("b")
40  		.$ref("c")
41  		.additionalProperties("d")
42  		.allOf("e")
43  		.cf("f")
44  		.collectionFormat("g")
45  		.d("h")
46  		.description("i")
47  		.df("j")
48  		.discriminator("k")
49  		.e("l")
50  		.emax(true)
51  		.emin(true)
52  		.exclusiveMaximum(true)
53  		.exclusiveMinimum(true)
54  		.externalDocs(ExternalDocsAnnotation.DEFAULT)
55  		.f("m")
56  		.format("n")
57  		.ignore(true)
58  		.items(ItemsAnnotation.DEFAULT)
59  		.max("o")
60  		.maxi(1)
61  		.maximum("p")
62  		.maxItems(2)
63  		.maxl(3)
64  		.maxLength(4)
65  		.maxp(5)
66  		.maxProperties(6)
67  		.min("q")
68  		.mini(7)
69  		.minimum("r")
70  		.minItems(8)
71  		.minl(9)
72  		.minLength(10)
73  		.minp(11)
74  		.minProperties(12)
75  		.mo("s")
76  		.multipleOf("t")
77  		.on("u")
78  		.onClass(X1.class)
79  		.p("v")
80  		.pattern("w")
81  		.properties("x")
82  		.r(true)
83  		.readOnly(true)
84  		.required(true)
85  		.ro(true)
86  		.t("z")
87  		.title("aa")
88  		.type("bb")
89  		.ui(true)
90  		.uniqueItems(true)
91  		.xml("cc")
92  		.build();
93  
94  	Schema a2 = SchemaAnnotation.create()
95  		.default_("a")
96  		.enum_("b")
97  		.$ref("c")
98  		.additionalProperties("d")
99  		.allOf("e")
100 		.cf("f")
101 		.collectionFormat("g")
102 		.d("h")
103 		.description("i")
104 		.df("j")
105 		.discriminator("k")
106 		.e("l")
107 		.emax(true)
108 		.emin(true)
109 		.exclusiveMaximum(true)
110 		.exclusiveMinimum(true)
111 		.externalDocs(ExternalDocsAnnotation.DEFAULT)
112 		.f("m")
113 		.format("n")
114 		.ignore(true)
115 		.items(ItemsAnnotation.DEFAULT)
116 		.max("o")
117 		.maxi(1)
118 		.maximum("p")
119 		.maxItems(2)
120 		.maxl(3)
121 		.maxLength(4)
122 		.maxp(5)
123 		.maxProperties(6)
124 		.min("q")
125 		.mini(7)
126 		.minimum("r")
127 		.minItems(8)
128 		.minl(9)
129 		.minLength(10)
130 		.minp(11)
131 		.minProperties(12)
132 		.mo("s")
133 		.multipleOf("t")
134 		.on("u")
135 		.onClass(X1.class)
136 		.p("v")
137 		.pattern("w")
138 		.properties("x")
139 		.r(true)
140 		.readOnly(true)
141 		.required(true)
142 		.ro(true)
143 		.t("z")
144 		.title("aa")
145 		.type("bb")
146 		.ui(true)
147 		.uniqueItems(true)
148 		.xml("cc")
149 		.build();
150 
151 	@Test void a01_basic() {
152 		assertBean(a1,
153 			"$ref,default_,enum_,additionalProperties,aev,allOf,allowEmptyValue,cf,collectionFormat,d,description,df,discriminator,e,emax,emin,exclusiveMaximum,exclusiveMinimum,externalDocs{description,url},f,format,ignore,items{$ref,default_,enum_,cf,collectionFormat,description,df,e,emax,emin,exclusiveMaximum,exclusiveMinimum,f,format,items{$ref,default_,enum_,cf,collectionFormat,description,df,e,emax,emin,exclusiveMaximum,exclusiveMinimum,f,format,items,max,maxItems,maxLength,maxi,maximum,maxl,min,minItems,minLength,mini,minimum,minl,mo,multipleOf,p,pattern,t,type,ui,uniqueItems},max,maxItems,maxLength,maxi,maximum,maxl,min,minItems,minLength,mini,minimum,minl,mo,multipleOf,p,pattern,t,type,ui,uniqueItems},max,maxItems,maxLength,maxProperties,maxi,maximum,maxl,maxp,min,minItems,minLength,minProperties,mini,minimum,minl,minp,mo,multipleOf,on,onClass,p,pattern,properties,r,readOnly,required,ro,sie,skipIfEmpty,t,title,type,ui,uniqueItems,xml",
154 			"c,[a],[b],[d],false,[e],false,f,g,[h],[i],[j],k,[l],true,true,true,true,{[],},m,n,true,{,[],[],,,[],[],[],false,false,false,false,,,{,[],[],,,[],[],[],false,false,false,false,,,[],,-1,-1,-1,,-1,,-1,-1,-1,,-1,,,,,,,false,false},,-1,-1,-1,,-1,,-1,-1,-1,,-1,,,,,,,false,false},o,2,4,6,1,p,3,5,q,8,10,12,7,r,9,11,s,t,[u],[X1],v,w,[x],true,true,true,true,false,false,z,aa,bb,true,true,[cc]");
155 	}
156 
157 	@Test void a02_testEquivalency() {
158 		assertEquals(a2, a1);
159 		assertNotEqualsAny(a1.hashCode(), 0, -1);
160 		assertEquals(a1.hashCode(), a2.hashCode());
161 	}
162 
163 	//------------------------------------------------------------------------------------------------------------------
164 	// PropertyStore equivalency.
165 	//------------------------------------------------------------------------------------------------------------------
166 
167 	@Test void b01_testEquivalencyInPropertyStores() {
168 		var bc1 = BeanContext.create().annotations(a1).build();
169 		var bc2 = BeanContext.create().annotations(a2).build();
170 		assertSame(bc1, bc2);
171 	}
172 
173 	//------------------------------------------------------------------------------------------------------------------
174 	// Other methods.
175 	//------------------------------------------------------------------------------------------------------------------
176 
177 	public static class C1 {
178 		public int f1;
179 		public void m1() {}  // NOSONAR
180 	}
181 	public static class C2 {
182 		public int f2;
183 		public void m2() {}  // NOSONAR
184 	}
185 
186 	@Test void c01_otherMethods() throws Exception {
187 		var c1 = SchemaAnnotation.create(C1.class).on(C2.class).build();
188 		var c2 = SchemaAnnotation.create("a").on("b").build();
189 		var c3 = SchemaAnnotation.create().on(C1.class.getField("f1")).on(C2.class.getField("f2")).build();
190 		var c4 = SchemaAnnotation.create().on(C1.class.getMethod("m1")).on(C2.class.getMethod("m2")).build();
191 
192 		assertBean(c1, "on", "["+CNAME+"$C1,"+CNAME+"$C2]");
193 		assertBean(c2, "on", "[a,b]");
194 		assertBean(c3, "on", "["+CNAME+"$C1.f1,"+CNAME+"$C2.f2]");
195 		assertBean(c4, "on", "["+CNAME+"$C1.m1(),"+CNAME+"$C2.m2()]");
196 	}
197 
198 	//------------------------------------------------------------------------------------------------------------------
199 	// Comparison with declared annotations.
200 	//------------------------------------------------------------------------------------------------------------------
201 
202 	@Schema(
203 		default_="a",
204 		enum_="b",
205 		$ref="c",
206 		additionalProperties="d",
207 		allOf="e",
208 		cf="f",
209 		collectionFormat="g",
210 		d="h",
211 		description="i",
212 		df="j",
213 		discriminator="k",
214 		e="l",
215 		emax=true,
216 		emin=true,
217 		exclusiveMaximum=true,
218 		exclusiveMinimum=true,
219 		externalDocs=@ExternalDocs,
220 		f="m",
221 		format="n",
222 		ignore=true,
223 		items=@Items,
224 		max="o",
225 		maxi=1,
226 		maximum="p",
227 		maxItems=2,
228 		maxl=3,
229 		maxLength=4,
230 		maxp=5,
231 		maxProperties=6,
232 		min="q",
233 		mini=7,
234 		minimum="r",
235 		minItems=8,
236 		minl=9,
237 		minLength=10,
238 		minp=11,
239 		minProperties=12,
240 		mo="s",
241 		multipleOf="t",
242 		on="u",
243 		onClass=X1.class,
244 		p="v",
245 		pattern="w",
246 		properties="x",
247 		r=true,
248 		readOnly=true,
249 		required=true,
250 		ro=true,
251 		t="z",
252 		title="aa",
253 		type="bb",
254 		ui=true,
255 		uniqueItems=true,
256 		xml="cc"
257 	)
258 	public static class D1 {}
259 	Schema d1 = D1.class.getAnnotationsByType(Schema.class)[0];
260 
261 	@Schema(
262 		default_="a",
263 		enum_="b",
264 		$ref="c",
265 		additionalProperties="d",
266 		allOf="e",
267 		cf="f",
268 		collectionFormat="g",
269 		d="h",
270 		description="i",
271 		df="j",
272 		discriminator="k",
273 		e="l",
274 		emax=true,
275 		emin=true,
276 		exclusiveMaximum=true,
277 		exclusiveMinimum=true,
278 		externalDocs=@ExternalDocs,
279 		f="m",
280 		format="n",
281 		ignore=true,
282 		items=@Items,
283 		max="o",
284 		maxi=1,
285 		maximum="p",
286 		maxItems=2,
287 		maxl=3,
288 		maxLength=4,
289 		maxp=5,
290 		maxProperties=6,
291 		min="q",
292 		mini=7,
293 		minimum="r",
294 		minItems=8,
295 		minl=9,
296 		minLength=10,
297 		minp=11,
298 		minProperties=12,
299 		mo="s",
300 		multipleOf="t",
301 		on="u",
302 		onClass=X1.class,
303 		p="v",
304 		pattern="w",
305 		properties="x",
306 		r=true,
307 		readOnly=true,
308 		required=true,
309 		ro=true,
310 		t="z",
311 		title="aa",
312 		type="bb",
313 		ui=true,
314 		uniqueItems=true,
315 		xml="cc"
316 	)
317 	public static class D2 {}
318 	Schema d2 = D2.class.getAnnotationsByType(Schema.class)[0];
319 
320 	@Test void d01_comparisonWithDeclarativeAnnotations() {
321 		assertEqualsAll(a1, d1, d2);
322 		assertNotEqualsAny(a1.hashCode(), 0, -1);
323 		assertEqualsAll(a1.hashCode(), d1.hashCode(), d2.hashCode());
324 	}
325 
326 	//------------------------------------------------------------------------------------------------------------------
327 	// JSON Schema Draft 2020-12 properties
328 	//------------------------------------------------------------------------------------------------------------------
329 
330 	Schema draft2020_1 = SchemaAnnotation.create()
331 		.const_("constantValue")
332 		.examples("example1", "example2")
333 		.$comment("This is a schema comment")
334 		.deprecatedProperty(true)
335 		.exclusiveMaximumValue("100")
336 		.exclusiveMinimumValue("0")
337 		.contentMediaType("application/json")
338 		.contentEncoding("base64")
339 		.prefixItems("string", "number")
340 		.unevaluatedItems("false")
341 		.unevaluatedProperties("false")
342 		.dependentSchemas("prop1:{type:'string'}")
343 		.dependentRequired("prop1:prop2,prop3")
344 		.if_("properties:{foo:{const:'bar'}}")
345 		.then_("required:['baz']")
346 		.else_("required:['qux']")
347 		.$defs("MyDef:{type:'string'}")
348 		.$id("https://example.com/schemas/my-schema")
349 		.build();
350 
351 	Schema draft2020_2 = SchemaAnnotation.create()
352 		.const_("constantValue")
353 		.examples("example1", "example2")
354 		.$comment("This is a schema comment")
355 		.deprecatedProperty(true)
356 		.exclusiveMaximumValue("100")
357 		.exclusiveMinimumValue("0")
358 		.contentMediaType("application/json")
359 		.contentEncoding("base64")
360 		.prefixItems("string", "number")
361 		.unevaluatedItems("false")
362 		.unevaluatedProperties("false")
363 		.dependentSchemas("prop1:{type:'string'}")
364 		.dependentRequired("prop1:prop2,prop3")
365 		.if_("properties:{foo:{const:'bar'}}")
366 		.then_("required:['baz']")
367 		.else_("required:['qux']")
368 		.$defs("MyDef:{type:'string'}")
369 		.$id("https://example.com/schemas/my-schema")
370 		.build();
371 
372 	@Test void e01_draft2020_basic() {
373 		assertBean(draft2020_1,
374 			"$comment,$defs,$id,const_,else_,if_,then_,contentEncoding,contentMediaType,dependentRequired,dependentSchemas,deprecatedProperty,examples,exclusiveMaximumValue,exclusiveMinimumValue,prefixItems,unevaluatedItems,unevaluatedProperties",
375 			"[This is a schema comment],[MyDef:{type:'string'}],https://example.com/schemas/my-schema,[constantValue],[required:['qux']],[properties:{foo:{const:'bar'}}],[required:['baz']],base64,application/json,[prop1:prop2,prop3],[prop1:{type:'string'}],true,[example1,example2],100,0,[string,number],[false],[false]");
376 	}
377 
378 	@Test void e02_draft2020_testEquivalency() {
379 		assertEquals(draft2020_2, draft2020_1);
380 		assertNotEqualsAny(draft2020_1.hashCode(), 0, -1);
381 		assertEquals(draft2020_1.hashCode(), draft2020_2.hashCode());
382 	}
383 
384 	@Test void e03_draft2020_testEquivalencyInPropertyStores() {
385 		var bc1 = BeanContext.create().annotations(draft2020_1).build();
386 		var bc2 = BeanContext.create().annotations(draft2020_2).build();
387 		assertSame(bc1, bc2);
388 	}
389 
390 	@Schema(
391 		const_="constantValue",
392 		examples={"example1", "example2"},
393 		$comment="This is a schema comment",
394 		deprecatedProperty=true,
395 		exclusiveMaximumValue="100",
396 		exclusiveMinimumValue="0",
397 		contentMediaType="application/json",
398 		contentEncoding="base64",
399 		prefixItems={"string", "number"},
400 		unevaluatedItems="false",
401 		unevaluatedProperties="false",
402 		dependentSchemas="prop1:{type:'string'}",
403 		dependentRequired="prop1:prop2,prop3",
404 		if_="properties:{foo:{const:'bar'}}",
405 		then_="required:['baz']",
406 		else_="required:['qux']",
407 		$defs="MyDef:{type:'string'}",
408 		$id="https://example.com/schemas/my-schema"
409 	)
410 	public static class E1 {}
411 	Schema e1 = E1.class.getAnnotationsByType(Schema.class)[0];
412 
413 	@Schema(
414 		const_="constantValue",
415 		examples={"example1", "example2"},
416 		$comment="This is a schema comment",
417 		deprecatedProperty=true,
418 		exclusiveMaximumValue="100",
419 		exclusiveMinimumValue="0",
420 		contentMediaType="application/json",
421 		contentEncoding="base64",
422 		prefixItems={"string", "number"},
423 		unevaluatedItems="false",
424 		unevaluatedProperties="false",
425 		dependentSchemas="prop1:{type:'string'}",
426 		dependentRequired="prop1:prop2,prop3",
427 		if_="properties:{foo:{const:'bar'}}",
428 		then_="required:['baz']",
429 		else_="required:['qux']",
430 		$defs="MyDef:{type:'string'}",
431 		$id="https://example.com/schemas/my-schema"
432 	)
433 	public static class E2 {}
434 	Schema e2 = E2.class.getAnnotationsByType(Schema.class)[0];
435 
436 	@Test void e04_draft2020_comparisonWithDeclarativeAnnotations() {
437 		assertEqualsAll(draft2020_1, e1, e2);
438 		assertNotEqualsAny(draft2020_1.hashCode(), 0, -1);
439 		assertEqualsAll(draft2020_1.hashCode(), e1.hashCode(), e2.hashCode());
440 	}
441 
442 	//------------------------------------------------------------------------------------------------------------------
443 	// Backward compatibility: exclusiveMaximum/exclusiveMinimum fallback
444 	//------------------------------------------------------------------------------------------------------------------
445 
446 	@Test void f01_backwardCompatibility_exclusiveMaxMin() {
447 		// Test that old boolean exclusiveMaximum/exclusiveMinimum still work
448 		Schema oldStyle = SchemaAnnotation.create()
449 			.exclusiveMaximum(true)
450 			.exclusiveMinimum(true)
451 			.maximum("100")
452 			.minimum("0")
453 			.build();
454 
455 		assertBean(oldStyle, "exclusiveMaximum,exclusiveMinimum,maximum,minimum", "true,true,100,0");
456 
457 		// Test that new numeric style takes precedence
458 		Schema newStyle = SchemaAnnotation.create()
459 			.exclusiveMaximumValue("100")
460 			.exclusiveMinimumValue("0")
461 			.build();
462 
463 		assertBean(newStyle, "exclusiveMaximumValue,exclusiveMinimumValue", "100,0");
464 
465 		// Test that new style takes precedence when both are set
466 		Schema mixed = SchemaAnnotation.create()
467 			.exclusiveMaximum(false)
468 			.exclusiveMinimum(false)
469 			.exclusiveMaximumValue("100")
470 			.exclusiveMinimumValue("0")
471 			.build();
472 
473 		assertBean(mixed, "exclusiveMaximum,exclusiveMinimum,exclusiveMaximumValue,exclusiveMinimumValue", "false,false,100,0");
474 	}
475 }