Skip to content

Commit 52b747e

Browse files
committed
Implemented JsonSchemaParser and several new validation rules
1 parent e65f99e commit 52b747e

File tree

9 files changed

+275
-55
lines changed

9 files changed

+275
-55
lines changed

src/main/java/org/javawebstack/abstractdata/schema/AbstractArraySchema.java

Lines changed: 46 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,18 +2,19 @@
22

33
import org.javawebstack.abstractdata.AbstractArray;
44
import org.javawebstack.abstractdata.AbstractElement;
5+
import org.javawebstack.abstractdata.AbstractObject;
56
import org.javawebstack.abstractdata.AbstractPath;
67

7-
import java.util.ArrayList;
8-
import java.util.List;
9-
import java.util.Locale;
8+
import java.util.*;
109

1110
public class AbstractArraySchema implements AbstractSchema {
1211

1312
private AbstractSchema itemSchema;
1413
private Integer min;
1514
private Integer max;
1615
private boolean allowNull = false;
16+
17+
private boolean unique;
1718
private final List<CustomValidation<AbstractArray>> customValidations = new ArrayList<>();
1819

1920
public AbstractArraySchema itemSchema(AbstractSchema schema) {
@@ -35,6 +36,10 @@ public AbstractArraySchema allowNull() {
3536
this.allowNull = true;
3637
return this;
3738
}
39+
public AbstractArraySchema unique(){
40+
this.unique = true;
41+
return this;
42+
}
3843

3944
public AbstractArraySchema customValidation(CustomValidation<AbstractArray> validation) {
4045
customValidations.add(validation);
@@ -57,6 +62,25 @@ public List<CustomValidation<AbstractArray>> getCustomValidations() {
5762
return customValidations;
5863
}
5964

65+
@Override
66+
public AbstractObject toJsonSchema() {
67+
AbstractObject obj = new AbstractObject();
68+
obj.set("type","array");
69+
if(min != null){
70+
obj.set("minItems",min);
71+
}
72+
if(max != null){
73+
obj.set("maxItems",max);
74+
}
75+
if(itemSchema != null){
76+
obj.set("items",itemSchema.toJsonSchema());
77+
}
78+
if(unique) {
79+
obj.set("uniqueItems","true");
80+
}
81+
return obj;
82+
}
83+
6084
public List<SchemaValidationError> validate(AbstractPath path, AbstractElement value) {
6185
List<SchemaValidationError> errors = new ArrayList<>();
6286
if(value.getType() != AbstractElement.Type.ARRAY) {
@@ -70,10 +94,11 @@ public List<SchemaValidationError> validate(AbstractPath path, AbstractElement v
7094
if(max != null && array.size() > max) {
7195
errors.add(new SchemaValidationError(path, "too_many_items").meta("max", String.valueOf(max)).meta("actual", String.valueOf(array.size())));
7296
}
73-
if(itemSchema != null) {
74-
for(int i=0; i<array.size(); i++) {
75-
AbstractElement item = array.get(i);
76-
AbstractPath itemPath = path.subPath(String.valueOf(i));
97+
List<AbstractElement> seen = new ArrayList<>();
98+
for(int i=0; i<array.size(); i++) {
99+
AbstractElement item = array.get(i);
100+
AbstractPath itemPath = path.subPath(String.valueOf(i));
101+
if(itemSchema != null) {
77102
if(item.isNull()) {
78103
if(!allowNull) {
79104
errors.add(new SchemaValidationError(itemPath, "null_not_allowed"));
@@ -82,7 +107,21 @@ public List<SchemaValidationError> validate(AbstractPath path, AbstractElement v
82107
}
83108
errors.addAll(itemSchema.validate(itemPath, array.get(i)));
84109
}
110+
111+
if(unique){
112+
if(seen.contains(item)){
113+
int originalIndex = seen.indexOf(item);
114+
AbstractPath originalPath = path.subPath(String.valueOf(originalIndex));
115+
errors.add(new SchemaValidationError(itemPath,"duplicate_array_value")
116+
.meta("value",item.toJsonString())
117+
.meta("first",originalPath.toString()));
118+
}
119+
seen.add(item);
120+
}
121+
85122
}
123+
124+
86125
for(CustomValidation<AbstractArray> validation : customValidations) {
87126
errors.addAll(validation.validate(path, array));
88127
}

src/main/java/org/javawebstack/abstractdata/schema/AbstractBooleanSchema.java

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package org.javawebstack.abstractdata.schema;
22

33
import org.javawebstack.abstractdata.AbstractElement;
4+
import org.javawebstack.abstractdata.AbstractObject;
45
import org.javawebstack.abstractdata.AbstractPath;
56
import org.javawebstack.abstractdata.AbstractPrimitive;
67

@@ -31,6 +32,16 @@ public List<CustomValidation<AbstractPrimitive>> getCustomValidations() {
3132
return customValidations;
3233
}
3334

35+
@Override
36+
public AbstractObject toJsonSchema() {
37+
AbstractObject obj = new AbstractObject()
38+
.set("type","boolean");
39+
if(staticValue != null){
40+
obj.set("const",staticValue);
41+
}
42+
return obj;
43+
}
44+
3445
public List<SchemaValidationError> validate(AbstractPath path, AbstractElement value) {
3546
List<SchemaValidationError> errors = new ArrayList<>();
3647
if(value.getType() != AbstractElement.Type.BOOLEAN) {

src/main/java/org/javawebstack/abstractdata/schema/AbstractNumberSchema.java

Lines changed: 64 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package org.javawebstack.abstractdata.schema;
22

33
import org.javawebstack.abstractdata.AbstractElement;
4+
import org.javawebstack.abstractdata.AbstractObject;
45
import org.javawebstack.abstractdata.AbstractPath;
56
import org.javawebstack.abstractdata.AbstractPrimitive;
67

@@ -14,18 +15,36 @@ public class AbstractNumberSchema implements AbstractSchema {
1415
private boolean integerOnly = false;
1516
private Number min;
1617
private Number max;
18+
private boolean minExclusive = false;
19+
private boolean maxExclusive = false;
20+
private Number step;
1721
private final List<CustomValidation<AbstractPrimitive>> customValidations = new ArrayList<>();
1822

1923
public AbstractNumberSchema min(Number min) {
24+
return min(min,false);
25+
}
26+
27+
public AbstractNumberSchema max(Number max) {
28+
return max(max,false);
29+
}
30+
31+
public AbstractNumberSchema min(Number min, boolean exclusive) {
32+
this.minExclusive = exclusive;
2033
this.min = min;
2134
return this;
2235
}
2336

24-
public AbstractNumberSchema max(Number max) {
37+
public AbstractNumberSchema max(Number max, boolean exclusive) {
38+
this.maxExclusive = exclusive;
2539
this.max = max;
2640
return this;
2741
}
2842

43+
public AbstractNumberSchema step(Number step){
44+
this.step = step;
45+
return this;
46+
}
47+
2948
public AbstractNumberSchema integerOnly() {
3049
this.integerOnly = true;
3150
return this;
@@ -52,6 +71,36 @@ public List<CustomValidation<AbstractPrimitive>> getCustomValidations() {
5271
return customValidations;
5372
}
5473

74+
@Override
75+
public AbstractObject toJsonSchema() {
76+
AbstractObject obj = new AbstractObject();
77+
78+
obj.set("type",integerOnly ? "integer" : "number");
79+
if(min != null && max != null && !minExclusive && !maxExclusive){
80+
BigDecimal dMin = (min instanceof Float || min instanceof Double) ? BigDecimal.valueOf(min.doubleValue()) : BigDecimal.valueOf(min.longValue());
81+
BigDecimal dMax = (max instanceof Float || max instanceof Double) ? BigDecimal.valueOf(max.doubleValue()) : BigDecimal.valueOf(max.longValue());
82+
83+
if(dMin.compareTo(dMax)==0){
84+
obj.set("const",min);
85+
}
86+
87+
}
88+
if(!obj.has("const")) {
89+
if (min != null) {
90+
obj.set(minExclusive ? "exclusiveMinimum" : "minimum", min);
91+
}
92+
if (max != null) {
93+
obj.set(maxExclusive ? "exclusiveMaximum" : "maximum", max);
94+
}
95+
if (step != null) {
96+
obj.set("multipleOf", step);
97+
}
98+
}
99+
100+
101+
return obj;
102+
}
103+
55104
public List<SchemaValidationError> validate(AbstractPath path, AbstractElement value) {
56105
List<SchemaValidationError> errors = new ArrayList<>();
57106
if(value.getType() != AbstractElement.Type.NUMBER) {
@@ -66,16 +115,27 @@ public List<SchemaValidationError> validate(AbstractPath path, AbstractElement v
66115
}
67116
if(min != null) {
68117
BigDecimal dMin = (min instanceof Float || min instanceof Double) ? BigDecimal.valueOf(min.doubleValue()) : BigDecimal.valueOf(min.longValue());
69-
if(dN.compareTo(dMin) < 0) {
118+
if(!(dN.compareTo(dMin) > (minExclusive ? 0 : -1))) {
70119
errors.add(new SchemaValidationError(path, "number_smaller_than_min").meta("min", dMin.toPlainString()).meta("actual", dN.toPlainString()));
71120
}
72121
}
73122
if(max != null) {
74-
BigDecimal dMax = (max instanceof Float || min instanceof Double) ? BigDecimal.valueOf(max.doubleValue()) : BigDecimal.valueOf(max.longValue());
75-
if(dN.compareTo(dMax) > 0) {
123+
BigDecimal dMax = (max instanceof Float || max instanceof Double) ? BigDecimal.valueOf(max.doubleValue()) : BigDecimal.valueOf(max.longValue());
124+
if(!(dN.compareTo(dMax) < (maxExclusive ? 0 : 1))) {
76125
errors.add(new SchemaValidationError(path, "number_larger_than_max").meta("max", dMax.toPlainString()).meta("actual", dN.toPlainString()));
77126
}
78127
}
128+
if(step != null) {
129+
if(min != null && minExclusive){
130+
throw new UnsupportedOperationException("Step is not supported together with an exclusive minimum");
131+
}
132+
BigDecimal dMin = min == null ? BigDecimal.ZERO : (min instanceof Float || min instanceof Double) ? BigDecimal.valueOf(min.doubleValue()) : BigDecimal.valueOf(min.longValue());
133+
BigDecimal dStep = (step instanceof Float || step instanceof Double) ? BigDecimal.valueOf(step.doubleValue()) : BigDecimal.valueOf(step.longValue());
134+
135+
if(dN.subtract(dMin).remainder(dStep).compareTo(BigDecimal.ZERO) != 0) {
136+
errors.add(new SchemaValidationError(path, "number_not_within_step").meta("step", dStep.toPlainString()).meta("actual", dN.toPlainString()).meta("start",dMin.toPlainString()));
137+
}
138+
}
79139
for(CustomValidation<AbstractPrimitive> validation : customValidations) {
80140
errors.addAll(validation.validate(path, value.primitive()));
81141
}

src/main/java/org/javawebstack/abstractdata/schema/AbstractObjectSchema.java

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package org.javawebstack.abstractdata.schema;
22

3+
import org.javawebstack.abstractdata.AbstractArray;
34
import org.javawebstack.abstractdata.AbstractElement;
45
import org.javawebstack.abstractdata.AbstractObject;
56
import org.javawebstack.abstractdata.AbstractPath;
@@ -41,6 +42,30 @@ public AbstractObjectSchema additionalProperties(AbstractSchema schema) {
4142
return this;
4243
}
4344

45+
@Override
46+
public AbstractObject toJsonSchema() {
47+
AbstractObject obj = new AbstractObject();
48+
obj.set("type","object");
49+
AbstractObject properties = new AbstractObject();
50+
this.properties.forEach((key, value)->{
51+
properties.set(key,value.toJsonSchema());
52+
});
53+
obj.set("properties",properties);
54+
55+
if(!requiredProperties.isEmpty()){
56+
AbstractArray required = new AbstractArray();
57+
requiredProperties.forEach(required::add);
58+
obj.set("required",required);
59+
}
60+
if(!allowAdditionalProperties){
61+
obj.set("additionalProperties",false);
62+
}else if(additionalPropertySchema != null){
63+
obj.set("additionalProperties",additionalPropertySchema.toJsonSchema());
64+
}
65+
66+
return obj;
67+
}
68+
4469
public List<SchemaValidationError> validate(AbstractPath path, AbstractElement value) {
4570
List<SchemaValidationError> errors = new ArrayList<>();
4671
if(value.getType() != AbstractElement.Type.OBJECT) {

src/main/java/org/javawebstack/abstractdata/schema/AbstractSchema.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import java.util.List;
88

99
public interface AbstractSchema {
10+
AbstractObject toJsonSchema();
1011

1112
default List<SchemaValidationError> validate(AbstractElement value) {
1213
return validate(AbstractPath.ROOT, value);

src/main/java/org/javawebstack/abstractdata/schema/AbstractStringSchema.java

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
package org.javawebstack.abstractdata.schema;
22

3-
import org.javawebstack.abstractdata.AbstractElement;
4-
import org.javawebstack.abstractdata.AbstractPath;
5-
import org.javawebstack.abstractdata.AbstractPrimitive;
3+
import org.javawebstack.abstractdata.*;
64

75
import java.util.*;
86
import java.util.regex.Matcher;
@@ -73,6 +71,30 @@ public List<CustomValidation<AbstractPrimitive>> getCustomValidations() {
7371
return customValidations;
7472
}
7573

74+
@Override
75+
public AbstractObject toJsonSchema() {
76+
AbstractObject obj = new AbstractObject();
77+
obj.set("type","string");
78+
if(minLength != null){
79+
obj.set("minLength",minLength);
80+
}
81+
if(maxLength != null){
82+
obj.set("maxLength",maxLength);
83+
}
84+
if(staticValue != null) {
85+
obj.set("const",staticValue);
86+
}
87+
if(regex != null) {
88+
obj.set("pattern",regex);
89+
}
90+
if(enumValues != null) {
91+
AbstractArray arr = new AbstractArray(enumValues.toArray());
92+
obj.set("enum",arr);
93+
}
94+
95+
return obj;
96+
}
97+
7698
public List<SchemaValidationError> validate(AbstractPath path, AbstractElement value) {
7799
List<SchemaValidationError> errors = new ArrayList<>();
78100
if(value.getType() != AbstractElement.Type.STRING) {

0 commit comments

Comments
 (0)