feat: introduce muscript-gson to support json parsing/writing from muScript and of muScript data from java
ci/woodpecker/push/woodpecker Pipeline was successful Details

This commit is contained in:
Johannes Frohnmeyer 2023-08-25 16:24:28 +02:00
parent 0ca5b4f323
commit e927d1f82c
Signed by: Johannes
GPG Key ID: E76429612C2929F4
41 changed files with 787 additions and 72 deletions

View File

@ -1,11 +1,8 @@
package io.gitlab.jfronny.commons.serialize.gson.api.v1;
import io.gitlab.jfronny.commons.ComparableVersion;
import io.gitlab.jfronny.commons.serialize.gson.impl.ComparableVersionAdapter;
import io.gitlab.jfronny.commons.serialize.gson.impl.GsonIgnoreExclusionStrategy;
import io.gitlab.jfronny.commons.serialize.gson.api.v2.GsonTransformer;
import io.gitlab.jfronny.gson.*;
import java.lang.reflect.Modifier;
import java.lang.reflect.Type;
import java.util.function.Consumer;
@ -13,21 +10,14 @@ import java.util.function.Consumer;
* Holds a common instance of the Gson object.
* Supports registering type adapters/etc. as needed
*/
@Deprecated
public class GsonHolder {
private final GsonBuilder builder = new GsonBuilder()
.registerTypeAdapter(ComparableVersion.class, new ComparableVersionAdapter())
.excludeFieldsWithModifiers(Modifier.TRANSIENT, Modifier.PRIVATE)
.setExclusionStrategies(new GsonIgnoreExclusionStrategy())
.setLenient();
private boolean clean = false;
private Gson gson;
private final io.gitlab.jfronny.commons.serialize.gson.api.v2.GsonHolder impl;
public GsonHolder() {
synchronized (GsonHolders.KNOWN_INSTANCES) {
GsonHolders.KNOWN_INSTANCES.add(this);
for (Consumer<GsonBuilder> modification : GsonHolders.KNOWN_MODIFICATIONS) modification.accept(builder);
}
this(new io.gitlab.jfronny.commons.serialize.gson.api.v2.GsonHolder());
}
public GsonHolder(io.gitlab.jfronny.commons.serialize.gson.api.v2.GsonHolder impl) {
this.impl = impl;
}
/**
@ -36,11 +26,7 @@ public class GsonHolder {
* @return The Gson instance
*/
public Gson getGson() {
if (!clean) {
gson = builder.create();
clean = true;
}
return gson;
return impl.getGson();
}
/**
@ -50,8 +36,7 @@ public class GsonHolder {
* @param typeAdapter The adapter type
*/
public GsonHolder registerTypeAdapter(Type type, Object typeAdapter) {
builder.registerTypeAdapter(type, typeAdapter);
clean = false;
impl.registerTypeAdapter(type, typeAdapter);
return this;
}
@ -61,8 +46,7 @@ public class GsonHolder {
* @param factory The factory to register
*/
public GsonHolder registerTypeAdapterFactory(TypeAdapterFactory factory) {
builder.registerTypeAdapterFactory(factory);
clean = false;
impl.registerTypeAdapterFactory(factory);
return this;
}
@ -72,8 +56,7 @@ public class GsonHolder {
* @param func The function to run
*/
public GsonHolder modifyBuilder(Consumer<GsonBuilder> func) {
func.accept(builder);
clean = false;
impl.apply(GsonTransformer.byConsumer(func));
return this;
}
}

View File

@ -1,47 +1,34 @@
package io.gitlab.jfronny.commons.serialize.gson.api.v1;
import io.gitlab.jfronny.commons.ref.WeakSet;
import io.gitlab.jfronny.commons.serialize.Serializer;
import io.gitlab.jfronny.commons.serialize.gson.impl.GsonHolderSerializer;
import io.gitlab.jfronny.commons.serialize.gson.api.v2.GsonTransformer;
import io.gitlab.jfronny.gson.GsonBuilder;
import io.gitlab.jfronny.gson.TypeAdapterFactory;
import org.jetbrains.annotations.ApiStatus;
import java.lang.reflect.Type;
import java.util.LinkedHashSet;
import java.util.Set;
import java.util.function.Consumer;
@Deprecated
public class GsonHolders {
@ApiStatus.Internal
static final Set<Consumer<GsonBuilder>> KNOWN_MODIFICATIONS = new LinkedHashSet<>();
@ApiStatus.Internal
static final WeakSet<GsonHolder> KNOWN_INSTANCES = new WeakSet<>();
public static final GsonHolder API = new GsonHolder();
public static final GsonHolder CONFIG = new GsonHolder().modifyBuilder(b -> b
.setOmitQuotes()
.serializeSpecialFloatingPointValues()
.serializeNulls()
.setPrettyPrinting()
);
public static final GsonHolder API = new GsonHolder(io.gitlab.jfronny.commons.serialize.gson.api.v2.GsonHolders.API);
public static final GsonHolder CONFIG = new GsonHolder(io.gitlab.jfronny.commons.serialize.gson.api.v2.GsonHolders.CONFIG);
public static void registerTypeAdapter(Type type, Object typeAdapter) {
modifyBuilder(b -> b.registerTypeAdapter(type, typeAdapter));
modifyBuilder(GsonTransformer.byTypeAdapter(type, typeAdapter));
}
public static void registerTypeAdapterFactory(TypeAdapterFactory factory) {
modifyBuilder(b -> b.registerTypeAdapterFactory(factory));
modifyBuilder(GsonTransformer.byTypeAdapterFactory(factory));
}
public static void modifyBuilder(Consumer<GsonBuilder> func) {
synchronized (KNOWN_INSTANCES) {
KNOWN_MODIFICATIONS.add(func);
for (GsonHolder holder : KNOWN_INSTANCES) holder.modifyBuilder(func);
}
modifyBuilder(GsonTransformer.byConsumer(func));
}
private static void modifyBuilder(GsonTransformer transformer) {
io.gitlab.jfronny.commons.serialize.gson.api.v2.GsonHolders.applyTransform(transformer);
}
public static void registerSerializer() {
Serializer.setInstance(new GsonHolderSerializer(API));
io.gitlab.jfronny.commons.serialize.gson.api.v2.GsonHolders.registerSerializer();
}
}

View File

@ -6,6 +6,7 @@ import java.lang.annotation.*;
* Mark a class/field to be ignored by Gson.
* May be used for metadata in serialized classes
*/
@Deprecated
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD, ElementType.TYPE})
public @interface Ignore {

View File

@ -0,0 +1,70 @@
package io.gitlab.jfronny.commons.serialize.gson.api.v2;
import io.gitlab.jfronny.gson.*;
import java.lang.reflect.Type;
/**
* Holds a common instance of the Gson object.
* Supports registering type adapters/etc. as needed
*/
public class GsonHolder {
private final GsonBuilder builder = new GsonBuilder();
private boolean clean = false;
private Gson gson;
public GsonHolder() {
synchronized (GsonHolders.KNOWN_INSTANCES) {
GsonHolders.KNOWN_INSTANCES.add(this);
GsonHolders.PAST_MODIFICATIONS.apply(builder);
}
}
/**
* Get the current gson instance or build it if needed
*
* @return The Gson instance
*/
public Gson getGson() {
if (!clean) {
gson = builder.create();
clean = true;
}
return gson;
}
/**
* Register a type adapter and mark the gson instance as unclean
*
* @param type The type for which to register the adapter
* @param typeAdapter The adapter type
*/
public GsonHolder registerTypeAdapter(Type type, Object typeAdapter) {
builder.registerTypeAdapter(type, typeAdapter);
clean = false;
return this;
}
/**
* Register a type adapter factory and mark the gson instance as unclean
*
* @param factory The factory to register
*/
public GsonHolder registerTypeAdapterFactory(TypeAdapterFactory factory) {
builder.registerTypeAdapterFactory(factory);
clean = false;
return this;
}
/**
* Run a function on the builder for modifying it and mark the gson instance as unclean
*
* @param transformer The transformer to apply
*/
public GsonHolder apply(GsonTransformer transformer) {
transformer.apply(builder);
clean = false;
return this;
}
}

View File

@ -0,0 +1,55 @@
package io.gitlab.jfronny.commons.serialize.gson.api.v2;
import io.gitlab.jfronny.commons.ref.WeakSet;
import io.gitlab.jfronny.commons.serialize.Serializer;
import io.gitlab.jfronny.commons.serialize.gson.impl.GsonHolderSerializer;
import org.jetbrains.annotations.ApiStatus;
import java.util.List;
import java.util.ServiceLoader;
import java.util.stream.Stream;
public class GsonHolders {
@ApiStatus.Internal
static GsonTransformer PAST_MODIFICATIONS = GsonTransformer.IDENTITY;
@ApiStatus.Internal
static final WeakSet<GsonHolder> KNOWN_INSTANCES = new WeakSet<>();
public static final GsonHolder API = new GsonHolder();
public static final GsonHolder CONFIG = new GsonHolder().apply(b -> b
.setOmitQuotes()
.serializeSpecialFloatingPointValues()
.serializeNulls()
.setPrettyPrinting()
);
public static void applyTransform(GsonTransformer transformer) {
synchronized (KNOWN_INSTANCES) {
PAST_MODIFICATIONS = GsonTransformer.concat(List.of(PAST_MODIFICATIONS, transformer));
for (GsonHolder holder : KNOWN_INSTANCES) holder.apply(transformer);
}
}
public static void applyTransforms(List<GsonTransformer> transformers) {
synchronized (KNOWN_INSTANCES) {
PAST_MODIFICATIONS = GsonTransformer.concat(Stream.concat(
Stream.of(PAST_MODIFICATIONS),
transformers.stream()
).toList());
for (GsonHolder holder : KNOWN_INSTANCES)
for (GsonTransformer transformer : transformers)
holder.apply(transformer);
}
}
static {
applyTransforms(ServiceLoader.load(GsonTransformer.class)
.stream()
.map(ServiceLoader.Provider::get)
.toList());
}
public static void registerSerializer() {
Serializer.setInstance(new GsonHolderSerializer(API));
}
}

View File

@ -0,0 +1,48 @@
package io.gitlab.jfronny.commons.serialize.gson.api.v2;
import io.gitlab.jfronny.commons.ref.R;
import io.gitlab.jfronny.commons.serialize.gson.impl.MultipleTransformer;
import io.gitlab.jfronny.gson.GsonBuilder;
import io.gitlab.jfronny.gson.TypeAdapterFactory;
import java.lang.reflect.Type;
import java.util.List;
import java.util.function.Consumer;
import java.util.stream.Stream;
/**
* Represents a transformation for {@code GsonBuilder} objects handled by {@code GsonHolder}.
* Transformers may be applied to individual holders, manually to all holders via {@code GsonHolders.applyTransforms}
* or automatically through the service loader mechanism.
*/
public interface GsonTransformer {
GsonTransformer IDENTITY = R::nop;
static GsonTransformer byConsumer(Consumer<GsonBuilder> source) {
return source::accept;
}
static GsonTransformer byTypeAdapter(Type type, Object typeAdapter) {
return b -> b.registerTypeAdapter(type, typeAdapter);
}
static GsonTransformer byTypeAdapterFactory(TypeAdapterFactory factory) {
return b -> b.registerTypeAdapterFactory(factory);
}
static GsonTransformer concat(List<GsonTransformer> transformers) {
transformers = transformers.stream()
.flatMap(t -> t instanceof MultipleTransformer mt
? mt.transformers().stream()
: Stream.of(t))
.filter(s -> s != IDENTITY)
.toList();
return switch (transformers.size()) {
case 0 -> IDENTITY;
case 1 -> transformers.get(0);
default -> new MultipleTransformer(transformers);
};
}
void apply(GsonBuilder builder);
}

View File

@ -0,0 +1,12 @@
package io.gitlab.jfronny.commons.serialize.gson.api.v2;
import java.lang.annotation.*;
/**
* Mark a class/field to be ignored by Gson.
* May be used for metadata in serialized classes
*/
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD, ElementType.TYPE})
public @interface Ignore {
}

View File

@ -0,0 +1,17 @@
package io.gitlab.jfronny.commons.serialize.gson.impl;
import io.gitlab.jfronny.commons.ComparableVersion;
import io.gitlab.jfronny.commons.serialize.gson.api.v2.GsonTransformer;
import io.gitlab.jfronny.gson.GsonBuilder;
import java.lang.reflect.Modifier;
public class DefaultTransformer implements GsonTransformer {
@Override
public void apply(GsonBuilder builder) {
builder.registerTypeAdapter(ComparableVersion.class, new ComparableVersionAdapter())
.excludeFieldsWithModifiers(Modifier.TRANSIENT, Modifier.PRIVATE)
.setExclusionStrategies(new GsonIgnoreExclusionStrategy())
.setLenient();
}
}

View File

@ -1,7 +1,7 @@
package io.gitlab.jfronny.commons.serialize.gson.impl;
import io.gitlab.jfronny.commons.serialize.Serializer;
import io.gitlab.jfronny.commons.serialize.gson.api.v1.GsonHolder;
import io.gitlab.jfronny.commons.serialize.gson.api.v2.GsonHolder;
import io.gitlab.jfronny.gson.*;
import java.io.IOException;

View File

@ -9,12 +9,14 @@ import io.gitlab.jfronny.gson.*;
public class GsonIgnoreExclusionStrategy implements ExclusionStrategy {
@Override
public boolean shouldSkipClass(Class<?> clazz) {
return clazz.isAnnotationPresent(Ignore.class);
return clazz.isAnnotationPresent(Ignore.class)
|| clazz.isAnnotationPresent(io.gitlab.jfronny.commons.serialize.gson.api.v2.Ignore.class);
}
@Override
public boolean shouldSkipField(FieldAttributes f) {
return f.getAnnotation(Ignore.class) != null
|| f.getDeclaringClass().isAnnotationPresent(Ignore.class);
|| f.getAnnotation(io.gitlab.jfronny.commons.serialize.gson.api.v2.Ignore.class) != null
|| shouldSkipClass(f.getDeclaringClass());
}
}

View File

@ -0,0 +1,15 @@
package io.gitlab.jfronny.commons.serialize.gson.impl;
import io.gitlab.jfronny.commons.serialize.gson.api.v2.GsonTransformer;
import io.gitlab.jfronny.gson.GsonBuilder;
import java.util.List;
public record MultipleTransformer(List<GsonTransformer> transformers) implements GsonTransformer {
@Override
public void apply(GsonBuilder builder) {
for (GsonTransformer transformer : transformers) {
transformer.apply(builder);
}
}
}

View File

@ -1,6 +1,8 @@
module io.gitlab.jfronny.commons.gson {
uses io.gitlab.jfronny.commons.serialize.gson.api.v2.GsonTransformer;
requires io.gitlab.jfronny.commons;
requires transitive io.gitlab.jfronny.gson;
requires static org.jetbrains.annotations;
exports io.gitlab.jfronny.commons.serialize.gson.api.v1;
exports io.gitlab.jfronny.commons.serialize.gson.api.v2;
}

View File

@ -0,0 +1 @@
io.gitlab.jfronny.commons.serialize.gson.impl.DefaultTransformer

View File

@ -0,0 +1,30 @@
import io.gitlab.jfronny.scripts.*
plugins {
id("commons.library")
}
dependencies {
implementation(project(":muscript"))
implementation(project(":commons-gson"))
testImplementation("org.junit.jupiter:junit-jupiter-api:5.9.3")
testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:5.9.3")
}
publishing {
publications {
create<MavenPublication>("maven") {
groupId = "io.gitlab.jfronny"
artifactId = "muscript-gson"
from(components["java"])
}
}
}
tasks.javadoc {
linksOffline("https://maven.frohnmeyer-wds.de/javadoc/artifacts/io/gitlab/jfronny/commons/$version/raw", project(":"))
linksOffline("https://maven.frohnmeyer-wds.de/javadoc/artifacts/io/gitlab/jfronny/commons-gson/$version/raw", project(":commons-gson"))
linksOffline("https://maven.frohnmeyer-wds.de/javadoc/artifacts/io/gitlab/jfronny/muscript-gson/$version/raw", project(":muscript"))
}

View File

@ -0,0 +1,28 @@
package io.gitlab.jfronny.muscript.gson;
import io.gitlab.jfronny.commons.serialize.gson.api.v2.GsonHolders;
import io.gitlab.jfronny.muscript.data.Scope;
import io.gitlab.jfronny.muscript.data.dynamic.*;
import io.gitlab.jfronny.muscript.data.dynamic.additional.DFinal;
public class GsonLib {
public static Scope addTo(Scope scope) {
return scope
.set("toJson", GsonLib::toJson)
.set("fromJson", GsonLib::fromJson);
}
public static DString toJson(DList args) {
if (args.isEmpty() || args.size() > 2) throw new IllegalArgumentException("Invalid number of arguments for toJson: expected 1 or 2 but got " + args.size());
Dynamic source = args.get(0);
boolean lenient = args.size() > 1 && args.get(1).asBool().getValue();
return DFinal.of((lenient ? GsonHolders.CONFIG : GsonHolders.API).getGson().toJson(source));
}
public static Dynamic fromJson(DList args) {
if (args.isEmpty() || args.size() > 2) throw new IllegalArgumentException("Invalid number of arguments for fromJson: expected 1 or 2 but got " + args.size());
String source = args.get(0).asString().getValue();
boolean lenient = args.size() > 1 && args.get(1).asBool().getValue();
return (lenient ? GsonHolders.CONFIG : GsonHolders.API).getGson().fromJson(source, Dynamic.class);
}
}

View File

@ -0,0 +1,11 @@
package io.gitlab.jfronny.muscript.gson.impl;
import io.gitlab.jfronny.commons.serialize.gson.api.v2.GsonTransformer;
import io.gitlab.jfronny.gson.GsonBuilder;
public class DynamicGsonTransformer implements GsonTransformer {
@Override
public void apply(GsonBuilder builder) {
new DynamicSerializer().registerTo(builder);
}
}

View File

@ -0,0 +1,70 @@
package io.gitlab.jfronny.muscript.gson.impl;
import io.gitlab.jfronny.gson.GsonBuilder;
import io.gitlab.jfronny.gson.TypeAdapter;
import io.gitlab.jfronny.gson.stream.JsonReader;
import io.gitlab.jfronny.gson.stream.JsonWriter;
import io.gitlab.jfronny.muscript.data.dynamic.*;
import io.gitlab.jfronny.muscript.data.dynamic.additional.*;
import io.gitlab.jfronny.muscript.gson.impl.typed.*;
import io.gitlab.jfronny.muscript.gson.impl.typed.additional.*;
import java.io.IOException;
public class DynamicSerializer extends TypeAdapter<Dynamic> {
public final DObjectSerializer dObject = new DObjectSerializer(this);
public final DListSerializer dList = new DListSerializer(this);
public final DNullSerializer dNull = new DNullSerializer();
public final DBoolSerializer dBool = new DBoolSerializer();
public final DNumberSerializer dNumber = new DNumberSerializer();
public final DStringSerializer dString = new DStringSerializer(this);
public final DCallableSerializer dCallable = new DCallableSerializer();
public final DynamicBaseSerializer dynamicBase = new DynamicBaseSerializer();
public final DDateSerializer dDate = new DDateSerializer();
public final DTimeSerializer dTime = new DTimeSerializer();
public final DEnumSerializer dEnum = new DEnumSerializer();
public void registerTo(GsonBuilder builder) {
builder.registerTypeAdapter(Dynamic.class, this);
builder.registerTypeHierarchyAdapter(DObject.class, dObject);
builder.registerTypeHierarchyAdapter(DList.class, dList);
builder.registerTypeAdapter(DNull.class, dNull);
builder.registerTypeHierarchyAdapter(DBool.class, dBool);
builder.registerTypeHierarchyAdapter(DNumber.class, dNumber);
builder.registerTypeHierarchyAdapter(DString.class, dString);
builder.registerTypeHierarchyAdapter(DCallable.class, dCallable);
builder.registerTypeHierarchyAdapter(DynamicBase.class, dynamicBase); // Custom dynamic implementations need custom support
builder.registerTypeAdapter(DDate.class, dDate);
builder.registerTypeAdapter(DTime.class, dTime);
builder.registerTypeAdapter(DEnum.class, dEnum);
}
@Override
public Dynamic read(JsonReader r) throws IOException {
return switch (r.peek()) {
case BEGIN_OBJECT -> dObject.read(r);
case BEGIN_ARRAY -> dList.read(r);
case NULL -> dNull.read(r);
case BOOLEAN -> dBool.read(r);
case NUMBER -> dNumber.read(r);
case STRING -> dString.read(r);
default -> throw new IllegalStateException("Unsupported token for beginning of Dynamic: " + r.peek());
};
}
@Override
public void write(JsonWriter w, Dynamic dynamic) throws IOException {
if (dynamic == null) w.nullValue();
else if (dynamic instanceof DDate date) dDate.write(w, date);
else if (dynamic instanceof DTime time) dTime.write(w, time);
else if (dynamic instanceof DEnum enm) dEnum.write(w, enm);
else if (dynamic.isObject()) dObject.write(w, dynamic.asObject());
else if (dynamic.isList()) dList.write(w, dynamic.asList());
else if (dynamic instanceof DNull n) dNull.write(w, n);
else if (dynamic.isBool()) dBool.write(w, dynamic.asBool());
else if (dynamic.isNumber()) dNumber.write(w, dynamic.asNumber());
else if (dynamic.isString()) dString.write(w, dynamic.asString());
else if (dynamic.isCallable()) dCallable.write(w, dynamic.asCallable());
else throw new IllegalArgumentException("Unexpected dynamic type for: " + dynamic);
}
}

View File

@ -0,0 +1,21 @@
package io.gitlab.jfronny.muscript.gson.impl.typed;
import io.gitlab.jfronny.gson.TypeAdapter;
import io.gitlab.jfronny.gson.stream.JsonReader;
import io.gitlab.jfronny.gson.stream.JsonWriter;
import io.gitlab.jfronny.muscript.data.dynamic.DBool;
import io.gitlab.jfronny.muscript.data.dynamic.additional.DFinal;
import java.io.IOException;
public class DBoolSerializer extends TypeAdapter<DBool> {
@Override
public void write(JsonWriter w, DBool dynamic) throws IOException {
w.value(dynamic.getValue());
}
@Override
public DBool read(JsonReader r) throws IOException {
return DFinal.of(r.nextBoolean());
}
}

View File

@ -0,0 +1,21 @@
package io.gitlab.jfronny.muscript.gson.impl.typed;
import io.gitlab.jfronny.gson.TypeAdapter;
import io.gitlab.jfronny.gson.stream.JsonReader;
import io.gitlab.jfronny.gson.stream.JsonWriter;
import io.gitlab.jfronny.muscript.data.dynamic.DCallable;
import java.io.IOException;
public class DCallableSerializer extends TypeAdapter<DCallable> {
@Override
public void write(JsonWriter w, DCallable dynamic) throws IOException {
if (dynamic.getName().equals(DCallable.DEFAULT_NAME)) throw new IllegalArgumentException("Unnamed callables cannot be serialized to json");
else w.value(dynamic.getName());
}
@Override
public DCallable read(JsonReader r) throws IOException {
throw new UnsupportedOperationException("DCallables cannot be deserialized from json");
}
}

View File

@ -0,0 +1,40 @@
package io.gitlab.jfronny.muscript.gson.impl.typed;
import io.gitlab.jfronny.gson.TypeAdapter;
import io.gitlab.jfronny.gson.stream.*;
import io.gitlab.jfronny.muscript.data.dynamic.DList;
import io.gitlab.jfronny.muscript.data.dynamic.Dynamic;
import io.gitlab.jfronny.muscript.data.dynamic.additional.DFinal;
import io.gitlab.jfronny.muscript.gson.impl.DynamicSerializer;
import java.io.IOException;
import java.util.LinkedList;
import java.util.List;
public class DListSerializer extends TypeAdapter<DList> {
private final DynamicSerializer base;
public DListSerializer(DynamicSerializer base) {
this.base = base;
}
@Override
public void write(JsonWriter w, DList dynamic) throws IOException {
w.beginArray();
for (Dynamic value : dynamic.getValue()) {
base.write(w, value);
}
w.endArray();
}
@Override
public DList read(JsonReader r) throws IOException {
List<Dynamic> elements = new LinkedList<>();
r.beginArray();
while (r.peek() != JsonToken.END_ARRAY) {
elements.add(base.read(r));
}
r.endArray();
return DFinal.of(elements);
}
}

View File

@ -0,0 +1,21 @@
package io.gitlab.jfronny.muscript.gson.impl.typed;
import io.gitlab.jfronny.gson.TypeAdapter;
import io.gitlab.jfronny.gson.stream.JsonReader;
import io.gitlab.jfronny.gson.stream.JsonWriter;
import io.gitlab.jfronny.muscript.data.dynamic.DNull;
import java.io.IOException;
public class DNullSerializer extends TypeAdapter<DNull> {
@Override
public void write(JsonWriter w, DNull dynamic) throws IOException {
w.nullValue();
}
@Override
public DNull read(JsonReader r) throws IOException {
r.nextNull();
return new DNull();
}
}

View File

@ -0,0 +1,23 @@
package io.gitlab.jfronny.muscript.gson.impl.typed;
import io.gitlab.jfronny.gson.TypeAdapter;
import io.gitlab.jfronny.gson.stream.JsonReader;
import io.gitlab.jfronny.gson.stream.JsonWriter;
import io.gitlab.jfronny.muscript.data.dynamic.DNumber;
import io.gitlab.jfronny.muscript.data.dynamic.additional.DFinal;
import java.io.IOException;
public class DNumberSerializer extends TypeAdapter<DNumber> {
@Override
public void write(JsonWriter w, DNumber dynamic) throws IOException {
double d = dynamic.getValue();
if (d % 1.0 != 0 || d > Long.MAX_VALUE) w.value(d);
else w.value((long) d);
}
@Override
public DNumber read(JsonReader r) throws IOException {
return DFinal.of(r.nextDouble());
}
}

View File

@ -0,0 +1,41 @@
package io.gitlab.jfronny.muscript.gson.impl.typed;
import io.gitlab.jfronny.gson.TypeAdapter;
import io.gitlab.jfronny.gson.stream.*;
import io.gitlab.jfronny.muscript.data.dynamic.DObject;
import io.gitlab.jfronny.muscript.data.dynamic.Dynamic;
import io.gitlab.jfronny.muscript.data.dynamic.additional.DFinal;
import io.gitlab.jfronny.muscript.gson.impl.DynamicSerializer;
import java.io.IOException;
import java.util.LinkedHashMap;
import java.util.Map;
public class DObjectSerializer extends TypeAdapter<DObject> {
private final DynamicSerializer base;
public DObjectSerializer(DynamicSerializer base) {
this.base = base;
}
@Override
public void write(JsonWriter w, DObject dynamic) throws IOException {
w.beginObject();
for (Map.Entry<String, ? extends Dynamic> entry : dynamic.getValue().entrySet()) {
w.name(entry.getKey());
base.write(w, entry.getValue());
}
w.endObject();
}
@Override
public DObject read(JsonReader r) throws IOException {
Map<String, Dynamic> elements = new LinkedHashMap<>();
r.beginObject();
while (r.peek() != JsonToken.END_OBJECT) {
elements.put(r.nextName(), base.read(r));
}
r.endObject();
return DFinal.of(elements);
}
}

View File

@ -0,0 +1,28 @@
package io.gitlab.jfronny.muscript.gson.impl.typed;
import io.gitlab.jfronny.gson.TypeAdapter;
import io.gitlab.jfronny.gson.stream.*;
import io.gitlab.jfronny.muscript.data.dynamic.DString;
import io.gitlab.jfronny.muscript.data.dynamic.additional.DFinal;
import io.gitlab.jfronny.muscript.gson.impl.DynamicSerializer;
import java.io.IOException;
public class DStringSerializer extends TypeAdapter<DString> {
private final DynamicSerializer base;
public DStringSerializer(DynamicSerializer base) {
this.base = base;
}
@Override
public void write(JsonWriter w, DString dynamic) throws IOException {
w.value(dynamic.getValue());
}
@Override
public DString read(JsonReader r) throws IOException {
if (r.peek() == JsonToken.STRING) return DFinal.of(r.nextString());
else return base.read(r).asString();
}
}

View File

@ -0,0 +1,20 @@
package io.gitlab.jfronny.muscript.gson.impl.typed;
import io.gitlab.jfronny.gson.TypeAdapter;
import io.gitlab.jfronny.gson.stream.JsonReader;
import io.gitlab.jfronny.gson.stream.JsonWriter;
import io.gitlab.jfronny.muscript.data.dynamic.DynamicBase;
import java.io.IOException;
public class DynamicBaseSerializer extends TypeAdapter<DynamicBase> {
@Override
public void write(JsonWriter w, DynamicBase dynamic) throws IOException {
throw new UnsupportedOperationException("Tried to write unsupported custom dynamic: " + dynamic);
}
@Override
public DynamicBase read(JsonReader r) throws IOException {
throw new UnsupportedOperationException("Tried to read unsupported custom dynamic");
}
}

View File

@ -0,0 +1,22 @@
package io.gitlab.jfronny.muscript.gson.impl.typed.additional;
import io.gitlab.jfronny.gson.TypeAdapter;
import io.gitlab.jfronny.gson.stream.JsonReader;
import io.gitlab.jfronny.gson.stream.JsonWriter;
import io.gitlab.jfronny.muscript.data.dynamic.additional.DDate;
import java.io.IOException;
import java.time.LocalDate;
public class DDateSerializer extends TypeAdapter<DDate> {
@Override
public void write(JsonWriter w, DDate dynamic) throws IOException {
w.value(dynamic.toString());
}
@Override
public DDate read(JsonReader r) throws IOException {
String s = r.nextString();
return new DDate(() -> LocalDate.parse(s));
}
}

View File

@ -0,0 +1,26 @@
package io.gitlab.jfronny.muscript.gson.impl.typed.additional;
import io.gitlab.jfronny.gson.TypeAdapter;
import io.gitlab.jfronny.gson.stream.JsonReader;
import io.gitlab.jfronny.gson.stream.JsonWriter;
import io.gitlab.jfronny.muscript.data.dynamic.additional.DEnum;
import java.io.IOException;
public class DEnumSerializer extends TypeAdapter<DEnum> {
@Override
public void write(JsonWriter w, DEnum dynamic) throws IOException {
if (dynamic.value() == null) {
w.beginArray();
for (String s : dynamic.values().keySet()) {
w.value(s);
}
w.endArray();
} else w.value(dynamic.value().value());
}
@Override
public DEnum read(JsonReader r) throws IOException {
throw new UnsupportedOperationException("Deserializing DEnum is unsupported");
}
}

View File

@ -0,0 +1,22 @@
package io.gitlab.jfronny.muscript.gson.impl.typed.additional;
import io.gitlab.jfronny.gson.TypeAdapter;
import io.gitlab.jfronny.gson.stream.JsonReader;
import io.gitlab.jfronny.gson.stream.JsonWriter;
import io.gitlab.jfronny.muscript.data.dynamic.additional.DTime;
import java.io.IOException;
import java.time.LocalTime;
public class DTimeSerializer extends TypeAdapter<DTime> {
@Override
public void write(JsonWriter w, DTime dynamic) throws IOException {
w.value(dynamic.toString());
}
@Override
public DTime read(JsonReader r) throws IOException {
String s = r.nextString();
return new DTime(() -> LocalTime.parse(s));
}
}

View File

@ -0,0 +1,7 @@
module io.gitlab.jfronny.commons.muscript.gson {
requires io.gitlab.jfronny.gson;
requires io.gitlab.jfronny.commons.muscript;
requires io.gitlab.jfronny.commons.gson;
exports io.gitlab.jfronny.muscript.gson;
// provides io.gitlab.jfronny.commons.serialize.gson.api.v2.GsonTransformer with io.gitlab.jfronny.muscript.gson.DynamicGsonTransformer;
}

View File

@ -0,0 +1 @@
io.gitlab.jfronny.muscript.gson.impl.DynamicGsonTransformer

View File

@ -0,0 +1,58 @@
package io.gitlab.jfronny.muscript.gson.test;
import io.gitlab.jfronny.commons.serialize.gson.api.v2.GsonHolders;
import io.gitlab.jfronny.muscript.StandardLib;
import io.gitlab.jfronny.muscript.compiler.Parser;
import io.gitlab.jfronny.muscript.data.Scope;
import io.gitlab.jfronny.muscript.data.dynamic.Dynamic;
import io.gitlab.jfronny.muscript.data.dynamic.additional.DFinal;
import io.gitlab.jfronny.muscript.gson.GsonLib;
import org.junit.jupiter.api.Test;
import java.util.Arrays;
import java.util.Map;
import static io.gitlab.jfronny.muscript.data.dynamic.additional.DFinal.of;
import static org.junit.jupiter.api.Assertions.assertEquals;
class JsonTest {
@Test
void simpleSerialize() {
assertEquals("\"Yes\"", serialize(of("Yes")));
assertEquals("{\"key\":3}", serialize(of(Map.of("key", of(3)))));
assertEquals("[true,12,\"7\"]", serialize(of(of(true), of(12), of(7).asString())));
}
@Test
void simpleDeserialize() {
assertEquals(of("Yes"), deserialize("\"Yes\"", Dynamic.class));
assertEquals(of(Map.of("key", of(3))), deserialize("{\"key\":3}", Dynamic.class));
assertEquals(of(of(true), of(12), of(7).asString()), deserialize("[true,12,\"7\"]", Dynamic.class));
}
@Test
void muscriptDSL() {
assertEquals("{\"key\":\"One\",\"key2\":3}", execute("{key = 'One', key2 = 3}::toJson()").asString().getValue());
assertEquals(of(Map.of("key", of(3), "value", of(false))), execute("args[0]::fromJson()", "{\"key\": 3, \"value\": false}"));
}
private Scope createScope() {
return GsonLib.addTo(StandardLib.createScope());
}
private Dynamic execute(String source, String... args) {
return Parser.parse(source)
.asDynamicExpr()
.get(createScope()
.set("args", DFinal.of(Arrays.stream(args).map(DFinal::of).toList()))
);
}
private String serialize(Object object) {
return GsonHolders.API.getGson().toJson(object);
}
private <T> T deserialize(String source, Class<T> klazz) {
return GsonHolders.API.getGson().fromJson(source, klazz);
}
}

View File

@ -113,7 +113,7 @@ public class StandardLib {
}
public static DNumber random(DList args) {
if (args.size() == 0) return of(rnd.nextDouble());
if (args.isEmpty()) return of(rnd.nextDouble());
else if (args.size() == 2) {
double min = args.get(0).asNumber().getValue();
double max = args.get(1).asNumber().getValue();
@ -257,7 +257,7 @@ public class StandardLib {
}
public static DObject try_(DList args) {
if (args.size() == 0) throw new IllegalArgumentException("Invalid number of arguments for try: expected 1 or more but got " + args.size());
if (args.isEmpty()) throw new IllegalArgumentException("Invalid number of arguments for try: expected 1 or more but got " + args.size());
var callable = args.get(0).asCallable();
var l = args.getValue();
var innerArgs = of(l.subList(1, l.size()));

View File

@ -28,7 +28,7 @@ public class StringCoerce extends StringExpr {
if (inner instanceof NullLiteral) return literal(location, "null");
if (inner instanceof BoolLiteral expr) return literal(location, StringFormatter.toString(expr.value));
if (inner instanceof DynamicLiteral<?> expr) return literal(location, StringFormatter.toString(expr.value));
if (inner instanceof NumberLiteral expr) return literal(location, StringFormatter.toString(expr.value));
if (inner instanceof NumberLiteral expr) return literal(location, StringFormatter.toStringPrecise(expr.value));
if (inner instanceof StringExpr expr) return expr;
return new StringCoerce(location, inner);
}

View File

@ -6,6 +6,8 @@ import io.gitlab.jfronny.muscript.data.dynamic.additional.NamedDCallable;
import java.util.function.Function;
public non-sealed interface DCallable extends Dynamic {
String DEFAULT_NAME = "<unnamed>";
default Dynamic call(DList args) {
return getValue().apply(args);
}
@ -15,7 +17,7 @@ public non-sealed interface DCallable extends Dynamic {
}
default String getName() {
return "<unnamed>";
return DEFAULT_NAME;
}
default DCallable named(String name) {

View File

@ -3,6 +3,7 @@ package io.gitlab.jfronny.muscript.data.dynamic;
import io.gitlab.jfronny.muscript.ast.DynamicExpr;
import io.gitlab.jfronny.muscript.ast.dynamic.ObjectLiteral;
import io.gitlab.jfronny.muscript.compiler.CodeLocation;
import io.gitlab.jfronny.muscript.data.dynamic.additional.DFinal;
import org.jetbrains.annotations.Nullable;
import java.util.Map;
@ -24,4 +25,13 @@ public non-sealed interface DObject extends Dynamic {
@Override
Map<String, ? extends Dynamic> getValue();
@Override
default DString asString() {
return DFinal.of(getValue()
.entrySet()
.stream()
.map(e -> e.getKey() + " = " + e.getValue().asString().getValue())
.collect(Collectors.joining(", ", "{", "}")));
}
}

View File

@ -5,12 +5,10 @@ import io.gitlab.jfronny.muscript.ast.dynamic.Call;
import io.gitlab.jfronny.muscript.ast.dynamic.Variable;
import io.gitlab.jfronny.muscript.ast.literal.NumberLiteral;
import io.gitlab.jfronny.muscript.compiler.CodeLocation;
import io.gitlab.jfronny.muscript.compiler.ExprWriter;
import io.gitlab.jfronny.muscript.data.dynamic.*;
import io.gitlab.jfronny.muscript.data.dynamic.lens.DNumberLens;
import io.gitlab.jfronny.muscript.data.dynamic.lens.DStringLens;
import java.io.IOException;
import java.time.LocalDate;
import java.util.List;
import java.util.Map;

View File

@ -1,6 +1,7 @@
package io.gitlab.jfronny.muscript.data.dynamic.additional;
import io.gitlab.jfronny.commons.StringFormatter;
import io.gitlab.jfronny.commons.data.ImmCollection;
import io.gitlab.jfronny.muscript.ast.DynamicExpr;
import io.gitlab.jfronny.muscript.ast.dynamic.Call;
import io.gitlab.jfronny.muscript.ast.dynamic.Variable;
@ -83,7 +84,7 @@ public record DEnum(Map<String, ? extends Dynamic> values, @Nullable DEnumEntry
for (int i = 0; i < values.size(); i++) {
result.put(values.get(i), new DEnumEntry(values.get(i), i, values.get(i).equals(value)));
}
return Map.copyOf(result);
return ImmCollection.of(result);
}
public record DEnumEntry(String value, int index, boolean selected) implements DString {

View File

@ -28,7 +28,7 @@ public class DFinal {
}
public static DObject of(Map<String, ? extends Dynamic> b) {
return new FObject(Map.copyOf(b));
return new FObject(ImmCollection.copyOf((Map<String, Dynamic>) b));
}
public static DList of(Dynamic... b) {
@ -36,7 +36,7 @@ public class DFinal {
}
public static DList of(List<? extends Dynamic> b) {
return new FList(ImmCollection.of((List<Dynamic>) b));
return new FList(ImmCollection.copyOf((List<Dynamic>) b));
}
public static DCallable of(Function<DList, ? extends Dynamic> b, Supplier<String> serialized) {
@ -81,7 +81,7 @@ public class DFinal {
@Override
public String toString() {
return StringFormatter.toString(value);
return StringFormatter.toStringPrecise(value);
}
}

View File

@ -6,3 +6,4 @@ include("commons-jlhttp")
include("commons-manifold")
include("commons-slf4j")
include("muscript")
include("muscript-gson")

View File

@ -6,25 +6,33 @@ import java.util.function.Function;
public class StringFormatter {
public static String toString(Object o) {
if (o == null) return "null";
else if (o instanceof Double d) return toString(d);
else if (o instanceof Float f) return toString(f);
else if (o instanceof Double d) return toString((double) d);
else if (o instanceof Float f) return toString((float) f);
else if (o instanceof Throwable t) return toString(t, Objects::toString);
else return o.toString();
}
public static String toString(double d) {
double abs = Math.abs(d);
if (abs % 1.0 == 0) return String.format(Locale.US, "%.0f", d);
else if (abs >= 1000) return String.format(Locale.US, "%.0f", d);
if (abs >= 1000 || abs % 1.0 == 0) return String.format(Locale.US, "%.0f", d);
else if (abs >= 0.1) return String.format(Locale.US, "%.4f", d);
else return toStringPrecise(d);
}
public static String toStringPrecise(double d) {
if (d % 1.0 == 0) return String.format(Locale.US, "%.0f", d);
else return String.format(Locale.US, "%s", d);
}
public static String toString(float f) {
float abs = Math.abs(f);
if (abs % 1.0f == 0) return String.format(Locale.US, "%.0f", f);
else if (abs >= 1000) return String.format(Locale.US, "%.0f", f);
if (abs >= 1000 || abs % 1.0 == 0) return String.format(Locale.US, "%.0f", f);
else if (abs >= 0.1f) return String.format(Locale.US, "%.4f", f);
else return toStringPrecise(f);
}
public static String toStringPrecise(float f) {
if (f % 1.0f == 0) return String.format(Locale.US, "%.0f", f);
else return String.format(Locale.US, "%s", f);
}

View File

@ -13,14 +13,26 @@ public class ImmCollection {
return new ImmutableList<>(list);
}
public static <T> List<T> copyOf(List<T> list) {
return of(new ArrayList<>(list));
}
public static <T> Set<T> of(Set<T> set) {
return new ImmutableSet<>(set);
}
public static <T> Set<T> copyOf(Set<T> set) {
return of(new LinkedHashSet<>(set));
}
public static <K, V> Map<K, V> of(Map<K, V> map) {
return new ImmutableMap<>(map);
}
public static <K, V> Map<K, V> copyOf(Map<K, V> map) {
return of(new LinkedHashMap<>(map));
}
public static <T> Iterable<T> of(Iterable<T> iterable) {
return new ImmutableIterable<>(iterable);
}