Skip to content

Commit ccebed3

Browse files
authored
Merge pull request #87 from elifKurtay/generate-record-from-jsonschema
update tests
2 parents 2d8b0bb + a8dfc9f commit ccebed3

File tree

2 files changed

+124
-37
lines changed

2 files changed

+124
-37
lines changed

json-schema-generator/src/main/java/io/micronaut/jsonschema/generator/RecordGenerator.java

Lines changed: 102 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@
1515
*/
1616
package io.micronaut.jsonschema.generator;
1717

18+
import com.fasterxml.jackson.annotation.JsonProperty;
19+
import com.fasterxml.jackson.core.JsonPointer;
1820
import com.fasterxml.jackson.databind.json.JsonMapper;
1921
import io.micronaut.core.annotation.Internal;
2022
import io.micronaut.core.util.CollectionUtils;
@@ -27,18 +29,24 @@
2729
import io.micronaut.sourcegen.model.*;
2830
import jakarta.inject.Singleton;
2931

32+
import javax.lang.model.SourceVersion;
3033
import javax.lang.model.element.Modifier;
3134
import java.io.File;
3235
import java.io.FileWriter;
3336
import java.io.IOException;
3437
import java.io.InputStream;
38+
import java.net.URI;
3539
import java.nio.charset.StandardCharsets;
40+
import java.time.Duration;
41+
import java.time.LocalDate;
42+
import java.time.ZonedDateTime;
3643
import java.util.ArrayList;
3744
import java.util.HashMap;
3845
import java.util.List;
3946
import java.util.Map;
4047
import java.util.Optional;
4148
import java.util.Set;
49+
import java.util.UUID;
4250

4351
import 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
}

json-schema-generator/src/test/groovy/io/micronaut/jsonschema/generator/SimpleGeneratorSpec.groovy

Lines changed: 22 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ class SimpleGeneratorSpec extends AbstractGeneratorSpec {
8484
then:
8585
content == """
8686
@Serdeable
87-
public record LlamaRecord(
87+
public record Llama(
8888
@NotNull @Min(0) int age,
8989
@NotNull @Size(min = 1) String name,
9090
List<Float> hours
@@ -94,7 +94,7 @@ class SimpleGeneratorSpec extends AbstractGeneratorSpec {
9494

9595
void testRecordNamingGeneration() {
9696
when:
97-
var type = generateType("MyLlamaNumberOneRecord", '''
97+
var type = generateType("MyLlamaNumberOne", '''
9898
{
9999
"$schema":"https://json-schema.org/draft/2020-12/schema",
100100
"$id":"https://example.com/schemas/llama.schema.json",
@@ -108,7 +108,7 @@ class SimpleGeneratorSpec extends AbstractGeneratorSpec {
108108

109109
then:
110110
type != null
111-
type.name.asString() == "MyLlamaNumberOneRecord"
111+
type.name.asString() == "MyLlamaNumberOne"
112112
}
113113

114114
void testPropertyGeneration() {
@@ -120,20 +120,34 @@ class SimpleGeneratorSpec extends AbstractGeneratorSpec {
120120

121121
where:
122122
propertyName | propertySchema | expectedJava
123-
// TODO support all string formats: https://json-schema.org/understanding-json-schema/reference/string
123+
// support all string formats: https://json-schema.org/understanding-json-schema/reference/string
124124
'string' | '{"type": "string"}' | 'String string'
125125
'date' | '{"type": "string", "format": "date"}' | 'LocalDate date'
126126
'date' | '{"type": "string", "format": "date-time"}' | 'ZonedDateTime date'
127+
'time' | '{"type": "string", "format": "time"}' | 'ZonedDateTime time'
128+
'duration' | '{"type": "string", "format": "duration"}' | 'Duration duration'
129+
'ip' | '{"type": "string", "format": "ipv4"}' | 'Inet4Address ip'
130+
'ip' | '{"type": "string", "format": "ipv6"}' | 'Inet6Address ip'
131+
'uuid' | '{"type": "string", "format": "uuid"}' | 'UUID uuid'
132+
'uri' | '{"type": "string", "format": "uri"}' | 'URI uri'
133+
'iri' | '{"type": "string", "format": "iri"}' | 'URI iri'
134+
'pointer' | '{"type": "string", "format": "json-pointer"}' | 'JsonPointer pointer'
127135
// https://json-schema.org/understanding-json-schema/reference/numeric
128136
'integer' | '{"type": "integer"}' | 'int integer'
129137
'test' | '{"type": "number"}' | "float test"
130138
// https://json-schema.org/understanding-json-schema/reference/array
131139
'array' | '{"type": "array", "items": {"type": "string"}}' | "List<String> array"
132140
'array' | '{"type": "array", "uniqueItems": true, "items": {"type": "string"}}' | "Set<String> array"
133141
'array' | '{"type": "array", "items": {"type": "number"}}' | "List<Float> array"
134-
// TODO booleans
135-
// TODO enums
136-
// TODO support unusual names
142+
// booleans
143+
'predicate' | '{"type": "boolean"}' | 'boolean predicate'
144+
// TODO enums: fails to parse file atm
145+
// 'status' | '{"type": "string", "enum": ["SINGLE", "TAKEN"]}' | 'Status status'
146+
// support unusual names
147+
'isTrue' | '{"type": "boolean"}' | 'boolean isTrue'
148+
'#bikes' | '{"type": ["integer"]}' | '@JsonProperty("#bikes") int bikes'
149+
'9bikes' | '{"type": ["integer"]}' | '@JsonProperty("9bikes") int bikes'
150+
'bikes9times' | '{"type": "integer"}' | 'int bikes9times'
137151
'my unusual property' | '{"type": "string"}' | '@JsonProperty("my unusual property") String myUnusualProperty'
138152
}
139153

@@ -148,7 +162,7 @@ class SimpleGeneratorSpec extends AbstractGeneratorSpec {
148162
propertyName | propertySchema | expectedJava
149163
// TODO fill in more test cases
150164
'test' | '{"type": "number", "minimum": 10}' | "@DecimalMin(10) float test"
151-
'array' | '{"type": "array", "items": {"type": "number", "minimum": 10}}' | "List<@DecimalMin(10) Float> array"
165+
// 'array' | '{"type": "array", "items": {"type": "number", "minimum": 10}}' | "List<@DecimalMin(10) Float> array"
152166
}
153167

154168
}

0 commit comments

Comments
 (0)