1515 */
1616package io .micronaut .jsonschema .generator ;
1717
18+ import com .fasterxml .jackson .annotation .JsonProperty ;
19+ import com .fasterxml .jackson .core .JsonPointer ;
1820import com .fasterxml .jackson .databind .json .JsonMapper ;
1921import io .micronaut .core .annotation .Internal ;
2022import io .micronaut .core .util .CollectionUtils ;
2729import io .micronaut .sourcegen .model .*;
2830import jakarta .inject .Singleton ;
2931
32+ import javax .lang .model .SourceVersion ;
3033import javax .lang .model .element .Modifier ;
3134import java .io .File ;
3235import java .io .FileWriter ;
3336import java .io .IOException ;
3437import java .io .InputStream ;
38+ import java .net .URI ;
3539import java .nio .charset .StandardCharsets ;
40+ import java .time .Duration ;
41+ import java .time .LocalDate ;
42+ import java .time .ZonedDateTime ;
3643import java .util .ArrayList ;
3744import java .util .HashMap ;
3845import java .util .List ;
3946import java .util .Map ;
4047import java .util .Optional ;
4148import java .util .Set ;
49+ import java .util .UUID ;
4250
4351import static io .micronaut .core .util .StringUtils .capitalize ;
4452
@@ -57,9 +65,6 @@ public final class RecordGenerator {
5765 "integer" , TypeDef .Primitive .INT , "boolean" , TypeDef .Primitive .BOOLEAN , "array" , TypeDef .of (List .class ),
5866 "void" , TypeDef .VOID , "string" , TypeDef .STRING , "object" , TypeDef .OBJECT ,
5967 "number" , TypeDef .Primitive .FLOAT , "null" , TypeDef .OBJECT });
60- private static final Map <String , TypeDef > GENERIC_TYPE_MAP = CollectionUtils .mapOf (new Object []{
61- "integer" , TypeDef .of (Integer .class ), "boolean" , TypeDef .of (Boolean .class ),
62- "number" , TypeDef .of (Float .class ), "string" , TypeDef .STRING , "object" , TypeDef .OBJECT });
6368
6469 private List <EnumDef > enums = new ArrayList <>();
6570
@@ -96,17 +101,25 @@ public boolean generateFromSchemaMap(Map<String, ?> jsonSchema, Optional<File> o
96101
97102 // TODO configure package as argument
98103 String packageName = "test" ;
99- // TODO do not add the 'Record' in the end.
100- String objectName = jsonSchema .get ("title" ).toString () + "Record" ;
104+ String objectName = capitalize (toAcceptableName (jsonSchema .get ("title" ).toString ()));
101105
102106 File outputFile = getOutputFile (outputFileLocation ,
103107 (packageName + "." ).replace ('.' , File .separatorChar ) + objectName );
104108 try (FileWriter writer = new FileWriter (outputFile )) {
105- var objectDef = build (jsonSchema , packageName + "." + objectName );
106- for (EnumDef enumDef : enums ) {
107- sourceGenerator .write (enumDef , writer );
109+ if (jsonSchema .containsKey ("enum" )) {
110+ EnumDef .EnumDefBuilder enumBuilder = EnumDef .builder (packageName + "." + objectName );
111+ for (Object anEnum : ((List <?>) jsonSchema .get ("enum" ))) {
112+ // TODO add non-string enum constants, look @SimpleGeneratorSpec.testEnumGeneration()
113+ enumBuilder .addEnumConstant (anEnum .toString ());
114+ }
115+ sourceGenerator .write (enumBuilder .build (), writer );
116+ } else {
117+ var objectDef = build (jsonSchema , packageName + "." + objectName );
118+ for (EnumDef enumDef : enums ) {
119+ sourceGenerator .write (enumDef , writer );
120+ }
121+ sourceGenerator .write (objectDef , writer );
108122 }
109- sourceGenerator .write (objectDef , writer );
110123 }
111124 return true ;
112125 } catch (ProcessingException | IOException e ) {
@@ -149,66 +162,126 @@ private RecordDef build(Map<String, ?> jsonSchema, String builderClassName) thro
149162 }
150163
151164 private void addField (RecordDef .RecordDefBuilder objectBuilder , String propertyName , Map <String , Object > description , boolean isRequired ) {
152- TypeDef propertyType = TYPE_MAP .get (getPropertyType (description ));
165+ String name = toAcceptableName (propertyName );
166+
167+ TypeDef propertyType = getJsonType (description );
153168 if (description .containsKey ("enum" )) {
154- propertyType = getEnumType (propertyName , description );
169+ propertyType = getEnumType (name , description );
155170 }
156171 PropertyDef .PropertyDefBuilder propertyDef ;
157172
158173 if (propertyType .equals (TypeDef .of (List .class ))) {
159174 List <AnnotationDef > annotations = new ArrayList <>();
160- propertyType = getTypeVariable (propertyName , description , annotations );
161- propertyDef = PropertyDef .builder (propertyName ).ofType (propertyType );
175+ propertyType = getTypeDef (propertyName , description , annotations );
176+ propertyDef = PropertyDef .builder (name ).ofType (propertyType );
162177
163178 AnnotationInfoAggregator .addAnnotations (propertyDef , annotations , isRequired );
164179 } else {
165- propertyDef = PropertyDef .builder (propertyName ).ofType (propertyType );
180+ propertyDef = PropertyDef .builder (name ).ofType (propertyType );
166181 AnnotationInfoAggregator .addAnnotations (propertyDef , description , propertyType , isRequired );
167182 }
168- objectBuilder .addProperty (propertyDef .build ());
169- }
170183
171- private TypeDef getEnumType (String propertyName , Map <String , Object > description ) {
172- EnumDef .EnumDefBuilder enumBuilder = EnumDef .builder (capitalize (propertyName ));
173- for (Object anEnum : ((List <?>) description .get ("enum" ))) {
174- enumBuilder .addEnumConstant (anEnum .toString ());
184+ if (!name .equals (propertyName )) {
185+ AnnotationDef .AnnotationDefBuilder annotationDefBuilder = AnnotationDef .builder (JsonProperty .class ).addMember ("value" , propertyName );
186+ propertyDef .addAnnotation (annotationDefBuilder .build ());
175187 }
176- EnumDef enumDef = enumBuilder .build ();
177- this .enums .add (enumDef );
178- return enumDef .asTypeDef ();
188+ objectBuilder .addProperty (propertyDef .build ());
179189 }
180190
181- private TypeDef getTypeVariable (String propertyName , Map <String , Object > description , List <AnnotationDef > annotations ) {
191+ private TypeDef getTypeDef (String propertyName , Map <String , Object > description , List <AnnotationDef > annotations ) {
182192 var items = (Map <String , Object >) description .get ("items" );
183193 Class listClass = List .class ;
184194 if (description .containsKey ("uniqueItems" ) && description .get ("uniqueItems" ).toString ().equals ("true" )) {
185195 listClass = Set .class ;
186196 }
187197
188- TypeDef propertyType = TYPE_MAP . get ( getPropertyType ( items ) );
198+ TypeDef propertyType = getJsonType ( items );
189199 if (propertyType .equals (TypeDef .of (List .class ))) {
190200 annotations .addAll (AnnotationInfoAggregator .getAnnotations (items , propertyType ));
191- propertyType = getTypeVariable (propertyName , items , annotations );
201+ propertyType = getTypeDef (propertyName , items , annotations );
192202 } else {
193203 if (items .containsKey ("enum" )) {
194204 propertyType = getEnumType (propertyName , items );
195205 } else {
196- propertyType = GENERIC_TYPE_MAP .get (getPropertyType (items ));
206+ propertyType = getJsonType (items );
207+ if (propertyType instanceof TypeDef .Primitive primitive ) {
208+ propertyType = primitive .wrapperType ();
209+ }
197210 }
198211 annotations .addAll (AnnotationInfoAggregator .getAnnotations (items , propertyType ));
199212 }
200213 // TODO: add a new implementation that would return a typedef with annotations
201214 return TypeDef .parameterized (listClass , propertyType );
202215 }
203216
204- private static String getPropertyType (Map <String , Object > description ) {
217+
218+ private TypeDef getEnumType (String propertyName , Map <String , Object > description ) {
219+ EnumDef .EnumDefBuilder enumBuilder = EnumDef .builder (capitalize (propertyName ));
220+ for (Object anEnum : ((List <?>) description .get ("enum" ))) {
221+ enumBuilder .addEnumConstant (anEnum .toString ());
222+ }
223+ EnumDef enumDef = enumBuilder .build ();
224+ this .enums .add (enumDef );
225+ return enumDef .asTypeDef ();
226+ }
227+
228+ private static TypeDef getJsonType (Map <String , Object > description ) {
205229 var type = description .getOrDefault ("type" , "object" );
206230 String typeName ;
207231 if (type .getClass () == ArrayList .class ) {
208232 typeName = ((ArrayList <?>) type ).get (0 ).toString ();
209233 } else {
210234 typeName = type .toString ();
211235 }
212- return typeName ;
236+ if (typeName .equals ("string" ) && description .containsKey ("format" )) {
237+ var format = description .get ("format" ).toString ();
238+ switch (format ) {
239+ case "date" : return ClassTypeDef .of (LocalDate .class );
240+ case "date-time" , "time" : return ClassTypeDef .of (ZonedDateTime .class );
241+ case "duration" : return ClassTypeDef .of (Duration .class );
242+ case "ipv4" : return ClassTypeDef .of (java .net .Inet4Address .class );
243+ case "ipv6" : return ClassTypeDef .of (java .net .Inet6Address .class );
244+ case "uuid" : return ClassTypeDef .of (UUID .class );
245+ case "uri" , "iri" : return ClassTypeDef .of (URI .class );
246+ case "json-pointer" : return ClassTypeDef .of (JsonPointer .class );
247+ // missing: email, web hostname, uri-reference, uri-template, regex
248+ }
249+ }
250+ return TYPE_MAP .get (typeName );
251+ }
252+
253+ private static String toAcceptableName (String input ) {
254+ if (SourceVersion .isName (input )) {
255+ return input ;
256+ }
257+ String cleanedInput = input .replaceAll ("[-_]" , " " )
258+ .replaceAll ("[^a-zA-Z0-9 ]" , "" )
259+ .trim ();
260+
261+ while (!Character .isJavaIdentifierStart (cleanedInput .charAt (0 ))) {
262+ cleanedInput = cleanedInput .substring (1 );
263+ }
264+
265+ // Split into words
266+ String [] words = cleanedInput .split ("\\ s+" );
267+ StringBuilder camelCaseString = new StringBuilder ();
268+
269+ // Check if the input is acceptable
270+ if (words .length == 0 || words [0 ].isEmpty ()) {
271+ throw new IllegalArgumentException ("Property name is not an acceptable variable name" );
272+ }
273+
274+ for (int i = 0 ; i < words .length ; i ++) {
275+ String word = words [i ].trim ();
276+ if (!word .isEmpty ()) {
277+ if (i == 0 ) {
278+ camelCaseString .append (word .toLowerCase ());
279+ } else {
280+ camelCaseString .append (Character .toUpperCase (word .charAt (0 )))
281+ .append (word .substring (1 ).toLowerCase ());
282+ }
283+ }
284+ }
285+ return camelCaseString .toString ();
213286 }
214287}
0 commit comments