1   /*
2    * SPDX-FileCopyrightText: none
3    * SPDX-License-Identifier: CC0-1.0
4    */
5   
6   package dev.metaschema.databind.codegen.typeinfo;
7   
8   import static org.junit.jupiter.api.Assertions.assertEquals;
9   import static org.junit.jupiter.api.Assertions.assertNotNull;
10  import static org.junit.jupiter.api.Assertions.assertTrue;
11  
12  import com.squareup.javapoet.ClassName;
13  import com.squareup.javapoet.ParameterizedTypeName;
14  import com.squareup.javapoet.TypeName;
15  import com.squareup.javapoet.WildcardTypeName;
16  
17  import org.jmock.Expectations;
18  import org.jmock.junit5.JUnit5Mockery;
19  import org.jmock.lib.concurrent.Synchroniser;
20  import org.junit.jupiter.api.Test;
21  import org.junit.jupiter.api.extension.RegisterExtension;
22  
23  import java.util.HashMap;
24  import java.util.List;
25  import java.util.Map;
26  
27  import dev.metaschema.core.model.IAssemblyDefinition;
28  import dev.metaschema.core.model.IChoiceGroupInstance;
29  import dev.metaschema.core.model.JsonGroupAsBehavior;
30  import dev.metaschema.core.util.ObjectUtils;
31  import dev.metaschema.databind.codegen.config.IBindingConfiguration;
32  import dev.metaschema.databind.codegen.config.IChoiceGroupBindingConfiguration;
33  import dev.metaschema.databind.codegen.config.IDefinitionBindingConfiguration;
34  import dev.metaschema.databind.codegen.typeinfo.def.IAssemblyDefinitionTypeInfo;
35  import edu.umd.cs.findbugs.annotations.NonNull;
36  
37  /**
38   * Unit tests for {@link ChoiceGroupTypeInfoImpl} field type generation.
39   */
40  class ChoiceGroupTypeInfoImplTest {
41  
42    @RegisterExtension
43    JUnit5Mockery context = new JUnit5Mockery() {
44      {
45        setThreadingPolicy(new Synchroniser());
46      }
47    };
48  
49    @NonNull
50    private final IChoiceGroupInstance choiceGroupInstance
51        = ObjectUtils.notNull(context.mock(IChoiceGroupInstance.class));
52    @NonNull
53    private final IAssemblyDefinition assemblyDefinition = ObjectUtils.notNull(context.mock(IAssemblyDefinition.class));
54    @NonNull
55    private final IAssemblyDefinitionTypeInfo parentTypeInfo
56        = ObjectUtils.notNull(context.mock(IAssemblyDefinitionTypeInfo.class));
57  
58    /**
59     * Test that getJavaFieldType returns List with wildcard type when useWildcard
60     * is true.
61     */
62    @Test
63    void testGetJavaFieldTypeReturnsWildcardTypeWhenConfigured() {
64      IBindingConfiguration bindingConfig = ObjectUtils.notNull(context.mock(IBindingConfiguration.class));
65      IDefinitionBindingConfiguration defConfig
66          = ObjectUtils.notNull(context.mock(IDefinitionBindingConfiguration.class));
67      IChoiceGroupBindingConfiguration choiceConfig
68          = ObjectUtils.notNull(context.mock(IChoiceGroupBindingConfiguration.class));
69  
70      String groupAsName = "test-choices";
71      String itemTypeName = "com.example.ITestInterface";
72      ClassName itemClass = ClassName.bestGuess(itemTypeName);
73  
74      Map<String, IChoiceGroupBindingConfiguration> choiceGroupBindings = new HashMap<>();
75      choiceGroupBindings.put(groupAsName, choiceConfig);
76  
77      // Create a real resolver with mocked binding configuration
78      DefaultTypeResolver typeResolver = new DefaultTypeResolver(bindingConfig);
79  
80      context.checking(new Expectations() {
81        {
82          // Setup expectations for parent and instance
83          allowing(parentTypeInfo).getTypeResolver();
84          will(returnValue(typeResolver));
85  
86          allowing(choiceGroupInstance).getMaxOccurs();
87          will(returnValue(-1)); // Unbounded collection
88  
89          allowing(choiceGroupInstance).getContainingDefinition();
90          will(returnValue(assemblyDefinition));
91  
92          allowing(bindingConfig).getBindingConfigurationForDefinition(assemblyDefinition);
93          will(returnValue(defConfig));
94  
95          allowing(choiceGroupInstance).getGroupAsName();
96          will(returnValue(groupAsName));
97  
98          allowing(defConfig).getChoiceGroupBindings();
99          will(returnValue(choiceGroupBindings));
100 
101         allowing(choiceConfig).getItemTypeName();
102         will(returnValue(itemTypeName));
103 
104         allowing(choiceConfig).isUseWildcard();
105         will(returnValue(true));
106 
107         allowing(choiceGroupInstance).getJsonGroupAsBehavior();
108         will(returnValue(JsonGroupAsBehavior.SINGLETON_OR_LIST));
109       }
110     });
111 
112     ChoiceGroupTypeInfoImpl typeInfo = new ChoiceGroupTypeInfoImpl(choiceGroupInstance, parentTypeInfo);
113     TypeName result = typeInfo.getJavaFieldType();
114 
115     assertNotNull(result);
116     assertTrue(result instanceof ParameterizedTypeName, "Expected ParameterizedTypeName");
117 
118     ParameterizedTypeName paramType = (ParameterizedTypeName) result;
119     assertEquals(ClassName.get(List.class), paramType.rawType);
120     assertEquals(1, paramType.typeArguments.size());
121 
122     TypeName itemType = paramType.typeArguments.get(0);
123     assertTrue(itemType instanceof WildcardTypeName, "Expected WildcardTypeName for item type");
124 
125     WildcardTypeName wildcardType = (WildcardTypeName) itemType;
126     assertEquals(1, wildcardType.upperBounds.size());
127     assertEquals(itemClass, wildcardType.upperBounds.get(0));
128   }
129 
130   /**
131    * Test that getJavaFieldType returns List with non-wildcard type when
132    * useWildcard is false.
133    */
134   @Test
135   void testGetJavaFieldTypeReturnsNonWildcardTypeWhenNotConfigured() {
136     IBindingConfiguration bindingConfig = ObjectUtils.notNull(context.mock(IBindingConfiguration.class));
137     IDefinitionBindingConfiguration defConfig
138         = ObjectUtils.notNull(context.mock(IDefinitionBindingConfiguration.class));
139     IChoiceGroupBindingConfiguration choiceConfig
140         = ObjectUtils.notNull(context.mock(IChoiceGroupBindingConfiguration.class));
141 
142     String groupAsName = "test-choices";
143     String itemTypeName = "com.example.ITestInterface";
144     ClassName itemClass = ClassName.bestGuess(itemTypeName);
145 
146     Map<String, IChoiceGroupBindingConfiguration> choiceGroupBindings = new HashMap<>();
147     choiceGroupBindings.put(groupAsName, choiceConfig);
148 
149     DefaultTypeResolver typeResolver = new DefaultTypeResolver(bindingConfig);
150 
151     context.checking(new Expectations() {
152       {
153         allowing(parentTypeInfo).getTypeResolver();
154         will(returnValue(typeResolver));
155 
156         allowing(choiceGroupInstance).getMaxOccurs();
157         will(returnValue(-1));
158 
159         allowing(choiceGroupInstance).getContainingDefinition();
160         will(returnValue(assemblyDefinition));
161 
162         allowing(bindingConfig).getBindingConfigurationForDefinition(assemblyDefinition);
163         will(returnValue(defConfig));
164 
165         allowing(choiceGroupInstance).getGroupAsName();
166         will(returnValue(groupAsName));
167 
168         allowing(defConfig).getChoiceGroupBindings();
169         will(returnValue(choiceGroupBindings));
170 
171         allowing(choiceConfig).getItemTypeName();
172         will(returnValue(itemTypeName));
173 
174         allowing(choiceConfig).isUseWildcard();
175         will(returnValue(false)); // No wildcard
176 
177         allowing(choiceGroupInstance).getJsonGroupAsBehavior();
178         will(returnValue(JsonGroupAsBehavior.SINGLETON_OR_LIST));
179       }
180     });
181 
182     ChoiceGroupTypeInfoImpl typeInfo = new ChoiceGroupTypeInfoImpl(choiceGroupInstance, parentTypeInfo);
183     TypeName result = typeInfo.getJavaFieldType();
184 
185     assertNotNull(result);
186     assertTrue(result instanceof ParameterizedTypeName, "Expected ParameterizedTypeName");
187 
188     ParameterizedTypeName paramType = (ParameterizedTypeName) result;
189     assertEquals(ClassName.get(List.class), paramType.rawType);
190     assertEquals(1, paramType.typeArguments.size());
191 
192     // Should be the item class directly, not a wildcard
193     TypeName itemType = paramType.typeArguments.get(0);
194     assertEquals(itemClass, itemType);
195   }
196 
197   /**
198    * Test that getJavaFieldType returns Map with wildcard type when keyed.
199    */
200   @Test
201   void testGetJavaFieldTypeReturnsMapWithWildcardForKeyedGroups() {
202     IBindingConfiguration bindingConfig = ObjectUtils.notNull(context.mock(IBindingConfiguration.class));
203     IDefinitionBindingConfiguration defConfig
204         = ObjectUtils.notNull(context.mock(IDefinitionBindingConfiguration.class));
205     IChoiceGroupBindingConfiguration choiceConfig
206         = ObjectUtils.notNull(context.mock(IChoiceGroupBindingConfiguration.class));
207 
208     String groupAsName = "test-choices";
209     String itemTypeName = "com.example.ITestInterface";
210     ClassName itemClass = ClassName.bestGuess(itemTypeName);
211 
212     Map<String, IChoiceGroupBindingConfiguration> choiceGroupBindings = new HashMap<>();
213     choiceGroupBindings.put(groupAsName, choiceConfig);
214 
215     DefaultTypeResolver typeResolver = new DefaultTypeResolver(bindingConfig);
216 
217     context.checking(new Expectations() {
218       {
219         allowing(parentTypeInfo).getTypeResolver();
220         will(returnValue(typeResolver));
221 
222         allowing(choiceGroupInstance).getMaxOccurs();
223         will(returnValue(-1));
224 
225         allowing(choiceGroupInstance).getContainingDefinition();
226         will(returnValue(assemblyDefinition));
227 
228         allowing(bindingConfig).getBindingConfigurationForDefinition(assemblyDefinition);
229         will(returnValue(defConfig));
230 
231         allowing(choiceGroupInstance).getGroupAsName();
232         will(returnValue(groupAsName));
233 
234         allowing(defConfig).getChoiceGroupBindings();
235         will(returnValue(choiceGroupBindings));
236 
237         allowing(choiceConfig).getItemTypeName();
238         will(returnValue(itemTypeName));
239 
240         allowing(choiceConfig).isUseWildcard();
241         will(returnValue(true));
242 
243         allowing(choiceGroupInstance).getJsonGroupAsBehavior();
244         will(returnValue(JsonGroupAsBehavior.KEYED)); // Map collection type
245       }
246     });
247 
248     ChoiceGroupTypeInfoImpl typeInfo = new ChoiceGroupTypeInfoImpl(choiceGroupInstance, parentTypeInfo);
249     TypeName result = typeInfo.getJavaFieldType();
250 
251     assertNotNull(result);
252     assertTrue(result instanceof ParameterizedTypeName, "Expected ParameterizedTypeName");
253 
254     ParameterizedTypeName paramType = (ParameterizedTypeName) result;
255     assertEquals(ClassName.get(Map.class), paramType.rawType);
256     assertEquals(2, paramType.typeArguments.size());
257 
258     // Key should be String
259     TypeName keyType = paramType.typeArguments.get(0);
260     assertEquals(ClassName.get(String.class), keyType);
261 
262     // Value should be wildcard
263     TypeName valueType = paramType.typeArguments.get(1);
264     assertTrue(valueType instanceof WildcardTypeName, "Expected WildcardTypeName for value type");
265 
266     WildcardTypeName wildcardType = (WildcardTypeName) valueType;
267     assertEquals(1, wildcardType.upperBounds.size());
268     assertEquals(itemClass, wildcardType.upperBounds.get(0));
269   }
270 
271   /**
272    * Test that getJavaFieldType returns non-collection type when maxOccurs is 1.
273    */
274   @Test
275   void testGetJavaFieldTypeReturnsSingletonWhenMaxOccursIsOne() {
276     ClassName itemClass = ClassName.get(Object.class);
277     IBindingConfiguration bindingConfig = ObjectUtils.notNull(context.mock(IBindingConfiguration.class));
278     DefaultTypeResolver typeResolver = new DefaultTypeResolver(bindingConfig);
279 
280     context.checking(new Expectations() {
281       {
282         allowing(parentTypeInfo).getTypeResolver();
283         will(returnValue(typeResolver));
284 
285         allowing(choiceGroupInstance).getMaxOccurs();
286         will(returnValue(1)); // Single occurrence
287 
288         allowing(choiceGroupInstance).getContainingDefinition();
289         will(returnValue(assemblyDefinition));
290 
291         allowing(bindingConfig).getBindingConfigurationForDefinition(assemblyDefinition);
292         will(returnValue(null));
293       }
294     });
295 
296     ChoiceGroupTypeInfoImpl typeInfo = new ChoiceGroupTypeInfoImpl(choiceGroupInstance, parentTypeInfo);
297     TypeName result = typeInfo.getJavaFieldType();
298 
299     assertNotNull(result);
300     assertEquals(itemClass, result);
301   }
302 
303   /**
304    * Test that getJavaFieldType returns List of Object when no binding
305    * configuration exists.
306    */
307   @Test
308   void testGetJavaFieldTypeReturnsListOfObjectWhenNoBindingConfig() {
309     ClassName itemClass = ClassName.get(Object.class);
310     IBindingConfiguration bindingConfig = ObjectUtils.notNull(context.mock(IBindingConfiguration.class));
311     DefaultTypeResolver typeResolver = new DefaultTypeResolver(bindingConfig);
312 
313     context.checking(new Expectations() {
314       {
315         allowing(parentTypeInfo).getTypeResolver();
316         will(returnValue(typeResolver));
317 
318         allowing(choiceGroupInstance).getMaxOccurs();
319         will(returnValue(-1));
320 
321         allowing(choiceGroupInstance).getContainingDefinition();
322         will(returnValue(assemblyDefinition));
323 
324         allowing(bindingConfig).getBindingConfigurationForDefinition(assemblyDefinition);
325         will(returnValue(null)); // No binding config
326 
327         allowing(choiceGroupInstance).getJsonGroupAsBehavior();
328         will(returnValue(JsonGroupAsBehavior.SINGLETON_OR_LIST));
329       }
330     });
331 
332     ChoiceGroupTypeInfoImpl typeInfo = new ChoiceGroupTypeInfoImpl(choiceGroupInstance, parentTypeInfo);
333     TypeName result = typeInfo.getJavaFieldType();
334 
335     assertNotNull(result);
336     assertTrue(result instanceof ParameterizedTypeName, "Expected ParameterizedTypeName");
337 
338     ParameterizedTypeName paramType = (ParameterizedTypeName) result;
339     assertEquals(ClassName.get(List.class), paramType.rawType);
340     assertEquals(1, paramType.typeArguments.size());
341     assertEquals(itemClass, paramType.typeArguments.get(0));
342   }
343 
344   /**
345    * Test that getJavaFieldType handles maxOccurs greater than 1.
346    */
347   @Test
348   void testGetJavaFieldTypeWithMaxOccursGreaterThanOne() {
349     IBindingConfiguration bindingConfig = ObjectUtils.notNull(context.mock(IBindingConfiguration.class));
350     IDefinitionBindingConfiguration defConfig
351         = ObjectUtils.notNull(context.mock(IDefinitionBindingConfiguration.class));
352     IChoiceGroupBindingConfiguration choiceConfig
353         = ObjectUtils.notNull(context.mock(IChoiceGroupBindingConfiguration.class));
354 
355     String groupAsName = "test-choices";
356     String itemTypeName = "com.example.ITestInterface";
357 
358     Map<String, IChoiceGroupBindingConfiguration> choiceGroupBindings = new HashMap<>();
359     choiceGroupBindings.put(groupAsName, choiceConfig);
360 
361     DefaultTypeResolver typeResolver = new DefaultTypeResolver(bindingConfig);
362 
363     context.checking(new Expectations() {
364       {
365         allowing(parentTypeInfo).getTypeResolver();
366         will(returnValue(typeResolver));
367 
368         allowing(choiceGroupInstance).getMaxOccurs();
369         will(returnValue(5)); // Fixed upper bound > 1
370 
371         allowing(choiceGroupInstance).getContainingDefinition();
372         will(returnValue(assemblyDefinition));
373 
374         allowing(bindingConfig).getBindingConfigurationForDefinition(assemblyDefinition);
375         will(returnValue(defConfig));
376 
377         allowing(choiceGroupInstance).getGroupAsName();
378         will(returnValue(groupAsName));
379 
380         allowing(defConfig).getChoiceGroupBindings();
381         will(returnValue(choiceGroupBindings));
382 
383         allowing(choiceConfig).getItemTypeName();
384         will(returnValue(itemTypeName));
385 
386         allowing(choiceConfig).isUseWildcard();
387         will(returnValue(true));
388 
389         allowing(choiceGroupInstance).getJsonGroupAsBehavior();
390         will(returnValue(JsonGroupAsBehavior.SINGLETON_OR_LIST));
391       }
392     });
393 
394     ChoiceGroupTypeInfoImpl typeInfo = new ChoiceGroupTypeInfoImpl(choiceGroupInstance, parentTypeInfo);
395     TypeName result = typeInfo.getJavaFieldType();
396 
397     assertNotNull(result);
398     assertTrue(result instanceof ParameterizedTypeName, "Expected ParameterizedTypeName");
399 
400     ParameterizedTypeName paramType = (ParameterizedTypeName) result;
401     assertEquals(ClassName.get(List.class), paramType.rawType);
402 
403     TypeName itemType = paramType.typeArguments.get(0);
404     assertTrue(itemType instanceof WildcardTypeName, "Expected WildcardTypeName");
405   }
406 }