Skip to content

Commit dec2280

Browse files
committed
feat: implement all simple json-schema data types. Add jackson annotations to generated bytecode for easier deserialization
1 parent 39637ef commit dec2280

File tree

16 files changed

+594
-98
lines changed

16 files changed

+594
-98
lines changed

pom.xml

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,17 @@
7070
<artifactId>junit-jupiter-params</artifactId>
7171
<scope>test</scope>
7272
</dependency>
73+
<dependency>
74+
<groupId>com.fasterxml.jackson.core</groupId>
75+
<artifactId>jackson-databind</artifactId>
76+
<version>2.16.1</version>
77+
</dependency>
78+
<dependency>
79+
<groupId>com.fasterxml.jackson.datatype</groupId>
80+
<artifactId>jackson-datatype-jsr310</artifactId>
81+
<version>2.17.2</version>
82+
<scope>test</scope>
83+
</dependency>
7384
<dependency>
7485
<groupId>io.github.zenwave360</groupId>
7586
<artifactId>json-schema-ref-parser-jvm</artifactId>

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,10 @@
2121
import static java.lang.constant.ClassDesc.of;
2222

2323
import es.nachobrito.jsonschema.compiler.domain.generator.ModelGenerator;
24+
import es.nachobrito.jsonschema.compiler.domain.schemareader.SchemaReader;
2425
import java.io.IOException;
2526
import java.lang.classfile.ClassBuilder;
2627
import java.lang.classfile.ClassFile;
27-
import java.lang.constant.ClassDesc;
2828
import java.net.URI;
2929
import java.nio.file.Path;
3030
import java.util.SortedMap;
@@ -84,10 +84,10 @@ private Path buildDestinationPath(String className) {
8484
}
8585

8686
private void writeRecord(
87-
String className, ClassBuilder classBuilder, SortedMap<String, ClassDesc> properties) {
87+
String className, ClassBuilder classBuilder, SortedMap<String, Property> properties) {
8888
classBuilder.withFlags(ACC_PUBLIC | ACC_FINAL).withSuperclass(of("java.lang.Record"));
8989

9090
var classDesc = of(className);
91-
ModelGenerator.of(classDesc, classBuilder, properties).forEach(ModelGenerator::generatePart);
91+
ModelGenerator.of(inputParameters, classDesc, classBuilder, properties).forEach(ModelGenerator::generatePart);
9292
}
9393
}

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,4 +41,8 @@ default Optional<Path> getJsonSchemaFile() {
4141
default Optional<String> getJsonSchemaCode() {
4242
return Optional.empty();
4343
}
44+
45+
default boolean withJacksonAnnotations() {
46+
return true;
47+
}
4448
}
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.lang.constant.ClassDesc;
20+
import java.util.regex.Pattern;
21+
22+
public record Property(String key, ClassDesc type) {
23+
private static final Pattern jsonIdPattern = Pattern.compile("[_\\-]([a-z])");
24+
25+
public String formattedName() {
26+
return jsonIdPattern.matcher(key).replaceAll(m -> m.group(1).toUpperCase());
27+
}
28+
}

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

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@
1616

1717
package es.nachobrito.jsonschema.compiler.domain;
1818

19-
import java.lang.constant.ClassDesc;
2019
import java.util.SortedMap;
2120

22-
public record Schema(String className, SortedMap<String, ClassDesc> properties) {}
21+
public record Schema(String className, SortedMap<String, Property> properties) {}

src/main/java/es/nachobrito/jsonschema/compiler/domain/generator/ConstructorGenerator.java

Lines changed: 45 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -21,33 +21,65 @@
2121
import static java.lang.constant.ConstantDescs.CD_void;
2222
import static java.lang.constant.ConstantDescs.INIT_NAME;
2323

24+
import com.fasterxml.jackson.annotation.JsonProperty;
25+
import es.nachobrito.jsonschema.compiler.domain.InputParameters;
26+
import es.nachobrito.jsonschema.compiler.domain.Property;
27+
import java.lang.classfile.Annotation;
28+
import java.lang.classfile.AnnotationElement;
2429
import java.lang.classfile.ClassBuilder;
30+
import java.lang.classfile.attribute.RuntimeVisibleAnnotationsAttribute;
31+
import java.lang.classfile.attribute.RuntimeVisibleParameterAnnotationsAttribute;
2532
import java.lang.constant.ClassDesc;
2633
import java.lang.constant.MethodTypeDesc;
34+
import java.util.List;
2735
import java.util.SortedMap;
2836

2937
record ConstructorGenerator(
30-
ClassDesc classDesc, ClassBuilder classBuilder, SortedMap<String, ClassDesc> properties)
38+
InputParameters inputParameters,
39+
ClassDesc classDesc,
40+
ClassBuilder classBuilder,
41+
SortedMap<String, es.nachobrito.jsonschema.compiler.domain.Property> properties)
3142
implements ModelGenerator {
3243
@Override
3344
public void generatePart() {
34-
var propertyTypes = properties.values().toArray(ClassDesc[]::new);
45+
var propertyTypes =
46+
properties.values().stream().map(Property::type).toArray(ClassDesc[]::new);
3547

36-
classBuilder.withMethodBody(
48+
classBuilder.withMethod(
3749
INIT_NAME,
3850
MethodTypeDesc.of(CD_void, propertyTypes),
3951
ACC_PUBLIC,
40-
builder -> {
41-
// Invoke parent constructor
42-
builder
43-
.aload(0)
44-
.invokespecial(of("java.lang.Record"), INIT_NAME, MethodTypeDesc.of(CD_void));
45-
// Set properties:
46-
int index = 1;
47-
for (var entry : properties.entrySet()) {
48-
builder.aload(0).aload(index++).putfield(classDesc, entry.getKey(), entry.getValue());
52+
methodBuilder -> {
53+
methodBuilder.withCode(
54+
codeBuilder -> {
55+
// Invoke parent constructor
56+
codeBuilder
57+
.aload(0)
58+
.invokespecial(of("java.lang.Record"), INIT_NAME, MethodTypeDesc.of(CD_void));
59+
// Set properties:
60+
int index = 1;
61+
for (var entry : properties.entrySet()) {
62+
codeBuilder
63+
.aload(0)
64+
.aload(index++)
65+
.putfield(
66+
classDesc, entry.getValue().formattedName(), entry.getValue().type());
67+
}
68+
codeBuilder.return_();
69+
});
70+
if (inputParameters.withJacksonAnnotations()) {
71+
var annotations =
72+
properties.values().stream()
73+
.map(
74+
property ->
75+
List.of(
76+
Annotation.of(
77+
ClassDesc.of(JsonProperty.class.getName()),
78+
AnnotationElement.ofString("value", property.key()))))
79+
.toList();
80+
81+
methodBuilder.with(RuntimeVisibleParameterAnnotationsAttribute.of(annotations));
4982
}
50-
builder.return_();
5183
});
5284
}
5385
}

src/main/java/es/nachobrito/jsonschema/compiler/domain/generator/EqualsGenerator.java

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@
1616

1717
package es.nachobrito.jsonschema.compiler.domain.generator;
1818

19+
import es.nachobrito.jsonschema.compiler.domain.InputParameters;
20+
1921
import static java.lang.classfile.ClassFile.ACC_FINAL;
2022
import static java.lang.classfile.ClassFile.ACC_PUBLIC;
2123
import static java.lang.constant.ConstantDescs.CD_Object;
@@ -30,7 +32,7 @@
3032
import java.util.SortedMap;
3133

3234
record EqualsGenerator(
33-
ClassDesc classDesc, ClassBuilder classBuilder, SortedMap<String, ClassDesc> properties)
35+
InputParameters inputParameters, ClassDesc classDesc, ClassBuilder classBuilder, SortedMap<String, es.nachobrito.jsonschema.compiler.domain.Property> properties)
3436
implements ModelGenerator {
3537
@Override
3638
public void generatePart() {
@@ -59,8 +61,8 @@ public void generatePart() {
5961
.entrySet()
6062
.forEach(
6163
entry -> {
62-
var propertyName = entry.getKey();
63-
var propertyDesc = entry.getValue();
64+
var propertyName = entry.getValue().formattedName();
65+
var propertyDesc = entry.getValue().type();
6466
compareProperty(propertyName, propertyDesc, cob, returnFalse);
6567
});
6668

src/main/java/es/nachobrito/jsonschema/compiler/domain/generator/HashCodeGenerator.java

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@
1616

1717
package es.nachobrito.jsonschema.compiler.domain.generator;
1818

19+
import es.nachobrito.jsonschema.compiler.domain.InputParameters;
20+
1921
import static java.lang.classfile.ClassFile.ACC_FINAL;
2022
import static java.lang.classfile.ClassFile.ACC_PUBLIC;
2123
import static java.lang.constant.ConstantDescs.*;
@@ -28,7 +30,7 @@
2830
import java.util.SortedMap;
2931

3032
record HashCodeGenerator(
31-
ClassDesc classDesc, ClassBuilder classBuilder, SortedMap<String, ClassDesc> properties)
33+
InputParameters inputParameters, ClassDesc classDesc, ClassBuilder classBuilder, SortedMap<String, es.nachobrito.jsonschema.compiler.domain.Property> properties)
3234
implements ModelGenerator {
3335

3436
/**
@@ -58,8 +60,8 @@ public void generatePart() {
5860
.entrySet()
5961
.forEach(
6062
entry -> {
61-
var propertyName = entry.getKey();
62-
var propertyDesc = entry.getValue();
63+
var propertyName = entry.getValue().formattedName();
64+
var propertyDesc = entry.getValue().type();
6365
cob.ldc(59).imul();
6466
loadFieldValue(propertyName, propertyDesc, cob);
6567
cob.iadd();

src/main/java/es/nachobrito/jsonschema/compiler/domain/generator/ModelGenerator.java

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@
1616

1717
package es.nachobrito.jsonschema.compiler.domain.generator;
1818

19+
import es.nachobrito.jsonschema.compiler.domain.InputParameters;
20+
import es.nachobrito.jsonschema.compiler.domain.Property;
1921
import java.lang.classfile.ClassBuilder;
2022
import java.lang.constant.ClassDesc;
2123
import java.util.Set;
@@ -24,13 +26,13 @@
2426
public interface ModelGenerator {
2527

2628
static Set<ModelGenerator> of(
27-
ClassDesc classDesc, ClassBuilder classBuilder, SortedMap<String, ClassDesc> properties) {
29+
InputParameters inputParameters, ClassDesc classDesc, ClassBuilder classBuilder, SortedMap<String, Property> properties) {
2830
return Set.of(
29-
new ConstructorGenerator(classDesc, classBuilder, properties),
30-
new PropertiesGenerator(classDesc, classBuilder, properties),
31-
new EqualsGenerator(classDesc, classBuilder, properties),
32-
new HashCodeGenerator(classDesc, classBuilder, properties),
33-
new ToStringGenerator(classDesc, classBuilder, properties));
31+
new ConstructorGenerator(inputParameters, classDesc, classBuilder, properties),
32+
new PropertiesGenerator(inputParameters, classDesc, classBuilder, properties),
33+
new EqualsGenerator(inputParameters, classDesc, classBuilder, properties),
34+
new HashCodeGenerator(inputParameters, classDesc, classBuilder, properties),
35+
new ToStringGenerator(inputParameters, classDesc, classBuilder, properties));
3436
}
3537

3638
void generatePart();

src/main/java/es/nachobrito/jsonschema/compiler/domain/generator/PropertiesGenerator.java

Lines changed: 35 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -18,37 +18,62 @@
1818

1919
import static java.lang.classfile.ClassFile.*;
2020

21+
import com.fasterxml.jackson.annotation.JsonProperty;
22+
import es.nachobrito.jsonschema.compiler.domain.InputParameters;
23+
import es.nachobrito.jsonschema.compiler.domain.Property;
24+
import java.lang.classfile.Annotation;
25+
import java.lang.classfile.AnnotationElement;
2126
import java.lang.classfile.ClassBuilder;
27+
import java.lang.classfile.attribute.RuntimeInvisibleAnnotationsAttribute;
2228
import java.lang.constant.ClassDesc;
2329
import java.lang.constant.MethodTypeDesc;
30+
import java.util.List;
2431
import java.util.SortedMap;
2532

2633
record PropertiesGenerator(
27-
ClassDesc classDesc, ClassBuilder classBuilder, SortedMap<String, ClassDesc> properties)
34+
InputParameters inputParameters,
35+
ClassDesc classDesc,
36+
ClassBuilder classBuilder,
37+
SortedMap<String, es.nachobrito.jsonschema.compiler.domain.Property> properties)
2838
implements ModelGenerator {
2939
@Override
3040
public void generatePart() {
3141
properties
3242
.entrySet()
33-
.forEach(entry -> buildProperty(classDesc, entry.getKey(), entry.getValue(), classBuilder));
43+
.forEach(entry -> buildProperty(classDesc, entry.getValue(), classBuilder));
3444
}
3545

36-
private void buildProperty(
37-
ClassDesc className, String name, ClassDesc type, ClassBuilder classBuilder) {
38-
buildField(name, type, classBuilder);
39-
buildAccessor(className, name, type, classBuilder);
46+
private void buildProperty(ClassDesc className, Property property, ClassBuilder classBuilder) {
47+
buildField(property, classBuilder);
48+
buildAccessor(className, property, classBuilder);
4049
}
4150

42-
private void buildAccessor(
43-
ClassDesc classDesc, String name, ClassDesc type, ClassBuilder classBuilder) {
51+
private void buildAccessor(ClassDesc classDesc, Property property, ClassBuilder classBuilder) {
52+
var name = property.formattedName();
53+
var type = property.type();
4454
classBuilder.withMethodBody(
4555
name,
4656
MethodTypeDesc.of(type),
4757
ACC_PUBLIC,
4858
builder -> builder.aload(0).getfield(classDesc, name, type).areturn());
4959
}
5060

51-
private void buildField(String name, ClassDesc type, ClassBuilder classBuilder) {
52-
classBuilder.withField(name, type, ACC_PRIVATE | ACC_FINAL);
61+
private void buildField(Property property, ClassBuilder classBuilder) {
62+
63+
classBuilder.withField(
64+
property.formattedName(),
65+
property.type(),
66+
fieldBuilder -> {
67+
fieldBuilder.withFlags(ACC_PRIVATE | ACC_FINAL);
68+
69+
if (inputParameters.withJacksonAnnotations()) {
70+
fieldBuilder.with(
71+
RuntimeInvisibleAnnotationsAttribute.of(
72+
List.of(
73+
Annotation.of(
74+
ClassDesc.of(JsonProperty.class.getName()),
75+
AnnotationElement.ofString("value", property.key())))));
76+
}
77+
});
5378
}
5479
}

src/main/java/es/nachobrito/jsonschema/compiler/domain/generator/ToStringGenerator.java

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -21,40 +21,44 @@
2121
import static java.lang.constant.ClassDesc.of;
2222
import static java.lang.constant.ConstantDescs.*;
2323

24+
import es.nachobrito.jsonschema.compiler.domain.InputParameters;
25+
import es.nachobrito.jsonschema.compiler.domain.Property;
2426
import java.lang.classfile.ClassBuilder;
2527
import java.lang.constant.*;
2628
import java.util.List;
2729
import java.util.SortedMap;
2830
import java.util.stream.Collectors;
2931

3032
record ToStringGenerator(
31-
ClassDesc classDesc, ClassBuilder classBuilder, SortedMap<String, ClassDesc> properties)
33+
InputParameters inputParameters, ClassDesc classDesc,
34+
ClassBuilder classBuilder,
35+
SortedMap<String, es.nachobrito.jsonschema.compiler.domain.Property> properties)
3236
implements ModelGenerator {
3337
@Override
3438
public void generatePart() {
3539
// loosely based on:
3640
// https://github.com/openjdk/babylon/blob/490332b12e479d8a0c164cb32dab1def982d8fce/hat/hat/src/main/java/hat/ifacemapper/ByteCodeGenerator.java#L36
3741
var nonArrayGetters =
3842
properties.entrySet().stream()
39-
.filter(entry -> !entry.getValue().isArray())
43+
.filter(entry -> !entry.getValue().type().isArray())
4044
.map(
4145
entry ->
4246
MethodHandleDesc.ofField(
4347
DirectMethodHandleDesc.Kind.GETTER,
4448
classDesc,
45-
entry.getKey(),
46-
entry.getValue()))
49+
entry.getValue().formattedName(),
50+
entry.getValue().type()))
4751
.toList();
4852

4953
var recipe =
5054
properties.entrySet().stream()
5155
.map(
5256
entry ->
53-
entry.getValue().isArray()
57+
entry.getValue().type().isArray()
5458
? String.format(
5559
"%s=%s%s",
56-
entry.getKey(), entry.getValue().arrayType().displayName(), "[]")
57-
: String.format("%s=\u0001", entry.getKey()))
60+
entry.getValue().formattedName(), entry.getValue().type().arrayType().displayName(), "[]")
61+
: String.format("%s=\u0001", entry.getValue().formattedName()))
5862
.collect(Collectors.joining(", ", classDesc.displayName() + "[", "]"));
5963

6064
DirectMethodHandleDesc bootstrap =
@@ -66,7 +70,7 @@ public void generatePart() {
6670
CD_Object.arrayType());
6771

6872
List<ClassDesc> getDescriptions =
69-
properties.values().stream().filter(it -> !it.isArray()).toList();
73+
properties.values().stream().map(Property::type).filter(it -> !it.isArray()).toList();
7074

7175
DynamicCallSiteDesc desc =
7276
DynamicCallSiteDesc.of(

0 commit comments

Comments
 (0)