Skip to content

Commit d4fd324

Browse files
committed
feat: add support for object type properties (multiple classes generation)
1 parent 574db05 commit d4fd324

File tree

12 files changed

+284
-38
lines changed

12 files changed

+284
-38
lines changed

src/main/java/es/nachobrito/jsonschema/compiler/application/cli/App.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,13 @@
1818

1919
import es.nachobrito.jsonschema.compiler.domain.Compiler;
2020
import es.nachobrito.jsonschema.compiler.infrastructure.jsonrefparser.JsonSchemaReader;
21+
import es.nachobrito.jsonschema.compiler.infrastructure.jsonrefparser.JsonSchemaReaderFactory;
2122

2223
public class App {
2324
public static void main(String[] args) {
2425
var params = RuntimeConfiguration.of(args);
2526

26-
var compiler = new Compiler(params, new JsonSchemaReader());
27+
var compiler = new Compiler(params, new JsonSchemaReaderFactory());
2728
var jsonSchemaFile = params.getJsonSchemaFile();
2829
var jsonSchemaCode = params.getJsonSchemaCode();
2930
if (jsonSchemaFile.isPresent()) {

src/main/java/es/nachobrito/jsonschema/compiler/domain/Compiler.java

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -22,21 +22,22 @@
2222

2323
import es.nachobrito.jsonschema.compiler.domain.generator.ModelGenerator;
2424
import es.nachobrito.jsonschema.compiler.domain.runtimeconfiguration.RuntimeConfiguration;
25-
import es.nachobrito.jsonschema.compiler.domain.schemareader.SchemaReader;
25+
import es.nachobrito.jsonschema.compiler.domain.schemareader.SchemaReaderFactory;
2626
import java.io.IOException;
2727
import java.lang.classfile.ClassBuilder;
2828
import java.lang.classfile.ClassFile;
2929
import java.net.URI;
3030
import java.nio.file.Path;
31+
import java.util.List;
3132
import java.util.SortedMap;
3233

3334
public class Compiler {
3435
private final RuntimeConfiguration runtimeConfiguration;
35-
private final SchemaReader schemaReader;
36+
private final SchemaReaderFactory schemaReaderFactory;
3637

37-
public Compiler(RuntimeConfiguration runtimeConfiguration, SchemaReader schemaReader) {
38+
public Compiler(RuntimeConfiguration runtimeConfiguration, SchemaReaderFactory schemaReaderFactory) {
3839
this.runtimeConfiguration = runtimeConfiguration;
39-
this.schemaReader = schemaReader;
40+
this.schemaReaderFactory = schemaReaderFactory;
4041
}
4142

4243
/**
@@ -45,7 +46,7 @@ public Compiler(RuntimeConfiguration runtimeConfiguration, SchemaReader schemaRe
4546
* @param schemaURI the uri of the file to compile
4647
*/
4748
public void compile(URI schemaURI) {
48-
compile(schemaReader.read(schemaURI));
49+
compileAll(schemaReaderFactory.makeSchemaReader().read(schemaURI));
4950
}
5051

5152
/**
@@ -54,10 +55,15 @@ public void compile(URI schemaURI) {
5455
* @param jsonSchema the schema definition
5556
*/
5657
public void compile(String jsonSchema) {
57-
compile(schemaReader.read(jsonSchema));
58+
compileAll(schemaReaderFactory.makeSchemaReader().read(jsonSchema));
5859
}
5960

60-
private void compile(Schema schema) {
61+
62+
private void compileAll(List<Schema> schemas) {
63+
schemas.forEach(this::compileSchema);
64+
}
65+
66+
private void compileSchema(Schema schema) {
6167
var className =
6268
runtimeConfiguration
6369
.getPackageName()
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
/*
2+
* Copyright 2024 Nacho Brito
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package es.nachobrito.jsonschema.compiler.domain;
18+
19+
import java.util.regex.Pattern;
20+
21+
public class JavaName {
22+
private static final Pattern jsonIdPattern = Pattern.compile("[\\W_]([a-z])");
23+
24+
public static String fromJsonIdentifier(String identifier) {
25+
var formatted = jsonIdPattern.matcher(identifier).replaceAll(m -> m.group(1).toUpperCase());
26+
return formatted;
27+
}
28+
}

src/main/java/es/nachobrito/jsonschema/compiler/domain/Property.java

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,10 @@
1919
import java.lang.constant.ClassDesc;
2020
import java.util.regex.Pattern;
2121

22-
public record Property(String key, ClassDesc type) {
23-
private static final Pattern jsonIdPattern = Pattern.compile("[_\\-]([a-z])");
22+
public record Property(String key, ClassDesc type, String formattedName) {
2423

25-
public String formattedName() {
26-
return jsonIdPattern.matcher(key).replaceAll(m -> m.group(1).toUpperCase());
24+
public Property(String key, ClassDesc type){
25+
this(key, type, JavaName.fromJsonIdentifier(key));
2726
}
27+
2828
}

src/main/java/es/nachobrito/jsonschema/compiler/domain/schemareader/AbstractSchemaReader.java

Lines changed: 65 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
import static java.util.stream.Collectors.toMap;
2222

2323
import es.nachobrito.jsonschema.compiler.domain.CompilerException;
24+
import es.nachobrito.jsonschema.compiler.domain.JavaName;
2425
import es.nachobrito.jsonschema.compiler.domain.Property;
2526
import es.nachobrito.jsonschema.compiler.domain.Schema;
2627
import java.io.IOException;
@@ -34,56 +35,101 @@
3435
import java.util.*;
3536

3637
public abstract class AbstractSchemaReader implements SchemaReader {
38+
private Map<String, Object> models;
39+
private Map<String, Schema> schemas = new HashMap<>();
40+
3741
@Override
38-
public Schema read(URI uri) {
39-
var models = loadModels(uri);
40-
return new Schema(getClassName(models), getProperties(models));
42+
public List<Schema> read(URI uri) {
43+
this.models = loadModels(uri);
44+
return createSchemas();
4145
}
4246

4347
@Override
44-
public Schema read(String jsonSchema) {
45-
var models = loadModels(jsonSchema);
46-
return new Schema(getClassName(models), getProperties(models));
48+
public List<Schema> read(String jsonSchema) {
49+
this.models = loadModels(jsonSchema);
50+
return createSchemas();
51+
}
52+
53+
private List<Schema> createSchemas() {
54+
schemas.clear();
55+
registerSchema(new Schema(getRootClassName(), processProperties(getRootProperties())));
56+
return schemas.values().stream().toList();
57+
}
58+
59+
private void registerSchema(Schema schema) {
60+
schemas.put(schema.className(), schema);
4761
}
4862

49-
public String getClassName(Map<String, Object> models) {
63+
private String getRootClassName() {
5064
return String.valueOf(models.getOrDefault("title", "UnknownClassName"));
5165
}
5266

53-
public SortedMap<String, Property> getProperties(Map<String, Object> models) {
54-
var definitions =
55-
((Map<String, Map<String, ?>>) models.getOrDefault("properties", Collections.emptyMap()));
67+
private SortedMap<String, Property> processProperties(Map<String, Map<String, ?>> definitions) {
5668
return definitions.entrySet().stream()
5769
.collect(
5870
toMap(
5971
Map.Entry::getKey,
60-
entry ->
61-
createProperty(
62-
entry.getKey(),
63-
(String) entry.getValue().get("type"),
64-
(String) entry.getValue().get("format")),
72+
entry -> createProperty(entry.getKey(), definitions),
6573
(v1, v2) -> {
6674
throw new CompilerException("Duplicate property found!");
6775
},
6876
TreeMap::new));
6977
}
7078

71-
protected Property createProperty(String key, String type, String format) {
72-
return new Property(key, getJavaType(type, format));
79+
private Property createProperty(String key, Map<String, Map<String, ?>> propertyDefinitions) {
80+
return new Property(key, getJavaType(key, propertyDefinitions));
7381
}
7482

75-
protected ClassDesc getJavaType(String jsonSchemaType, String jsonSchemaFormat) {
83+
private ClassDesc getJavaType(
84+
String propertyKey, Map<String, Map<String, ?>> propertyDefinitions) {
85+
var property = getModelPropertyDefinition(propertyKey, propertyDefinitions);
86+
var jsonSchemaType = (String) property.get("type");
87+
var jsonSchemaFormat = (String) property.get("format");
7688
return switch (jsonSchemaType) {
7789
case "string" -> getJavaStringType(jsonSchemaFormat);
7890
case "integer" -> CD_Integer;
7991
case "number" -> CD_Double;
8092
case "boolean" -> CD_Boolean;
8193
// case "array" -> ?;
82-
// case "object" -> ?;
94+
case "object" -> getJavaObjectType(propertyKey);
8395
default -> CD_Object;
8496
};
8597
}
8698

99+
private ClassDesc getJavaObjectType(String propertyKey) {
100+
var definition = getModelPropertyDefinition(propertyKey, getRootProperties());
101+
var properties = definition.get("properties");
102+
var name = getPropertyName(propertyKey, definition);
103+
var schema =
104+
schemas.computeIfAbsent(
105+
name,
106+
it -> new Schema(it, processProperties((Map<String, Map<String, ?>>) properties)));
107+
return ClassDesc.of(schema.className());
108+
}
109+
110+
private String getPropertyName(String propertyKey, Map<String, ?> definition) {
111+
if (definition.containsKey("title")) {
112+
return (String) definition.get("title");
113+
}
114+
115+
return JavaName.fromJsonIdentifier("%s_%s".formatted(getRootClassName(), propertyKey));
116+
}
117+
118+
private Map<String, ?> getModelPropertyDefinition(
119+
String propertyKey, Map<String, Map<String, ?>> properties) {
120+
var definition = properties.get(propertyKey);
121+
if (definition == null) {
122+
throw new CompilerException("Cannot load property %s".formatted(propertyKey));
123+
}
124+
return definition;
125+
}
126+
127+
private Map<String, Map<String, ?>> getRootProperties() {
128+
//noinspection unchecked
129+
return ((Map<String, Map<String, ?>>)
130+
models.getOrDefault("properties", Collections.emptyMap()));
131+
}
132+
87133
private ClassDesc getJavaStringType(String jsonSchemaFormat) {
88134
if (jsonSchemaFormat == null) {
89135
return CD_String;

src/main/java/es/nachobrito/jsonschema/compiler/domain/schemareader/SchemaReader.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,11 @@
1818

1919
import es.nachobrito.jsonschema.compiler.domain.Schema;
2020
import java.net.URI;
21+
import java.util.List;
2122

2223
public interface SchemaReader {
2324

24-
Schema read(URI uri);
25+
List<Schema> read(URI uri);
2526

26-
Schema read(String jsonSchema);
27+
List<Schema> read(String jsonSchema);
2728
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
/*
2+
* Copyright 2024 Nacho Brito
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package es.nachobrito.jsonschema.compiler.domain.schemareader;
18+
19+
public interface SchemaReaderFactory {
20+
SchemaReader makeSchemaReader();
21+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
/*
2+
* Copyright 2024 Nacho Brito
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package es.nachobrito.jsonschema.compiler.infrastructure.jsonrefparser;
18+
19+
import es.nachobrito.jsonschema.compiler.domain.schemareader.SchemaReader;
20+
import es.nachobrito.jsonschema.compiler.domain.schemareader.SchemaReaderFactory;
21+
22+
public class JsonSchemaReaderFactory implements SchemaReaderFactory {
23+
@Override
24+
public SchemaReader makeSchemaReader() {
25+
return new JsonSchemaReader();
26+
}
27+
}

src/test/java/es/nachobrito/jsonschema/compiler/CompilerTest.java

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@
1717

1818
import static org.junit.jupiter.api.Assertions.assertTrue;
1919

20+
import com.fasterxml.jackson.databind.ObjectMapper;
21+
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
2022
import es.nachobrito.jsonschema.compiler.domain.Compiler;
2123
import es.nachobrito.jsonschema.compiler.domain.runtimeconfiguration.RuntimeConfigurationRecord;
2224
import es.nachobrito.jsonschema.compiler.infrastructure.jsonrefparser.JsonSchemaReader;
@@ -29,6 +31,8 @@
2931
import java.nio.file.Files;
3032
import java.nio.file.Path;
3133
import java.util.Comparator;
34+
35+
import es.nachobrito.jsonschema.compiler.infrastructure.jsonrefparser.JsonSchemaReaderFactory;
3236
import org.junit.jupiter.api.AfterEach;
3337
import org.junit.jupiter.api.BeforeEach;
3438

@@ -51,7 +55,8 @@ protected static Class<?> compileSampleSchemaFromFile(String filePath, String ex
5155
var uri = URI.create(filePath);
5256
Path destPath = Path.of(CompilerSmokeTest.TARGET_GENERATED_CLASSES);
5357
var destURL = destPath.toAbsolutePath().toFile().toURI().toURL();
54-
var compiler = new Compiler(new RuntimeConfigurationRecord(destPath, ""), new JsonSchemaReader());
58+
var compiler =
59+
new Compiler(new RuntimeConfigurationRecord(destPath, ""), new JsonSchemaReaderFactory());
5560
compiler.compile(uri);
5661
assertTrue(destPath.toFile().exists());
5762

@@ -62,7 +67,8 @@ protected static Class<?> compileSampleSchemaFromString(String jsonSchema, Strin
6267
throws IOException, ClassNotFoundException {
6368
Path destPath = Path.of(CompilerSmokeTest.TARGET_GENERATED_CLASSES);
6469
var destURL = destPath.toAbsolutePath().toFile().toURI().toURL();
65-
var compiler = new Compiler(new RuntimeConfigurationRecord(destPath, ""), new JsonSchemaReader());
70+
var compiler =
71+
new Compiler(new RuntimeConfigurationRecord(destPath, ""), new JsonSchemaReaderFactory());
6672
compiler.compile(jsonSchema);
6773
assertTrue(destPath.toFile().exists());
6874

@@ -82,4 +88,8 @@ void afterAll() throws IOException {
8288
.map(Path::toFile)
8389
.forEach(File::delete);
8490
}
91+
92+
protected ObjectMapper createObjectMapper() {
93+
return new ObjectMapper().registerModule(new JavaTimeModule());
94+
}
8595
}

src/test/java/es/nachobrito/jsonschema/compiler/DeserializationTest.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,7 @@
2020
import static org.junit.jupiter.api.Assertions.assertNotNull;
2121

2222
import com.fasterxml.jackson.annotation.JsonProperty;
23-
import com.fasterxml.jackson.databind.ObjectMapper;
24-
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
23+
2524
import java.io.IOException;
2625
import java.lang.reflect.InvocationTargetException;
2726
import java.time.LocalDate;
@@ -71,7 +70,7 @@ void expectJsonDocumentsDeserializable()
7170
}
7271
"""
7372
.formatted(theUUID.toString());
74-
var mapper = new ObjectMapper().registerModule(new JavaTimeModule());
73+
var mapper = createObjectMapper();
7574

7675
//First, verify deserialization works for the equivalent manually generated record:
7776
record User2(
@@ -90,4 +89,5 @@ record User2(
9089
assertEquals(LocalDate.parse("1970-04-01"), cls.getDeclaredMethod("dateOfBirth").invoke(user));
9190
assertEquals(theUUID, cls.getDeclaredMethod("uuid").invoke(user));
9291
}
92+
9393
}

0 commit comments

Comments
 (0)