diff --git a/.github/workflows/maven-deploy-release.yml b/.github/workflows/maven-deploy-release.yml index 2dd9cd8..49b71b5 100644 --- a/.github/workflows/maven-deploy-release.yml +++ b/.github/workflows/maven-deploy-release.yml @@ -20,7 +20,6 @@ jobs: - name: Deploy to OSSRH Repository run: mvn deploy -DbuildVersion=${{ github.event.release.tag_name }} -s build/settings.xml env: - DEPLOYMENT_USERNAME: ${{ secrets.DEPLOYMENT_USERNAME }} - DEPLOYMENT_PASSWORD: ${{ secrets.DEPLOYMENT_PASSWORD }} - GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }} - OSSRH_PASSWORD: ${{ secrets.OSSRH_PASSWORD }} \ No newline at end of file + CENTRAL_USERNAME: ${{ secrets.CENTRAL_USERNAME }} + CENTRAL_PASSWORD: ${{ secrets.CENTRAL_PASSWORD }} + GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }} \ No newline at end of file diff --git a/.github/workflows/maven-deploy.yml b/.github/workflows/maven-deploy.yml index 1141b8d..4554782 100644 --- a/.github/workflows/maven-deploy.yml +++ b/.github/workflows/maven-deploy.yml @@ -21,7 +21,6 @@ jobs: - name: Deploy to OSSRH Repository run: mvn deploy -s build/settings.xml env: - DEPLOYMENT_USERNAME: ${{ secrets.DEPLOYMENT_USERNAME }} - DEPLOYMENT_PASSWORD: ${{ secrets.DEPLOYMENT_PASSWORD }} - GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }} - OSSRH_PASSWORD: ${{ secrets.OSSRH_PASSWORD }} \ No newline at end of file + CENTRAL_USERNAME: ${{ secrets.CENTRAL_USERNAME }} + CENTRAL_PASSWORD: ${{ secrets.CENTRAL_PASSWORD }} + GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }} \ No newline at end of file diff --git a/README.md b/README.md index 021e4ff..4ca0d32 100644 --- a/README.md +++ b/README.md @@ -22,6 +22,6 @@ You can find the current docs on our [website](https://docs.javawebstack.org/fra org.javawebstack abstract-data - 1.0.6 + 1.0.7 ``` diff --git a/build/settings.xml b/build/settings.xml index 07e7726..88571fa 100644 --- a/build/settings.xml +++ b/build/settings.xml @@ -5,19 +5,9 @@ > - javawebstack-snapshots - ${env.DEPLOYMENT_USERNAME} - ${env.DEPLOYMENT_PASSWORD} - - - javawebstack-releases - ${env.DEPLOYMENT_USERNAME} - ${env.DEPLOYMENT_PASSWORD} - - - ossrh - JavaWebStack - ${env.OSSRH_PASSWORD} + central + ${env.CENTRAL_USERNAME} + ${env.CENTRAL_PASSWORD} gpg diff --git a/pom.xml b/pom.xml index 401f5c6..5353647 100644 --- a/pom.xml +++ b/pom.xml @@ -7,7 +7,7 @@ 8 8 - 1.0.6-SNAPSHOT + 1.0.7-SNAPSHOT org.javawebstack @@ -49,7 +49,7 @@ org.mongodb bson - 4.10.2 + 5.0.0 true @@ -58,22 +58,24 @@ 5.10.0 test + + org.mockito + mockito-core + 4.11.0 + test + - maven-deploy-plugin - 2.7 - - - default-deploy - deploy - - deploy - - - + org.sonatype.central + central-publishing-maven-plugin + 0.9.0 + true + + central + org.apache.maven.plugins @@ -104,7 +106,7 @@ org.apache.maven.plugins maven-gpg-plugin - 3.0.1 + 3.2.4 sign-artifacts @@ -113,7 +115,7 @@ sign - A313520526A8DFE1C2A30399C35A3D43C557B112 + EC9CCFF8901F0AA22191DCEDD619376246C066D0 gpg --no-tty @@ -129,13 +131,9 @@ - ossrh - https://s01.oss.sonatype.org/content/repositories/snapshots + central + https://central.sonatype.com/repository/maven-snapshots/ - - ossrh - https://s01.oss.sonatype.org/service/local/staging/deploy/maven2/ - - \ No newline at end of file + diff --git a/src/main/java/org/javawebstack/abstractdata/AbstractArray.java b/src/main/java/org/javawebstack/abstractdata/AbstractArray.java index 2d97ea4..c92fa98 100644 --- a/src/main/java/org/javawebstack/abstractdata/AbstractArray.java +++ b/src/main/java/org/javawebstack/abstractdata/AbstractArray.java @@ -22,84 +22,117 @@ public AbstractArray array(boolean strict) throws AbstractCoercingException { } public AbstractObject object(boolean strict) throws AbstractCoercingException { - if(strict) + if (strict) throw new AbstractCoercingException(Type.OBJECT, Type.ARRAY); AbstractObject object = new AbstractObject(); - for(int i=0; i< elements.size(); i++) + for (int i = 0; i < elements.size(); i++) object.set(String.valueOf(i), elements.get(i)); return object; } + @Deprecated public AbstractObject object(String key) throws AbstractCoercingException { - return query(key).object(); + AbstractElement e = query(key); + return e != null ? e.object() : null; } + @Deprecated public AbstractArray array(String key) throws AbstractCoercingException { - return query(key).array(); + AbstractElement e = query(key); + return e != null ? e.array() : null; } + @Deprecated public AbstractPrimitive primitive(String key) throws AbstractCoercingException { - return query(key).primitive(); + AbstractElement e = query(key); + return e != null ? e.primitive() : null; } + @Deprecated public String string(String key) throws AbstractCoercingException { - return query(key).string(); + AbstractElement e = query(key); + return e != null ? e.string() : null; } + @Deprecated public Boolean bool(String key) throws AbstractCoercingException { - return query(key).bool(); + AbstractElement e = query(key); + return e != null ? e.bool() : null; } + @Deprecated public Number number(String key) throws AbstractCoercingException { - return query(key).number(); + AbstractElement e = query(key); + return e != null ? e.number() : null; } + @Deprecated public AbstractObject object(String key, AbstractObject orElse) throws AbstractCoercingException { - return query(key, orElse).object(); + AbstractElement e = query(key, orElse); + return e != null ? e.object() : null; } + @Deprecated public AbstractArray array(String key, AbstractArray orElse) throws AbstractCoercingException { - return query(key, orElse).array(); + AbstractElement e = query(key, orElse); + return e != null ? e.array() : null; } + @Deprecated public AbstractPrimitive primitive(String key, AbstractPrimitive orElse) throws AbstractCoercingException { - return query(key, orElse).primitive(); + AbstractElement e = query(key, orElse); + return e != null ? e.primitive() : null; } + @Deprecated public String string(String key, String orElse) throws AbstractCoercingException { - return query(key, new AbstractPrimitive(orElse)).string(); + AbstractPrimitive orElsePrimitive = orElse != null ? new AbstractPrimitive(orElse) : null; + AbstractElement e = primitive(key, orElsePrimitive); + return e != null ? e.string() : null; } + @Deprecated public Boolean bool(String key, Boolean orElse) throws AbstractCoercingException { - return query(key, new AbstractPrimitive(orElse)).bool(); + AbstractPrimitive orElsePrimitive = orElse != null ? new AbstractPrimitive(orElse) : null; + AbstractElement e = primitive(key, orElsePrimitive); + return e != null ? e.bool() : null; } + @Deprecated public Number number(String key, Number orElse) throws AbstractCoercingException { - return query(key, new AbstractPrimitive(orElse)).number(); + AbstractPrimitive orElsePrimitive = orElse != null ? new AbstractPrimitive(orElse) : null; + AbstractElement e = primitive(key, orElsePrimitive); + return e != null ? e.number() : null; } public AbstractObject object(int index) throws AbstractCoercingException { - return get(index).object(); + AbstractElement e = get(index); + return e != null ? e.object() : null; } public AbstractArray array(int index) throws AbstractCoercingException { - return get(index).array(); + AbstractElement e = get(index); + return e != null ? e.array() : null; } public AbstractPrimitive primitive(int index) throws AbstractCoercingException { - return get(index).primitive(); + AbstractElement e = get(index); + return e != null ? e.primitive() : null; } public String string(int index) throws AbstractCoercingException { - return get(index).string(); + AbstractElement e = get(index); + return e != null ? e.string() : null; } public Boolean bool(int index) throws AbstractCoercingException { - return get(index).bool(); + AbstractElement e = get(index); + return e != null ? e.bool() : null; } public Number number(int index) throws AbstractCoercingException { - return get(index).number(); + AbstractElement e = get(index); + return e != null ? e.number() : null; } public AbstractObject object(int index, AbstractObject orElse) throws AbstractCoercingException { @@ -214,6 +247,8 @@ public AbstractElement[] toArray() { } public AbstractElement get(int i) { + if(i >= elements.size()) + return null; return elements.get(i); } @@ -223,15 +258,17 @@ public AbstractElement get(int index, AbstractElement orElse) { } public AbstractElement query(String query) { + if(query == null) + throw new IllegalArgumentException("query can not be null"); String[] q = query.split("\\.", 2); try { int index = Integer.parseInt(q[0]); AbstractElement e = get(index); - if(e == null || q.length == 1) + if (e == null || q.length == 1) return e; - if(e.isObject()) + if (e.isObject()) return e.object().query(q[1]); - if(e.isArray()) + if (e.isArray()) return e.array().query(q[1]); return null; } catch (NumberFormatException nfe) { @@ -281,7 +318,7 @@ public List toStringList() { public List toStringList(boolean strict) { List list = new ArrayList<>(); - for(AbstractElement e : elements) + for (AbstractElement e : elements) list.add(e.string(strict)); return list; } @@ -292,7 +329,7 @@ public List toObjectList() { public List toObjectList(boolean strict) { List list = new ArrayList<>(); - for(AbstractElement e : elements) + for (AbstractElement e : elements) list.add(e.object(strict)); return list; } @@ -365,11 +402,13 @@ public boolean equals(Object obj, boolean strict) { } } - public boolean equals (Object obj) { + public boolean equals(Object obj) { return equals(obj, false); } public AbstractArray addAll(AbstractArray array) { + if(array == null) + throw new IllegalArgumentException("array may not be null"); elements.addAll(array.elements); return this; } diff --git a/src/main/java/org/javawebstack/abstractdata/AbstractElement.java b/src/main/java/org/javawebstack/abstractdata/AbstractElement.java index 852a783..2db30ca 100644 --- a/src/main/java/org/javawebstack/abstractdata/AbstractElement.java +++ b/src/main/java/org/javawebstack/abstractdata/AbstractElement.java @@ -7,6 +7,8 @@ import org.javawebstack.abstractdata.json.JsonDumper; import org.javawebstack.abstractdata.json.JsonParser; import org.javawebstack.abstractdata.util.QueryString; +import org.javawebstack.abstractdata.yaml.YamlDumper; +import org.javawebstack.abstractdata.yaml.YamlParser; import org.yaml.snakeyaml.DumperOptions; import org.yaml.snakeyaml.Yaml; @@ -110,16 +112,7 @@ default String toJsonString() { Object toObject(); default String toYaml(boolean pretty) { - Yaml yaml; - if (pretty) { - DumperOptions options = new DumperOptions(); - options.setDefaultFlowStyle(DumperOptions.FlowStyle.BLOCK); - options.setPrettyFlow(true); - yaml = new Yaml(options); - } else { - yaml = new Yaml(); - } - return yaml.dump(toObject()); + return new YamlDumper().setPretty(pretty).dump(this); } default String toYaml() { @@ -127,24 +120,23 @@ default String toYaml() { } static AbstractElement fromYaml(String source, boolean singleRoot) { - Yaml yaml = new Yaml(); - Object object = yaml.load(source); - if (singleRoot && object instanceof List) { - List list = (List) object; - if (list.size() == 0) { - object = new HashMap<>(); - } else { - object = list.get(0); - } + try { + return new YamlParser().setSingleRoot(singleRoot).parse(source); + } catch (ParseException e) { + throw new RuntimeException(e); } - return fromAbstractObject(object); } static AbstractElement fromYaml(String source) { return fromYaml(source, false); } + @Deprecated static AbstractElement fromAbstractObject(Object object) { + return fromObject(object); + } + + static AbstractElement fromObject(Object object) { if (object == null) return AbstractNull.VALUE; if (object instanceof List) { diff --git a/src/main/java/org/javawebstack/abstractdata/AbstractNull.java b/src/main/java/org/javawebstack/abstractdata/AbstractNull.java index de84b06..cb949b5 100644 --- a/src/main/java/org/javawebstack/abstractdata/AbstractNull.java +++ b/src/main/java/org/javawebstack/abstractdata/AbstractNull.java @@ -60,7 +60,7 @@ public boolean equals(Object obj, boolean strict) { return obj == null || obj instanceof AbstractNull; } - public boolean equals (Object obj) { + public boolean equals(Object obj) { return equals(obj, false); } } diff --git a/src/main/java/org/javawebstack/abstractdata/AbstractObject.java b/src/main/java/org/javawebstack/abstractdata/AbstractObject.java index 4130884..904f6d8 100644 --- a/src/main/java/org/javawebstack/abstractdata/AbstractObject.java +++ b/src/main/java/org/javawebstack/abstractdata/AbstractObject.java @@ -71,7 +71,7 @@ public AbstractObject object(boolean strict) throws AbstractCoercingException { } public AbstractArray array(boolean strict) throws AbstractCoercingException { - if(strict) + if (strict) throw new AbstractCoercingException(Type.ARRAY, Type.OBJECT); AbstractArray array = new AbstractArray(); for (int i = 0; i < size(); i++) { @@ -94,17 +94,17 @@ public AbstractElement get(String key, AbstractElement orElse) { public AbstractElement query(String query) { String[] q = query.split("\\.", 2); AbstractElement e = get(q[0]); - if(e == null || q.length == 1) + if (e == null || q.length == 1) return e; - if(e.isObject()) + if (e.isObject()) return e.object().query(q[1]); - if(e.isArray()) + if (e.isArray()) return e.array().query(q[1]); return null; } public AbstractElement query(String query, AbstractElement orElse) { - if(orElse == null) + if (orElse == null) orElse = AbstractNull.VALUE; AbstractElement value = query(query); return (value != null && !value.isNull()) ? value : orElse; @@ -288,7 +288,7 @@ public boolean equals(Object obj, boolean strict) { } } - public boolean equals (Object obj) { + public boolean equals(Object obj) { return equals(obj, false); } } diff --git a/src/main/java/org/javawebstack/abstractdata/AbstractPath.java b/src/main/java/org/javawebstack/abstractdata/AbstractPath.java new file mode 100644 index 0000000..0347f2e --- /dev/null +++ b/src/main/java/org/javawebstack/abstractdata/AbstractPath.java @@ -0,0 +1,97 @@ +package org.javawebstack.abstractdata; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +public class AbstractPath { + + public static final AbstractPath ROOT = new AbstractPath(null, null); + + private final AbstractPath parent; + private final String name; + + public AbstractPath(String name) { + this(ROOT, name); + if (name == null || name.isEmpty()) + throw new IllegalArgumentException("Name can not be null or empty"); + } + + private AbstractPath(AbstractPath parent, String name) { + this.parent = parent; + this.name = name; + } + + public String getName() { + return name; + } + + public AbstractPath getParent() { + return parent; + } + + public AbstractPath subPath(String name) { + return new AbstractPath(this, name); + } + + public AbstractPath clone() { + return new AbstractPath( + this.parent != null ? this.parent.clone() : null, + name + ); + } + + public AbstractPath concat(AbstractPath path) { + AbstractPath cloned = clone(); + for (String part : path.getParts()) + cloned = cloned.subPath(part); + return cloned; + } + + public List getParts() { + List parts = parent != null ? parent.getParts() : new ArrayList<>(); + if (name != null) + parts.add(name); + return parts; + } + + public String toString() { + return String.join(".", getParts()); + } + + public static AbstractPath parse(String s) { + s = s.trim(); + if (s.isEmpty()) + return ROOT; + String[] spl = s.split("\\."); + AbstractPath path = new AbstractPath(spl[0]); + for (int i = 1; i < spl.length; i++) { + String sub = spl[i]; + if (sub.isEmpty()) + throw new IllegalArgumentException("Invalid empty sub-path"); + path = path.subPath(spl[i]); + } + return path; + } + + public boolean equals(Object obj) { + if (!(obj instanceof AbstractPath)) + return false; + AbstractPath other = (AbstractPath) obj; + if (parent != null) { + if (!parent.equals(other.parent)) + return false; + } else { + if (other.parent != null) + return false; + } + if (name == null) + return other.name == null; + return name.equals(other.name); + } + + public int hashCode() { + return Objects.hash(parent, name); + } + +} diff --git a/src/main/java/org/javawebstack/abstractdata/AbstractPrimitive.java b/src/main/java/org/javawebstack/abstractdata/AbstractPrimitive.java index 8a7f3d5..188c6fe 100644 --- a/src/main/java/org/javawebstack/abstractdata/AbstractPrimitive.java +++ b/src/main/java/org/javawebstack/abstractdata/AbstractPrimitive.java @@ -11,19 +11,19 @@ public class AbstractPrimitive implements AbstractElement { private final Object value; public AbstractPrimitive(Number value) { - if(value == null) + if (value == null) throw new NullPointerException("AbstractPrimitive value can not be null"); this.value = value; } public AbstractPrimitive(Boolean value) { - if(value == null) + if (value == null) throw new NullPointerException("AbstractPrimitive value can not be null"); this.value = value; } public AbstractPrimitive(String value) { - if(value == null) + if (value == null) throw new NullPointerException("AbstractPrimitive value can not be null"); this.value = value; } @@ -53,8 +53,8 @@ public String string() throws AbstractCoercingException { } public String string(boolean strict) throws AbstractCoercingException { - if(!(value instanceof String)) { - if(strict) + if (!(value instanceof String)) { + if (strict) throw new AbstractCoercingException(Type.STRING, getType()); switch (getType()) { case BOOLEAN: @@ -73,8 +73,8 @@ public Number number() throws AbstractCoercingException { } public Number number(boolean strict) throws AbstractCoercingException { - if(!(value instanceof Number)) { - if(strict) + if (!(value instanceof Number)) { + if (strict) throw new AbstractCoercingException(Type.NUMBER, getType()); switch (getType()) { case BOOLEAN: @@ -98,8 +98,8 @@ public Boolean bool() throws AbstractCoercingException { } public Boolean bool(boolean strict) throws AbstractCoercingException { - if(!(value instanceof Boolean)) { - if(strict) + if (!(value instanceof Boolean)) { + if (strict) throw new AbstractCoercingException(Type.BOOLEAN, getType()); switch (getType()) { case STRING: { @@ -120,9 +120,9 @@ public Boolean bool(boolean strict) throws AbstractCoercingException { } case NUMBER: { long l = ((Number) value).longValue(); - if(l == 0) + if (l == 0) return false; - if(l == 1) + if (l == 1) return true; throw new AbstractCoercingException(Type.BOOLEAN, this); } @@ -182,17 +182,18 @@ public boolean equals(Object obj, boolean strict) { return false; AbstractPrimitive primitive = (AbstractPrimitive) obj; try { - if(isBoolean()) + if (isBoolean()) return bool() == primitive.bool(strict); - if(isNumber()) + if (isNumber()) return number().equals(primitive.number(strict)); - if(isString()) + if (isString()) return string().equals(primitive.string(strict)); - } catch (AbstractCoercingException ignored) {} + } catch (AbstractCoercingException ignored) { + } return false; } - public boolean equals (Object obj) { + public boolean equals(Object obj) { return equals(obj, false); } } diff --git a/src/main/java/org/javawebstack/abstractdata/NamingPolicy.java b/src/main/java/org/javawebstack/abstractdata/NamingPolicy.java index ce6814c..39a41b8 100644 --- a/src/main/java/org/javawebstack/abstractdata/NamingPolicy.java +++ b/src/main/java/org/javawebstack/abstractdata/NamingPolicy.java @@ -1,7 +1,7 @@ package org.javawebstack.abstractdata; public enum NamingPolicy { - + NONE(org.javawebstack.abstractdata.mapper.naming.NamingPolicy.NONE), CAMEL_CASE(org.javawebstack.abstractdata.mapper.naming.NamingPolicy.CAMEL_CASE), PASCAL_CASE(org.javawebstack.abstractdata.mapper.naming.NamingPolicy.PASCAL_CASE), diff --git a/src/main/java/org/javawebstack/abstractdata/bson/BsonConverter.java b/src/main/java/org/javawebstack/abstractdata/bson/BsonConverter.java index c0d8123..1ddea0f 100644 --- a/src/main/java/org/javawebstack/abstractdata/bson/BsonConverter.java +++ b/src/main/java/org/javawebstack/abstractdata/bson/BsonConverter.java @@ -97,72 +97,73 @@ public AbstractElement toAbstract(BsonValue value) { } public BsonValue toBson(AbstractElement element) { - if(element == null || element.isNull()) + if (element == null || element.isNull()) return BsonNull.VALUE; - if(element.isString()) + if (element.isString()) return new BsonString(element.string()); - if(element.isBoolean()) + if (element.isBoolean()) return new BsonBoolean(element.bool()); - if(element.isNumber()) { + if (element.isNumber()) { Number n = element.number(); - if(n instanceof Integer || n instanceof Short || n instanceof Byte) { + if (n instanceof Integer || n instanceof Short || n instanceof Byte) { return new BsonInt32(n.intValue()); - } else if(n instanceof Long) { + } else if (n instanceof Long) { return new BsonInt64(n.longValue()); - } else if(n instanceof Float || n instanceof Double) { + } else if (n instanceof Float || n instanceof Double) { return new BsonDouble(n.doubleValue()); } } - if(element.isArray()) { + if (element.isArray()) { BsonArray a = new BsonArray(); - for(AbstractElement e : element.array()) + for (AbstractElement e : element.array()) a.add(toBson(e)); return a; } AbstractObject o = element.object(); - if(o.size() == 1 && o.has("$oid") && o.get("$oid").isString()) + if (o.size() == 1 && o.has("$oid") && o.get("$oid").isString()) return new BsonObjectId(new ObjectId(o.string("$oid"))); - if(o.size() == 1 && o.has("$undefined") && o.get("$undefined").isBoolean()) + if (o.size() == 1 && o.has("$undefined") && o.get("$undefined").isBoolean()) return new BsonUndefined(); - if(o.size() == 1 && o.has("$date") && o.get("$date").isString()) { + if (o.size() == 1 && o.has("$date") && o.get("$date").isString()) { try { return new BsonDateTime(dateFormat.parse(o.string("$date")).getTime()); - } catch (ParseException ignored) {} + } catch (ParseException ignored) { + } } - if(o.size() == 1 && o.has("$numberDecimal") && o.get("$numberDecimal").isString()) + if (o.size() == 1 && o.has("$numberDecimal") && o.get("$numberDecimal").isString()) return new BsonDecimal128(Decimal128.parse(o.string("$numberDecimal"))); - if(o.size() == 1 && o.has("$minKey") && o.get("$minKey").isNumber()) + if (o.size() == 1 && o.has("$minKey") && o.get("$minKey").isNumber()) return new BsonMinKey(); - if(o.size() == 1 && o.has("$maxKey") && o.get("$maxKey").isNumber()) + if (o.size() == 1 && o.has("$maxKey") && o.get("$maxKey").isNumber()) return new BsonMinKey(); - if(o.size() == 1 && o.has("$symbol") && o.get("$symbol").isString()) + if (o.size() == 1 && o.has("$symbol") && o.get("$symbol").isString()) return new BsonSymbol(o.string("$symbol")); - if(o.size() == 1 && o.has("$code") && o.get("$code").isString()) + if (o.size() == 1 && o.has("$code") && o.get("$code").isString()) return new BsonJavaScript(o.string("$code")); - if(o.size() == 2 && o.has("$code") && o.has("$scope") && o.get("$code").isString() && o.get("$scope").isObject()) + if (o.size() == 2 && o.has("$code") && o.has("$scope") && o.get("$code").isString() && o.get("$scope").isObject()) return new BsonJavaScriptWithScope(o.string("$code"), toBson(o.get("$scope")).asDocument()); - if(o.size() == 1 && o.has("$timestamp") && o.get("$timestamp").isObject()) { + if (o.size() == 1 && o.has("$timestamp") && o.get("$timestamp").isObject()) { AbstractObject ts = o.object("$timestamp"); - if(ts.has("t") && ts.has("i") && ts.get("t").isNumber() && ts.get("i").isNumber()) { + if (ts.has("t") && ts.has("i") && ts.get("t").isNumber() && ts.get("i").isNumber()) { return new BsonTimestamp((int) ts.number("t").longValue(), (int) ts.number("i").longValue()); } } - if(o.size() == 1 && o.has("$regularExpression") && o.get("$regularExpression").isObject()) { + if (o.size() == 1 && o.has("$regularExpression") && o.get("$regularExpression").isObject()) { AbstractObject re = o.object("$regularExpression"); - if(re.has("pattern") && re.has("options") && re.get("pattern").isString() && (re.get("options").isString() || re.get("options").isNull())) { + if (re.has("pattern") && re.has("options") && re.get("pattern").isString() && (re.get("options").isString() || re.get("options").isNull())) { return new BsonRegularExpression(re.string("pattern"), (String) re.toObject()); } } - if(o.size() == 1 && o.has("$binary") && o.get("$binary").isObject()) { + if (o.size() == 1 && o.has("$binary") && o.get("$binary").isObject()) { AbstractObject bin = o.object("$binary"); - if(bin.has("base64") && bin.has("subType") && bin.get("base64").isString() && bin.get("subType").isString()) { + if (bin.has("base64") && bin.has("subType") && bin.get("base64").isString() && bin.get("subType").isString()) { byte[] data = Base64.getDecoder().decode(bin.string("base64")); byte type = (byte) Integer.parseInt(bin.string("subType"), 16); return new BsonBinary(type, data); } } BsonDocument doc = new BsonDocument(o.size()); - for(String k : o.keys()) + for (String k : o.keys()) doc.put(k, toBson(o.get(k))); return doc; } diff --git a/src/main/java/org/javawebstack/abstractdata/bson/BsonTypeAdapter.java b/src/main/java/org/javawebstack/abstractdata/bson/BsonTypeAdapter.java index 7f06556..9489bcb 100644 --- a/src/main/java/org/javawebstack/abstractdata/bson/BsonTypeAdapter.java +++ b/src/main/java/org/javawebstack/abstractdata/bson/BsonTypeAdapter.java @@ -28,36 +28,36 @@ public BsonTypeAdapter(BsonConverter converter) { } public AbstractElement toAbstract(MapperContext context, Object value) throws MapperException { - if(value instanceof byte[]) { + if (value instanceof byte[]) { ByteBuffer buffer = ByteBuffer.wrap((byte[]) value); buffer.order(ByteOrder.LITTLE_ENDIAN); return converter.toAbstract(new BsonDocumentCodec().decode(new BsonBinaryReader(buffer), DecoderContext.builder().build())); } - if(value instanceof ObjectId) + if (value instanceof ObjectId) return converter.toAbstract((ObjectId) value); - if(value instanceof Decimal128) + if (value instanceof Decimal128) return converter.toAbstract((Decimal128) value); return converter.toAbstract((BsonValue) value); } public Object fromAbstract(MapperContext context, AbstractElement element, Class type) throws MapperException { BsonValue value = converter.toBson(element); - if(value instanceof BsonNull && !BsonNull.class.equals(type)) + if (value instanceof BsonNull && !BsonNull.class.equals(type)) return null; - if(byte[].class.equals(type)) { + if (byte[].class.equals(type)) { BasicOutputBuffer outputBuffer = new BasicOutputBuffer(); new BsonDocumentCodec().encode(new BsonBinaryWriter(outputBuffer), value.asDocument(), EncoderContext.builder().build()); return outputBuffer.toByteArray(); } - if(ObjectId.class.equals(type)) + if (ObjectId.class.equals(type)) return value.asObjectId().getValue(); - if(Decimal128.class.equals(type)) + if (Decimal128.class.equals(type)) return value.asDecimal128().getValue(); return value; } public Class[] getSupportedTypes() { - return new Class[] { + return new Class[]{ ObjectId.class, Decimal128.class, BsonValue.class, diff --git a/src/main/java/org/javawebstack/abstractdata/collector/AbstractArrayCollector.java b/src/main/java/org/javawebstack/abstractdata/collector/AbstractArrayCollector.java index d924278..07b2d76 100644 --- a/src/main/java/org/javawebstack/abstractdata/collector/AbstractArrayCollector.java +++ b/src/main/java/org/javawebstack/abstractdata/collector/AbstractArrayCollector.java @@ -30,7 +30,10 @@ public BiConsumer, T> accumulator() { } public BinaryOperator> combiner() { - return (l1, l2) -> { l1.addAll(l2); return l1; }; + return (l1, l2) -> { + l1.addAll(l2); + return l1; + }; } public Function, AbstractArray> finisher() { @@ -43,7 +46,6 @@ public Function, AbstractArray> finisher() { public Set characteristics() { Set characteristics = new HashSet<>(); - characteristics.add(Characteristics.UNORDERED); return characteristics; } diff --git a/src/main/java/org/javawebstack/abstractdata/collector/AbstractObjectCollector.java b/src/main/java/org/javawebstack/abstractdata/collector/AbstractObjectCollector.java index 8b53da0..2cefabc 100644 --- a/src/main/java/org/javawebstack/abstractdata/collector/AbstractObjectCollector.java +++ b/src/main/java/org/javawebstack/abstractdata/collector/AbstractObjectCollector.java @@ -32,7 +32,10 @@ public BiConsumer, T> accumulator() { } public BinaryOperator> combiner() { - return (m1, m2) -> { m1.putAll(m2); return m1; }; + return (m1, m2) -> { + m1.putAll(m2); + return m1; + }; } public Function, AbstractObject> finisher() { diff --git a/src/main/java/org/javawebstack/abstractdata/json/JsonDumper.java b/src/main/java/org/javawebstack/abstractdata/json/JsonDumper.java index 375e639..f33abf5 100644 --- a/src/main/java/org/javawebstack/abstractdata/json/JsonDumper.java +++ b/src/main/java/org/javawebstack/abstractdata/json/JsonDumper.java @@ -29,31 +29,31 @@ public String dump(AbstractElement element) { } private List dumpLines(AbstractElement element) { - if(element == null || element.isNull()) + if (element == null || element.isNull()) return Collections.singletonList("null"); - if(element.isBoolean()) + if (element.isBoolean()) return Collections.singletonList(element.bool().toString()); - if(element.isNumber()) + if (element.isNumber()) return Collections.singletonList(element.number().toString()); - if(element.isString()) + if (element.isString()) return Collections.singletonList("\"" + escape(element.string()) + "\""); - if(element.isObject()) { - List lines =new ArrayList<>(); - if(element.object().size() == 0) { + if (element.isObject()) { + List lines = new ArrayList<>(); + if (element.object().size() == 0) { lines.add("{}"); return lines; } - if(pretty) { + if (pretty) { lines.add("{"); List keys = new ArrayList<>(element.object().keys()); - for(int i=0; i vLines = dumpLines(element.object().get(k)); lines.add(indent + "\"" + escape(k) + "\": " + vLines.get(0)); - for(int j=1; j dumpLines(AbstractElement element) { } return lines; } - if(element.isArray()) { - List lines =new ArrayList<>(); - if(element.array().size() == 0) { + if (element.isArray()) { + List lines = new ArrayList<>(); + if (element.array().size() == 0) { lines.add("[]"); return lines; } - if(pretty) { + if (pretty) { lines.add("["); AbstractArray array = element.array(); - for(int i=0; i vLines = dumpLines(array.get(i)); - for(int j=0; j dumpLines(AbstractElement element) { } return lines; } - return new ArrayList<>(); + throw new IllegalArgumentException("Unknown element type"); // Should be unreachable } private static String escape(String s) { StringBuilder sb = new StringBuilder(); - for(int i=0; i= '\u007F' && ch <= '\u009F' || ch >= '\u2000' && ch <= '\u20FF') { String hex = Integer.toHexString(ch); sb.append("\\u"); - for(int k=0; k < 4-hex.length(); k++) + for (int k = 0; k < 4 - hex.length(); k++) sb.append('0'); sb.append(hex.toUpperCase(Locale.ROOT)); } else { diff --git a/src/main/java/org/javawebstack/abstractdata/json/JsonParser.java b/src/main/java/org/javawebstack/abstractdata/json/JsonParser.java index 549f096..980a7f0 100644 --- a/src/main/java/org/javawebstack/abstractdata/json/JsonParser.java +++ b/src/main/java/org/javawebstack/abstractdata/json/JsonParser.java @@ -3,36 +3,33 @@ import org.javawebstack.abstractdata.*; import java.text.ParseException; -import java.util.ArrayDeque; -import java.util.ArrayList; -import java.util.Deque; -import java.util.List; +import java.util.*; public class JsonParser { public AbstractElement parse(String json) throws ParseException { char[] primChars = json.toCharArray(); List chars = new ArrayList<>(primChars.length); - for(int i=0; i stack = new ArrayDeque<>(chars); AbstractElement parsed; try { parsed = parse(stack); - } catch (NullPointerException ex) { + } catch (NoSuchElementException | NullPointerException ex) { throw new ParseException("Unexpected character ", primChars.length); } - if(parsed == null) { + if (parsed == null) { int line = 1; int pos = 1; - for(int i=0; i stack) { return parseString(stack); case 't': { stack.pop(); - if(stack.peek() != 'r') + if (stack.peek() != 'r') return null; stack.pop(); - if(stack.peek() != 'u') + if (stack.peek() != 'u') return null; stack.pop(); - if(stack.peek() != 'e') + if (stack.peek() != 'e') return null; stack.pop(); return new AbstractPrimitive(true); } case 'f': { stack.pop(); - if(stack.peek() != 'a') + if (stack.peek() != 'a') return null; stack.pop(); - if(stack.peek() != 'l') + if (stack.peek() != 'l') return null; stack.pop(); - if(stack.peek() != 's') + if (stack.peek() != 's') return null; stack.pop(); - if(stack.peek() != 'e') + if (stack.peek() != 'e') return null; stack.pop(); return new AbstractPrimitive(false); } case 'n': { stack.pop(); - if(stack.peek() != 'u') + if (stack.peek() != 'u') return null; stack.pop(); - if(stack.peek() != 'l') + if (stack.peek() != 'l') return null; stack.pop(); - if(stack.peek() != 'l') + if (stack.peek() != 'l') return null; stack.pop(); return AbstractNull.VALUE; @@ -114,14 +111,14 @@ private void popWhitespace(Deque stack) { private AbstractPrimitive parseNumber(Deque stack) { StringBuilder sb = new StringBuilder(); - while (Character.isDigit(stack.peek()) || stack.peek() == '.' || stack.peek() == '-' || stack.peek() == 'E' || stack.peek() == 'e') + while (stack.peek() != null && (Character.isDigit(stack.peek()) || stack.peek() == '.' || stack.peek() == '+' || stack.peek() == '-' || stack.peek() == 'E' || stack.peek() == 'e')) sb.append(stack.pop()); String s = sb.toString(); - if(s.contains(".")) { + if (s.contains(".")) { return new AbstractPrimitive(Double.parseDouble(s)); } else { long l = Long.parseLong(s); - if(l >= Integer.MIN_VALUE && l <= Integer.MAX_VALUE) + if (l >= Integer.MIN_VALUE && l <= Integer.MAX_VALUE) return new AbstractPrimitive((int) l); return new AbstractPrimitive(l); } @@ -132,9 +129,9 @@ private AbstractPrimitive parseString(Deque stack) { StringBuilder sb = new StringBuilder(); while (true) { char c = stack.pop(); - if(c == '"') + if (c == '"') break; - if(c == '\\') { + if (c == '\\') { c = stack.pop(); switch (c) { case 'n': @@ -176,25 +173,26 @@ private AbstractObject parseObject(Deque stack) { AbstractObject object = new AbstractObject(); while (true) { popWhitespace(stack); - if(stack.peek() == '}') { + if (stack.peek() == '}') { stack.pop(); break; } AbstractPrimitive key = parseString(stack); - if(key == null) - return null; popWhitespace(stack); - if(stack.peek() != ':') + if (stack.peek() != ':') return null; stack.pop(); popWhitespace(stack); AbstractElement value = parse(stack); - if(value == null) + if (value == null) return null; object.set(key.string(), value); popWhitespace(stack); - if(stack.peek() == ',') + if (stack.peek() == ',') { stack.pop(); + } else if(stack.peek() != '}') { + return null; + } } return object; } @@ -204,17 +202,20 @@ private AbstractArray parseArray(Deque stack) { AbstractArray array = new AbstractArray(); while (true) { popWhitespace(stack); - if(stack.peek() == ']') { + if (stack.peek() == ']') { stack.pop(); break; } AbstractElement value = parse(stack); - if(value == null) + if (value == null) return null; array.add(value); popWhitespace(stack); - if(stack.peek() == ',') + if (stack.peek() == ',') { stack.pop(); + } else if(stack.peek() != ']') { + return null; + } } return array; } diff --git a/src/main/java/org/javawebstack/abstractdata/mapper/DefaultMappers.java b/src/main/java/org/javawebstack/abstractdata/mapper/DefaultMappers.java index 2ed0b60..0a7a682 100644 --- a/src/main/java/org/javawebstack/abstractdata/mapper/DefaultMappers.java +++ b/src/main/java/org/javawebstack/abstractdata/mapper/DefaultMappers.java @@ -263,7 +263,7 @@ public AbstractElement toAbstract(MapperContext context, Object value) throws Ma public Object fromAbstract(MapperContext context, AbstractElement element, Class type) throws MapperException { try { - DateFormat df = context.getAnnotation(DateFormat.class); + DateFormat df = context.getField().getAnnotation(DateFormat.class); Date date; if (df != null && df.epoch()) { long time = element.number(context.getMapper().isStrict()).longValue(); diff --git a/src/main/java/org/javawebstack/abstractdata/mapper/Mapper.java b/src/main/java/org/javawebstack/abstractdata/mapper/Mapper.java index ad2f235..64a4b4f 100644 --- a/src/main/java/org/javawebstack/abstractdata/mapper/Mapper.java +++ b/src/main/java/org/javawebstack/abstractdata/mapper/Mapper.java @@ -29,27 +29,27 @@ public T map(AbstractElement element, Class type) throws MapperException } public T map(MapperContext context, AbstractElement element, Class type) throws MapperException { - if(type == null || element == null || element.isNull()) + if (type == null || element == null || element.isNull()) return null; - if(type.isArray()) { - if(!element.isArray()) + if (type.isArray()) { + if (!element.isArray()) throw new MapperWrongTypeException(null, "array", Helpers.typeName(element)); Object arr = Array.newInstance(type.getComponentType(), element.array().size()); - for(int i=0; i type) { MapperTypeAdapter adapter = adapters.get(type); - if(adapter != null) + if (adapter != null) return adapter; - for(Class t : adapters.keySet()) { - if(t.isAssignableFrom(type)) { + for (Class t : adapters.keySet()) { + if (t.isAssignableFrom(type)) { return adapters.get(t); } } @@ -61,15 +61,15 @@ public AbstractElement map(Object obj) throws MapperException { } public AbstractElement map(MapperContext context, Object obj) throws MapperException { - if(obj == null) + if (obj == null) return AbstractNull.VALUE; - if(obj.getClass().isArray()) { + if (obj.getClass().isArray()) { AbstractArray array = new AbstractArray(); - for(int i=0; i type, MapperTypeAdapter adapter) { public Mapper adapter(MapperTypeAdapter adapter) { Class[] types = adapter.getSupportedTypes(); - if(types != null) { - for(Class type : types) + if (types != null) { + for (Class type : types) adapter(type, adapter); } return this; diff --git a/src/main/java/org/javawebstack/abstractdata/mapper/MapperContext.java b/src/main/java/org/javawebstack/abstractdata/mapper/MapperContext.java index baecb73..1b90aac 100644 --- a/src/main/java/org/javawebstack/abstractdata/mapper/MapperContext.java +++ b/src/main/java/org/javawebstack/abstractdata/mapper/MapperContext.java @@ -47,13 +47,13 @@ public Map, List> getAnnotations() { } public List getAnnotations(Class type) { - if(!annotations.containsKey(type)) + if (!annotations.containsKey(type)) return new ArrayList<>(); return (List) annotations.get(type); } public T getAnnotation(Class type) { - if(!annotations.containsKey(type)) + if (!annotations.containsKey(type)) return null; return (T) annotations.get(type).get(0); } diff --git a/src/main/java/org/javawebstack/abstractdata/mapper/MapperTypeAdapter.java b/src/main/java/org/javawebstack/abstractdata/mapper/MapperTypeAdapter.java index 7c3fb0a..449f70e 100644 --- a/src/main/java/org/javawebstack/abstractdata/mapper/MapperTypeAdapter.java +++ b/src/main/java/org/javawebstack/abstractdata/mapper/MapperTypeAdapter.java @@ -6,7 +6,9 @@ public interface MapperTypeAdapter { AbstractElement toAbstract(MapperContext context, Object value) throws MapperException; + Object fromAbstract(MapperContext context, AbstractElement element, Class type) throws MapperException; + default Class[] getSupportedTypes() { return null; } diff --git a/src/main/java/org/javawebstack/abstractdata/mapper/MapperTypeSpec.java b/src/main/java/org/javawebstack/abstractdata/mapper/MapperTypeSpec.java index d8c01d1..de0b2d3 100644 --- a/src/main/java/org/javawebstack/abstractdata/mapper/MapperTypeSpec.java +++ b/src/main/java/org/javawebstack/abstractdata/mapper/MapperTypeSpec.java @@ -15,19 +15,19 @@ public class MapperTypeSpec { private static final Map, MapperTypeSpec> typeSpecs = new HashMap<>(); public static MapperTypeSpec get(Class type) throws MapperException { - if(type.isArray() || type.isEnum() || type.isPrimitive()) + if (type.isArray() || type.isEnum() || type.isPrimitive()) return null; - if(Integer.class.equals(type)) + if (Integer.class.equals(type)) return null; - if(Long.class.equals(type)) + if (Long.class.equals(type)) return null; - if(Double.class.equals(type)) + if (Double.class.equals(type)) return null; - if(Float.class.equals(type)) + if (Float.class.equals(type)) return null; - if(Short.class.equals(type)) + if (Short.class.equals(type)) return null; - if(Boolean.class.equals(type)) + if (Boolean.class.equals(type)) return null; return typeSpecs.computeIfAbsent(type, MapperTypeSpec::new); } @@ -49,8 +49,8 @@ public MapperTypeSpec(Class type) throws MapperException { current = current.getSuperclass(); } while (!Object.class.equals(current)); // Collect parent classes recursively while (!classes.empty()) { - for(Field f : classes.pop().getDeclaredFields()) { - if(Modifier.isStatic(f.getModifiers())) // Don't include static fields + for (Field f : classes.pop().getDeclaredFields()) { + if (Modifier.isStatic(f.getModifiers())) // Don't include static fields continue; checkoutField(f); } @@ -60,11 +60,11 @@ public MapperTypeSpec(Class type) throws MapperException { private void checkoutField(Field field) throws MapperException { Map, List> annotations = new HashMap<>(); - for(Annotation annotation : field.getDeclaredAnnotations()) + for (Annotation annotation : field.getDeclaredAnnotations()) annotations.computeIfAbsent(annotation.annotationType(), k -> new ArrayList<>()).add(annotation); - if(annotations.containsKey(Additional.class)) { - if(!field.getType().equals(AbstractObject.class)) + if (annotations.containsKey(Additional.class)) { + if (!field.getType().equals(AbstractObject.class)) throw new MapperException("Additional field '" + field.getName() + "' in type '" + field.getDeclaringClass().getName() + "' needs to be of type AbstractObject, found '" + field.getType().getName() + "'"); additionalField = field; return; @@ -76,11 +76,11 @@ private void checkoutField(Field field) throws MapperException { spec.field = field; spec.annotations = annotations; - if(annotations.containsKey(MapperOptions.class)) { + if (annotations.containsKey(MapperOptions.class)) { MapperOptions options = (MapperOptions) annotations.get(MapperOptions.class).get(0); - if(options.name().length() > 0) + if (options.name().length() > 0) spec.name = options.name(); - if(!options.adapter().equals(MapperTypeAdapter.class)) { + if (!options.adapter().equals(MapperTypeAdapter.class)) { try { spec.adapter = options.adapter().newInstance(); } catch (InstantiationException | IllegalAccessException e) { @@ -104,7 +104,7 @@ public List getFieldSpecs() { } public Field getAdditionalField() { - if(additionalField != null) + if (additionalField != null) additionalField.setAccessible(true); return additionalField; } diff --git a/src/main/java/org/javawebstack/abstractdata/mapper/annotation/DateFormat.java b/src/main/java/org/javawebstack/abstractdata/mapper/annotation/DateFormat.java index bcedb86..cc74d26 100644 --- a/src/main/java/org/javawebstack/abstractdata/mapper/annotation/DateFormat.java +++ b/src/main/java/org/javawebstack/abstractdata/mapper/annotation/DateFormat.java @@ -1,10 +1,20 @@ package org.javawebstack.abstractdata.mapper.annotation; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target(ElementType.FIELD) +@Retention(RetentionPolicy.RUNTIME) public @interface DateFormat { String value() default ""; + String timezone() default ""; + boolean epoch() default false; + boolean millis() default false; } diff --git a/src/main/java/org/javawebstack/abstractdata/mapper/annotation/MapperOptions.java b/src/main/java/org/javawebstack/abstractdata/mapper/annotation/MapperOptions.java index 6a8947d..5900e4e 100644 --- a/src/main/java/org/javawebstack/abstractdata/mapper/annotation/MapperOptions.java +++ b/src/main/java/org/javawebstack/abstractdata/mapper/annotation/MapperOptions.java @@ -12,11 +12,17 @@ public @interface MapperOptions { String name() default ""; + int order() default 0; + Class[] generic() default {}; + boolean expose() default false; + boolean hidden() default false; + boolean omitNull() default true; + Class adapter() default MapperTypeAdapter.class; } diff --git a/src/main/java/org/javawebstack/abstractdata/mapper/naming/CamelCaseNamingPolicy.java b/src/main/java/org/javawebstack/abstractdata/mapper/naming/CamelCaseNamingPolicy.java index 9b7620c..c22a80b 100644 --- a/src/main/java/org/javawebstack/abstractdata/mapper/naming/CamelCaseNamingPolicy.java +++ b/src/main/java/org/javawebstack/abstractdata/mapper/naming/CamelCaseNamingPolicy.java @@ -9,7 +9,7 @@ public class CamelCaseNamingPolicy implements NamingPolicy { public String toAbstract(String source) { List words = Helpers.words(source); StringBuilder sb = new StringBuilder(words.get(0)); - for(int i=1; i fieldNames); } diff --git a/src/main/java/org/javawebstack/abstractdata/schema/AbstractArraySchema.java b/src/main/java/org/javawebstack/abstractdata/schema/AbstractArraySchema.java new file mode 100644 index 0000000..dd14a37 --- /dev/null +++ b/src/main/java/org/javawebstack/abstractdata/schema/AbstractArraySchema.java @@ -0,0 +1,134 @@ +package org.javawebstack.abstractdata.schema; + +import org.javawebstack.abstractdata.AbstractArray; +import org.javawebstack.abstractdata.AbstractElement; +import org.javawebstack.abstractdata.AbstractObject; +import org.javawebstack.abstractdata.AbstractPath; + +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; + +public class AbstractArraySchema implements AbstractSchema { + + private AbstractSchema itemSchema; + private Integer min; + private Integer max; + private boolean allowNull = false; + + private boolean unique; + private final List> customValidations = new ArrayList<>(); + + public AbstractArraySchema itemSchema(AbstractSchema schema) { + this.itemSchema = schema; + return this; + } + + public AbstractArraySchema min(int min) { + this.min = min; + return this; + } + + public AbstractArraySchema max(int max) { + this.max = max; + return this; + } + + public AbstractArraySchema allowNull() { + this.allowNull = true; + return this; + } + + public AbstractArraySchema unique() { + this.unique = true; + return this; + } + + public AbstractArraySchema customValidation(CustomValidation validation) { + customValidations.add(validation); + return this; + } + + public AbstractSchema getItemSchema() { + return itemSchema; + } + + public Integer getMin() { + return min; + } + + public Integer getMax() { + return max; + } + + public List> getCustomValidations() { + return customValidations; + } + + @Override + public AbstractObject toJsonSchema() { + AbstractObject obj = new AbstractObject(); + obj.set("type", "array"); + if (min != null) { + obj.set("minItems", min); + } + if (max != null) { + obj.set("maxItems", max); + } + if (itemSchema != null) { + obj.set("items", itemSchema.toJsonSchema()); + } + if (unique) { + obj.set("uniqueItems", "true"); + } + return obj; + } + + public List validate(AbstractPath path, AbstractElement value) { + List errors = new ArrayList<>(); + if (value.getType() != AbstractElement.Type.ARRAY) { + errors.add(new SchemaValidationError(path, "invalid_type").meta("expected", "array").meta("actual", value.getType().name().toLowerCase(Locale.ROOT))); + return errors; + } + AbstractArray array = value.array(); + if (min != null && array.size() < min) { + errors.add(new SchemaValidationError(path, "not_enough_items").meta("min", String.valueOf(min)).meta("actual", String.valueOf(array.size()))); + } + if (max != null && array.size() > max) { + errors.add(new SchemaValidationError(path, "too_many_items").meta("max", String.valueOf(max)).meta("actual", String.valueOf(array.size()))); + } + List seen = new ArrayList<>(); + for (int i = 0; i < array.size(); i++) { + AbstractElement item = array.get(i); + AbstractPath itemPath = path.subPath(String.valueOf(i)); + if (itemSchema != null) { + if (item.isNull()) { + if (!allowNull) { + errors.add(new SchemaValidationError(itemPath, "null_not_allowed")); + } + continue; + } + errors.addAll(itemSchema.validate(itemPath, array.get(i))); + } + + if (unique) { + if (seen.contains(item)) { + int originalIndex = seen.indexOf(item); + AbstractPath originalPath = path.subPath(String.valueOf(originalIndex)); + errors.add(new SchemaValidationError(itemPath, "duplicate_array_value") + .meta("value", item.toJsonString()) + .meta("first", originalPath.toString())); + } + seen.add(item); + } + + } + + + for (CustomValidation validation : customValidations) { + errors.addAll(validation.validate(path, array)); + } + return errors; + } + +} diff --git a/src/main/java/org/javawebstack/abstractdata/schema/AbstractBooleanSchema.java b/src/main/java/org/javawebstack/abstractdata/schema/AbstractBooleanSchema.java new file mode 100644 index 0000000..8df83d5 --- /dev/null +++ b/src/main/java/org/javawebstack/abstractdata/schema/AbstractBooleanSchema.java @@ -0,0 +1,60 @@ +package org.javawebstack.abstractdata.schema; + +import org.javawebstack.abstractdata.AbstractElement; +import org.javawebstack.abstractdata.AbstractObject; +import org.javawebstack.abstractdata.AbstractPath; +import org.javawebstack.abstractdata.AbstractPrimitive; + +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; + +public class AbstractBooleanSchema implements AbstractSchema { + + private Boolean staticValue; + private final List> customValidations = new ArrayList<>(); + + public AbstractBooleanSchema staticValue(boolean value) { + this.staticValue = value; + return this; + } + + public AbstractBooleanSchema customValidation(CustomValidation validation) { + customValidations.add(validation); + return this; + } + + public Boolean getStaticValue() { + return staticValue; + } + + public List> getCustomValidations() { + return customValidations; + } + + @Override + public AbstractObject toJsonSchema() { + AbstractObject obj = new AbstractObject() + .set("type", "boolean"); + if (staticValue != null) { + obj.set("const", staticValue); + } + return obj; + } + + public List validate(AbstractPath path, AbstractElement value) { + List errors = new ArrayList<>(); + if (value.getType() != AbstractElement.Type.BOOLEAN) { + errors.add(new SchemaValidationError(path, "invalid_type").meta("expected", "boolean").meta("actual", value.getType().name().toLowerCase(Locale.ROOT))); + return errors; + } + if (staticValue != null && staticValue != value.bool()) { + errors.add(new SchemaValidationError(path, "invalid_static_value").meta("expected", staticValue.toString()).meta("actual", value.bool().toString())); + } + for (CustomValidation validation : customValidations) { + errors.addAll(validation.validate(path, value.primitive())); + } + return errors; + } + +} diff --git a/src/main/java/org/javawebstack/abstractdata/schema/AbstractNumberSchema.java b/src/main/java/org/javawebstack/abstractdata/schema/AbstractNumberSchema.java new file mode 100644 index 0000000..0354bde --- /dev/null +++ b/src/main/java/org/javawebstack/abstractdata/schema/AbstractNumberSchema.java @@ -0,0 +1,145 @@ +package org.javawebstack.abstractdata.schema; + +import org.javawebstack.abstractdata.AbstractElement; +import org.javawebstack.abstractdata.AbstractObject; +import org.javawebstack.abstractdata.AbstractPath; +import org.javawebstack.abstractdata.AbstractPrimitive; + +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; + +public class AbstractNumberSchema implements AbstractSchema { + + private boolean integerOnly = false; + private Number min; + private Number max; + private boolean minExclusive = false; + private boolean maxExclusive = false; + private Number step; + private final List> customValidations = new ArrayList<>(); + + public AbstractNumberSchema min(Number min) { + return min(min, false); + } + + public AbstractNumberSchema max(Number max) { + return max(max, false); + } + + public AbstractNumberSchema min(Number min, boolean exclusive) { + this.minExclusive = exclusive; + this.min = min; + return this; + } + + public AbstractNumberSchema max(Number max, boolean exclusive) { + this.maxExclusive = exclusive; + this.max = max; + return this; + } + + public AbstractNumberSchema step(Number step) { + this.step = step; + return this; + } + + public AbstractNumberSchema integerOnly() { + this.integerOnly = true; + return this; + } + + public AbstractNumberSchema customValidation(CustomValidation validation) { + customValidations.add(validation); + return this; + } + + public Number getMin() { + return min; + } + + public Number getMax() { + return max; + } + + public boolean isIntegerOnly() { + return integerOnly; + } + + public List> getCustomValidations() { + return customValidations; + } + + @Override + public AbstractObject toJsonSchema() { + AbstractObject obj = new AbstractObject(); + + obj.set("type", integerOnly ? "integer" : "number"); + if (min != null && max != null && !minExclusive && !maxExclusive) { + BigDecimal dMin = (min instanceof Float || min instanceof Double) ? BigDecimal.valueOf(min.doubleValue()) : BigDecimal.valueOf(min.longValue()); + BigDecimal dMax = (max instanceof Float || max instanceof Double) ? BigDecimal.valueOf(max.doubleValue()) : BigDecimal.valueOf(max.longValue()); + + if (dMin.compareTo(dMax) == 0) { + obj.set("const", min); + } + + } + if (!obj.has("const")) { + if (min != null) { + obj.set(minExclusive ? "exclusiveMinimum" : "minimum", min); + } + if (max != null) { + obj.set(maxExclusive ? "exclusiveMaximum" : "maximum", max); + } + if (step != null) { + obj.set("multipleOf", step); + } + } + + + return obj; + } + + public List validate(AbstractPath path, AbstractElement value) { + List errors = new ArrayList<>(); + if (value.getType() != AbstractElement.Type.NUMBER) { + errors.add(new SchemaValidationError(path, "invalid_type").meta("expected", integerOnly ? "integer" : "number").meta("actual", value.getType().name().toLowerCase(Locale.ROOT))); + return errors; + } + Number n = value.number(); + BigDecimal dN = (n instanceof Float || n instanceof Double) ? BigDecimal.valueOf(n.doubleValue()) : BigDecimal.valueOf(n.longValue()); + if (integerOnly && (n instanceof Float || n instanceof Double)) { + errors.add(new SchemaValidationError(path, "invalid_type").meta("expected", "integer").meta("actual", "number")); + return errors; + } + if (min != null) { + BigDecimal dMin = (min instanceof Float || min instanceof Double) ? BigDecimal.valueOf(min.doubleValue()) : BigDecimal.valueOf(min.longValue()); + if (!(dN.compareTo(dMin) > (minExclusive ? 0 : -1))) { + errors.add(new SchemaValidationError(path, "number_smaller_than_min").meta("min", dMin.toPlainString()).meta("actual", dN.toPlainString())); + } + } + if (max != null) { + BigDecimal dMax = (max instanceof Float || max instanceof Double) ? BigDecimal.valueOf(max.doubleValue()) : BigDecimal.valueOf(max.longValue()); + if (!(dN.compareTo(dMax) < (maxExclusive ? 0 : 1))) { + errors.add(new SchemaValidationError(path, "number_larger_than_max").meta("max", dMax.toPlainString()).meta("actual", dN.toPlainString())); + } + } + if (step != null) { + if (min != null && minExclusive) { + throw new UnsupportedOperationException("Step is not supported together with an exclusive minimum"); + } + BigDecimal dMin = min == null ? BigDecimal.ZERO : (min instanceof Float || min instanceof Double) ? BigDecimal.valueOf(min.doubleValue()) : BigDecimal.valueOf(min.longValue()); + BigDecimal dStep = (step instanceof Float || step instanceof Double) ? BigDecimal.valueOf(step.doubleValue()) : BigDecimal.valueOf(step.longValue()); + + if (dN.subtract(dMin).remainder(dStep).compareTo(BigDecimal.ZERO) != 0) { + errors.add(new SchemaValidationError(path, "number_not_within_step").meta("step", dStep.toPlainString()).meta("actual", dN.toPlainString()).meta("start", dMin.toPlainString())); + } + } + for (CustomValidation validation : customValidations) { + errors.addAll(validation.validate(path, value.primitive())); + } + return errors; + } + +} diff --git a/src/main/java/org/javawebstack/abstractdata/schema/AbstractObjectSchema.java b/src/main/java/org/javawebstack/abstractdata/schema/AbstractObjectSchema.java new file mode 100644 index 0000000..1b822b4 --- /dev/null +++ b/src/main/java/org/javawebstack/abstractdata/schema/AbstractObjectSchema.java @@ -0,0 +1,104 @@ +package org.javawebstack.abstractdata.schema; + +import org.javawebstack.abstractdata.AbstractArray; +import org.javawebstack.abstractdata.AbstractElement; +import org.javawebstack.abstractdata.AbstractObject; +import org.javawebstack.abstractdata.AbstractPath; + +import java.util.*; + +public class AbstractObjectSchema implements AbstractSchema { + + private final Map properties = new HashMap<>(); + private final Set requiredProperties = new HashSet<>(); + private final List> customValidations = new ArrayList<>(); + private boolean allowAdditionalProperties = false; + private AbstractSchema additionalPropertySchema; + + public AbstractObjectSchema requiredProperty(String name, AbstractSchema schema) { + properties.put(name, schema); + requiredProperties.add(name); + return this; + } + + public AbstractObjectSchema optionalProperty(String name, AbstractSchema schema) { + properties.put(name, schema); + requiredProperties.remove(name); + return this; + } + + public AbstractObjectSchema customValidation(CustomValidation validation) { + customValidations.add(validation); + return this; + } + + public AbstractObjectSchema additionalProperties() { + return additionalProperties(null); + } + + public AbstractObjectSchema additionalProperties(AbstractSchema schema) { + allowAdditionalProperties = true; + additionalPropertySchema = schema; + return this; + } + + @Override + public AbstractObject toJsonSchema() { + AbstractObject obj = new AbstractObject(); + obj.set("type", "object"); + AbstractObject properties = new AbstractObject(); + this.properties.forEach((key, value) -> { + properties.set(key, value.toJsonSchema()); + }); + obj.set("properties", properties); + + if (!requiredProperties.isEmpty()) { + AbstractArray required = new AbstractArray(); + requiredProperties.forEach(required::add); + obj.set("required", required); + } + if (!allowAdditionalProperties) { + obj.set("additionalProperties", false); + } else if (additionalPropertySchema != null) { + obj.set("additionalProperties", additionalPropertySchema.toJsonSchema()); + } + + return obj; + } + + public List validate(AbstractPath path, AbstractElement value) { + List errors = new ArrayList<>(); + if (value.getType() != AbstractElement.Type.OBJECT) { + errors.add(new SchemaValidationError(path, "invalid_type").meta("expected", "object").meta("actual", value.getType().name().toLowerCase(Locale.ROOT))); + return errors; + } + AbstractObject object = value.object(); + for (String prop : requiredProperties) { + if (!object.has(prop) || object.get(prop).isNull()) { + errors.add(new SchemaValidationError(path.subPath(prop), "missing_required_property")); + } + } + for (String prop : object.keys()) { + AbstractElement propValue = object.get(prop); + AbstractPath propPath = path.subPath(prop); + if (properties.containsKey(prop)) { + if (propValue.isNull()) + continue; + errors.addAll(properties.get(prop).validate(propPath, propValue)); + } else { + if (allowAdditionalProperties) { + if (additionalPropertySchema != null) { + errors.addAll(additionalPropertySchema.validate(propPath, propValue)); + } + } else { + errors.add(new SchemaValidationError(propPath, "unexpected_property")); + } + } + } + for (CustomValidation validation : customValidations) { + errors.addAll(validation.validate(path, object)); + } + return errors; + } + +} diff --git a/src/main/java/org/javawebstack/abstractdata/schema/AbstractSchema.java b/src/main/java/org/javawebstack/abstractdata/schema/AbstractSchema.java new file mode 100644 index 0000000..9385246 --- /dev/null +++ b/src/main/java/org/javawebstack/abstractdata/schema/AbstractSchema.java @@ -0,0 +1,78 @@ +package org.javawebstack.abstractdata.schema; + +import org.javawebstack.abstractdata.AbstractElement; +import org.javawebstack.abstractdata.AbstractObject; +import org.javawebstack.abstractdata.AbstractPath; + +import java.util.List; + +public interface AbstractSchema { + AbstractObject toJsonSchema(); + + default List validate(AbstractElement value) { + return validate(AbstractPath.ROOT, value); + } + + List validate(AbstractPath path, AbstractElement value); + + static AbstractArraySchema array(AbstractSchema itemSchema) { + return array().itemSchema(itemSchema); + } + + static AbstractArraySchema array() { + return new AbstractArraySchema(); + } + + static AbstractObjectSchema object() { + return new AbstractObjectSchema(); + } + + static AbstractStringSchema staticString(String s) { + return string().staticValue(s); + } + + static AbstractStringSchema enumString(Class> enumType) { + return string().enumValues(enumType); + } + + static AbstractStringSchema enumString(String... values) { + return string().enumValues(values); + } + + static AbstractStringSchema string() { + return new AbstractStringSchema(); + } + + static AbstractNumberSchema integer(int min, int max) { + return integer().min(min).max(max); + } + + static AbstractNumberSchema integer() { + return new AbstractNumberSchema().integerOnly(); + } + + static AbstractNumberSchema number() { + return new AbstractNumberSchema(); + } + + static AbstractNumberSchema number(double min, double max) { + return number().min(min).max(max); + } + + static AbstractBooleanSchema staticBool(boolean v) { + return bool().staticValue(v); + } + + static AbstractBooleanSchema bool() { + return new AbstractBooleanSchema(); + } + + static OneOfSchema oneOf(AbstractSchema... schemas) { + return new OneOfSchema(schemas); + } + + static AbstractSchema fromJsonSchema(AbstractObject schema) { + return new JsonSchemaParser().parse(schema); + } + +} diff --git a/src/main/java/org/javawebstack/abstractdata/schema/AbstractStringSchema.java b/src/main/java/org/javawebstack/abstractdata/schema/AbstractStringSchema.java new file mode 100644 index 0000000..bd1742f --- /dev/null +++ b/src/main/java/org/javawebstack/abstractdata/schema/AbstractStringSchema.java @@ -0,0 +1,129 @@ +package org.javawebstack.abstractdata.schema; + +import org.javawebstack.abstractdata.*; + +import java.util.*; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class AbstractStringSchema implements AbstractSchema { + + private Integer minLength; + private Integer maxLength; + private String staticValue; + private String regex; + private Pattern regexPattern; + private Set enumValues; + private final List> customValidations = new ArrayList<>(); + + public AbstractStringSchema staticValue(String value) { + this.staticValue = value; + return this; + } + + public AbstractStringSchema enumValues(Class> enumType) { + Set values = new HashSet<>(); + for (Enum v : enumType.getEnumConstants()) { + values.add(v.name()); + } + return enumValues(values); + } + + public AbstractStringSchema enumValues(String... values) { + return enumValues(new HashSet<>(Arrays.asList(values))); + } + + public AbstractStringSchema enumValues(Set values) { + this.enumValues = values; + return this; + } + + public AbstractStringSchema minLength(int min) { + this.minLength = min; + return this; + } + + public AbstractStringSchema maxLength(int max) { + this.maxLength = max; + return this; + } + + public AbstractStringSchema regex(String regex) { + this.regex = regex; + this.regexPattern = Pattern.compile(regex); + return this; + } + + public AbstractStringSchema customValidation(CustomValidation validation) { + customValidations.add(validation); + return this; + } + + public String getRegex() { + return regex; + } + + public String getStaticValue() { + return staticValue; + } + + public List> getCustomValidations() { + return customValidations; + } + + @Override + public AbstractObject toJsonSchema() { + AbstractObject obj = new AbstractObject(); + obj.set("type", "string"); + if (minLength != null) { + obj.set("minLength", minLength); + } + if (maxLength != null) { + obj.set("maxLength", maxLength); + } + if (staticValue != null) { + obj.set("const", staticValue); + } + if (regex != null) { + obj.set("pattern", regex); + } + if (enumValues != null) { + AbstractArray arr = new AbstractArray(enumValues.toArray()); + obj.set("enum", arr); + } + + return obj; + } + + public List validate(AbstractPath path, AbstractElement value) { + List errors = new ArrayList<>(); + if (value.getType() != AbstractElement.Type.STRING) { + errors.add(new SchemaValidationError(path, "invalid_type").meta("expected", "string").meta("actual", value.getType().name().toLowerCase(Locale.ROOT))); + return errors; + } + String s = value.string(); + if (staticValue != null && !staticValue.equals(s)) { + errors.add(new SchemaValidationError(path, "invalid_static_value").meta("expected", staticValue).meta("actual", s)); + } + if (enumValues != null && !enumValues.contains(s)) { + errors.add(new SchemaValidationError(path, "invalid_enum_value").meta("expected", String.join(", ", enumValues)).meta("actual", s)); + } + if (minLength != null && s.length() < minLength) { + errors.add(new SchemaValidationError(path, "value_too_short").meta("min", minLength.toString()).meta("actual", String.valueOf(s.length()))); + } + if (maxLength != null && s.length() > maxLength) { + errors.add(new SchemaValidationError(path, "value_too_long").meta("max", maxLength.toString()).meta("actual", String.valueOf(s.length()))); + } + if (regexPattern != null) { + Matcher matcher = regexPattern.matcher(s); + if (!matcher.matches()) { + errors.add(new SchemaValidationError(path, "invalid_pattern").meta("pattern", regex).meta("actual", s)); + } + } + for (CustomValidation validation : customValidations) { + errors.addAll(validation.validate(path, value.primitive())); + } + return errors; + } + +} diff --git a/src/main/java/org/javawebstack/abstractdata/schema/CustomValidation.java b/src/main/java/org/javawebstack/abstractdata/schema/CustomValidation.java new file mode 100644 index 0000000..1001f0d --- /dev/null +++ b/src/main/java/org/javawebstack/abstractdata/schema/CustomValidation.java @@ -0,0 +1,12 @@ +package org.javawebstack.abstractdata.schema; + +import org.javawebstack.abstractdata.AbstractElement; +import org.javawebstack.abstractdata.AbstractPath; + +import java.util.List; + +public interface CustomValidation { + + List validate(AbstractPath path, T value); + +} diff --git a/src/main/java/org/javawebstack/abstractdata/schema/JsonSchemaParser.java b/src/main/java/org/javawebstack/abstractdata/schema/JsonSchemaParser.java new file mode 100644 index 0000000..7d2a3c1 --- /dev/null +++ b/src/main/java/org/javawebstack/abstractdata/schema/JsonSchemaParser.java @@ -0,0 +1,208 @@ +package org.javawebstack.abstractdata.schema; + +import org.javawebstack.abstractdata.AbstractArray; +import org.javawebstack.abstractdata.AbstractElement; +import org.javawebstack.abstractdata.AbstractObject; + +import java.util.Collections; +import java.util.HashSet; +import java.util.List; + +/* +{ + "$id": "1224", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "title": "Person", + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "emails": { + "type": "array" + "items": { + "type": "string" + } + }, + "age": { + "type": "integer", + "minimum": 0, + "maximum": 120 + } + }, + "required": [ + "name", + "emails" + ] +} + */ + +public class JsonSchemaParser { + + public static void main(String[] args) { + + } + + public AbstractSchema parse(AbstractObject schema) { + if (schema.has("type")) { + switch (schema.string("type")) { + case "object": { + return parseObject(schema); + } + case "array": { + return parseArray(schema); + } + case "string": { + return parseString(schema); + } + case "number": + case "integer": { + return parseNumber(schema); + } + case "boolean": { + return parseBoolean(schema); + } + default: { + throw new UnsupportedOperationException("Unknown type: " + schema.string("type")); + } + } + } + if (schema.has("$ref")) { + throw new UnsupportedOperationException("$ref is currently not supported"); + } + if (schema.has("oneOf")) { + return parseOneOf(schema); + } + throw new IllegalArgumentException("Invalid json schema"); + } + + private OneOfSchema parseOneOf(AbstractObject schema) { + if (!schema.hasArray("oneOf")) { + throw new IllegalArgumentException("Not a valid oneOf schema"); + } + AbstractArray absArr = schema.array("oneOf"); + AbstractSchema[] schemas = new AbstractSchema[absArr.size()]; + for (int i = 0; i < schemas.length; i++) { + schemas[i] = parse(absArr.object(i)); + } + return new OneOfSchema(schemas); + } + + private AbstractStringSchema parseString(AbstractObject schema) { + if (!schema.string("type").equals("string")) + throw new IllegalArgumentException("Not a valid string schema"); + AbstractStringSchema s = new AbstractStringSchema(); + + if (schema.has("minLength")) { + s.minLength(schema.number("minLength").intValue()); + } + if (schema.has("maxLength")) { + s.maxLength(schema.number("maxLength").intValue()); + } + if (schema.has("pattern")) { + s.regex(schema.string("pattern")); + } + if (schema.has("enum")) { + s.enumValues(new HashSet<>(schema.array("enum").toStringList())); + } + if (schema.has("const")) { + s.staticValue(schema.string("const")); + } + return s; + } + + private AbstractNumberSchema parseNumber(AbstractObject schema) { + boolean isInteger = schema.string("type").equals("integer"); + + if (!isInteger && !schema.string("type").equals("number")) { + throw new IllegalArgumentException("Not a valid number schema"); + } + AbstractNumberSchema s = new AbstractNumberSchema(); + if (isInteger) { + s.integerOnly(); + } + if (schema.has("minimum")) + s.min(schema.number("minimum")); + if (schema.has("maximum")) + s.max(schema.number("maximum")); + if (schema.has("exclusiveMinimum")) + s.min(schema.number("exclusiveMinimum"), true); + if (schema.has("exclusiveMaximum")) + s.max(schema.number("exclusiveMaximum"), true); + if (schema.has("const")) { + Number staticValue = schema.number("const"); + s.min(staticValue); + s.max(staticValue); + } + if (schema.has("multipleOf")) { + s.step(schema.number("multipleOf")); + } + + return s; + } + + + private AbstractObjectSchema parseObject(AbstractObject schema) { + if (!schema.string("type").equals("object")) + throw new IllegalArgumentException("Not a valid object schema"); + AbstractObjectSchema s = new AbstractObjectSchema(); + List required = schema.has("required") ? schema.array("required").toStringList() : Collections.emptyList(); + if (schema.has("properties")) { + schema.object("properties").forEach((key, propSchema) -> { + if (required.contains(key)) { + s.requiredProperty(key, parse(propSchema.object())); + } else { + s.optionalProperty(key, parse(propSchema.object())); + } + }); + } + AbstractElement additionalProperties = schema.get("additionalProperties"); + if (additionalProperties != null) { + if (additionalProperties.isBoolean()) { + if (additionalProperties.bool()) { + s.additionalProperties(); + } + } else { + s.additionalProperties(parse(additionalProperties.object())); + } + + + } else { + s.additionalProperties(); + } + return s; + } + + private AbstractArraySchema parseArray(AbstractObject schema) { + if (!schema.string("type").equals("array")) + throw new IllegalArgumentException("Not a valid array schema"); + AbstractArraySchema s = new AbstractArraySchema(); + if (schema.has("items")) { + s.itemSchema(parse(schema.object("items"))); + } + if (schema.has("minItems")) { + s.min(schema.number("minItems").intValue()); + } + if (schema.has("maxItems")) { + s.max(schema.number("maxItems").intValue()); + } + if (schema.has("uniqueItems") && schema.bool("uniqueItems")) { + s.unique(); + } + return s; + } + + private AbstractBooleanSchema parseBoolean(AbstractObject schema) { + if (!schema.string("type").equals("boolean")) + throw new IllegalArgumentException("Not a valid boolean schema"); + AbstractBooleanSchema s = new AbstractBooleanSchema(); + if (schema.has("const")) { + s.staticValue(schema.bool("const")); + } + + + return new AbstractBooleanSchema(); + + } + +} diff --git a/src/main/java/org/javawebstack/abstractdata/schema/OneOfSchema.java b/src/main/java/org/javawebstack/abstractdata/schema/OneOfSchema.java new file mode 100644 index 0000000..ce4611e --- /dev/null +++ b/src/main/java/org/javawebstack/abstractdata/schema/OneOfSchema.java @@ -0,0 +1,48 @@ +package org.javawebstack.abstractdata.schema; + +import org.javawebstack.abstractdata.AbstractArray; +import org.javawebstack.abstractdata.AbstractElement; +import org.javawebstack.abstractdata.AbstractObject; +import org.javawebstack.abstractdata.AbstractPath; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +public class OneOfSchema implements AbstractSchema { + + private final List schemas = new ArrayList<>(); + + public OneOfSchema(AbstractSchema... schemas) { + if (schemas.length == 0) + throw new IllegalArgumentException("At least one schema is required"); + this.schemas.addAll(Arrays.asList(schemas)); + } + + @Override + public AbstractObject toJsonSchema() { + AbstractArray arr = new AbstractArray(); + for (AbstractSchema schema : schemas) { + arr.add(schema.toJsonSchema()); + } + + + return new AbstractObject().set("oneOf", arr); + } + + public List validate(AbstractPath path, AbstractElement value) { + List> schemaErrors = new ArrayList<>(); + for (AbstractSchema schema : schemas) { + List errors = schema.validate(path, value); + if (errors.isEmpty()) + return errors; + schemaErrors.add(errors); + } + for (List errors : schemaErrors) { + if (!(errors.size() == 1 && errors.get(0).getError().equals("invalid_type"))) + return errors; + } + return schemaErrors.get(0); + } + +} diff --git a/src/main/java/org/javawebstack/abstractdata/schema/SchemaValidationError.java b/src/main/java/org/javawebstack/abstractdata/schema/SchemaValidationError.java new file mode 100644 index 0000000..fd6d018 --- /dev/null +++ b/src/main/java/org/javawebstack/abstractdata/schema/SchemaValidationError.java @@ -0,0 +1,82 @@ +package org.javawebstack.abstractdata.schema; + +import org.javawebstack.abstractdata.AbstractPath; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class SchemaValidationError { + + private static final Map BUILTIN_DESCRIPTIONS = new HashMap() {{ + put("invalid_type", "Expected value of type {expected}, got {actual}"); + put("not_enough_items", "At least {min} item(s) required, got {actual}"); + put("too_many_items", "Not more than {max} item(s) allowed, got {actual}"); + put("null_not_allowed", "Value null is not allowed"); + put("invalid_static_value", "Static value does not match, expected '{expected}' but got '{actual}'"); + put("number_smaller_than_min", "The value '{actual}' is smaller than {min}"); + put("number_larger_than_max", "The value '{actual}' is larger than {max}"); + put("missing_required_property", "The property is required but missing"); + put("unexpected_property", "Unexpected property, additional properties are not allowed"); + put("value_too_short", "The length of the value ({actual}) is shorter than the minimum of {min}"); + put("value_too_long", "The length of the value ({actual}) is longer than the maximum of {max}"); + put("invalid_pattern", "The value '{actual}' does not match the pattern '{pattern}'"); + put("number_not_within_step", "The value '{start}' was not in steps of {step} starting from {start}"); + put("duplicate_array_value", "The value '{value}' is a duplicate of '{first}'"); + }}; + + private AbstractPath path; + private String error; + private Map errorMeta = new HashMap<>(); + + public SchemaValidationError(AbstractPath path, String error) { + this.path = path; + this.error = error; + } + + public SchemaValidationError meta(String key, String value) { + errorMeta.put(key, value); + return this; + } + + public AbstractPath getPath() { + return path; + } + + public String getError() { + return error; + } + + public Map getErrorMeta() { + return errorMeta; + } + + public String getErrorDescription() { + return getErrorDescription(new HashMap<>()); + } + + public String getErrorDescription(Map customDescriptions) { + String message; + if (customDescriptions.containsKey(error)) { + message = customDescriptions.get(error); + } else if (BUILTIN_DESCRIPTIONS.containsKey(error)) { + message = BUILTIN_DESCRIPTIONS.get(error); + } else { + return error; + } + for (String key : errorMeta.keySet()) { + message = message.replace("{" + key + "}", errorMeta.get(key)); + } + return message; + } + + public static Map> groupErrors(List errors) { + Map> errorMap = new HashMap<>(); + for (SchemaValidationError e : errors) { + errorMap.computeIfAbsent(e.getPath(), k -> new ArrayList<>()).add(e); + } + return errorMap; + } + +} diff --git a/src/main/java/org/javawebstack/abstractdata/util/Helpers.java b/src/main/java/org/javawebstack/abstractdata/util/Helpers.java index 4247a9e..856289a 100644 --- a/src/main/java/org/javawebstack/abstractdata/util/Helpers.java +++ b/src/main/java/org/javawebstack/abstractdata/util/Helpers.java @@ -10,29 +10,29 @@ public class Helpers { public static String typeName(AbstractElement e) { - if(e.isArray()) + if (e.isArray()) return "array"; - if(e.isObject()) + if (e.isObject()) return "object"; - if(e.isString()) + if (e.isString()) return "string"; - if(e.isNumber()) + if (e.isNumber()) return "number"; - if(e.isBoolean()) + if (e.isBoolean()) return "boolean"; return "null"; } public static Class guessGeneric(AbstractElement e) { - if(e.isArray()) + if (e.isArray()) return AbstractArray.class; - if(e.isObject()) + if (e.isObject()) return AbstractObject.class; - if(e.isString()) + if (e.isString()) return String.class; - if(e.isNumber()) + if (e.isNumber()) return Number.class; - if(e.isBoolean()) + if (e.isBoolean()) return Boolean.class; return null; } @@ -40,21 +40,21 @@ public static Class guessGeneric(AbstractElement e) { public static List words(String s) { List words = new ArrayList<>(); StringBuilder sb = new StringBuilder(); - for(int i=0; i 0) { + for (int i = 0; i < s.length(); i++) { + if (s.charAt(i) == '_' || s.charAt(i) == '-') { + if (sb.length() > 0) { words.add(sb.toString()); sb = new StringBuilder(); } continue; } - if(i > 0 && (Character.isLowerCase(s.charAt(i - 1)) && Character.isUpperCase(s.charAt(i)))) { + if (i > 0 && (Character.isLowerCase(s.charAt(i - 1)) && Character.isUpperCase(s.charAt(i)))) { words.add(sb.toString()); sb = new StringBuilder(); } sb.append(Character.toLowerCase(s.charAt(i))); } - if(sb.length() > 0) + if (sb.length() > 0) words.add(sb.toString()); return words; } diff --git a/src/main/java/org/javawebstack/abstractdata/xml/LegacyXMLParser.java b/src/main/java/org/javawebstack/abstractdata/xml/LegacyXMLParser.java index ddde196..b7077af 100644 --- a/src/main/java/org/javawebstack/abstractdata/xml/LegacyXMLParser.java +++ b/src/main/java/org/javawebstack/abstractdata/xml/LegacyXMLParser.java @@ -28,13 +28,13 @@ private static XMLElement convert(Element element) { String tag = element.getTagName(); XMLElement e = new XMLElement(tag); NamedNodeMap attrNodeMap = element.getAttributes(); - for(int i=0; i dumpLines(XMLNode node) { - if(node instanceof XMLTextNode) { + if (node instanceof XMLTextNode) { XMLTextNode textNode = (XMLTextNode) node; return Arrays.asList(escape(textNode.getText(), true)); } @@ -41,14 +41,14 @@ private List dumpLines(XMLNode node) { boolean selfClosing = useSelfClosing && element.getChildNodes().size() == 0; String openingTag = renderOpeningTag(element.tagName(), element.getAttributes(), selfClosing); String closingTag = selfClosing ? "" : ""; - if(pretty) { - if(element.getChildNodes().size() == 0) { + if (pretty) { + if (element.getChildNodes().size() == 0) { lines.add(openingTag + closingTag); - } else if(element.getChildNodes().size() == 1 && element.getChildNodes().get(0) instanceof XMLTextNode) { + } else if (element.getChildNodes().size() == 1 && element.getChildNodes().get(0) instanceof XMLTextNode) { lines.add(openingTag + escape(((XMLTextNode) element.getChildNodes().get(0)).getText(), true) + closingTag); } else { lines.add(openingTag); - for(XMLNode child : element.getChildNodes()) { + for (XMLNode child : element.getChildNodes()) { lines.addAll(dumpLines(child).stream().map(l -> indent + l).collect(Collectors.toList())); } lines.add(closingTag); @@ -56,7 +56,7 @@ private List dumpLines(XMLNode node) { } else { StringBuilder sb = new StringBuilder(); sb.append(openingTag); - for(XMLNode child : element.getChildNodes()) + for (XMLNode child : element.getChildNodes()) dumpLines(child).forEach(sb::append); sb.append(closingTag); lines.add(sb.toString()); @@ -68,7 +68,7 @@ public String renderOpeningTag(String tagName, Map attributes, b StringBuilder sb = new StringBuilder(); sb.append('<'); sb.append(escape(tagName, false)); - for(String attrName : attributes.keySet()) { + for (String attrName : attributes.keySet()) { sb.append(" "); sb.append(escape(attrName, false)); sb.append('='); @@ -76,7 +76,7 @@ public String renderOpeningTag(String tagName, Map attributes, b sb.append(escape(attributes.get(attrName), false)); sb.append('"'); } - if(selfClosing) + if (selfClosing) sb.append("/"); sb.append('>'); return sb.toString(); @@ -85,7 +85,7 @@ public String renderOpeningTag(String tagName, Map attributes, b private String escape(String value, boolean text) { value = value.replace("<", "<"); value = value.replace("&", "&"); - if(!text) { + if (!text) { value = value.replace("\"", """); value = value.replace("'", "'"); value = value.replace(">", ">"); diff --git a/src/main/java/org/javawebstack/abstractdata/xml/XMLElement.java b/src/main/java/org/javawebstack/abstractdata/xml/XMLElement.java index c9771ab..fed5f99 100644 --- a/src/main/java/org/javawebstack/abstractdata/xml/XMLElement.java +++ b/src/main/java/org/javawebstack/abstractdata/xml/XMLElement.java @@ -14,14 +14,14 @@ public class XMLElement implements XMLNode { private final List childNodes = new ArrayList<>(); public XMLElement(String tagName) { - if(tagName == null) + if (tagName == null) throw new IllegalArgumentException("tagName can not be null"); this.tagName = tagName; } public XMLElement(String tagName, String text) { this(tagName); - if(text == null) + if (text == null) throw new IllegalArgumentException("text can not be null"); text(text); } @@ -54,7 +54,7 @@ public String attr(String name) { } public XMLElement attr(String name, String value) { - if(value == null) { + if (value == null) { attributes.remove(name); return this; } @@ -78,7 +78,7 @@ public XMLElement child(XMLNode childNode) { } public XMLElement onlyIf(boolean condition, Consumer fn) { - if(condition) + if (condition) fn.accept(this); return this; } diff --git a/src/main/java/org/javawebstack/abstractdata/yaml/LegacyYamlDumper.java b/src/main/java/org/javawebstack/abstractdata/yaml/LegacyYamlDumper.java new file mode 100644 index 0000000..937540b --- /dev/null +++ b/src/main/java/org/javawebstack/abstractdata/yaml/LegacyYamlDumper.java @@ -0,0 +1,22 @@ +package org.javawebstack.abstractdata.yaml; + +import org.javawebstack.abstractdata.AbstractElement; +import org.yaml.snakeyaml.DumperOptions; +import org.yaml.snakeyaml.Yaml; + +public class LegacyYamlDumper { + + protected static String dump(AbstractElement e, boolean pretty) { + Yaml yaml; + DumperOptions options = new DumperOptions(); + if (pretty) { + options.setDefaultFlowStyle(DumperOptions.FlowStyle.BLOCK); + options.setPrettyFlow(true); + } else { + options.setPrettyFlow(false); + } + yaml = new Yaml(options); + return yaml.dump(e.toObject()); + } + +} diff --git a/src/main/java/org/javawebstack/abstractdata/yaml/LegacyYamlParser.java b/src/main/java/org/javawebstack/abstractdata/yaml/LegacyYamlParser.java new file mode 100644 index 0000000..fe230f5 --- /dev/null +++ b/src/main/java/org/javawebstack/abstractdata/yaml/LegacyYamlParser.java @@ -0,0 +1,34 @@ +package org.javawebstack.abstractdata.yaml; + +import org.javawebstack.abstractdata.AbstractElement; +import org.javawebstack.abstractdata.AbstractNull; +import org.yaml.snakeyaml.Yaml; +import org.yaml.snakeyaml.parser.ParserException; + +import java.text.ParseException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +public class LegacyYamlParser { + + protected static AbstractElement parse(String source, boolean singleRoot) throws ParseException { + if(source.trim().equalsIgnoreCase("null")) + return AbstractNull.VALUE; + if(source.trim().isEmpty()) + throw new ParseException("Invalid yaml", 0); + try { + Yaml yaml = new Yaml(); + List list = new ArrayList<>(); + yaml.loadAll(source).forEach(list::add); + Object object = list; + if (singleRoot && list.size() == 1) { + object = list.get(0); + } + return AbstractElement.fromObject(object); + } catch (ParserException e) { + throw new ParseException(e.getMessage(), e.getProblemMark().getIndex()); + } + } + +} diff --git a/src/main/java/org/javawebstack/abstractdata/yaml/YamlDumper.java b/src/main/java/org/javawebstack/abstractdata/yaml/YamlDumper.java new file mode 100644 index 0000000..f92734b --- /dev/null +++ b/src/main/java/org/javawebstack/abstractdata/yaml/YamlDumper.java @@ -0,0 +1,22 @@ +package org.javawebstack.abstractdata.yaml; + +import org.javawebstack.abstractdata.AbstractElement; + +public class YamlDumper { + + private boolean pretty = false; + + public YamlDumper setPretty(boolean pretty) { + this.pretty = pretty; + return this; + } + + public boolean isPretty() { + return pretty; + } + + public String dump(AbstractElement e) { + return LegacyYamlDumper.dump(e, pretty); + } + +} diff --git a/src/main/java/org/javawebstack/abstractdata/yaml/YamlParser.java b/src/main/java/org/javawebstack/abstractdata/yaml/YamlParser.java new file mode 100644 index 0000000..14b8177 --- /dev/null +++ b/src/main/java/org/javawebstack/abstractdata/yaml/YamlParser.java @@ -0,0 +1,24 @@ +package org.javawebstack.abstractdata.yaml; + +import org.javawebstack.abstractdata.AbstractElement; + +import java.text.ParseException; + +public class YamlParser { + + boolean singleRoot = false; + + public YamlParser setSingleRoot(boolean singleRoot) { + this.singleRoot = singleRoot; + return this; + } + + public boolean isSingleRoot() { + return singleRoot; + } + + public AbstractElement parse(String source) throws ParseException { + return LegacyYamlParser.parse(source, singleRoot); + } + +} diff --git a/src/test/java/org/javawebstack/abstractdata/AbstractArrayTest.java b/src/test/java/org/javawebstack/abstractdata/AbstractArrayTest.java index d02306f..0c503f9 100644 --- a/src/test/java/org/javawebstack/abstractdata/AbstractArrayTest.java +++ b/src/test/java/org/javawebstack/abstractdata/AbstractArrayTest.java @@ -1,11 +1,200 @@ package org.javawebstack.abstractdata; +import org.javawebstack.abstractdata.exception.AbstractCoercingException; import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.*; class AbstractArrayTest { + @Test + void testIsArrayReturnsTrue() { + assertTrue(new AbstractArray().isArray()); + } + + @Test + void testObjectThrowsInStrictMode() { + assertThrows(AbstractCoercingException.class, () -> new AbstractArray().object(true)); + } + + @Test + void testObjectCoercingWhenNotInStrictMode() { + AbstractArray a = new AbstractArray(); + a.add(0); + a.add("1"); + AbstractObject o = assertDoesNotThrow(() -> a.object(false)); + assertTrue(o.hasNumber("0")); + assertTrue(o.hasString("1")); + } + + @Test + void testObjectQueryReturnsNullWhenKeyIsNotFound() { + assertNull(new AbstractArray().add(new AbstractObject()).object("1")); + } + + @Test + void testObjectQueryReturnsValueWhenKeyIsFound() { + assertNotNull(new AbstractArray().add(new AbstractObject()).object("0")); + } + + @Test + void testArrayQueryReturnsNullWhenKeyIsNotFound() { + assertNull(new AbstractArray().add(new AbstractArray()).array("1")); + } + + @Test + void testArrayQueryReturnsValueWhenKeyIsFound() { + assertNotNull(new AbstractArray().add(new AbstractArray()).array("0")); + } + + @Test + void testPrimitiveQueryReturnsNullWhenKeyIsNotFound() { + assertNull(new AbstractArray().add(0).primitive("1")); + } + + @Test + void testPrimitiveQueryReturnsValueWhenKeyIsFound() { + assertNotNull(new AbstractArray().add(0).primitive("0")); + } + + @Test + void testStringQueryReturnsNullWhenKeyIsNotFound() { + assertNull(new AbstractArray().add("abc").string("1")); + } + + @Test + void testStringQueryReturnsValueWhenKeyIsFound() { + assertEquals("abc", new AbstractArray().add("abc").string("0")); + } + + @Test + void testNumberQueryReturnsNullWhenKeyIsNotFound() { + assertNull(new AbstractArray().add(123).number("1")); + } + + @Test + void testNumberQueryReturnsValueWhenKeyIsFound() { + assertEquals(123, new AbstractArray().add(123).number("0")); + } + + @Test + void testBoolQueryReturnsNullWhenKeyIsNotFound() { + assertNull(new AbstractArray().add(true).bool("1")); + } + + @Test + void testBoolQueryReturnsValueWhenKeyIsFound() { + assertEquals(true, new AbstractArray().add(true).bool("0")); + } + + @Test + void testQueryOrElseReturnsValueIfFoundAndNonNull() { + AbstractElement expected = new AbstractPrimitive(123); + AbstractElement orElse = new AbstractPrimitive("456"); + AbstractArray array = new AbstractArray().add(expected); + assertEquals(expected, array.query("0", orElse)); + } + + @Test + void testQueryOrElseReturnsOrElseIfFoundAndNull() { + AbstractElement orElse = new AbstractPrimitive("456"); + AbstractArray array = new AbstractArray().addNull(); + assertEquals(orElse, array.query("0", orElse)); + } + + @Test + void testQueryOrElseReturnsOrElseIfNotFound() { + AbstractElement unexpected = new AbstractPrimitive(123); + AbstractElement orElse = new AbstractPrimitive("456"); + AbstractArray array = new AbstractArray().add(unexpected); + assertEquals(orElse, array.query("1", orElse)); + } + + @Test + void testObjectQueryOrElseReturnsOrElseIfNotFound() { + AbstractObject orElse = new AbstractObject().set("a", 1); + assertSame(orElse, new AbstractArray().object("0", orElse)); + } + + @Test + void testArrayQueryOrElseReturnsOrElseIfNotFound() { + AbstractArray orElse = new AbstractArray().add(123); + assertSame(orElse, new AbstractArray().array("0", orElse)); + } + + @Test + void testPrimitiveQueryOrElseReturnsOrElseIfNotFound() { + AbstractPrimitive orElse = new AbstractPrimitive(123); + assertSame(orElse, new AbstractArray().primitive("0", orElse)); + } + + @Test + void testStringQueryOrElseReturnsOrElseIfNotFound() { + String orElse = "abc"; + assertEquals(orElse, new AbstractArray().string("0", orElse)); + } + + @Test + void testNumberQueryOrElseReturnsOrElseIfNotFound() { + Number orElse = 123; + assertEquals(orElse, new AbstractArray().number("0", orElse)); + } + + @Test + void testBoolQueryOrElseReturnsOrElseIfNotFound() { + Boolean orElse = true; + assertEquals(orElse, new AbstractArray().bool("0", orElse)); + } + + @Test + void testQueryThrowsOnNull() { + assertThrows(IllegalArgumentException.class, () -> new AbstractArray().query(null)); + } + + @Test + void testQueryWithNonIntPathReturnsNull() { + assertNull(new AbstractArray().add(1).query("abc")); + } + + @Test + void testQueryWithNonExistingIndexReturnsNull() { + assertNull(new AbstractArray().add(0).query("1")); + } + + @Test + void testQueryWithOnlyOneSegmentReturnsValue() { + assertNotNull(new AbstractArray().add(1).query("0")); + } + + @Test + void testClearClearsTheArrayAndReturnsThis() { + AbstractArray array = new AbstractArray().add(1).add(2).add(3); + assertEquals(3, array.size()); + assertSame(array, array.clear()); + assertEquals(0, array.size()); + } + + @Test + void testQueryWithObjectValueQueriesObject() { + AbstractArray array = new AbstractArray(); + array.add(new AbstractObject().set("a", 1)); + assertEquals(1, array.query("0.a").number().intValue()); + } + + @Test + void testQueryWithArrayValueQueriesArray() { + AbstractArray array = new AbstractArray(); + array.add(new AbstractArray().add(1)); + assertEquals(1, array.query("0.0").number().intValue()); + } + + @Test + void testQueryWithPrimitiveValueReturnsNull() { + AbstractArray array = new AbstractArray(); + array.add("abc"); + assertNull(array.query("0.abc")); + } + @Test void testEqualsNormal() { AbstractArray first = new AbstractArray(); diff --git a/src/test/java/org/javawebstack/abstractdata/collector/AbstractArrayCollectorTest.java b/src/test/java/org/javawebstack/abstractdata/collector/AbstractArrayCollectorTest.java new file mode 100644 index 0000000..1d1c4a5 --- /dev/null +++ b/src/test/java/org/javawebstack/abstractdata/collector/AbstractArrayCollectorTest.java @@ -0,0 +1,20 @@ +package org.javawebstack.abstractdata.collector; + +import org.javawebstack.abstractdata.AbstractArray; +import org.javawebstack.abstractdata.AbstractPrimitive; +import org.junit.jupiter.api.Test; +import java.util.stream.Stream; +import static org.junit.jupiter.api.Assertions.*; + +public class AbstractArrayCollectorTest { + + @Test + public void testCollection() { + AbstractArray array = Stream.of("a", "b", "c").parallel().collect(new AbstractArrayCollector<>(AbstractPrimitive::new)); + assertEquals(3, array.size()); + assertEquals("a", array.string(0)); + assertEquals("b", array.string(1)); + assertEquals("c", array.string(2)); + } + +} diff --git a/src/test/java/org/javawebstack/abstractdata/collector/AbstractObjectCollectorTest.java b/src/test/java/org/javawebstack/abstractdata/collector/AbstractObjectCollectorTest.java new file mode 100644 index 0000000..c79416a --- /dev/null +++ b/src/test/java/org/javawebstack/abstractdata/collector/AbstractObjectCollectorTest.java @@ -0,0 +1,21 @@ +package org.javawebstack.abstractdata.collector; + +import org.javawebstack.abstractdata.AbstractObject; +import org.javawebstack.abstractdata.AbstractPrimitive; +import org.junit.jupiter.api.Test; + +import java.util.stream.Stream; +import static org.junit.jupiter.api.Assertions.*; + +public class AbstractObjectCollectorTest { + + @Test + public void testCollection() { + AbstractObject object = Stream.of("a", "bc", "def").parallel().collect(new AbstractObjectCollector<>(s -> s, s -> new AbstractPrimitive(s.length()))); + assertEquals(3, object.size()); + assertEquals(1, object.number("a")); + assertEquals(2, object.number("bc")); + assertEquals(3, object.number("def")); + } + +} diff --git a/src/test/java/org/javawebstack/abstractdata/json/JsonDumperTest.java b/src/test/java/org/javawebstack/abstractdata/json/JsonDumperTest.java new file mode 100644 index 0000000..1134be7 --- /dev/null +++ b/src/test/java/org/javawebstack/abstractdata/json/JsonDumperTest.java @@ -0,0 +1,116 @@ +package org.javawebstack.abstractdata.json; + +import org.javawebstack.abstractdata.*; +import org.junit.jupiter.api.Test; +import static org.mockito.Mockito.*; + +import java.util.HashMap; +import java.util.Map; +import java.util.function.Function; + +import static org.junit.jupiter.api.Assertions.*; + +public class JsonDumperTest { + + @Test + public void testDumpJavaNull() { + assertEquals("null", new JsonDumper().dump(null)); + } + + @Test + public void testDumpNull() { + assertEquals("null", new JsonDumper().dump(AbstractNull.VALUE)); + } + + @Test + public void testDumpString() { + assertEquals("\"abc\"", new JsonDumper().dump(new AbstractPrimitive("abc"))); + } + + @Test + public void testDumpNumber() { + assertEquals("123.456", new JsonDumper().dump(new AbstractPrimitive(123.456))); + } + + @Test + public void testDumpBoolean() { + assertEquals("true", new JsonDumper().dump(new AbstractPrimitive(true))); + assertEquals("false", new JsonDumper().dump(new AbstractPrimitive(false))); + } + + @Test + public void testDumpObject() { + assertEquals("{\"a\":1,\"b\":2}", new JsonDumper().dump(new AbstractObject().set("a", 1).set("b", 2))); + } + + @Test + public void testDumpArray() { + assertEquals("[1,2,3]", new JsonDumper().dump(new AbstractArray().add(1).add(2).add(3))); + } + + @Test + public void testEscapeStringEscapeSeq() { + Map escapes = new HashMap<>(); + escapes.put("\"", "\\\""); + escapes.put("\\", "\\\\"); + escapes.put("\b", "\\b"); + escapes.put("\f", "\\f"); + escapes.put("\n", "\\n"); + escapes.put("\r", "\\r"); + escapes.put("\t", "\\t"); + escapes.put("\0", "\\0"); + escapes.put("/", "\\/"); + for(String c : escapes.keySet()) { + assertEquals("\"" + escapes.get(c) + "\"", new JsonDumper().dump(new AbstractPrimitive(c))); + } + } + + @Test + public void testEscapeStringUnicode() { + assertEquals("\"\\u001F\"", new JsonDumper().dump(new AbstractPrimitive("\u001F"))); + } + + @Test + public void testPrettyFlag() { + AbstractObject o = new AbstractObject() + .set("a", 1) + .set("b", new AbstractArray().add("a").add("b")); + String expectedNonPretty = "{\"a\":1,\"b\":[\"a\",\"b\"]}"; + String expectedPretty = "{\n \"a\": 1,\n \"b\": [\n \"a\",\n \"b\"\n ]\n}"; + assertEquals(expectedNonPretty, new JsonDumper().dump(o)); + assertEquals(expectedPretty, new JsonDumper().setPretty(true).dump(o)); + } + + @Test + public void testIndent() { + AbstractObject o = new AbstractObject() + .set("a", 1) + .set("b", new AbstractArray().add("a").add("b")); + String defaultIndent = " "; + String shortIndent = " "; + Function expectedFn = indent -> "{\n" + indent + "\"a\": 1,\n" + indent + "\"b\": [\n" + indent + indent + "\"a\",\n" + indent + indent + "\"b\"\n" + indent + "]\n}"; + assertEquals(expectedFn.apply(defaultIndent), new JsonDumper().setPretty(true).dump(o)); + assertEquals(expectedFn.apply(shortIndent), new JsonDumper().setIndent(shortIndent).setPretty(true).dump(o)); + } + + @Test + public void testNoNewlineOnEmptyObject() { + AbstractObject object = new AbstractObject().set("a", new AbstractObject()); + String expected = "{\n \"a\": {}\n}"; + assertEquals(expected, new JsonDumper().setPretty(true).dump(object)); + } + + @Test + public void testNoNewlineOnEmptyArray() { + AbstractObject object = new AbstractObject().set("a", new AbstractArray()); + String expected = "{\n \"a\": []\n}"; + assertEquals(expected, new JsonDumper().setPretty(true).dump(object)); + } + + @Test + public void testDumpUnknownType() { + AbstractElement e = mock(AbstractElement.class); + assertThrows(IllegalArgumentException.class, () -> new JsonDumper().dump(e)); + } + +} diff --git a/src/test/java/org/javawebstack/abstractdata/json/JsonParserTest.java b/src/test/java/org/javawebstack/abstractdata/json/JsonParserTest.java new file mode 100644 index 0000000..5c96faa --- /dev/null +++ b/src/test/java/org/javawebstack/abstractdata/json/JsonParserTest.java @@ -0,0 +1,169 @@ +package org.javawebstack.abstractdata.json; + +import org.javawebstack.abstractdata.AbstractElement; +import org.javawebstack.abstractdata.AbstractPrimitive; +import org.junit.jupiter.api.Test; + +import java.text.ParseException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.*; + +public class JsonParserTest { + + @Test + public void testParseEmptyString() { + ParseException e = assertThrows(ParseException.class, () -> new JsonParser().parse("")); + assertEquals("Unexpected character ", e.getMessage()); + } + + @Test + public void testParseUnexpectedEOF() { + ParseException e = assertThrows(ParseException.class, () -> new JsonParser().parse("{")); + assertEquals("Unexpected character ", e.getMessage()); + e = assertThrows(ParseException.class, () -> new JsonParser().parse("[")); + assertEquals("Unexpected character ", e.getMessage()); + e = assertThrows(ParseException.class, () -> new JsonParser().parse("\"")); + assertEquals("Unexpected character ", e.getMessage()); + e = assertThrows(ParseException.class, () -> new JsonParser().parse("t")); + assertEquals("Unexpected character ", e.getMessage()); + } + + @Test + public void testParseUnexpectedCharacter() { + ParseException e = assertThrows(ParseException.class, () -> new JsonParser().parse("{\n \"abc\":x\n}")); + assertEquals("Unexpected character 'x' at line 2 pos 11", e.getMessage()); + assertEquals(12, e.getErrorOffset()); + } + + @Test + public void testParseBooleanTrue() { + AbstractElement trueElement = assertDoesNotThrow(() -> new JsonParser().parse("true")); + assertTrue(trueElement.isBoolean()); + assertTrue(trueElement.bool()); + assertThrows(ParseException.class, () -> new JsonParser().parse("trux")); + assertThrows(ParseException.class, () -> new JsonParser().parse("trxx")); + assertThrows(ParseException.class, () -> new JsonParser().parse("txxx")); + } + + @Test + public void testParseBooleanFalse() { + AbstractElement falseElement = assertDoesNotThrow(() -> new JsonParser().parse("false")); + assertTrue(falseElement.isBoolean()); + assertFalse(falseElement.bool()); + assertThrows(ParseException.class, () -> new JsonParser().parse("falsx")); + assertThrows(ParseException.class, () -> new JsonParser().parse("falxx")); + assertThrows(ParseException.class, () -> new JsonParser().parse("faxxx")); + assertThrows(ParseException.class, () -> new JsonParser().parse("fxxxx")); + } + + @Test + public void testParseBooleanNull() { + AbstractElement nullElement = assertDoesNotThrow(() -> new JsonParser().parse("null")); + assertTrue(nullElement.isNull()); + assertThrows(ParseException.class, () -> new JsonParser().parse("nulx")); + assertThrows(ParseException.class, () -> new JsonParser().parse("nuxx")); + assertThrows(ParseException.class, () -> new JsonParser().parse("nxxx")); + } + + @Test + public void testParseInteger() { + AbstractElement e = assertDoesNotThrow(() -> new JsonParser().parse("123")); + assertTrue(e.isNumber()); + assertEquals(123, e.number()); + } + + @Test + public void testParseLong() { + long expected = 1L + Integer.MAX_VALUE; + AbstractElement e = assertDoesNotThrow(() -> new JsonParser().parse(String.valueOf(expected))); + assertTrue(e.isNumber()); + assertEquals(expected, e.number()); + } + + @Test + public void testParseDouble() { + AbstractElement e = assertDoesNotThrow(() -> new JsonParser().parse("123.456")); + assertTrue(e.isNumber()); + assertEquals(123.456, e.number()); + } + + @Test + public void testParseStringEscapeSeq() { + Map escapes = new HashMap<>(); + escapes.put("\\\"", "\""); + escapes.put("\\\\", "\\"); + escapes.put("\\b", "\b"); + escapes.put("\\f", "\f"); + escapes.put("\\n", "\n"); + escapes.put("\\r", "\r"); + escapes.put("\\t", "\t"); + escapes.put("\\0", "\0"); + escapes.put("\\/", "/"); + for(String c : escapes.keySet()) { + AbstractElement e = assertDoesNotThrow(() -> new JsonParser().parse("\"" + c + "\"")); + assertTrue(e.isString()); + assertEquals(escapes.get(c), e.string()); + } + } + + @Test + public void testParseStringUnicode() { + AbstractElement e = assertDoesNotThrow(() -> new JsonParser().parse("\"\\u001F\"")); + assertTrue(e.isString()); + assertEquals("\u001F", e.string()); + } + + @Test + public void testParseValidObject() { + AbstractElement e = assertDoesNotThrow(() -> new JsonParser().parse("{\"a\":1,\"b\":2}")); + assertTrue(e.isObject()); + assertEquals(2, e.object().size()); + List keys = new ArrayList<>(e.object().keys()); + assertEquals("a", keys.get(0)); + assertEquals("b", keys.get(1)); + AbstractElement aElement = e.object().get("a"); + assertTrue(aElement.isNumber()); + assertEquals(1, aElement.number()); + AbstractElement bElement = e.object().get("b"); + assertTrue(bElement.isNumber()); + assertEquals(2, bElement.number()); + } + + @Test + public void testParseInvalidObjectWithMissingColon() { + assertThrows(ParseException.class, () -> new JsonParser().parse("{\"a\"1}")); + } + + @Test + public void testParseInvalidObjectWithMissingComma() { + assertThrows(ParseException.class, () -> new JsonParser().parse("{\"a\":1\"b\":2}")); + } + + @Test + public void testParseValidArray() { + AbstractElement e = assertDoesNotThrow(() -> new JsonParser().parse("[1,2]")); + assertTrue(e.isArray()); + assertEquals(2, e.array().size()); + AbstractElement firstElement = e.array().get(0); + assertTrue(firstElement.isNumber()); + assertEquals(1, firstElement.number()); + AbstractElement secondElement = e.array().get(1); + assertTrue(secondElement.isNumber()); + assertEquals(2, secondElement.number()); + } + + @Test + public void testParseInvalidArrayWithInvalidValue() { + assertThrows(ParseException.class, () -> new JsonParser().parse("[talse]")); + } + + @Test + public void testParseInvalidArrayWithMissingComma() { + assertThrows(ParseException.class, () -> new JsonParser().parse("[\"a\"\"b\"]")); + } + +} diff --git a/src/test/java/org/javawebstack/abstractdata/yaml/YamlDumperTest.java b/src/test/java/org/javawebstack/abstractdata/yaml/YamlDumperTest.java new file mode 100644 index 0000000..d211544 --- /dev/null +++ b/src/test/java/org/javawebstack/abstractdata/yaml/YamlDumperTest.java @@ -0,0 +1,38 @@ +package org.javawebstack.abstractdata.yaml; + +import org.javawebstack.abstractdata.AbstractArray; +import org.javawebstack.abstractdata.AbstractObject; +import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.*; + +public class YamlDumperTest { + + @Test + public void testDump() { + AbstractObject object = new AbstractObject() + .set("a", 1) + .set("b", new AbstractArray()); + String expected = "a: 1\nb: []\n"; // Not actually minified but that's what snakeyaml outputs + String dumped = new YamlDumper().dump(object); + assertEquals(expected, dumped); + } + + @Test + public void testDumpPretty() { + AbstractObject object = new AbstractObject() + .set("a", 1) + .set("b", new AbstractArray()); + String expected = "a: 1\nb: [\n ]\n"; // Not actually expected, as it's not pretty but that's what snakeyaml calls pretty + String dumped = new YamlDumper().setPretty(true).dump(object); + assertEquals(expected, dumped); + } + + @Test + public void testSetPretty() { + YamlDumper dumper = new YamlDumper(); + assertFalse(dumper.isPretty()); + dumper.setPretty(true); + assertTrue(dumper.isPretty()); + } + +} diff --git a/src/test/java/org/javawebstack/abstractdata/yaml/YamlParserTest.java b/src/test/java/org/javawebstack/abstractdata/yaml/YamlParserTest.java new file mode 100644 index 0000000..770264f --- /dev/null +++ b/src/test/java/org/javawebstack/abstractdata/yaml/YamlParserTest.java @@ -0,0 +1,62 @@ +package org.javawebstack.abstractdata.yaml; + +import org.javawebstack.abstractdata.AbstractElement; +import org.junit.jupiter.api.Test; +import org.yaml.snakeyaml.Yaml; + +import java.text.ParseException; + +import static org.junit.jupiter.api.Assertions.*; + +public class YamlParserTest { + + @Test + public void testParse() { + AbstractElement e = assertDoesNotThrow(() -> new YamlParser().setSingleRoot(true).parse("test:\n a: 123\n b: false\n c: 'abc'")); + assertNotNull(e); + assertTrue(e.isObject()); + assertEquals(1, e.object().size()); + AbstractElement testElement = e.object().get("test"); + assertNotNull(testElement); + assertTrue(testElement.isObject()); + assertEquals(3, testElement.object().size()); + AbstractElement aElement = testElement.object().get("a"); + assertNotNull(aElement); + assertTrue(aElement.isNumber()); + assertEquals(123, aElement.number()); + AbstractElement bElement = testElement.object().get("b"); + assertNotNull(bElement); + assertTrue(bElement.isBoolean()); + assertEquals(false, bElement.bool()); + AbstractElement cElement = testElement.object().get("c"); + assertNotNull(cElement); + assertTrue(cElement.isString()); + assertEquals("abc", cElement.string()); + } + + @Test + public void testParseEmptyDocument() { + assertThrows(ParseException.class, () -> new YamlParser().parse("")); + } + + @Test + public void testParseNull() { + AbstractElement e = assertDoesNotThrow(() -> new YamlParser().parse("null")); + assertNotNull(e); + assertTrue(e.isNull()); + } + + @Test + public void testParseInvalidDocument() { + assertThrows(ParseException.class, () -> new YamlParser().parse("\"a\"1")); + } + + @Test + public void testSetSingleRoot() { + YamlParser parser = new YamlParser(); + assertFalse(parser.isSingleRoot()); + parser.setSingleRoot(true); + assertTrue(parser.isSingleRoot()); + } + +}