getType();
33 |
34 | /**
35 | * Invokes clojure.pprint/pprint, which writes a pretty-printed representation of the object to the currently bound
36 | * value of *out*, which defaults to System.out (stdout).
37 | */
38 | void prettyPrint();
39 |
40 | /**
41 | * Like {@link DynamicObject#prettyPrint}, but returns the pretty-printed string instead of writing it to *out*.
42 | */
43 | String toFormattedString();
44 |
45 | /**
46 | * Return a copy of this instance with {@code other}'s fields merged in (nulls don't count). If a given field is
47 | * present in both instances, the fields in {@code other} will take precedence.
48 | *
49 | * Equivalent to: {@code (merge-with (fn [a b] (if (nil? b) a b)) this other)}
50 | */
51 | D merge(D other);
52 |
53 | /**
54 | * Recursively compares this instance with {@code other}, returning a new instance containing all of the common
55 | * elements of both {@code this} and {@code other}. Maps and lists are compared recursively; everything else,
56 | * including sets, strings, and POJOs, is treated atomically.
57 | *
58 | * Equivalent to: {@code (nth (clojure.data/diff this other) 2)}
59 | */
60 | D intersect(D other);
61 |
62 | /**
63 | * Recursively compares this instance with {@code other}, similar to {@link #intersect}, but returning the fields that
64 | * are unique to {@code this}. Uses the same recursion strategy as {@code intersect}.
65 | *
66 | * Equivalent to: {@code (nth (clojure.data/diff this other) 0)}
67 | */
68 | D subtract(D other);
69 |
70 | /**
71 | * Validate that all fields annotated with @Required are non-null, and that all present fields are of the correct
72 | * type. Returns the validated instance unchanged, which allows the validate method to be called at the end of a
73 | * fluent builder chain.
74 | */
75 | D validate();
76 |
77 | /**
78 | * Post-deserialization hook. The intended use of this method is to facilitate format upgrades. For example, if a
79 | * new field has been added to a DynamicObject schema, this method can be implemented to add that field (with
80 | * some appropriate default value) to older data upon deserialization.
81 | *
82 | * If not implemented, this method does nothing.
83 | *
84 | * @deprecated This method is experimental.
85 | */
86 | @Deprecated
87 | D afterDeserialization();
88 |
89 | /**
90 | * Serialize the given object to Edn. Any {@code EdnTranslator}s that have been registered through
91 | * {@link DynamicObject#registerType} will be invoked as needed.
92 | */
93 | static String serialize(Object o) {
94 | return EdnSerialization.serialize(o);
95 | }
96 |
97 | static void serialize(Object o, Writer w) {
98 | EdnSerialization.serialize(o, w);
99 | }
100 |
101 | /**
102 | * Deserializes a DynamicObject or registered type from a String.
103 | *
104 | * @param edn The Edn representation of the object.
105 | * @param type The type of class to deserialize. Must be an interface that extends DynamicObject.
106 | */
107 | static T deserialize(String edn, Class type) {
108 | return EdnSerialization.deserialize(edn, type);
109 | }
110 |
111 | /**
112 | * Lazily deserialize a stream of top-level Edn elements as the given type.
113 | */
114 | static Stream deserializeStream(PushbackReader streamReader, Class type) {
115 | return EdnSerialization.deserializeStream(streamReader, type);
116 | }
117 |
118 | /**
119 | * Serialize a single object {@code o} to binary Fressian data.
120 | */
121 | static byte[] toFressianByteArray(Object o) {
122 | return FressianSerialization.toFressianByteArray(o);
123 | }
124 |
125 | /**
126 | * Deserialize and return the Fressian-encoded object in {@code bytes}.
127 | */
128 | static T fromFressianByteArray(byte[] bytes) {
129 | return FressianSerialization.fromFressianByteArray(bytes);
130 | }
131 |
132 | /**
133 | * Create a {@link FressianReader} instance to read from {@code is}. The reader will be created with support for all
134 | * the basic Java and Clojure types, all DynamicObject types registered by calling {@link #registerTag(Class,
135 | * String)}, and any other types registered by calling {@link #registerType(Class, String, ReadHandler,
136 | * WriteHandler)}. If {@code validateChecksum} is true, the data will be checksummed as it is read; this checksum
137 | * can later be compared to the expected checksum in the Fressian footer by calling {@link
138 | * FressianReader#validateFooter()}.
139 | */
140 | static FressianReader createFressianReader(InputStream is, boolean validateChecksum) {
141 | return FressianSerialization.createFressianReader(is, validateChecksum);
142 | }
143 |
144 | /**
145 | * Create a {@link FressianWriter} instance to write to {@code os}. The writer will be created with support for all
146 | * the basic Java and Clojure types, all DynamicObject types registered by calling {@link #registerTag(Class,
147 | * String)}, and any other types registered by calling {@link #registerType(Class, String, ReadHandler,
148 | * WriteHandler)}. If desired, a Fressian footer (containing an Adler32 checksum of all data written) can be written
149 | * by calling {@link FressianWriter#writeFooter()}.
150 | */
151 | static FressianWriter createFressianWriter(OutputStream os) {
152 | return FressianSerialization.createFressianWriter(os);
153 | }
154 |
155 | /**
156 | * Lazily deserialize a stream of Fressian-encoded values as the given type. A Fressian footer, if encountered, will
157 | * be validated.
158 | */
159 | static Stream deserializeFressianStream(InputStream is, Class type) {
160 | return FressianSerialization.deserializeFressianStream(is, type);
161 | }
162 |
163 | /**
164 | * Use the supplied {@code map} to back an instance of {@code type}.
165 | */
166 | static > D wrap(Map map, Class type) {
167 | return Instances.wrap(map, type);
168 | }
169 |
170 | /**
171 | * Create a "blank" instance of {@code type}, backed by an empty Clojure map. All fields will be null.
172 | */
173 | static > D newInstance(Class type) {
174 | return Instances.newInstance(type);
175 | }
176 |
177 | /**
178 | * Register an {@link EdnTranslator} to enable instances of {@code type} to be serialized to and deserialized from
179 | * Edn using reader tags.
180 | */
181 | static void registerType(Class type, EdnTranslator translator) {
182 | EdnSerialization.registerType(type, translator);
183 | }
184 |
185 | /**
186 | * Register a {@link org.fressian.handlers.ReadHandler} and {@link org.fressian.handlers.WriteHandler} to enable
187 | * instances of {@code type} to be serialized to and deserialized from Fressian data.
188 | */
189 | static void registerType(Class type, String tag, ReadHandler readHandler, WriteHandler writeHandler) {
190 | FressianSerialization.registerType(type, tag, readHandler, writeHandler);
191 | }
192 |
193 | /**
194 | * Deregister the given {@code translator}. After this method is invoked, it will no longer be possible to read or
195 | * write instances of {@code type} unless another translator is registered.
196 | */
197 | static void deregisterType(Class type) {
198 | Serialization.deregisterType(type);
199 | }
200 |
201 | /**
202 | * Register a reader tag for a DynamicObject type. This is useful for reading Edn representations of Clojure
203 | * records.
204 | */
205 | static > void registerTag(Class type, String tag) {
206 | Serialization.registerTag(type, tag);
207 | }
208 |
209 | /**
210 | * Deregister the reader tag for the given DynamicObject type.
211 | */
212 | static > void deregisterTag(Class type) {
213 | Serialization.deregisterTag(type);
214 | }
215 |
216 | /**
217 | * Specify a default reader, which is a function that will be called when any unknown reader tags are encountered.
218 | * The function will be passed the reader tag (as a string) and the tagged Edn element, and can return whatever it
219 | * wants.
220 | *
221 | * DynamicObject comes with a built-in default reader for unknown elements, which returns an instance of {@link
222 | * com.github.rschmitt.dynamicobject.Unknown}, which simply captures the (tag, element) tuple from the Edn reader.
223 | * The {@code Unknown} class is handled specially during serialization so that unknown elements can be serialized
224 | * correctly; this allows unknown types to be passed through transparently.
225 | *
226 | * To disable the default reader, call {@code DynamicObject.setDefaultReader(null)}. This will cause the reader to
227 | * throw an exception if unknown reader tags are encountered.
228 | */
229 | static void setDefaultReader(BiFunction reader) {
230 | EdnSerialization.setDefaultReader(reader);
231 | }
232 | }
233 |
--------------------------------------------------------------------------------
/src/main/java/com/github/rschmitt/dynamicobject/DynamicObjectSerializer.java:
--------------------------------------------------------------------------------
1 | package com.github.rschmitt.dynamicobject;
2 |
3 | import com.github.rschmitt.dynamicobject.internal.EdnSerialization;
4 | import com.github.rschmitt.dynamicobject.internal.FressianSerialization;
5 |
6 | import org.fressian.FressianReader;
7 | import org.fressian.FressianWriter;
8 |
9 | import java.io.InputStream;
10 | import java.io.OutputStream;
11 | import java.io.PushbackReader;
12 | import java.io.Writer;
13 | import java.util.stream.Stream;
14 |
15 | /**
16 | * A utility class for DynamicObject (de)serialization. All of the methods in this class delegate
17 | * directly to the static methods in {@linkplain DynamicObject}. The difference is that this class
18 | * is instantiable, and can therefore participate in dependency injection. This makes it
19 | * straightforward to ensure that types and serialization tags are registered with DynamicObject
20 | * before any serialization is attempted.
21 | *
22 | * For example, if you are using Guice , you can write
23 | * a {@code DynamicObjectSerializer} provider method that registers types:
24 | * @Provides
25 | * @Singleton
26 | * DynamicObjectSerializer getDynamicObjectSerializer() {
27 | * DynamicObject.registerTag(Record.class, "recordtag");
28 | * DynamicObject.registerType(Identifier.class, new IdentifierTranslator());
29 | * return new DynamicObjectSerializer();
30 | * }
31 | *
32 | * Classes that need to perform serialization can then have a {@code DynamicObjectSerializer}
33 | * injected at construction time:
34 | * private final DynamicObjectSerializer serializer;
35 | *
36 | * @Inject
37 | * public FlatFileWriter(DynamicObjectSerializer serializer) {
38 | * this.serializer = serializer;
39 | * }
40 | *
41 | * public void persist(Record rec) throws IOException {
42 | * File file = new File("record.txt");
43 | * try (
44 | * OutputStream os = new BufferedOutputStream(new FileOutputStream(file));
45 | * Writer w = new OutputStreamWriter(os, StandardCharsets.UTF_8)
46 | * ) {
47 | * serializer.serialize(rec, w);
48 | * }
49 | * }
50 | *
51 | */
52 | public class DynamicObjectSerializer {
53 | /**
54 | * @see DynamicObject#serialize(Object)
55 | */
56 | public String serialize(Object o) {
57 | return EdnSerialization.serialize(o);
58 | }
59 |
60 | /**
61 | * @see DynamicObject#serialize(Object, Writer)
62 | */
63 | public void serialize(Object o, Writer w) {
64 | EdnSerialization.serialize(o, w);
65 | }
66 |
67 | /**
68 | * @see DynamicObject#deserialize(String, Class)
69 | */
70 | public T deserialize(String edn, Class type) {
71 | return EdnSerialization.deserialize(edn, type);
72 | }
73 |
74 | /**
75 | * @see DynamicObject#deserializeStream(PushbackReader, Class)
76 | */
77 | public Stream deserializeStream(PushbackReader streamReader, Class type) {
78 | return EdnSerialization.deserializeStream(streamReader, type);
79 | }
80 |
81 | /**
82 | * @see DynamicObject#toFressianByteArray(Object)
83 | */
84 | public byte[] toFressianByteArray(Object o) {
85 | return FressianSerialization.toFressianByteArray(o);
86 | }
87 |
88 | /**
89 | * @see DynamicObject#fromFressianByteArray(byte[])
90 | */
91 | public T fromFressianByteArray(byte[] bytes) {
92 | return FressianSerialization.fromFressianByteArray(bytes);
93 | }
94 |
95 | /**
96 | * @see DynamicObject#createFressianReader(InputStream, boolean)
97 | */
98 | public FressianReader createFressianReader(InputStream is, boolean validateChecksum) {
99 | return FressianSerialization.createFressianReader(is, validateChecksum);
100 | }
101 |
102 | /**
103 | * @see DynamicObject#createFressianWriter(OutputStream)
104 | */
105 | public FressianWriter createFressianWriter(OutputStream os) {
106 | return FressianSerialization.createFressianWriter(os);
107 | }
108 |
109 | /**
110 | * @see DynamicObject#deserializeFressianStream(InputStream, Class)
111 | */
112 | public Stream deserializeFressianStream(InputStream is, Class type) {
113 | return FressianSerialization.deserializeFressianStream(is, type);
114 | }
115 | }
116 |
--------------------------------------------------------------------------------
/src/main/java/com/github/rschmitt/dynamicobject/EdnTranslator.java:
--------------------------------------------------------------------------------
1 | package com.github.rschmitt.dynamicobject;
2 |
3 | import java.io.IOException;
4 | import java.io.StringWriter;
5 | import java.io.Writer;
6 |
7 | public interface EdnTranslator {
8 | /**
9 | * Read a tagged Edn object as its intended type.
10 | */
11 | T read(Object obj);
12 |
13 | /**
14 | * Return an Edn representation of the given object.
15 | */
16 | default String write(T obj) {
17 | StringWriter stringWriter = new StringWriter();
18 | write(obj, stringWriter);
19 | return stringWriter.toString();
20 | }
21 |
22 | /**
23 | * Return the tag literal to use during serialization.
24 | */
25 | String getTag();
26 |
27 | /**
28 | * Write an Edn representation of the given object to the given Writer.
29 | */
30 | default void write(T obj, Writer writer) {
31 | try {
32 | writer.write(write(obj));
33 | } catch (IOException ex) {
34 | throw new RuntimeException(ex);
35 | }
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/src/main/java/com/github/rschmitt/dynamicobject/FressianReadHandler.java:
--------------------------------------------------------------------------------
1 | package com.github.rschmitt.dynamicobject;
2 |
3 | import org.fressian.Reader;
4 | import org.fressian.handlers.ReadHandler;
5 |
6 | import java.io.IOException;
7 | import java.util.Map;
8 |
9 | public class FressianReadHandler> implements ReadHandler {
10 | private final Class type;
11 |
12 | public FressianReadHandler(Class type) {
13 | this.type = type;
14 | }
15 |
16 | @Override
17 | @SuppressWarnings("deprecation")
18 | public Object read(Reader r, Object tag, int componentCount) throws IOException {
19 | return DynamicObject.wrap((Map) r.readObject(), type).afterDeserialization();
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/src/main/java/com/github/rschmitt/dynamicobject/FressianWriteHandler.java:
--------------------------------------------------------------------------------
1 | package com.github.rschmitt.dynamicobject;
2 |
3 | import org.fressian.CachedObject;
4 | import org.fressian.Writer;
5 | import org.fressian.handlers.WriteHandler;
6 |
7 | import javax.annotation.concurrent.Immutable;
8 | import javax.annotation.concurrent.NotThreadSafe;
9 | import java.io.IOException;
10 | import java.util.AbstractCollection;
11 | import java.util.Iterator;
12 | import java.util.Map;
13 | import java.util.Map.Entry;
14 | import java.util.Set;
15 | import java.util.function.BiFunction;
16 | import java.util.function.Function;
17 |
18 | @SuppressWarnings("rawtypes")
19 | public class FressianWriteHandler> implements WriteHandler {
20 | private final Class type;
21 | private final String tag;
22 | private final Set cachedKeys;
23 |
24 | public FressianWriteHandler(Class type, String tag, Set cachedKeys) {
25 | this.type = type;
26 | this.tag = tag;
27 | this.cachedKeys = cachedKeys;
28 | }
29 |
30 | @Override
31 | public void write(Writer w, Object instance) throws IOException {
32 | // We manually serialize the backing map so that we can apply caching transformations to specific subcomponents.
33 | // To avoid needless copying we do this via an adapter rather than copying to a temporary list.
34 | w.writeTag(tag, 1);
35 | w.writeTag("map", 1);
36 |
37 | Map map = ((DynamicObject) instance).getMap();
38 | w.writeList(new TransformedMap(map, this::transformKey, this::transformValue));
39 | }
40 |
41 | /*
42 | * Although Fressian will automatically cache the string components of each Keyword, by default we still spend a
43 | * minimum of three bytes per keyword - one for the keyword directive itself, one for the namespace (usually null),
44 | * and one for the cached reference to the keyword's string. By requesting caching of the Keyword object itself like
45 | * this, we can get this down to one byte (after the initial cache miss).
46 | *
47 | * The downside of this is that cache misses incur additional an additional byte marking the key as being LRU-cache
48 | * capable, and the cache misses will also result in two cache entries being introduced, causing more cache churn
49 | * than there would be otherwise.
50 | */
51 | private Object transformKey(Object key) {
52 | return new CachedObject(key);
53 | }
54 |
55 | @SuppressWarnings("unchecked")
56 | private Object transformValue(Object key, Object value) {
57 | if (cachedKeys.contains(key)) {
58 | return new CachedObject(value);
59 | }
60 |
61 | return value;
62 | }
63 |
64 | @Immutable
65 | @SuppressWarnings("unchecked")
66 | private static class TransformedMap extends AbstractCollection {
67 | private final Map backingMap;
68 | private final Function keysTransformation;
69 | private final BiFunction valuesTransformation;
70 |
71 | private TransformedMap(
72 | Map backingMap,
73 | Function keysTransformation,
74 | BiFunction valuesTransformation
75 | ) {
76 | this.backingMap = backingMap;
77 | this.keysTransformation = keysTransformation;
78 | this.valuesTransformation = valuesTransformation;
79 | }
80 |
81 | @Override
82 | public Iterator iterator() {
83 | return new TransformingKeyValueIterator(backingMap.entrySet().iterator(), keysTransformation, valuesTransformation);
84 | }
85 |
86 | @Override
87 | public int size() {
88 | return backingMap.size() * 2;
89 | }
90 | }
91 |
92 | @NotThreadSafe
93 | private static class TransformingKeyValueIterator implements Iterator {
94 | private final Iterator entryIterator;
95 | private final Function keysTransformation;
96 | private final BiFunction valuesTransformation;
97 |
98 | Object pendingValue = null;
99 | boolean hasPendingValue = false;
100 |
101 | private TransformingKeyValueIterator(
102 | Iterator entryIterator,
103 | Function keysTransformation,
104 | BiFunction valuesTransformation
105 | ) {
106 | this.entryIterator = entryIterator;
107 | this.keysTransformation = keysTransformation;
108 | this.valuesTransformation = valuesTransformation;
109 | }
110 |
111 | @Override
112 | public boolean hasNext() {
113 | return hasPendingValue || entryIterator.hasNext();
114 | }
115 |
116 | @Override
117 | public Object next() {
118 | if (hasPendingValue) {
119 | Object value = pendingValue;
120 | pendingValue = null;
121 | hasPendingValue = false;
122 |
123 | return value;
124 | }
125 |
126 | Map.Entry entry = entryIterator.next();
127 | pendingValue = valuesTransformation.apply(entry.getKey(), entry.getValue());
128 |
129 | hasPendingValue = true;
130 |
131 | return keysTransformation.apply(entry.getKey());
132 | }
133 | }
134 | }
135 |
--------------------------------------------------------------------------------
/src/main/java/com/github/rschmitt/dynamicobject/Key.java:
--------------------------------------------------------------------------------
1 | package com.github.rschmitt.dynamicobject;
2 |
3 | import java.lang.annotation.ElementType;
4 | import java.lang.annotation.Retention;
5 | import java.lang.annotation.RetentionPolicy;
6 | import java.lang.annotation.Target;
7 |
8 | @Target({ElementType.METHOD})
9 | @Retention(RetentionPolicy.RUNTIME)
10 | public @interface Key {
11 | String value();
12 | }
13 |
--------------------------------------------------------------------------------
/src/main/java/com/github/rschmitt/dynamicobject/Meta.java:
--------------------------------------------------------------------------------
1 | package com.github.rschmitt.dynamicobject;
2 |
3 | import java.lang.annotation.ElementType;
4 | import java.lang.annotation.Retention;
5 | import java.lang.annotation.RetentionPolicy;
6 | import java.lang.annotation.Target;
7 |
8 | /**
9 | * Mark a field as metadata. Metadata is used to annotate a value with additional information that is not logically
10 | * considered part of the value. Metadata is ignored for the purposes of equality and serialization.
11 | */
12 | @Target({ElementType.METHOD})
13 | @Retention(RetentionPolicy.RUNTIME)
14 | public @interface Meta {
15 | }
16 |
--------------------------------------------------------------------------------
/src/main/java/com/github/rschmitt/dynamicobject/Required.java:
--------------------------------------------------------------------------------
1 | package com.github.rschmitt.dynamicobject;
2 |
3 | import java.lang.annotation.ElementType;
4 | import java.lang.annotation.Retention;
5 | import java.lang.annotation.RetentionPolicy;
6 | import java.lang.annotation.Target;
7 |
8 | /**
9 | * Marks a field as required. Fields marked with this annotation will throw a NullPointerException when accessed if a
10 | * nonnull value is not present, or when the validate() method is called on the instance.
11 | */
12 | @Target({ElementType.METHOD})
13 | @Retention(RetentionPolicy.RUNTIME)
14 | public @interface Required {
15 | }
16 |
--------------------------------------------------------------------------------
/src/main/java/com/github/rschmitt/dynamicobject/Unknown.java:
--------------------------------------------------------------------------------
1 | package com.github.rschmitt.dynamicobject;
2 |
3 | import java.io.IOException;
4 | import java.io.StringWriter;
5 | import java.io.Writer;
6 | import java.util.Map;
7 |
8 | import com.github.rschmitt.dynamicobject.internal.ClojureStuff;
9 |
10 | /**
11 | * A generic container for tagged Edn elements. This class preserves everything the Edn reader sees when an unknown
12 | * reader tag is encountered.
13 | */
14 | public class Unknown {
15 | private final String tag;
16 | private final Object element;
17 |
18 | /**
19 | * For internal use only. Serialize a tagged element of an unknown type.
20 | */
21 | @SuppressWarnings("unused")
22 | public static Object serialize(Unknown unknown, Writer w) throws IOException {
23 | w.append('#');
24 | w.append(unknown.getTag());
25 | if (!(unknown.getElement() instanceof Map))
26 | w.append(' ');
27 | ClojureStuff.PrOn.invoke(unknown.getElement(), w);
28 | return null;
29 | }
30 |
31 | public Unknown(String tag, Object element) {
32 | this.tag = tag;
33 | this.element = element;
34 | }
35 |
36 | public String getTag() {
37 | return tag;
38 | }
39 |
40 | public Object getElement() {
41 | return element;
42 | }
43 |
44 | @Override
45 | public boolean equals(Object o) {
46 | if (this == o) return true;
47 | if (o == null || getClass() != o.getClass()) return false;
48 |
49 | Unknown unknown = (Unknown) o;
50 |
51 | if (!element.equals(unknown.element)) return false;
52 | if (!tag.equals(unknown.tag)) return false;
53 |
54 | return true;
55 | }
56 |
57 | @Override
58 | public int hashCode() {
59 | int result = tag.hashCode();
60 | result = 31 * result + element.hashCode();
61 | return result;
62 | }
63 |
64 | @Override
65 | public String toString() {
66 | try {
67 | StringWriter stringWriter = new StringWriter();
68 | serialize(this, stringWriter);
69 | return stringWriter.toString();
70 | } catch (IOException ex) {
71 | throw new RuntimeException(ex);
72 | }
73 | }
74 | }
75 |
--------------------------------------------------------------------------------
/src/main/java/com/github/rschmitt/dynamicobject/internal/ClojureStuff.java:
--------------------------------------------------------------------------------
1 | package com.github.rschmitt.dynamicobject.internal;
2 |
3 | import clojure.lang.IFn;
4 |
5 | import java.util.Map;
6 |
7 | import static clojure.java.api.Clojure.read;
8 | import static clojure.java.api.Clojure.var;
9 |
10 | @SuppressWarnings("rawtypes")
11 | public class ClojureStuff {
12 | public static final Map EmptyMap = (Map) read("{}");
13 | public static final Object EmptySet = read("#{}");
14 | public static final Object EmptyVector = read("[]");
15 | public static final Object Readers = read(":readers");
16 | public static final Object Default = read(":default");
17 |
18 | public static final IFn Assoc = var("clojure.core/assoc");
19 | public static final IFn AssocBang = var("clojure.core/assoc!");
20 | public static final IFn Bigint = var("clojure.core/bigint");
21 | public static final IFn Biginteger = var("clojure.core/biginteger");
22 | public static final IFn ConjBang = var("clojure.core/conj!");
23 | public static final IFn Deref = var("clojure.core/deref");
24 | public static final IFn Dissoc = var("clojure.core/dissoc");
25 | public static final IFn Eval = var("clojure.core/eval");
26 | public static final IFn Get = var("clojure.core/get");
27 | public static final IFn Memoize = var("clojure.core/memoize");
28 | public static final IFn MergeWith = var("clojure.core/merge-with");
29 | public static final IFn Meta = var("clojure.core/meta");
30 | public static final IFn Nth = var("clojure.core/nth");
31 | public static final IFn Persistent = var("clojure.core/persistent!");
32 | public static final IFn PreferMethod = var("clojure.core/prefer-method");
33 | public static final IFn PrOn = var("clojure.core/pr-on");
34 | public static final IFn Read = var("clojure.edn/read");
35 | public static final IFn ReadString = var("clojure.edn/read-string");
36 | public static final IFn RemoveMethod = var("clojure.core/remove-method");
37 | public static final IFn Transient = var("clojure.core/transient");
38 | public static final IFn VaryMeta = var("clojure.core/vary-meta");
39 |
40 | public static final Object PrintMethod = Deref.invoke(var("clojure.core/print-method"));
41 | public static final IFn CachedRead = (IFn) Memoize.invoke(var("clojure.edn/read-string"));
42 | public static final IFn Pprint;
43 | public static final IFn SimpleDispatch;
44 | public static final IFn Diff;
45 |
46 | public static final Map clojureReadHandlers;
47 | public static final Map clojureWriteHandlers;
48 |
49 | static {
50 | IFn require = var("clojure.core/require");
51 | require.invoke(read("clojure.pprint"));
52 | require.invoke(read("clojure.data"));
53 | require.invoke(read("clojure.data.fressian"));
54 |
55 | Pprint = var("clojure.pprint/pprint");
56 | Diff = var("clojure.data/diff");
57 |
58 | SimpleDispatch = (IFn) Deref.invoke(var("clojure.pprint/simple-dispatch"));
59 |
60 | clojureReadHandlers = (Map) Deref.invoke(var("clojure.data.fressian/clojure-read-handlers"));
61 | clojureWriteHandlers = (Map) Deref.invoke(var("clojure.data.fressian/clojure-write-handlers"));
62 | }
63 |
64 | public static Object cachedRead(String edn) {
65 | return CachedRead.invoke(edn);
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/src/main/java/com/github/rschmitt/dynamicobject/internal/Conversions.java:
--------------------------------------------------------------------------------
1 | package com.github.rschmitt.dynamicobject.internal;
2 |
3 | import com.github.rschmitt.collider.ClojureMap;
4 | import com.github.rschmitt.collider.Collider;
5 | import com.github.rschmitt.collider.TransientMap;
6 | import com.github.rschmitt.dynamicobject.DynamicObject;
7 |
8 | import java.lang.reflect.ParameterizedType;
9 | import java.lang.reflect.Type;
10 | import java.time.Instant;
11 | import java.util.Arrays;
12 | import java.util.Collection;
13 | import java.util.Date;
14 | import java.util.List;
15 | import java.util.Map;
16 | import java.util.Optional;
17 | import java.util.Set;
18 |
19 | @SuppressWarnings("rawtypes")
20 | class Conversions {
21 | /*
22 | * Convert a Java object (e.g. passed in to a builder method) into the Clojure-style representation used internally.
23 | * This is done according to the following rules:
24 | * * Boxed and unboxed numerics, as well as BigInteger, will be losslessly converted to Long, Double, or BigInt.
25 | * * Values wrapped in an Optional will be unwrapped and stored as either null or the underlying value.
26 | * * Supported collection types (List, Set, Map) will have their elements converted according to these rules. This
27 | * also applies to nested collections. For instance, a List> will effectively be converted to a
28 | * List>.
29 | */
30 | static Object javaToClojure(Object obj) {
31 | Object val = Numerics.maybeUpconvert(obj);
32 | if (val instanceof DynamicObject)
33 | return obj;
34 | else if (val instanceof Instant)
35 | return java.util.Date.from((Instant) val);
36 | else if (val instanceof List)
37 | return convertCollectionToClojureTypes((Collection>) val, ClojureStuff.EmptyVector);
38 | else if (val instanceof Set)
39 | return convertCollectionToClojureTypes((Collection>) val, ClojureStuff.EmptySet);
40 | else if (val instanceof Map)
41 | return convertMapToClojureTypes((Map, ?>) val);
42 | else if (val instanceof Optional) {
43 | Optional> opt = (Optional>) val;
44 | if (opt.isPresent())
45 | return javaToClojure(opt.get());
46 | else
47 | return null;
48 | } else
49 | return val;
50 | }
51 |
52 | private static Object convertCollectionToClojureTypes(Collection> val, Object empty) {
53 | Object ret = ClojureStuff.Transient.invoke(empty);
54 | for (Object o : val)
55 | ret = ClojureStuff.ConjBang.invoke(ret, javaToClojure(o));
56 | return ClojureStuff.Persistent.invoke(ret);
57 | }
58 |
59 | private static Object convertMapToClojureTypes(Map, ?> map) {
60 | Object ret = ClojureStuff.Transient.invoke(ClojureStuff.EmptyMap);
61 | for (Map.Entry, ?> entry : map.entrySet())
62 | ret = ClojureStuff.AssocBang.invoke(ret, javaToClojure(entry.getKey()), javaToClojure(entry.getValue()));
63 | return ClojureStuff.Persistent.invoke(ret);
64 | }
65 |
66 | /*
67 | * Convert a Clojure object (i.e. a value somewhere in a DynamicObject's map) into the expected Java representation.
68 | * This representation is determined by the generic return type of the method. The conversion is performed as
69 | * follows:
70 | * * If the return type is a numeric type, the Clojure numeric will be downconverted to the expected type (e.g.
71 | * Long -> Integer).
72 | * * If the return type is a nested DynamicObject, we wrap the Clojure value as the expected DynamicObject type.
73 | * * If the return type is an Optional, we convert the value and then wrap it by calling Optional#ofNullable.
74 | * * If the return type is a collection type, there are a few possibilities:
75 | * * If it is a raw type, no action is taken.
76 | * * If it is a wildcard type (e.g. List>), an UnsupportedOperationException is thrown.
77 | * * If the type variable is a Class, the elements of the collection are enumerated over to convert numerics and
78 | * wraps DynamicObjects.
79 | * * If the type variable is another collection type, the algorithm recurses.
80 | */
81 | @SuppressWarnings("unchecked")
82 | static Object clojureToJava(Object obj, Type genericReturnType) {
83 | Class> rawReturnType = Reflection.getRawType(genericReturnType);
84 | if (rawReturnType.equals(Optional.class)) {
85 | Type nestedType = Reflection.getTypeArgument(genericReturnType, 0);
86 | return Optional.ofNullable(clojureToJava(obj, nestedType));
87 | }
88 |
89 | if (obj == null) return null;
90 | if (genericReturnType instanceof Class) {
91 | Class> returnType = (Class>) genericReturnType;
92 | if (Numerics.isNumeric(returnType))
93 | return Numerics.maybeDownconvert(returnType, obj);
94 | if (Instant.class.equals(returnType))
95 | return ((Date) obj).toInstant();
96 | if (DynamicObject.class.isAssignableFrom(returnType))
97 | return DynamicObject.wrap((Map) obj, (Class extends DynamicObject>) returnType);
98 | }
99 |
100 | if (obj instanceof List) {
101 | obj = ((Collection>)obj).stream().map(
102 | elem -> convertCollectionElementToJavaTypes(elem, genericReturnType)
103 | ).collect(Collider.toClojureList());
104 | } else if (obj instanceof Set) {
105 | obj = ((Collection>)obj).stream().map(
106 | elem -> convertCollectionElementToJavaTypes(elem, genericReturnType)
107 | ).collect(Collider.toClojureSet());
108 | } else if (obj instanceof Map) {
109 | obj = convertMapToJavaTypes((Map, ?>) obj, genericReturnType);
110 | if (rawReturnType.equals(ClojureMap.class))
111 | return Collider.intoClojureMap((Map) obj);
112 | }
113 | return obj;
114 | }
115 |
116 | private static Object convertCollectionElementToJavaTypes(Object element, Type genericCollectionType) {
117 | if (genericCollectionType instanceof ParameterizedType) {
118 | ParameterizedType parameterizedType = (ParameterizedType) genericCollectionType;
119 | List typeArgs = Arrays.asList(parameterizedType.getActualTypeArguments());
120 | assert typeArgs.size() == 1;
121 | return clojureToJava(element, typeArgs.get(0));
122 | } else
123 | return clojureToJava(element, Object.class);
124 | }
125 |
126 | private static Object convertMapToJavaTypes(Map, ?> unwrappedMap, Type genericReturnType) {
127 | Type keyType, valType;
128 | if (genericReturnType instanceof ParameterizedType) {
129 | Type[] actualTypeArguments = ((ParameterizedType) genericReturnType).getActualTypeArguments();
130 | assert actualTypeArguments.length == 2;
131 | keyType = actualTypeArguments[0];
132 | valType = actualTypeArguments[1];
133 | } else {
134 | keyType = valType = Object.class;
135 | }
136 |
137 | TransientMap transientMap = Collider.transientMap();
138 |
139 | for (Map.Entry, ?> entry : unwrappedMap.entrySet())
140 | transientMap.put(clojureToJava(entry.getKey(), keyType), clojureToJava(entry.getValue(), valType));
141 |
142 | return transientMap.toPersistent();
143 | }
144 | }
145 |
--------------------------------------------------------------------------------
/src/main/java/com/github/rschmitt/dynamicobject/internal/CustomValidationHook.java:
--------------------------------------------------------------------------------
1 | package com.github.rschmitt.dynamicobject.internal;
2 |
3 | import com.github.rschmitt.dynamicobject.DynamicObject;
4 |
5 | public interface CustomValidationHook> {
6 | D $$customValidate();
7 | }
8 |
--------------------------------------------------------------------------------
/src/main/java/com/github/rschmitt/dynamicobject/internal/DynamicObjectInstance.java:
--------------------------------------------------------------------------------
1 | package com.github.rschmitt.dynamicobject.internal;
2 |
3 | import clojure.lang.*;
4 | import com.github.rschmitt.dynamicobject.DynamicObject;
5 |
6 | import java.io.StringWriter;
7 | import java.io.Writer;
8 | import java.lang.reflect.Type;
9 | import java.util.Collection;
10 | import java.util.Iterator;
11 | import java.util.Map;
12 | import java.util.Set;
13 | import java.util.concurrent.ConcurrentHashMap;
14 |
15 | import static java.lang.String.format;
16 |
17 | @SuppressWarnings({"rawtypes", "unchecked"})
18 | public abstract class DynamicObjectInstance> extends AFn implements Map, IPersistentMap, IObj, MapEquivalence, IHashEq, DynamicObjectPrintHook, CustomValidationHook {
19 | private static final Object Default = new Object();
20 | private static final Object Null = new Object();
21 |
22 | private final Map map;
23 | private final Class type;
24 | private final ConcurrentHashMap valueCache = new ConcurrentHashMap();
25 |
26 | public DynamicObjectInstance(Map map, Class type) {
27 | this.map = map;
28 | this.type = type;
29 | }
30 |
31 | public Map getMap() {
32 | return map;
33 | }
34 |
35 | public Class getType() {
36 | return type;
37 | }
38 |
39 | @Override
40 | public String toString() {
41 | return DynamicObject.serialize(this);
42 | }
43 |
44 | @Override
45 | public int hashCode() {
46 | return map.hashCode();
47 | }
48 |
49 | @Override
50 | public boolean equals(Object other) {
51 | if (other == this) return true;
52 | if (other == null) return false;
53 |
54 | if (other instanceof DynamicObject)
55 | return map.equals(((DynamicObject) other).getMap());
56 | else
57 | return other.equals(map);
58 | }
59 |
60 | public void prettyPrint() {
61 | ClojureStuff.Pprint.invoke(this);
62 | }
63 |
64 | public String toFormattedString() {
65 | Writer w = new StringWriter();
66 | ClojureStuff.Pprint.invoke(this, w);
67 | return w.toString();
68 | }
69 |
70 | public D merge(D other) {
71 | AFn ignoreNulls = new AFn() {
72 | public Object invoke(Object arg1, Object arg2) {
73 | return (arg2 == null) ? arg1 : arg2;
74 | }
75 | };
76 | Map mergedMap = (Map) ClojureStuff.MergeWith.invoke(ignoreNulls, map, other.getMap());
77 | return DynamicObject.wrap(mergedMap, type);
78 | }
79 |
80 | public D intersect(D arg) {
81 | return diff(arg, 2);
82 | }
83 |
84 | public D subtract(D arg) {
85 | return diff(arg, 0);
86 | }
87 |
88 | private D diff(D arg, int idx) {
89 | Object array = ClojureStuff.Diff.invoke(map, arg.getMap());
90 | Object union = ClojureStuff.Nth.invoke(array, idx);
91 | if (union == null) union = ClojureStuff.EmptyMap;
92 | return DynamicObject.wrap((Map) union, type);
93 | }
94 |
95 | public D convertAndAssoc(Object key, Object value) {
96 | return (D) assoc(key, Conversions.javaToClojure(value));
97 | }
98 |
99 | @Override
100 | public IPersistentMap assoc(Object key, Object value) {
101 | return (DynamicObjectInstance) DynamicObject.wrap((Map) ClojureStuff.Assoc.invoke(map, key, value), type);
102 | }
103 |
104 | public D assocMeta(Object key, Object value) {
105 | return DynamicObject.wrap((Map) ClojureStuff.VaryMeta.invoke(map, ClojureStuff.Assoc, key, value), type);
106 | }
107 |
108 | public Object getMetadataFor(Object key) {
109 | Object meta = ClojureStuff.Meta.invoke(map);
110 | return ClojureStuff.Get.invoke(meta, key);
111 | }
112 |
113 | public Object invokeGetter(Object key, boolean isRequired, Type genericReturnType) {
114 | Object value = getAndCacheValueFor(key, genericReturnType);
115 | if (value == null && isRequired)
116 | throw new NullPointerException(format("Required field %s was null", key.toString()));
117 | return value;
118 | }
119 |
120 | @SuppressWarnings("unchecked")
121 | public Object getAndCacheValueFor(Object key, Type genericReturnType) {
122 | Object cachedValue = valueCache.getOrDefault(key, Default);
123 | if (cachedValue == Null) return null;
124 | if (cachedValue != Default) return cachedValue;
125 | Object value = getValueFor(key, genericReturnType);
126 | if (value == null)
127 | valueCache.putIfAbsent(key, Null);
128 | else
129 | valueCache.putIfAbsent(key, value);
130 | return value;
131 | }
132 |
133 | public Object getValueFor(Object key, Type genericReturnType) {
134 | Object val = map.get(key);
135 | return Conversions.clojureToJava(val, genericReturnType);
136 | }
137 |
138 | public Object $$noop() {
139 | return this;
140 | }
141 |
142 | @Override
143 | public int size() {
144 | return map.size();
145 | }
146 |
147 | @Override
148 | public boolean isEmpty() {
149 | return map.isEmpty();
150 | }
151 |
152 | @Override
153 | public boolean containsKey(Object key) {
154 | return map.containsKey(key);
155 | }
156 |
157 | @Override
158 | public boolean containsValue(Object value) {
159 | return map.containsValue(value);
160 | }
161 |
162 | @Override
163 | public Object get(Object key) {
164 | return map.get(key);
165 | }
166 |
167 | @Override
168 | public Object put(Object key, Object value) {
169 | throw new UnsupportedOperationException();
170 | }
171 |
172 | @Override
173 | public Object remove(Object key) {
174 | throw new UnsupportedOperationException();
175 | }
176 |
177 | @Override
178 | public void putAll(Map m) {
179 | throw new UnsupportedOperationException();
180 | }
181 |
182 | @Override
183 | public void clear() {
184 | throw new UnsupportedOperationException();
185 | }
186 |
187 | @Override
188 | public Set keySet() {
189 | return map.keySet();
190 | }
191 |
192 | @Override
193 | public Collection values() {
194 | return map.values();
195 | }
196 |
197 | @Override
198 | public Set entrySet() {
199 | return map.entrySet();
200 | }
201 |
202 | @Override
203 | public IMapEntry entryAt(Object key) {
204 | return ((Associative) map).entryAt(key);
205 | }
206 |
207 | @Override
208 | public Object valAt(Object key) {
209 | return ((Associative) map).valAt(key);
210 | }
211 |
212 | @Override
213 | public Object valAt(Object key, Object notFound) {
214 | return ((Associative) map).valAt(key, notFound);
215 | }
216 |
217 | @Override
218 | public int count() {
219 | return ((IPersistentCollection) map).count();
220 | }
221 |
222 | @Override
223 | public IPersistentCollection cons(Object o) {
224 | Map newMap = (Map) ((IPersistentCollection) map).cons(o);
225 | return (DynamicObjectInstance) DynamicObject.wrap(newMap, type);
226 | }
227 |
228 | @Override
229 | public IPersistentCollection empty() {
230 | return (DynamicObjectInstance) DynamicObject.wrap(ClojureStuff.EmptyMap, type);
231 | }
232 |
233 | @Override
234 | public boolean equiv(Object o) {
235 | return ((IPersistentCollection) map).equiv(o);
236 | }
237 |
238 | @Override
239 | public ISeq seq() {
240 | return ((Seqable) map).seq();
241 | }
242 |
243 | @Override
244 | public IPersistentMap assocEx(Object key, Object val) {
245 | Object newMap = ((IPersistentMap) map).assocEx(key, val);
246 | return (DynamicObjectInstance) DynamicObject.wrap((Map) newMap, type);
247 | }
248 |
249 | @Override
250 | public IPersistentMap without(Object key) {
251 | Object newMap = ((IPersistentMap) map).without(key);
252 | return (DynamicObjectInstance) DynamicObject.wrap((Map) newMap, type);
253 | }
254 |
255 | @Override
256 | public IPersistentMap meta() {
257 | return (IPersistentMap) ClojureStuff.Meta.invoke(map);
258 | }
259 |
260 | @Override
261 | public IObj withMeta(IPersistentMap meta) {
262 | Object newMap = ClojureStuff.VaryMeta.invoke(map, meta);
263 | return (DynamicObjectInstance) DynamicObject.wrap((Map) newMap, type);
264 | }
265 |
266 | @Override
267 | public int hasheq() {
268 | return ((IHashEq) map).hasheq();
269 | }
270 |
271 | @Override
272 | public Object invoke(Object arg1) {
273 | return valAt(arg1);
274 | }
275 |
276 | @Override
277 | public Object invoke(Object arg1, Object notFound) {
278 | return valAt(arg1, notFound);
279 | }
280 |
281 | @Override
282 | public Iterator iterator() {
283 | return ((IPersistentMap) map).iterator();
284 | }
285 |
286 | public Iterator valIterator() throws ReflectiveOperationException {
287 | Class extends Map> aClass = map.getClass();
288 | return (Iterator) aClass.getMethod("valIterator").invoke(map);
289 | }
290 |
291 | public Iterator keyIterator() throws ReflectiveOperationException {
292 | Class extends Map> aClass = map.getClass();
293 | return (Iterator) aClass.getMethod("keyIterator").invoke(map);
294 | }
295 | }
296 |
--------------------------------------------------------------------------------
/src/main/java/com/github/rschmitt/dynamicobject/internal/DynamicObjectPrintHook.java:
--------------------------------------------------------------------------------
1 | package com.github.rschmitt.dynamicobject.internal;
2 |
3 | // Marker interface for print-method dispatch
4 | public interface DynamicObjectPrintHook {
5 | }
6 |
--------------------------------------------------------------------------------
/src/main/java/com/github/rschmitt/dynamicobject/internal/EdnSerialization.java:
--------------------------------------------------------------------------------
1 | package com.github.rschmitt.dynamicobject.internal;
2 |
3 | import clojure.java.api.Clojure;
4 | import clojure.lang.AFn;
5 | import clojure.lang.IPersistentMap;
6 | import com.github.rschmitt.dynamicobject.DynamicObject;
7 | import com.github.rschmitt.dynamicobject.EdnTranslator;
8 | import com.github.rschmitt.dynamicobject.Unknown;
9 |
10 | import java.io.IOException;
11 | import java.io.PushbackReader;
12 | import java.io.StringReader;
13 | import java.io.StringWriter;
14 | import java.io.Writer;
15 | import java.util.Iterator;
16 | import java.util.Map;
17 | import java.util.NoSuchElementException;
18 | import java.util.Spliterator;
19 | import java.util.Spliterators;
20 | import java.util.concurrent.ConcurrentHashMap;
21 | import java.util.concurrent.atomic.AtomicReference;
22 | import java.util.function.BiFunction;
23 | import java.util.stream.Stream;
24 | import java.util.stream.StreamSupport;
25 |
26 | import static com.github.rschmitt.dynamicobject.internal.ClojureStuff.*;
27 | import static java.lang.String.format;
28 |
29 | @SuppressWarnings({"rawtypes", "unchecked"})
30 | public class EdnSerialization {
31 | static {
32 | String clojureCode =
33 | "(defmethod print-method com.github.rschmitt.dynamicobject.internal.DynamicObjectPrintHook " +
34 | "[o, ^java.io.Writer w]" +
35 | "(com.github.rschmitt.dynamicobject.internal.EdnSerialization/invokePrintMethod o w))";
36 | Eval.invoke(ReadString.invoke(clojureCode));
37 | PreferMethod.invoke(PrintMethod, DynamicObjectPrintHook.class, IPersistentMap.class);
38 | PreferMethod.invoke(PrintMethod, DynamicObjectPrintHook.class, Map.class);
39 |
40 | clojureCode =
41 | "(defmethod clojure.pprint/simple-dispatch com.github.rschmitt.dynamicobject.internal.DynamicObjectPrintHook " +
42 | "[o] " +
43 | "(com.github.rschmitt.dynamicobject.internal.EdnSerialization/invokePrettyPrint o))";
44 | Eval.invoke(ReadString.invoke(clojureCode));
45 | PreferMethod.invoke(SimpleDispatch, DynamicObjectPrintHook.class, IPersistentMap.class);
46 | PreferMethod.invoke(SimpleDispatch, DynamicObjectPrintHook.class, Map.class);
47 | }
48 |
49 | public static class DynamicObjectPrintMethod extends AFn {
50 | @Override
51 | public Object invoke(Object arg1, Object arg2) {
52 | DynamicObject dynamicObject = (DynamicObject) arg1;
53 | Writer writer = (Writer) arg2;
54 | String tag = recordTagCache.getOrDefault(dynamicObject.getType(), null);
55 | try {
56 | if (tag != null) {
57 | writer.write("#");
58 | writer.write(tag);
59 | }
60 | ClojureStuff.PrOn.invoke(dynamicObject.getMap(), writer);
61 | } catch (IOException ex) {
62 | throw new RuntimeException(ex);
63 | }
64 | return null;
65 | }
66 | }
67 |
68 | public static class DynamicObjectPrettyPrint extends AFn {
69 | @Override
70 | public Object invoke(Object arg1) {
71 | Object arg2 = Deref.invoke(Clojure.var("clojure.core/*out*"));
72 | DynamicObject dynamicObject = (DynamicObject) arg1;
73 | Writer writer = (Writer) arg2;
74 | String tag = recordTagCache.getOrDefault(dynamicObject.getType(), null);
75 | try {
76 | if (tag != null) {
77 | writer.write("#");
78 | writer.write(tag);
79 | }
80 | SimpleDispatch.invoke(dynamicObject.getMap());
81 | } catch (IOException ex) {
82 | throw new RuntimeException(ex);
83 | }
84 | return null;
85 | }
86 | }
87 |
88 | private static final DynamicObjectPrettyPrint dynamicObjectPrettyPrint = new DynamicObjectPrettyPrint();
89 | private static final DynamicObjectPrintMethod dynamicObjectPrintMethod = new DynamicObjectPrintMethod();
90 | private static final AtomicReference translators = new AtomicReference<>(ClojureStuff.EmptyMap);
91 | private static final ConcurrentHashMap, EdnTranslatorAdapter>> translatorCache = new ConcurrentHashMap<>();
92 | private static final AtomicReference defaultReader = new AtomicReference<>(getUnknownReader());
93 | private static final ConcurrentHashMap, String> recordTagCache = new ConcurrentHashMap<>();
94 | private static final Object EOF = Clojure.read(":eof");
95 |
96 | public static String serialize(Object obj) {
97 | StringWriter stringWriter = new StringWriter();
98 | serialize(obj, stringWriter);
99 | return stringWriter.toString();
100 | }
101 |
102 | public static void serialize(Object object, Writer writer) {
103 | ClojureStuff.PrOn.invoke(object, writer);
104 | try {
105 | writer.flush();
106 | } catch (IOException ex) {
107 | throw new RuntimeException(ex);
108 | }
109 | }
110 |
111 | public static T deserialize(String edn, Class type) {
112 | return deserialize(new PushbackReader(new StringReader(edn)), type);
113 | }
114 |
115 | @SuppressWarnings({"unchecked", "deprecation"})
116 | static > T deserialize(PushbackReader streamReader, Class type) {
117 | Object opts = getReadOptions();
118 | opts = ClojureStuff.Assoc.invoke(opts, EOF, EOF);
119 | Object obj = ClojureStuff.Read.invoke(opts, streamReader);
120 | if (EOF.equals(obj))
121 | throw new NoSuchElementException();
122 | if (DynamicObject.class.isAssignableFrom(type) && !(obj instanceof DynamicObject)) {
123 | obj = Instances.wrap((Map) obj, (Class) type).afterDeserialization();
124 | }
125 | return type.cast(obj);
126 | }
127 |
128 | public static Stream deserializeStream(PushbackReader streamReader, Class type) {
129 | Iterator iterator = Serialization.deserializeStreamToIterator(() -> deserialize(streamReader, type), type);
130 | Spliterator spliterator = Spliterators.spliteratorUnknownSize(iterator, Spliterator.IMMUTABLE);
131 | return StreamSupport.stream(spliterator, false);
132 | }
133 |
134 | private static AFn getUnknownReader() {
135 | String clojureCode = format("(defmethod print-method %s [o, ^java.io.Writer w]" +
136 | "(com.github.rschmitt.dynamicobject.Unknown/serialize o w))", Unknown.class.getTypeName());
137 | ClojureStuff.Eval.invoke(ClojureStuff.ReadString.invoke(clojureCode));
138 | return wrapReaderFunction(Unknown::new);
139 | }
140 |
141 | public static void setDefaultReader(BiFunction reader) {
142 | if (reader == null) {
143 | defaultReader.set(null);
144 | return;
145 | }
146 | defaultReader.set(wrapReaderFunction(reader));
147 | }
148 |
149 | private static AFn wrapReaderFunction(BiFunction reader) {
150 | return new AFn() {
151 | @Override
152 | public Object invoke(Object arg1, Object arg2) {
153 | return reader.apply(arg1.toString(), arg2);
154 | }
155 | };
156 | }
157 |
158 | private static Object getReadOptions() {
159 | Object map = ClojureStuff.Assoc.invoke(ClojureStuff.EmptyMap, ClojureStuff.Readers, translators.get());
160 | AFn defaultReader = EdnSerialization.defaultReader.get();
161 | if (defaultReader != null) {
162 | map = ClojureStuff.Assoc.invoke(map, ClojureStuff.Default, defaultReader);
163 | }
164 | return map;
165 | }
166 |
167 | public static synchronized void registerType(Class type, EdnTranslator translator) {
168 | EdnTranslatorAdapter adapter = new EdnTranslatorAdapter<>(translator);
169 | EdnTranslatorAdapter> currentAdapter = translatorCache.put(type, adapter);
170 | if (currentAdapter != null) {
171 | // already registered
172 | return;
173 | }
174 | translators.getAndUpdate(translators -> ClojureStuff.Assoc.invoke(translators, ClojureStuff.cachedRead(
175 | translator.getTag()), adapter));
176 | String clojureCode = format(
177 | "(defmethod print-method %s " +
178 | "[o, ^java.io.Writer w] " +
179 | "(com.github.rschmitt.dynamicobject.internal.EdnSerialization/invokeWriter o w \"%s\"))",
180 | type.getTypeName(), translator.getTag());
181 | ClojureStuff.Eval.invoke(ClojureStuff.ReadString.invoke(clojureCode));
182 | }
183 |
184 | public static synchronized void deregisterType(Class type) {
185 | EdnTranslatorAdapter adapter = (EdnTranslatorAdapter) translatorCache.get(type);
186 | translators.getAndUpdate(translators -> ClojureStuff.Dissoc.invoke(translators, ClojureStuff.cachedRead(
187 | adapter.getTag())));
188 | ClojureStuff.RemoveMethod.invoke(PrintMethod, adapter);
189 | translatorCache.remove(type);
190 | }
191 |
192 | public static synchronized > void registerTag(Class type, String tag) {
193 | String currentTagForType = recordTagCache.put(type, tag);
194 | if (currentTagForType != null) {
195 | // already registered
196 | return;
197 | }
198 |
199 | translators.getAndUpdate(translators -> ClojureStuff.Assoc.invoke(translators, ClojureStuff.cachedRead(
200 | tag), new RecordReader<>(type)));
201 | }
202 |
203 | public static synchronized > void deregisterTag(Class type) {
204 | String tag = recordTagCache.get(type);
205 | translators.getAndUpdate(translators -> ClojureStuff.Dissoc.invoke(translators, ClojureStuff.cachedRead(tag)));
206 | recordTagCache.remove(type);
207 | }
208 |
209 | @SuppressWarnings("unused")
210 | public static Object invokeWriter(Object obj, Writer writer, String tag) {
211 | EdnTranslatorAdapter translator = (EdnTranslatorAdapter>) ClojureStuff.Get.invoke(translators.get(), ClojureStuff
212 | .cachedRead(tag));
213 | return translator.invoke(obj, writer);
214 | }
215 |
216 | public static Object invokePrintMethod(Object arg1, Object arg2) {
217 | return dynamicObjectPrintMethod.invoke(arg1, arg2);
218 | }
219 |
220 | public static Object invokePrettyPrint(Object o) {
221 | return dynamicObjectPrettyPrint.invoke(o);
222 | }
223 | }
224 |
--------------------------------------------------------------------------------
/src/main/java/com/github/rschmitt/dynamicobject/internal/EdnTranslatorAdapter.java:
--------------------------------------------------------------------------------
1 | package com.github.rschmitt.dynamicobject.internal;
2 |
3 | import java.io.IOException;
4 | import java.io.Writer;
5 |
6 | import com.github.rschmitt.dynamicobject.EdnTranslator;
7 | import clojure.lang.AFn;
8 |
9 | final class EdnTranslatorAdapter extends AFn {
10 | private final EdnTranslator ednTranslator;
11 |
12 | EdnTranslatorAdapter(EdnTranslator ednTranslator) {
13 | this.ednTranslator = ednTranslator;
14 | }
15 |
16 | @Override
17 | public Object invoke(Object arg) {
18 | return ednTranslator.read(arg);
19 | }
20 |
21 | @Override
22 | @SuppressWarnings("unchecked")
23 | public Object invoke(Object arg1, Object arg2) {
24 | Writer writer = (Writer) arg2;
25 | try {
26 | writer.write(String.format("#%s", ednTranslator.getTag()));
27 | ednTranslator.write((T) arg1, writer);
28 | } catch (IOException ex) {
29 | throw new RuntimeException(ex);
30 | }
31 | return null;
32 | }
33 |
34 | public final String getTag() {
35 | return ednTranslator.getTag();
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/src/main/java/com/github/rschmitt/dynamicobject/internal/FressianSerialization.java:
--------------------------------------------------------------------------------
1 | package com.github.rschmitt.dynamicobject.internal;
2 |
3 | import com.github.rschmitt.dynamicobject.DynamicObject;
4 | import com.github.rschmitt.dynamicobject.FressianReadHandler;
5 | import com.github.rschmitt.dynamicobject.FressianWriteHandler;
6 | import org.fressian.FressianReader;
7 | import org.fressian.FressianWriter;
8 | import org.fressian.handlers.ReadHandler;
9 | import org.fressian.handlers.WriteHandler;
10 | import org.fressian.impl.Handlers;
11 | import org.fressian.impl.InheritanceLookup;
12 | import org.fressian.impl.MapLookup;
13 |
14 | import java.io.ByteArrayInputStream;
15 | import java.io.ByteArrayOutputStream;
16 | import java.io.IOException;
17 | import java.io.InputStream;
18 | import java.io.OutputStream;
19 | import java.util.Iterator;
20 | import java.util.Map;
21 | import java.util.Spliterator;
22 | import java.util.Spliterators;
23 | import java.util.concurrent.ConcurrentHashMap;
24 | import java.util.stream.Stream;
25 | import java.util.stream.StreamSupport;
26 |
27 | @SuppressWarnings({"rawtypes", "unchecked"})
28 | public class FressianSerialization {
29 | private static final ConcurrentHashMap> fressianWriteHandlers = new ConcurrentHashMap<>();
30 | private static final ConcurrentHashMap fressianReadHandlers = new ConcurrentHashMap<>();
31 | private static final ConcurrentHashMap, String> binaryTagCache = new ConcurrentHashMap<>();
32 | private static final ConcurrentHashMap, String> binaryTypeCache = new ConcurrentHashMap<>();
33 |
34 | static {
35 | fressianWriteHandlers.putAll(ClojureStuff.clojureWriteHandlers);
36 | fressianReadHandlers.putAll(ClojureStuff.clojureReadHandlers);
37 | }
38 |
39 | public static Stream deserializeFressianStream(InputStream is, Class type) {
40 | FressianReader fressianReader = new FressianReader(is, new MapLookup<>(fressianReadHandlers));
41 | Iterator iterator = Serialization.deserializeStreamToIterator(() -> (T) fressianReader.readObject(), type);
42 | Spliterator spliterator = Spliterators.spliteratorUnknownSize(iterator, Spliterator.IMMUTABLE);
43 | return StreamSupport.stream(spliterator, false);
44 | }
45 |
46 | public static FressianReader createFressianReader(InputStream is, boolean validateChecksum) {
47 | return new FressianReader(is, new MapLookup<>(fressianReadHandlers), validateChecksum);
48 | }
49 |
50 | public static FressianWriter createFressianWriter(OutputStream os) {
51 | return new FressianWriter(os, new InheritanceLookup<>(new MapLookup<>(fressianWriteHandlers)));
52 | }
53 |
54 | public static byte[] toFressianByteArray(Object o) {
55 | ByteArrayOutputStream baos = new ByteArrayOutputStream();
56 | try (FressianWriter fressianWriter = DynamicObject.createFressianWriter(baos)) {
57 | fressianWriter.writeObject(o);
58 | } catch (IOException ex) {
59 | throw new RuntimeException(ex);
60 | }
61 | return baos.toByteArray();
62 | }
63 |
64 | public static T fromFressianByteArray(byte[] bytes) {
65 | ByteArrayInputStream bais = new ByteArrayInputStream(bytes);
66 | FressianReader fressianReader = DynamicObject.createFressianReader(bais, false);
67 | try {
68 | return (T) fressianReader.readObject();
69 | } catch (IOException ex) {
70 | throw new RuntimeException(ex);
71 | }
72 | }
73 |
74 | public static synchronized void registerType(Class type, String tag, ReadHandler readHandler, WriteHandler writeHandler) {
75 | String currentTagForType = binaryTypeCache.put(type, tag);
76 | if (currentTagForType != null) {
77 | // already registered
78 | return;
79 | }
80 | Handlers.installHandler(fressianWriteHandlers, type, tag, writeHandler);
81 | fressianReadHandlers.putIfAbsent(tag, readHandler);
82 | }
83 |
84 | static synchronized void deregisterType(Class type) {
85 | fressianWriteHandlers.remove(type);
86 | String tag = binaryTypeCache.remove(type);
87 | if (tag != null) {
88 | fressianReadHandlers.remove(tag);
89 | }
90 | }
91 |
92 | static synchronized > void registerTag(Class type, String tag) {
93 | String currentTagForType = binaryTagCache.put(type, tag);
94 | if (currentTagForType != null) {
95 | // already registered
96 | return;
97 | }
98 | Handlers.installHandler(fressianWriteHandlers, type, tag, new FressianWriteHandler(type, tag, Reflection.cachedKeys(type)));
99 | fressianReadHandlers.putIfAbsent(tag, new FressianReadHandler(type));
100 | }
101 |
102 | static synchronized > void deregisterTag(Class type) {
103 | String tag = binaryTagCache.remove(type);
104 | fressianWriteHandlers.remove(type);
105 | if (tag != null) {
106 | fressianReadHandlers.remove(tag);
107 | }
108 | }
109 | }
110 |
--------------------------------------------------------------------------------
/src/main/java/com/github/rschmitt/dynamicobject/internal/Instances.java:
--------------------------------------------------------------------------------
1 | package com.github.rschmitt.dynamicobject.internal;
2 |
3 | import com.github.rschmitt.dynamicobject.DynamicObject;
4 | import com.github.rschmitt.dynamicobject.internal.indyproxy.DynamicProxy;
5 |
6 | import java.util.Map;
7 | import java.util.concurrent.ConcurrentHashMap;
8 | import java.util.concurrent.ConcurrentMap;
9 |
10 | import static com.github.rschmitt.dynamicobject.internal.ClojureStuff.EmptyMap;
11 |
12 | @SuppressWarnings("rawtypes")
13 | public class Instances {
14 | private static final ConcurrentMap proxyCache = new ConcurrentHashMap<>();
15 |
16 | public static > D newInstance(Class type) {
17 | return wrap(EmptyMap, type);
18 | }
19 |
20 | public static > D wrap(Map map, Class type) {
21 | if (map == null)
22 | throw new NullPointerException("A null reference cannot be used as a DynamicObject");
23 | if (map instanceof DynamicObject)
24 | return type.cast(map);
25 |
26 | return createIndyProxy(map, type);
27 | }
28 |
29 | private static > D createIndyProxy(Map map, Class type) {
30 | ensureInitialized(type);
31 | try {
32 | DynamicProxy dynamicProxy;
33 | // Use ConcurrentHashMap#computeIfAbsent only when key is not present to avoid locking: JDK-8161372
34 | if (proxyCache.containsKey(type)) {
35 | dynamicProxy = proxyCache.get(type);
36 | } else {
37 | dynamicProxy = proxyCache.computeIfAbsent(type, Instances::createProxy);
38 | }
39 | Object proxy = dynamicProxy
40 | .constructor()
41 | .invoke(map, type);
42 | return type.cast(proxy);
43 | } catch (Throwable t) {
44 | throw new RuntimeException(t);
45 | }
46 | }
47 |
48 | // This is to avoid hitting JDK-8062841 in the case where 'type' has a static field of type D
49 | // that has not yet been initialized.
50 | private static > void ensureInitialized(Class c) {
51 | if (!proxyCache.containsKey(c))
52 | load(c);
53 | }
54 |
55 | private static void load(Class> c) {
56 | try {
57 | Class.forName(c.getName());
58 | } catch (ClassNotFoundException e) {
59 | throw new RuntimeException(e);
60 | }
61 | }
62 |
63 | private static DynamicProxy createProxy(Class dynamicObjectType) {
64 | String[] slices = dynamicObjectType.getName().split("\\.");
65 | String name = slices[slices.length - 1] + "Impl";
66 | try {
67 | DynamicProxy.Builder builder = DynamicProxy.builder()
68 | .withInterfaces(dynamicObjectType, CustomValidationHook.class)
69 | .withSuperclass(DynamicObjectInstance.class)
70 | .withInvocationHandler(new InvokeDynamicInvocationHandler(dynamicObjectType))
71 | .withConstructor(Map.class, Class.class)
72 | .withPackageName(dynamicObjectType.getPackage().getName())
73 | .withClassName(name);
74 | try {
75 | Class> iMapIterable = Class.forName("clojure.lang.IMapIterable");
76 | builder = builder.withInterfaces(iMapIterable);
77 | } catch (ClassNotFoundException ignore) {}
78 | return builder.build();
79 | } catch (Exception e) {
80 | throw new RuntimeException(e);
81 | }
82 | }
83 | }
84 |
--------------------------------------------------------------------------------
/src/main/java/com/github/rschmitt/dynamicobject/internal/InvokeDynamicInvocationHandler.java:
--------------------------------------------------------------------------------
1 | package com.github.rschmitt.dynamicobject.internal;
2 |
3 | import com.github.rschmitt.dynamicobject.DynamicObject;
4 | import com.github.rschmitt.dynamicobject.internal.indyproxy.DynamicInvocationHandler;
5 |
6 | import java.lang.invoke.CallSite;
7 | import java.lang.invoke.ConstantCallSite;
8 | import java.lang.invoke.MethodHandle;
9 | import java.lang.invoke.MethodHandles;
10 | import java.lang.invoke.MethodType;
11 | import java.lang.reflect.Method;
12 | import java.lang.reflect.Type;
13 |
14 | import static java.lang.invoke.MethodType.methodType;
15 |
16 | @SuppressWarnings("rawtypes")
17 | public class InvokeDynamicInvocationHandler implements DynamicInvocationHandler {
18 | private final Class dynamicObjectType;
19 |
20 | public InvokeDynamicInvocationHandler(Class dynamicObjectType) {
21 | this.dynamicObjectType = dynamicObjectType;
22 | }
23 |
24 | @Override
25 | @SuppressWarnings("unchecked")
26 | public CallSite handleInvocation(
27 | MethodHandles.Lookup lookup,
28 | String methodName,
29 | MethodType methodType,
30 | MethodHandle superMethod
31 | ) throws Throwable {
32 | Class proxyType = methodType.parameterArray()[0];
33 | MethodHandle mh;
34 | if (superMethod != null && !"validate".equals(methodName)) {
35 | mh = superMethod.asType(methodType);
36 | return new ConstantCallSite(mh);
37 | }
38 | if ("validate".equals(methodName)) {
39 | mh = Validation.buildValidatorFor(dynamicObjectType).asType(methodType);
40 | } else if ("$$customValidate".equals(methodName)) {
41 | try {
42 | mh = lookup.findSpecial(dynamicObjectType, "validate", methodType(dynamicObjectType), proxyType);
43 | } catch (NoSuchMethodException ex) {
44 | mh = lookup.findSpecial(DynamicObjectInstance.class, "$$noop", methodType(Object.class, new Class[]{}), proxyType);
45 | }
46 | mh = mh.asType(methodType);
47 | } else if ("afterDeserialization".equals(methodName)) {
48 | mh = lookup.findSpecial(DynamicObjectInstance.class, "$$noop", methodType(Object.class, new Class[]{}), proxyType).asType(methodType);
49 | } else {
50 | Method method = dynamicObjectType.getMethod(methodName, methodType.dropParameterTypes(0, 1).parameterArray());
51 |
52 | if (isBuilderMethod(method)) {
53 | Object key = Reflection.getKeyForBuilder(method);
54 | if (Reflection.isMetadataBuilder(method)) {
55 | mh = lookup.findSpecial(DynamicObjectInstance.class, "assocMeta", methodType(DynamicObject.class, Object.class, Object.class), proxyType);
56 | mh = MethodHandles.insertArguments(mh, 1, key);
57 | mh = mh.asType(methodType);
58 | } else {
59 | mh = lookup.findSpecial(DynamicObjectInstance.class, "convertAndAssoc", methodType(DynamicObject.class, Object.class, Object.class), proxyType);
60 | mh = MethodHandles.insertArguments(mh, 1, key);
61 | mh = mh.asType(methodType);
62 | }
63 | } else {
64 | Object key = Reflection.getKeyForGetter(method);
65 | if (Reflection.isMetadataGetter(method)) {
66 | mh = lookup.findSpecial(DynamicObjectInstance.class, "getMetadataFor", methodType(Object.class, Object.class), proxyType);
67 | mh = MethodHandles.insertArguments(mh, 1, key);
68 | mh = mh.asType(methodType);
69 | } else {
70 | boolean isRequired = Reflection.isRequired(method);
71 | Type genericReturnType = method.getGenericReturnType();
72 | mh = lookup.findSpecial(DynamicObjectInstance.class, "invokeGetter", methodType(Object.class, Object.class, boolean.class, Type.class), proxyType);
73 | mh = MethodHandles.insertArguments(mh, 1, key, isRequired, genericReturnType);
74 | mh = mh.asType(methodType);
75 | }
76 | }
77 | }
78 | return new ConstantCallSite(mh);
79 | }
80 |
81 | private boolean isBuilderMethod(Method method) {
82 | return method.getReturnType().equals(dynamicObjectType) && method.getParameterCount() == 1;
83 | }
84 | }
85 |
--------------------------------------------------------------------------------
/src/main/java/com/github/rschmitt/dynamicobject/internal/Numerics.java:
--------------------------------------------------------------------------------
1 | package com.github.rschmitt.dynamicobject.internal;
2 |
3 | import java.math.BigInteger;
4 | import java.util.Collections;
5 | import java.util.HashMap;
6 | import java.util.HashSet;
7 | import java.util.Map;
8 | import java.util.Set;
9 |
10 | import static com.github.rschmitt.dynamicobject.internal.ClojureStuff.Bigint;
11 | import static com.github.rschmitt.dynamicobject.internal.ClojureStuff.Biginteger;
12 |
13 | /*
14 | * This class deals with the numeric types that need to be converted to and from long/double/clojure.lang.BigInt.
15 | */
16 | public class Numerics {
17 | private static final Set> numericTypes;
18 | private static final Map, Class>> numericConversions;
19 | private static final Class> BigInt = Bigint.invoke(0).getClass();
20 |
21 | static {
22 | Set> types = new HashSet<>();
23 | types.add(int.class);
24 | types.add(Integer.class);
25 | types.add(float.class);
26 | types.add(Float.class);
27 | types.add(short.class);
28 | types.add(Short.class);
29 | types.add(byte.class);
30 | types.add(Byte.class);
31 | types.add(BigInteger.class);
32 | numericTypes = Collections.unmodifiableSet(types);
33 |
34 | Map, Class>> conversions = new HashMap<>();
35 | conversions.put(Byte.class, Long.class);
36 | conversions.put(Short.class, Long.class);
37 | conversions.put(Integer.class, Long.class);
38 | conversions.put(Float.class, Double.class);
39 | conversions.put(BigInteger.class, BigInt);
40 | numericConversions = conversions;
41 | }
42 |
43 | static boolean isNumeric(Class> type) {
44 | return numericTypes.contains(type);
45 | }
46 |
47 | static Object maybeDownconvert(Class> type, Object val) {
48 | if (val == null) return null;
49 | if (type.equals(int.class) || type.equals(Integer.class)) return ((Long) val).intValue();
50 | if (type.equals(float.class) || type.equals(Float.class)) return ((Double) val).floatValue();
51 | if (type.equals(short.class) || type.equals(Short.class)) return ((Long) val).shortValue();
52 | if (type.equals(byte.class) || type.equals(Byte.class)) return ((Long) val).byteValue();
53 | if (type.equals(BigInt)) return Biginteger.invoke(val);
54 | return val;
55 | }
56 |
57 | static Object maybeUpconvert(Object val) {
58 | if (val instanceof Float) return Double.parseDouble(String.valueOf(val));
59 | else if (val instanceof Short) return (long) ((short) val);
60 | else if (val instanceof Byte) return (long) ((byte) val);
61 | else if (val instanceof Integer) return (long) ((int) val);
62 | else if (val instanceof BigInteger) return Bigint.invoke(val);
63 | return val;
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/src/main/java/com/github/rschmitt/dynamicobject/internal/Primitives.java:
--------------------------------------------------------------------------------
1 | package com.github.rschmitt.dynamicobject.internal;
2 |
3 | import java.util.HashMap;
4 | import java.util.Map;
5 |
6 | /*
7 | * This class deals with the primitive types that need to be boxed and unboxed.
8 | */
9 | class Primitives {
10 | private static final Map, Class>> unboxedToBoxed;
11 |
12 | static {
13 | Map, Class>> mapping = new HashMap<>();
14 | mapping.put(boolean.class, Boolean.class);
15 | mapping.put(char.class, Character.class);
16 | mapping.put(byte.class, Byte.class);
17 | mapping.put(short.class, Short.class);
18 | mapping.put(int.class, Integer.class);
19 | mapping.put(long.class, Long.class);
20 | mapping.put(float.class, Float.class);
21 | mapping.put(double.class, Double.class);
22 | unboxedToBoxed = mapping;
23 | }
24 |
25 | static Class> box(Class> type) {
26 | return unboxedToBoxed.getOrDefault(type, type);
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/src/main/java/com/github/rschmitt/dynamicobject/internal/RecordReader.java:
--------------------------------------------------------------------------------
1 | package com.github.rschmitt.dynamicobject.internal;
2 |
3 | import clojure.lang.AFn;
4 | import com.github.rschmitt.dynamicobject.DynamicObject;
5 |
6 | import java.util.Map;
7 |
8 | public final class RecordReader> extends AFn {
9 | private final Class type;
10 |
11 | RecordReader(Class type) {
12 | this.type = type;
13 | }
14 |
15 | /**
16 | * For use by clojure.edn/read only. Do not call directly.
17 | */
18 | @Override
19 | @SuppressWarnings("deprecation")
20 | public Object invoke(Object map) {
21 | return DynamicObject.wrap((Map) map, type).afterDeserialization();
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/src/main/java/com/github/rschmitt/dynamicobject/internal/Reflection.java:
--------------------------------------------------------------------------------
1 | package com.github.rschmitt.dynamicobject.internal;
2 |
3 | import com.github.rschmitt.dynamicobject.Cached;
4 | import com.github.rschmitt.dynamicobject.DynamicObject;
5 | import com.github.rschmitt.dynamicobject.Key;
6 | import com.github.rschmitt.dynamicobject.Meta;
7 | import com.github.rschmitt.dynamicobject.Required;
8 |
9 | import java.lang.annotation.Annotation;
10 | import java.lang.reflect.Method;
11 | import java.lang.reflect.Modifier;
12 | import java.lang.reflect.ParameterizedType;
13 | import java.lang.reflect.Type;
14 | import java.util.Arrays;
15 | import java.util.Collection;
16 | import java.util.LinkedHashSet;
17 | import java.util.List;
18 | import java.util.Set;
19 | import java.util.stream.Stream;
20 |
21 | import static com.github.rschmitt.dynamicobject.internal.ClojureStuff.cachedRead;
22 | import static java.util.stream.Collectors.toSet;
23 |
24 | class Reflection {
25 | static > Collection requiredFields(Class type) {
26 | Collection fields = fieldGetters(type);
27 | return fields.stream().filter(Reflection::isRequired).collect(toSet());
28 | }
29 |
30 | static > Set cachedKeys(Class type) {
31 | return Arrays.stream(type.getMethods())
32 | .flatMap(Reflection::getCachedKeysForMethod)
33 | .collect(toSet());
34 | }
35 |
36 | private static Stream getCachedKeysForMethod(Method method) {
37 | if (isGetter(method)) {
38 | if (method.getAnnotation(Cached.class) != null) {
39 | return Stream.of(getKeyForGetter(method));
40 | } else {
41 | return Stream.empty();
42 | }
43 | } else if (isBuilder(method)) {
44 | if (method.getAnnotation(Cached.class) != null) {
45 | return Stream.of(getKeyForBuilder(method));
46 | } else {
47 | // If the getter has an annotation it'll contribute it directly
48 | return Stream.empty();
49 | }
50 | } else {
51 | return Stream.empty();
52 | }
53 | }
54 |
55 | static > Collection fieldGetters(Class type) {
56 | Collection ret = new LinkedHashSet<>();
57 | for (Method method : type.getDeclaredMethods())
58 | if (isGetter(method))
59 | ret.add(method);
60 | return ret;
61 | }
62 |
63 | private static boolean isBuilder(Method method) {
64 | return method.getParameterCount() == 1 && method.getDeclaringClass().isAssignableFrom(method.getReturnType());
65 | }
66 |
67 | private static boolean isAnyGetter(Method method) {
68 | return method.getParameterCount() == 0 && !method.isDefault() && !method.isSynthetic()
69 | && (method.getModifiers() & Modifier.STATIC) == 0
70 | && method.getReturnType() != Void.TYPE;
71 | }
72 |
73 | private static boolean isGetter(Method method) {
74 | return isAnyGetter(method) && !isMetadataGetter(method);
75 | }
76 |
77 | static boolean isMetadataGetter(Method method) {
78 | return isAnyGetter(method) && hasAnnotation(method, Meta.class);
79 | }
80 |
81 | static boolean isRequired(Method getter) {
82 | return hasAnnotation(getter, Required.class);
83 | }
84 |
85 | private static boolean hasAnnotation(Method method, Class extends Annotation> ann) {
86 | List annotations = Arrays.asList(method.getAnnotations());
87 | for (Annotation annotation : annotations)
88 | if (annotation.annotationType().equals(ann))
89 | return true;
90 | return false;
91 | }
92 |
93 | static boolean isMetadataBuilder(Method method) {
94 | if (method.getParameterCount() != 1)
95 | return false;
96 | if (hasAnnotation(method, Meta.class))
97 | return true;
98 | if (hasAnnotation(method, Key.class))
99 | return false;
100 | Method correspondingGetter = getCorrespondingGetter(method);
101 | return hasAnnotation(correspondingGetter, Meta.class);
102 | }
103 |
104 | static Object getKeyForGetter(Method method) {
105 | Key annotation = getMethodAnnotation(method, Key.class);
106 | if (annotation == null)
107 | return stringToKey(":" + method.getName());
108 | else
109 | return stringToKey(annotation.value());
110 | }
111 |
112 | private static Object stringToKey(String keyName) {
113 | if (keyName.charAt(0) == ':')
114 | return cachedRead(keyName);
115 | else
116 | return keyName;
117 | }
118 |
119 | @SuppressWarnings("unchecked")
120 | private static T getMethodAnnotation(Method method, Class annotationType) {
121 | for (Annotation annotation : method.getAnnotations())
122 | if (annotation.annotationType().equals(annotationType))
123 | return (T) annotation;
124 | return null;
125 | }
126 |
127 | static Object getKeyForBuilder(Method method) {
128 | Key annotation = getMethodAnnotation(method, Key.class);
129 | if (annotation == null)
130 | return getKeyForGetter(getCorrespondingGetter(method));
131 | else
132 | return stringToKey(annotation.value());
133 | }
134 |
135 | private static Method getCorrespondingGetter(Method builderMethod) {
136 | try {
137 | Class> type = builderMethod.getDeclaringClass();
138 | Method correspondingGetter = type.getMethod(builderMethod.getName());
139 | return correspondingGetter;
140 | } catch (NoSuchMethodException ex) {
141 | throw new IllegalStateException("Builder method " + builderMethod + " must have a corresponding getter method or a @Key annotation.", ex);
142 | }
143 | }
144 |
145 | static Class> getRawType(Type type) {
146 | if (type instanceof Class)
147 | return (Class>) type;
148 | else if (type instanceof ParameterizedType) {
149 | ParameterizedType parameterizedType = (ParameterizedType) type;
150 | return (Class>) parameterizedType.getRawType();
151 | } else
152 | throw new UnsupportedOperationException();
153 | }
154 |
155 | static Type getTypeArgument(Type type, int idx) {
156 | if (type instanceof Class)
157 | return Object.class;
158 | else if (type instanceof ParameterizedType) {
159 | ParameterizedType parameterizedType = (ParameterizedType) type;
160 | return parameterizedType.getActualTypeArguments()[idx];
161 | } else
162 | throw new UnsupportedOperationException();
163 | }
164 | }
165 |
--------------------------------------------------------------------------------
/src/main/java/com/github/rschmitt/dynamicobject/internal/Serialization.java:
--------------------------------------------------------------------------------
1 | package com.github.rschmitt.dynamicobject.internal;
2 |
3 | import java.io.EOFException;
4 | import java.io.IOException;
5 | import java.util.Iterator;
6 | import java.util.NoSuchElementException;
7 |
8 | import com.github.rschmitt.dynamicobject.DynamicObject;
9 |
10 | public class Serialization {
11 | @SuppressWarnings("unchecked")
12 | public static synchronized void deregisterType(Class type) {
13 | EdnSerialization.deregisterType(type);
14 | FressianSerialization.deregisterType(type);
15 | }
16 |
17 | public static synchronized > void registerTag(Class type, String tag) {
18 | EdnSerialization.registerTag(type, tag);
19 | FressianSerialization.registerTag(type, tag);
20 | }
21 |
22 | public static synchronized > void deregisterTag(Class type) {
23 | EdnSerialization.deregisterTag(type);
24 | FressianSerialization.deregisterTag(type);
25 | }
26 |
27 | @FunctionalInterface
28 | interface IOSupplier {
29 | T get() throws IOException;
30 | }
31 |
32 | static Iterator deserializeStreamToIterator(IOSupplier streamReader, Class type) {
33 | return new Iterator() {
34 | private T stash = null;
35 | private boolean done = false;
36 |
37 | @Override
38 | public boolean hasNext() {
39 | populateStash();
40 | return !done || stash != null;
41 | }
42 |
43 | @Override
44 | public T next() {
45 | if (hasNext()) {
46 | T ret = stash;
47 | stash = null;
48 | return ret;
49 | } else
50 | throw new NoSuchElementException();
51 | }
52 |
53 | private void populateStash() {
54 | if (stash != null || done)
55 | return;
56 | try {
57 | stash = streamReader.get();
58 | } catch (NoSuchElementException | EOFException ignore) {
59 | done = true;
60 | } catch (IOException ex) {
61 | throw new RuntimeException(ex);
62 | }
63 | }
64 | };
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/src/main/java/com/github/rschmitt/dynamicobject/internal/indyproxy/ConcreteMethodTracker.java:
--------------------------------------------------------------------------------
1 | package com.github.rschmitt.dynamicobject.internal.indyproxy;
2 |
3 | import java.lang.reflect.Method;
4 | import java.lang.reflect.Modifier;
5 | import java.util.HashSet;
6 |
7 | class ConcreteMethodTracker {
8 | private HashSet contributors = new HashSet<>();
9 |
10 | public void add(Method m) {
11 | if ((m.getModifiers() & Modifier.ABSTRACT) != 0) return;
12 |
13 | // Remove any concrete implementations that come from superclasses of the class that owns this new method
14 | // (this new class shadows them).
15 | contributors.removeIf(m2 -> m2.getDeclaringClass().isAssignableFrom(m.getDeclaringClass()));
16 |
17 | // Conversely, if this new implementation is shadowed by any existing implementations, we'll drop it instead
18 | if (contributors.stream().anyMatch(m2 -> m.getDeclaringClass().isAssignableFrom(m2.getDeclaringClass()))) {
19 | return;
20 | }
21 |
22 | contributors.add(m);
23 | }
24 |
25 | public Method getOnlyContributor() {
26 | if (contributors.size() != 1) return null;
27 |
28 | return contributors.iterator().next();
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/src/main/java/com/github/rschmitt/dynamicobject/internal/indyproxy/DefaultInvocationHandler.java:
--------------------------------------------------------------------------------
1 | package com.github.rschmitt.dynamicobject.internal.indyproxy;
2 |
3 | import java.lang.invoke.*;
4 |
5 | class DefaultInvocationHandler implements DynamicInvocationHandler {
6 | private static final MethodHandles.Lookup LOOKUP = MethodHandles.lookup();
7 | private static final MethodHandle THROW_UNSUPPORTED;
8 |
9 | static {
10 | try {
11 | THROW_UNSUPPORTED = LOOKUP.findStatic(DefaultInvocationHandler.class,
12 | "throwUnsupported",
13 | MethodType.methodType(Object.class));
14 | } catch (Exception e) {
15 | throw new Error(e);
16 | }
17 | }
18 |
19 | private static Object throwUnsupported() {
20 | throw new UnsupportedOperationException();
21 | }
22 |
23 | @Override
24 | public CallSite handleInvocation(
25 | MethodHandles.Lookup proxyLookup,
26 | String methodName,
27 | MethodType methodType,
28 | MethodHandle superMethod
29 | ) {
30 | if (superMethod != null) {
31 | return new ConstantCallSite(superMethod.asType(methodType));
32 | }
33 |
34 | return new ConstantCallSite(
35 | MethodHandles.dropArguments(THROW_UNSUPPORTED, 0, methodType.parameterList()).asType(methodType)
36 | );
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/src/main/java/com/github/rschmitt/dynamicobject/internal/indyproxy/DynamicInvocationHandler.java:
--------------------------------------------------------------------------------
1 | package com.github.rschmitt.dynamicobject.internal.indyproxy;
2 |
3 | import java.lang.invoke.CallSite;
4 | import java.lang.invoke.MethodHandle;
5 | import java.lang.invoke.MethodHandles;
6 | import java.lang.invoke.MethodType;
7 |
8 | public interface DynamicInvocationHandler {
9 | /**
10 | * This callback is invoked the first time a proxy method is invoked. It should return a CallSite to be bound to the
11 | * proxy method in question. The CallSite's method arguments (which can also be inspected via methodType) will
12 | * consist of the arguments of the interface or superclass method in question, plus a prepended argument for the
13 | * proxy itself.
14 | *
15 | * Note that the type of the method bound to the callsite must exactly match methodType. Using
16 | * {@link java.lang.invoke.MethodHandle#asType} is recommended.
17 | *
18 | * If multiple calls race, it's possible this method may be invoked multiple times for the same method. In this case,
19 | * one of the returns will be selected arbitrarily and used for all calls.
20 | *
21 | * @param proxyLookup A Lookup instance with the access rights of the proxy class. This can be used to look up
22 | * proxy superclass methods.
23 | * @param methodName The name of the proxy method that is being invoked
24 | * @param methodType The type of the callee (same as the type of the proxy method, but with the proxy instance
25 | * itself prepended)
26 | * @param superMethod If an unambiguous supermethod was found, this has a handle to that supermethod. Otherwise,
27 | * this is null.
28 | * @return A call site to bind to the proxy method.
29 | */
30 | public CallSite handleInvocation(MethodHandles.Lookup proxyLookup, String methodName, MethodType methodType, MethodHandle superMethod)
31 | throws Throwable;
32 | }
33 |
--------------------------------------------------------------------------------
/src/main/java/com/github/rschmitt/dynamicobject/internal/indyproxy/MethodIdentifier.java:
--------------------------------------------------------------------------------
1 | package com.github.rschmitt.dynamicobject.internal.indyproxy;
2 |
3 | import org.objectweb.asm.Type;
4 |
5 | import java.lang.reflect.Method;
6 | import java.util.Arrays;
7 |
8 | class MethodIdentifier {
9 | private final String name;
10 | private final Class> returnType;
11 | private final Class>[] args;
12 |
13 | public static MethodIdentifier create(Method m) {
14 | return new MethodIdentifier(m.getName(), m.getReturnType(), m.getParameterTypes());
15 | }
16 |
17 | public MethodIdentifier(String name, Class> returnType, Class>[] args) {
18 | this.name = name;
19 | this.returnType = returnType;
20 | this.args = args.clone();
21 | }
22 |
23 | public String getName() {
24 | return name;
25 | }
26 |
27 | public String getDescriptor() {
28 | Type[] typeArgs = new Type[args.length];
29 | for (int i = 0; i < args.length; i++) {
30 | typeArgs[i] = Type.getType(args[i]);
31 | }
32 |
33 | return Type.getMethodType(
34 | Type.getType(returnType),
35 | typeArgs
36 | ).getDescriptor();
37 | }
38 |
39 | public Class>[] getArgs() {
40 | return args.clone();
41 | }
42 |
43 | public Class> getReturnType() {
44 | return returnType;
45 | }
46 |
47 | @Override
48 | public boolean equals(Object o) {
49 | if (this == o) return true;
50 | if (o == null || getClass() != o.getClass()) return false;
51 |
52 | MethodIdentifier that = (MethodIdentifier) o;
53 |
54 | if (!Arrays.equals(args, that.args)) return false;
55 | if (!name.equals(that.name)) return false;
56 | if (!returnType.equals(that.returnType)) return false;
57 |
58 | return true;
59 | }
60 |
61 | @Override
62 | public int hashCode() {
63 | int result = name.hashCode();
64 | result = 31 * result + returnType.hashCode();
65 | result = 31 * result + Arrays.hashCode(args);
66 | return result;
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/src/test/java/com/github/rschmitt/dynamicobject/AcceptanceTest.java:
--------------------------------------------------------------------------------
1 | package com.github.rschmitt.dynamicobject;
2 |
3 | import static java.util.UUID.randomUUID;
4 | import static org.junit.jupiter.api.Assertions.assertEquals;
5 |
6 | import java.util.Date;
7 | import java.util.LinkedHashSet;
8 | import java.util.Set;
9 | import java.util.UUID;
10 |
11 | import org.junit.jupiter.api.AfterEach;
12 | import org.junit.jupiter.api.BeforeEach;
13 | import org.junit.jupiter.api.Test;
14 |
15 | public class AcceptanceTest {
16 | @BeforeEach
17 | public void setup() {
18 | DynamicObject.registerType(Path.class, new PathTranslator());
19 | }
20 |
21 | @AfterEach
22 | public void teardown() {
23 | DynamicObject.deregisterType(Path.class);
24 | }
25 |
26 | @Test
27 | public void acceptanceTest() {
28 | Document document = DynamicObject.newInstance(Document.class);
29 | roundTrip(document);
30 |
31 | document = document.name("Mr. Show").uuid(randomUUID()).date(new Date());
32 | roundTrip(document);
33 |
34 | document = document.documentPointer(DynamicObject.deserialize("{:location \"/prod-bucket/home\"}", DocumentPointer.class));
35 | roundTrip(document);
36 |
37 | Set paths = new LinkedHashSet<>();
38 | paths.add(newPath());
39 | paths.add(newPath());
40 | paths.add(newPath());
41 | document = document.paths(paths);
42 | roundTrip(document);
43 | }
44 |
45 | private void roundTrip(Document document) {
46 | assertEquals(document, DynamicObject.deserialize(DynamicObject.serialize(document), Document.class));
47 | }
48 |
49 | private Path newPath() {
50 | return new Path(randomString(), randomString(), randomString());
51 | }
52 |
53 | private String randomString() {
54 | return randomUUID().toString().substring(0, 8);
55 | }
56 |
57 | public interface DocumentPointer extends DynamicObject {
58 | String location();
59 |
60 | DocumentPointer location(String location);
61 | }
62 |
63 | public interface Document extends DynamicObject {
64 | UUID uuid();
65 | String name();
66 | Date date();
67 | Set paths();
68 | @Key(":document-pointer") DocumentPointer documentPointer();
69 |
70 | Document uuid(UUID uuid);
71 | Document name(String name);
72 | Document date(Date date);
73 | Document paths(Set paths);
74 | Document documentPointer(DocumentPointer documentPointer);
75 | }
76 | }
77 |
78 | class Path {
79 | private final String a;
80 | private final String b;
81 | private final String c;
82 |
83 | Path(String a, String b, String c) {
84 | this.a = a;
85 | this.b = b;
86 | this.c = c;
87 | }
88 |
89 | public String getA() {
90 | return a;
91 | }
92 |
93 | public String getB() {
94 | return b;
95 | }
96 |
97 | public String getC() {
98 | return c;
99 | }
100 |
101 | @Override
102 | public boolean equals(Object o) {
103 | if (this == o) return true;
104 | if (o == null || getClass() != o.getClass()) return false;
105 |
106 | Path path = (Path) o;
107 |
108 | if (a != null ? !a.equals(path.a) : path.a != null) return false;
109 | if (b != null ? !b.equals(path.b) : path.b != null) return false;
110 | if (c != null ? !c.equals(path.c) : path.c != null) return false;
111 |
112 | return true;
113 | }
114 |
115 | @Override
116 | public int hashCode() {
117 | int result = a != null ? a.hashCode() : 0;
118 | result = 31 * result + (b != null ? b.hashCode() : 0);
119 | result = 31 * result + (c != null ? c.hashCode() : 0);
120 | return result;
121 | }
122 |
123 | @Override
124 | public String toString() {
125 | return "Path{" +
126 | "a='" + a + '\'' +
127 | ", b='" + b + '\'' +
128 | ", c='" + c + '\'' +
129 | '}';
130 | }
131 | }
132 |
133 | class PathTranslator implements EdnTranslator {
134 | @Override
135 | public Path read(Object obj) {
136 | String str = (String) obj;
137 | String[] split = str.split("\\/");
138 | return new Path(split[0], split[1], split[2]);
139 | }
140 |
141 | @Override
142 | public String write(Path obj) {
143 | return String.format("\"%s/%s/%s\"", obj.getA(), obj.getB(), obj.getC());
144 | }
145 |
146 | @Override
147 | public String getTag() {
148 | return "Path";
149 | }
150 | }
151 |
--------------------------------------------------------------------------------
/src/test/java/com/github/rschmitt/dynamicobject/BuilderTest.java:
--------------------------------------------------------------------------------
1 | package com.github.rschmitt.dynamicobject;
2 |
3 | import static com.github.rschmitt.dynamicobject.TestUtils.assertEquivalent;
4 | import static org.junit.jupiter.api.Assertions.assertEquals;
5 |
6 | import org.junit.jupiter.api.Test;
7 |
8 | import clojure.lang.PersistentHashMap;
9 |
10 | public class BuilderTest {
11 | @Test
12 | public void createEmptyInstance() {
13 | Buildable obj = DynamicObject.newInstance(Buildable.class);
14 | assertEquals(PersistentHashMap.EMPTY, obj.getMap());
15 | assertEquals("{}", DynamicObject.serialize(obj));
16 | }
17 |
18 | @Test
19 | public void invokeBuilderMethod() {
20 | Buildable obj = DynamicObject.newInstance(Buildable.class).str("string");
21 | assertEquals("{:str \"string\"}", DynamicObject.serialize(obj));
22 | assertEquals("string", obj.str());
23 | }
24 |
25 | @Test
26 | public void invokeBuilderWithPrimitive() {
27 | Buildable obj = DynamicObject.newInstance(Buildable.class).i(4).s((short) 127).l(Long.MAX_VALUE).d(3.14).f((float) 3.14);
28 | assertEquivalent("{:f 3.14, :d 3.14, :l 9223372036854775807, :s 127, :i 4}", DynamicObject.serialize(obj));
29 | assertEquals(4, obj.i());
30 | assertEquals(127, obj.s());
31 | assertEquals(Long.MAX_VALUE, obj.l());
32 | assertEquals(3.14, obj.f(), 0.00001);
33 | assertEquals(3.14, obj.d(), 0.00001);
34 | }
35 |
36 | @Test
37 | public void invokeBuilderWithNull() {
38 | Buildable obj = DynamicObject.newInstance(Buildable.class).str(null);
39 | assertEquals("{:str nil}", DynamicObject.serialize(obj));
40 | }
41 |
42 | public interface Buildable extends DynamicObject {
43 | String str();
44 | int i();
45 | long l();
46 | short s();
47 | float f();
48 | double d();
49 |
50 | Buildable str(String str);
51 | Buildable i(int i);
52 | Buildable l(long l);
53 | Buildable s(short s);
54 | Buildable f(float f);
55 | Buildable d(double d);
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/src/test/java/com/github/rschmitt/dynamicobject/CollectionsTest.java:
--------------------------------------------------------------------------------
1 | package com.github.rschmitt.dynamicobject;
2 |
3 | import static com.github.rschmitt.dynamicobject.DynamicObject.deserialize;
4 | import static com.github.rschmitt.dynamicobject.DynamicObject.newInstance;
5 | import static java.util.Arrays.asList;
6 | import static java.util.stream.Collectors.toList;
7 | import static java.util.stream.Collectors.toMap;
8 | import static java.util.stream.IntStream.range;
9 | import static org.junit.jupiter.api.Assertions.assertEquals;
10 | import static org.junit.jupiter.api.Assertions.assertTrue;
11 |
12 | import java.util.Base64;
13 | import java.util.HashMap;
14 | import java.util.List;
15 | import java.util.Map;
16 | import java.util.Random;
17 | import java.util.Set;
18 |
19 | import org.junit.jupiter.api.AfterEach;
20 | import org.junit.jupiter.api.BeforeEach;
21 | import org.junit.jupiter.api.Test;
22 |
23 | public class CollectionsTest {
24 | private static final Random Random = new Random();
25 | private static final Base64.Encoder Encoder = Base64.getEncoder();
26 |
27 | @BeforeEach
28 | public void setup() {
29 | DynamicObject.registerTag(ListSchema.class, "ls");
30 | DynamicObject.registerTag(MapSchema.class, "ms");
31 | DynamicObject.registerTag(SetSchema.class, "ss");
32 | }
33 |
34 | @AfterEach
35 | public void teardown() {
36 | DynamicObject.deregisterTag(ListSchema.class);
37 | DynamicObject.deregisterTag(MapSchema.class);
38 | DynamicObject.deregisterTag(SetSchema.class);
39 | }
40 |
41 | @Test
42 | public void listOfStrings() {
43 | ListSchema listSchema = deserialize("{:strings [\"one\" \"two\" \"three\"]}", ListSchema.class);
44 | List stringList = listSchema.strings();
45 | assertEquals("one", stringList.get(0));
46 | assertEquals("two", stringList.get(1));
47 | assertEquals("three", stringList.get(2));
48 | binaryRoundTrip(listSchema);
49 | }
50 |
51 | // This is just here to prove a point about Java<->Clojure interop.
52 | @Test
53 | public void listStream() {
54 | ListSchema listSchema = deserialize("{:strings [\"one\" \"two\" \"three\"]}", ListSchema.class);
55 | List stringList = listSchema.strings();
56 |
57 | List collect = stringList.stream().map(x -> x.length()).collect(toList());
58 |
59 | assertEquals(3, collect.get(0).intValue());
60 | assertEquals(3, collect.get(1).intValue());
61 | assertEquals(5, collect.get(2).intValue());
62 | binaryRoundTrip(listSchema);
63 | }
64 |
65 | @Test
66 | public void setOfStrings() {
67 | SetSchema setSchema = deserialize("{:strings #{\"one\" \"two\" \"three\"}}", SetSchema.class);
68 | Set stringSet = setSchema.strings();
69 | assertEquals(3, stringSet.size());
70 | assertTrue(stringSet.contains("one"));
71 | assertTrue(stringSet.contains("two"));
72 | assertTrue(stringSet.contains("three"));
73 | binaryRoundTrip(setSchema);
74 | }
75 |
76 | @Test
77 | public void embeddedMap() {
78 | String edn = "{:dictionary {\"key\" \"value\"}}";
79 | MapSchema mapSchema = deserialize(edn, MapSchema.class);
80 | assertEquals("value", mapSchema.dictionary().get("key"));
81 | assertEquals(1, mapSchema.dictionary().size());
82 | binaryRoundTrip(mapSchema);
83 | }
84 |
85 | @Test
86 | public void listOfIntegers() {
87 | ListSchema deserialized = deserialize("{:ints [nil 2 nil 4 nil]}", ListSchema.class);
88 | List builtList = asList(null, 2, null, 4, null);
89 |
90 | ListSchema built = newInstance(ListSchema.class).ints(builtList);
91 |
92 | assertEquals(builtList, deserialized.ints());
93 | assertEquals(builtList, built.ints());
94 | binaryRoundTrip(built);
95 | binaryRoundTrip(deserialized);
96 | }
97 |
98 | @Test
99 | public void mapOfIntegersToIntegers() {
100 | MapSchema deserialized = deserialize("{:ints {1 2, 3 4}}", MapSchema.class);
101 | Map builtMap = new HashMap<>();
102 | builtMap.put(1, 2);
103 | builtMap.put(3, 4);
104 | MapSchema built = newInstance(MapSchema.class).ints(builtMap);
105 |
106 | assertEquals(builtMap, deserialized.ints());
107 | assertEquals(builtMap, built.ints());
108 | binaryRoundTrip(deserialized);
109 | binaryRoundTrip(built);
110 | }
111 |
112 | @Test
113 | public void largeList() {
114 | List strings = range(0, 10_000).mapToObj(n -> string()).collect(toList());
115 |
116 | ListSchema listSchema = newInstance(ListSchema.class).strings(strings);
117 |
118 | assertEquals(strings, listSchema.strings());
119 | binaryRoundTrip(listSchema);
120 | }
121 |
122 | @Test
123 | public void largeMap() {
124 | Map map = range(0, 10_000).boxed().collect(toMap(n -> string(), n -> string()));
125 |
126 | MapSchema mapSchema = newInstance(MapSchema.class).dictionary(map);
127 |
128 | assertEquals(map.size(), mapSchema.dictionary().size());
129 | assertEquals(map, mapSchema.dictionary());
130 | binaryRoundTrip(mapSchema);
131 | }
132 |
133 | private void binaryRoundTrip(Object expected) {
134 | Object actual = DynamicObject.fromFressianByteArray(DynamicObject.toFressianByteArray(expected));
135 | assertEquals(expected, actual);
136 | }
137 |
138 | private static String string() {
139 | byte[] buf = new byte[64];
140 | Random.nextBytes(buf);
141 | return Encoder.encodeToString(buf);
142 | }
143 |
144 | public interface ListSchema extends DynamicObject {
145 | List strings();
146 | List ints();
147 |
148 | ListSchema strings(List strings);
149 | ListSchema ints(List ints);
150 | }
151 |
152 | public interface SetSchema extends DynamicObject {
153 | Set strings();
154 | }
155 |
156 | public interface MapSchema extends DynamicObject {
157 | Map dictionary();
158 | Map ints();
159 |
160 | MapSchema dictionary(Map dictionary);
161 | MapSchema ints(Map ints);
162 | }
163 | }
164 |
--------------------------------------------------------------------------------
/src/test/java/com/github/rschmitt/dynamicobject/ColliderTest.java:
--------------------------------------------------------------------------------
1 | package com.github.rschmitt.dynamicobject;
2 |
3 | import static com.github.rschmitt.collider.Collider.clojureList;
4 | import static com.github.rschmitt.collider.Collider.clojureMap;
5 | import static com.github.rschmitt.collider.Collider.clojureSet;
6 | import static com.github.rschmitt.dynamicobject.DynamicObject.deserialize;
7 | import static com.github.rschmitt.dynamicobject.DynamicObject.fromFressianByteArray;
8 | import static com.github.rschmitt.dynamicobject.DynamicObject.newInstance;
9 | import static com.github.rschmitt.dynamicobject.DynamicObject.toFressianByteArray;
10 | import static java.util.Optional.empty;
11 | import static java.util.Optional.of;
12 | import static org.junit.jupiter.api.Assertions.assertEquals;
13 | import static org.junit.jupiter.api.Assertions.assertTrue;
14 |
15 | import java.time.Instant;
16 | import java.util.List;
17 | import java.util.Map;
18 | import java.util.Optional;
19 | import java.util.Set;
20 |
21 | import org.junit.jupiter.api.BeforeAll;
22 | import org.junit.jupiter.api.Test;
23 |
24 | import com.github.rschmitt.collider.ClojureList;
25 | import com.github.rschmitt.collider.ClojureMap;
26 | import com.github.rschmitt.collider.ClojureSet;
27 |
28 | public class ColliderTest {
29 | static final Batch emptyBatch = newInstance(Batch.class);
30 | static final Instant inst = Instant.parse("1985-04-12T23:20:50.52Z");
31 |
32 | @BeforeAll
33 | public static void setup() {
34 | DynamicObject.registerTag(Batch.class, "batch");
35 | }
36 |
37 | @Test
38 | public void clojureMapDeserialization() throws Exception {
39 | Batch batch = deserialize("{:map {\"key\" 3}}", Batch.class);
40 |
41 | ClojureMap map = batch.map();
42 |
43 | assertEquals(3, map.get("key").intValue());
44 | assertTrue(map.dissoc("key").isEmpty());
45 | fressianRoundTrip(batch);
46 | }
47 |
48 | @Test
49 | public void clojureSetDeserialization() throws Exception {
50 | Batch batch = deserialize("{:set #{#inst \"1985-04-12T23:20:50.520-00:00\"}}", Batch.class);
51 |
52 | ClojureSet set = batch.set();
53 |
54 | assertTrue(set.contains(inst));
55 | fressianRoundTrip(batch);
56 | }
57 |
58 | @Test
59 | public void clojureListDeserialization() throws Exception {
60 | Batch batch = deserialize("{:list [\"a\" nil \"c\"]}", Batch.class);
61 |
62 | ClojureList> list = batch.list();
63 |
64 | assertEquals(of("a"), list.get(0));
65 | assertEquals(empty(), list.get(1));
66 | assertEquals(of("c"), list.get(2));
67 | assertEquals(of("d"), list.append(of("d")).get(3));
68 | fressianRoundTrip(batch);
69 | }
70 |
71 | @Test
72 | public void clojureMapBuilders() throws Exception {
73 | ClojureMap map = clojureMap("key", 3);
74 |
75 | Batch batch = emptyBatch.map(map);
76 |
77 | assertEquals(map, batch.map());
78 | fressianRoundTrip(batch);
79 | }
80 |
81 | @Test
82 | public void clojureSetBuilders() throws Exception {
83 | ClojureSet set = clojureSet(inst);
84 |
85 | Batch batch = emptyBatch.set(set);
86 |
87 | assertEquals(set, batch.set());
88 | fressianRoundTrip(batch);
89 | }
90 |
91 | @Test
92 | public void clojureListBuilders() throws Exception {
93 | ClojureList> list = clojureList(of("a"), empty(), of("c"));
94 |
95 | Batch batch = emptyBatch.list(list);
96 |
97 | assertEquals(list, batch.list());
98 | fressianRoundTrip(batch);
99 | }
100 |
101 | @Test
102 | public void mapBuilders() throws Exception {
103 | ClojureMap map = clojureMap("key", 3);
104 |
105 | Batch batch = emptyBatch.map2(map);
106 |
107 | assertEquals(map, batch.map());
108 | fressianRoundTrip(batch);
109 | }
110 |
111 | @Test
112 | public void setBuilders() throws Exception {
113 | ClojureSet set = clojureSet(inst);
114 |
115 | Batch batch = emptyBatch.set2(set);
116 |
117 | assertEquals(set, batch.set());
118 | fressianRoundTrip(batch);
119 | }
120 |
121 | @Test
122 | public void listBuilders() throws Exception {
123 | ClojureList> list = clojureList(of("a"), empty(), of("c"));
124 |
125 | Batch batch = emptyBatch.list2(list);
126 |
127 | assertEquals(list, batch.list());
128 | fressianRoundTrip(batch);
129 | }
130 |
131 | private void fressianRoundTrip(Batch batch) {
132 | Batch actual = fromFressianByteArray(toFressianByteArray(batch));
133 |
134 | assertEquals(batch, actual);
135 | assertEquals(batch.map(), actual.map());
136 | assertEquals(batch.set(), actual.set());
137 | assertEquals(batch.list(), actual.list());
138 | }
139 |
140 | public interface Batch extends DynamicObject {
141 | ClojureMap map();
142 | ClojureSet set();
143 | ClojureList> list();
144 |
145 | Batch map(Map map);
146 | Batch set(Set set);
147 | Batch list(List> list);
148 |
149 | @Key(":map") Batch map2(ClojureMap map);
150 | @Key(":set") Batch set2(ClojureSet set);
151 | @Key(":list") Batch list2(ClojureList> list);
152 | }
153 | }
154 |
--------------------------------------------------------------------------------
/src/test/java/com/github/rschmitt/dynamicobject/CustomKeyTest.java:
--------------------------------------------------------------------------------
1 | package com.github.rschmitt.dynamicobject;
2 |
3 | import static com.github.rschmitt.dynamicobject.DynamicObject.deserialize;
4 | import static com.github.rschmitt.dynamicobject.DynamicObject.newInstance;
5 | import static org.junit.jupiter.api.Assertions.assertEquals;
6 |
7 | import org.junit.jupiter.api.Test;
8 |
9 | public class CustomKeyTest {
10 | @Test
11 | public void customKeywordSupport() {
12 | String edn = "{:a-sample-int 5}";
13 | KeywordInterface object = deserialize(edn, KeywordInterface.class);
14 | assertEquals(5, object.aSampleInt());
15 | assertEquals(object, newInstance(KeywordInterface.class).aSampleInt(5));
16 | }
17 |
18 | @Test
19 | public void customStringSupport() {
20 | String edn = "{\"a-sample-string\", \"a-sample-value\"}";
21 | StringInterface object = deserialize(edn, StringInterface.class);
22 | assertEquals("a-sample-value", object.sampleString());
23 | assertEquals(object, newInstance(StringInterface.class).sampleString("a-sample-value"));
24 | }
25 |
26 | @Test
27 | public void customBuilderSupport() {
28 | String edn = "{:element \"a string\"}";
29 |
30 | CustomBuilder expected = deserialize(edn, CustomBuilder.class);
31 | CustomBuilder actual = newInstance(CustomBuilder.class).withElement("a string");
32 |
33 | assertEquals(expected, actual);
34 | }
35 |
36 | public interface KeywordInterface extends DynamicObject {
37 | @Key(":a-sample-int") int aSampleInt();
38 |
39 | KeywordInterface aSampleInt(int aSampleInt);
40 | }
41 |
42 | public interface StringInterface extends DynamicObject {
43 | @Key("a-sample-string") String sampleString();
44 |
45 | StringInterface sampleString(String sampleString);
46 | }
47 |
48 | public interface CustomBuilder extends DynamicObject {
49 | @Key(":element") String getElement();
50 | @Key(":element") CustomBuilder withElement(String element);
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/src/test/java/com/github/rschmitt/dynamicobject/CustomMethodTest.java:
--------------------------------------------------------------------------------
1 | package com.github.rschmitt.dynamicobject;
2 |
3 | import static org.junit.jupiter.api.Assertions.assertEquals;
4 |
5 | import org.junit.jupiter.api.Test;
6 |
7 | public class CustomMethodTest {
8 | @Test
9 | public void invokeCustomMethod() {
10 | CustomMethod obj = DynamicObject.newInstance(CustomMethod.class);
11 | assertEquals("asdf", obj.customMethod());
12 | }
13 |
14 | @Test
15 | public void invokeGettersFromCustomMethod() {
16 | CustomMethod obj = DynamicObject.newInstance(CustomMethod.class).str("a string");
17 | assertEquals("a string", obj.callIntoGetter());
18 | }
19 |
20 | @Test
21 | public void invokeCustomWither() {
22 | CustomMethod obj = DynamicObject.newInstance(CustomMethod.class).customWither(4);
23 | assertEquals("4", obj.callIntoGetter());
24 | }
25 |
26 | public interface CustomMethod extends DynamicObject {
27 | String str();
28 |
29 | CustomMethod str(String str);
30 |
31 | default String customMethod() {
32 | return "asdf";
33 | }
34 |
35 | default String callIntoGetter() {
36 | return str();
37 | }
38 |
39 | default CustomMethod customWither(int x) {
40 | return str(String.valueOf(x));
41 | }
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/src/test/java/com/github/rschmitt/dynamicobject/DefaultReaderTest.java:
--------------------------------------------------------------------------------
1 | package com.github.rschmitt.dynamicobject;
2 |
3 | import clojure.java.api.Clojure;
4 | import org.junit.jupiter.api.Test;
5 |
6 | import java.util.ArrayList;
7 | import java.util.HashMap;
8 |
9 | import static com.github.rschmitt.dynamicobject.DynamicObject.serialize;
10 | import static org.junit.jupiter.api.Assertions.assertEquals;
11 | import static org.junit.jupiter.api.Assertions.assertThrows;
12 |
13 | @SuppressWarnings("rawtypes")
14 | public class DefaultReaderTest {
15 | @Test
16 | public void testUnknownReader() {
17 | String edn = "#some-namespace/some-record-name{:key :value}";
18 | Object obj = DynamicObject.deserialize(edn, Object.class);
19 | Unknown unknown = (Unknown) obj;
20 |
21 | assertEquals("some-namespace/some-record-name", unknown.getTag());
22 | assertEquals(Clojure.read("{:key :value}"), unknown.getElement());
23 | assertEquals(serialize(unknown), edn);
24 | }
25 |
26 | @Test
27 | public void disableUnknownReader() {
28 | DynamicObject.setDefaultReader(null);
29 | assertThrows(RuntimeException.class, () -> DynamicObject.deserialize("#unknown{}", Object.class));
30 | DynamicObject.setDefaultReader(Unknown::new);
31 | }
32 |
33 | @Test
34 | public void testUnknownSerialization() {
35 | Unknown map = new Unknown("tag", new HashMap());
36 | Unknown str = new Unknown("tag", "asdf");
37 | Unknown vec = new Unknown("tag", new ArrayList());
38 |
39 | assertEquals("#tag{}", serialize(map));
40 | assertEquals("#tag \"asdf\"", serialize(str));
41 | assertEquals("#tag []", serialize(vec));
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/src/test/java/com/github/rschmitt/dynamicobject/DeserializationHookTest.java:
--------------------------------------------------------------------------------
1 | package com.github.rschmitt.dynamicobject;
2 |
3 | import org.junit.jupiter.api.BeforeAll;
4 | import org.junit.jupiter.api.Test;
5 |
6 | import static com.github.rschmitt.dynamicobject.DynamicObject.*;
7 | import static org.junit.jupiter.api.Assertions.assertEquals;
8 |
9 | @SuppressWarnings("deprecation")
10 | public class DeserializationHookTest {
11 | @BeforeAll
12 | public static void setup() {
13 | registerTag(Registered.class, "Reg");
14 | }
15 |
16 | @Test
17 | public void ednDeserialization() {
18 | Registered r = (Registered) deserialize("#Reg{}", Object.class);
19 |
20 | assertEquals(42L, r.value());
21 | }
22 |
23 | @Test
24 | public void fressianDeserialization() throws Exception {
25 | Registered oldVersion = newInstance(Registered.class);
26 |
27 | byte[] bytes = toFressianByteArray(oldVersion);
28 | Registered newVersion = fromFressianByteArray(bytes);
29 |
30 | assertEquals(42, newVersion.value());
31 | }
32 |
33 | @Test
34 | public void hintedEdnDeserialization() throws Exception {
35 | Unregistered u = deserialize("{}", Unregistered.class);
36 | assertEquals(42L, u.value());
37 | }
38 |
39 | public interface Registered extends DynamicObject {
40 | long value();
41 |
42 | Registered value(long value);
43 |
44 | @Override
45 | default Registered afterDeserialization() {
46 | return value(42);
47 | }
48 | }
49 |
50 | public interface Unregistered extends DynamicObject {
51 | long value();
52 |
53 | Unregistered value(long value);
54 |
55 | @Override
56 | default Unregistered afterDeserialization() {
57 | return value(42);
58 | }
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/src/test/java/com/github/rschmitt/dynamicobject/DiffTest.java:
--------------------------------------------------------------------------------
1 | package com.github.rschmitt.dynamicobject;
2 |
3 | import static com.github.rschmitt.dynamicobject.DynamicObject.deserialize;
4 | import static com.github.rschmitt.dynamicobject.DynamicObject.serialize;
5 | import static com.github.rschmitt.dynamicobject.TestUtils.assertEquivalent;
6 | import static org.junit.jupiter.api.Assertions.assertEquals;
7 | import static org.junit.jupiter.api.Assertions.assertNotEquals;
8 | import static org.junit.jupiter.api.Assertions.assertNull;
9 |
10 | import java.util.List;
11 | import java.util.Set;
12 |
13 | import org.junit.jupiter.api.AfterEach;
14 | import org.junit.jupiter.api.BeforeEach;
15 | import org.junit.jupiter.api.Test;
16 |
17 | public class DiffTest {
18 | @BeforeEach
19 | public void setup() {
20 | DynamicObject.registerTag(Diffable.class, "D");
21 | }
22 |
23 | @AfterEach
24 | public void teardown() {
25 | DynamicObject.deregisterTag(Diffable.class);
26 | }
27 |
28 | @Test
29 | public void union() {
30 | Diffable a = deserialize("#D{:a \"a\", :b \"b\"}", Diffable.class);
31 | Diffable b = deserialize("#D{:a \"a\", :b \"b\", :c \"c\"}", Diffable.class);
32 |
33 | Diffable c = a.intersect(b);
34 |
35 | assertEquals(c, a);
36 | assertNotEquals(c, b);
37 | assertEquivalent("#D{:a \"a\", :b \"b\"}", serialize(c));
38 | }
39 |
40 | @Test
41 | public void emptyUnion() {
42 | Diffable a = deserialize("#D{:a \"a\"}", Diffable.class);
43 | Diffable b = deserialize("#D{:b \"b\"}", Diffable.class);
44 |
45 | Diffable c = a.intersect(b);
46 |
47 | assertNull(c.a());
48 | assertNull(c.b());
49 | assertEquals("#D{}", serialize(c));
50 | }
51 |
52 | @Test
53 | public void mapSubdiff() {
54 | Diffable a = deserialize("#D{:d #D{:a \"inner\"}, :a \"a\", :b \"?\"}", Diffable.class);
55 | Diffable b = deserialize("#D{:d #D{:a \"inner\", :b \"ignored\"}, :a \"a\", :b \"!\"}", Diffable.class);
56 |
57 | Diffable c = a.intersect(b);
58 |
59 | assertEquals("a", c.a());
60 | assertNull(c.b());
61 | assertEquals(a.d(), c.d());
62 | assertEquals("inner", a.d().a());
63 | }
64 |
65 | @Test
66 | public void setsAreNotSubdiffed() {
67 | Diffable a = deserialize("#D{:s #{1 2 3}}", Diffable.class);
68 | Diffable b = deserialize("#D{:s #{1 2 3 4}}", Diffable.class);
69 |
70 | Diffable c = a.intersect(b);
71 |
72 | assertEquals(null, c.set());
73 | }
74 |
75 | @Test
76 | public void listsAreSubdiffed() {
77 | Diffable a = deserialize("#D{:list [5 4 3]}", Diffable.class);
78 | Diffable b = deserialize("#D{:list [1 2 3]}", Diffable.class);
79 |
80 | Diffable c = a.intersect(b);
81 |
82 | assertEquals(null, c.list().get(0));
83 | assertEquals(null, c.list().get(1));
84 | assertEquals(Integer.valueOf(3), c.list().get(2));
85 | }
86 |
87 | @Test
88 | public void subtraction() {
89 | Diffable a = deserialize("#D{:a \"same\", :b \"different\", :set #{1 2 3}, :list [1 2 1]}", Diffable.class);
90 | Diffable b = deserialize("#D{:a \"same\", :b \"?????????\", :set #{1 2 3}, :list [1 2 3]}", Diffable.class);
91 |
92 | Diffable diff = a.subtract(b);
93 |
94 | assertNull(diff.a());
95 | assertNull(diff.set());
96 | assertEquals("different", diff.b());
97 | assertNull(diff.list().get(0));
98 | assertNull(diff.list().get(1));
99 | assertEquals(Integer.valueOf(1), diff.list().get(2));
100 | }
101 |
102 | public interface Diffable extends DynamicObject {
103 | String a();
104 | String b();
105 | Diffable d();
106 | Set set();
107 | List list();
108 | }
109 | }
110 |
--------------------------------------------------------------------------------
/src/test/java/com/github/rschmitt/dynamicobject/ExtensibilityTest.java:
--------------------------------------------------------------------------------
1 | package com.github.rschmitt.dynamicobject;
2 |
3 | import static com.github.rschmitt.dynamicobject.DynamicObject.deserialize;
4 | import static com.github.rschmitt.dynamicobject.DynamicObject.fromFressianByteArray;
5 | import static com.github.rschmitt.dynamicobject.DynamicObject.serialize;
6 | import static com.github.rschmitt.dynamicobject.DynamicObject.toFressianByteArray;
7 | import static com.github.rschmitt.dynamicobject.TestUtils.assertEquivalent;
8 | import static java.lang.String.format;
9 | import static org.junit.jupiter.api.Assertions.assertEquals;
10 |
11 | import java.io.IOException;
12 | import java.util.List;
13 | import java.util.Map;
14 |
15 | import org.fressian.Reader;
16 | import org.fressian.Writer;
17 | import org.fressian.handlers.ReadHandler;
18 | import org.fressian.handlers.WriteHandler;
19 | import org.junit.jupiter.api.AfterEach;
20 | import org.junit.jupiter.api.BeforeEach;
21 | import org.junit.jupiter.api.Test;
22 |
23 | public class ExtensibilityTest {
24 | private static final String Edn = "#dh{:dumb [#MyDumbClass{:version 1, :str \"str\"}]}";
25 |
26 | @BeforeEach
27 | public void setup() {
28 | DynamicObject.registerType(DumbClass.class, new DumbClassTranslator());
29 | DynamicObject.registerTag(DumbClassHolder.class, "dh");
30 | DynamicObject.registerType(DumbClass.class, "dumb", new DumbClassReader(), new DumbClassWriter());
31 | }
32 |
33 | @AfterEach
34 | public void teardown() {
35 | DynamicObject.deregisterType(DumbClass.class);
36 | DynamicObject.deregisterTag(DumbClassHolder.class);
37 | }
38 |
39 | @Test
40 | public void roundTrip() {
41 | DumbClassHolder holder = deserialize(Edn, DumbClassHolder.class);
42 |
43 | String serialized = serialize(holder);
44 |
45 | assertEquivalent(Edn, serialized);
46 | assertEquals(new DumbClass(1, "str"), holder.dumb().get(0));
47 | assertEquals(holder, fromFressianByteArray(toFressianByteArray(holder)));
48 | }
49 |
50 | @Test
51 | public void serializeRegisteredType() {
52 | DumbClass dumbClass = new DumbClass(24, "twenty-four");
53 |
54 | String serialized = serialize(dumbClass);
55 |
56 | assertEquivalent("#MyDumbClass{:version 24, :str \"twenty-four\"}", serialized);
57 | }
58 |
59 | @Test
60 | public void deserializeRegisteredType() {
61 | String edn = "#MyDumbClass{:version 24, :str \"twenty-four\"}";
62 |
63 | DumbClass instance = deserialize(edn, DumbClass.class);
64 |
65 | assertEquals(new DumbClass(24, "twenty-four"), instance);
66 | }
67 |
68 | @Test
69 | public void prettyPrint() {
70 | DumbClassHolder holder = deserialize(Edn, DumbClassHolder.class);
71 | String expectedFormattedString = format("#dh{:dumb [#MyDumbClass{:version 1, :str \"str\"}]}%n");
72 | assertEquivalent(expectedFormattedString, holder.toFormattedString());
73 | }
74 |
75 | @Test
76 | public void serializeBuiltinType() {
77 | assertEquals("true", serialize(true));
78 | assertEquals("false", serialize(false));
79 | assertEquals("25", serialize(25));
80 | assertEquals("\"asdf\"", serialize("asdf"));
81 | }
82 |
83 | // This is a DynamicObject that contains a regular POJO.
84 | public interface DumbClassHolder extends DynamicObject {
85 | List dumb();
86 | }
87 | }
88 |
89 | // This is a translation class that functions as an Edn reader/writer for its associated POJO.
90 | class DumbClassTranslator implements EdnTranslator {
91 | @Override
92 | public DumbClass read(Object obj) {
93 | DumbClassProxy proxy = DynamicObject.wrap((Map) obj, DumbClassProxy.class);
94 | return new DumbClass(proxy.version(), proxy.str());
95 | }
96 |
97 | @Override
98 | public String write(DumbClass obj) {
99 | DumbClassProxy proxy = DynamicObject.newInstance(DumbClassProxy.class);
100 | proxy = proxy.str(obj.getStr());
101 | proxy = proxy.version(obj.getVersion());
102 | return serialize(proxy);
103 | }
104 |
105 | @Override
106 | public String getTag() {
107 | return "MyDumbClass"; // This is deliberately different from the class name.
108 | }
109 |
110 | public interface DumbClassProxy extends DynamicObject {
111 | long version();
112 | String str();
113 |
114 | DumbClassProxy version(long version);
115 | DumbClassProxy str(String str);
116 | }
117 | }
118 |
119 | class DumbClassReader implements ReadHandler {
120 | @Override
121 | public Object read(Reader r, Object tag, int componentCount) throws IOException {
122 | return new DumbClass(r.readInt(), (String) r.readObject());
123 | }
124 | }
125 |
126 | class DumbClassWriter implements WriteHandler {
127 | @Override
128 | public void write(Writer w, Object instance) throws IOException {
129 | DumbClass dumb = (DumbClass) instance;
130 | w.writeTag("dumb", 2);
131 | w.writeInt(dumb.getVersion());
132 | w.writeObject(dumb.getStr());
133 | }
134 | }
135 |
136 | // This is a POJO that has no knowledge of Edn.
137 | class DumbClass {
138 | private final long version;
139 | private final String str;
140 |
141 | DumbClass(long version, String str) {
142 | this.version = version;
143 | this.str = str;
144 | }
145 |
146 | public long getVersion() {
147 | return version;
148 | }
149 |
150 | public String getStr() {
151 | return str;
152 | }
153 |
154 | @Override
155 | public boolean equals(Object o) {
156 | if (this == o) return true;
157 | if (o == null || getClass() != o.getClass()) return false;
158 |
159 | DumbClass dumbClass = (DumbClass) o;
160 |
161 | if (version != dumbClass.version) return false;
162 | return str != null ? str.equals(dumbClass.str) : dumbClass.str == null;
163 | }
164 |
165 | @Override
166 | public int hashCode() {
167 | int result = (int) (version ^ (version >>> 32));
168 | result = 31 * result + (str != null ? str.hashCode() : 0);
169 | return result;
170 | }
171 |
172 | @Override
173 | public String toString() {
174 | throw new UnsupportedOperationException("I'm a useless legacy toString() method that doesn't produce Edn!");
175 | }
176 | }
177 |
--------------------------------------------------------------------------------
/src/test/java/com/github/rschmitt/dynamicobject/FressianTest.java:
--------------------------------------------------------------------------------
1 | package com.github.rschmitt.dynamicobject;
2 |
3 | import static org.junit.jupiter.api.Assertions.assertEquals;
4 | import static org.junit.jupiter.api.Assertions.assertTrue;
5 |
6 | import java.util.Arrays;
7 | import java.util.Base64;
8 | import java.util.List;
9 |
10 | import org.junit.jupiter.api.AfterEach;
11 | import org.junit.jupiter.api.BeforeEach;
12 | import org.junit.jupiter.api.Test;
13 |
14 | public class FressianTest {
15 | public static final BinarySerialized SAMPLE_VALUE
16 | = DynamicObject.newInstance(BinarySerialized.class).withHello("world");
17 |
18 | @BeforeEach
19 | public void setup() {
20 | DynamicObject.registerTag(BinarySerialized.class, "BinarySerialized");
21 | }
22 |
23 | @AfterEach
24 | public void teardown() {
25 | DynamicObject.deregisterTag(BinarySerialized.class);
26 | }
27 |
28 | @Test
29 | public void smokeTest() throws Exception {
30 | byte[] bytes = DynamicObject.toFressianByteArray(SAMPLE_VALUE);
31 | Object o = DynamicObject.fromFressianByteArray(bytes);
32 |
33 | assertEquals(o, SAMPLE_VALUE);
34 | }
35 |
36 | @Test
37 | public void testNullValues() throws Exception {
38 | BinarySerialized testValue = SAMPLE_VALUE.withNull(null);
39 |
40 | byte[] bytes = DynamicObject.toFressianByteArray(testValue);
41 | Object o = DynamicObject.fromFressianByteArray(bytes);
42 |
43 | assertEquals(o, testValue);
44 | }
45 |
46 | @Test
47 | public void formatCompatibilityTest() throws Exception {
48 | String encoded = "7+MQQmluYXJ5U2VyaWFsaXplZAHA5sr3zd9oZWxsb993b3JsZA==";
49 | BinarySerialized deserialized = DynamicObject.fromFressianByteArray(Base64.getDecoder().decode(encoded));
50 |
51 | assertEquals(SAMPLE_VALUE, deserialized);
52 | }
53 |
54 | @Test
55 | public void cachedKeys_canBeRoundTripped() throws Exception {
56 | String cachedValue = "cached value";
57 | BinarySerialized value = DynamicObject.newInstance(BinarySerialized.class).withCached(cachedValue);
58 |
59 | byte[] fressian = DynamicObject.toFressianByteArray(Arrays.asList(value, value));
60 | List deserialized = DynamicObject.fromFressianByteArray(fressian);
61 |
62 | assertEquals(value, deserialized.get(0));
63 | assertEquals(value, deserialized.get(1));
64 | }
65 |
66 | @Test
67 | public void deregisteringAClassRepeatedly_doesNotThrowAnNPE() throws Exception {
68 | // First call to deregister & remove values from internal caches
69 | DynamicObject.deregisterTag(BinarySerialized.class);
70 |
71 | // This call should not throw an exception
72 | DynamicObject.deregisterTag(BinarySerialized.class);
73 | }
74 |
75 | @Test
76 | public void cachedKeys_areNotRepeated() throws Exception {
77 | String cachedValue = "cached value";
78 | BinarySerialized value = DynamicObject.newInstance(BinarySerialized.class).withCached(cachedValue);
79 |
80 | byte[] fressian = DynamicObject.toFressianByteArray(Arrays.asList(value, value));
81 | // Interpret as an 8-bit charset just to make it easy to find the embedded string(s)
82 | String s = new String(fressian, "ISO-8859-1");
83 |
84 | int firstIndex = s.indexOf(cachedValue);
85 | assertTrue(firstIndex >= 0);
86 |
87 | int secondIndex = s.indexOf(cachedValue, firstIndex + 1);
88 | assertEquals(-1, secondIndex);
89 | }
90 |
91 | public interface BinarySerialized extends DynamicObject {
92 | @Key(":hello") BinarySerialized withHello(String hello);
93 | @Key(":null") BinarySerialized withNull(Object nil);
94 | @Cached @Key(":cached") BinarySerialized withCached(String cached);
95 | }
96 | }
97 |
--------------------------------------------------------------------------------
/src/test/java/com/github/rschmitt/dynamicobject/InstantTest.java:
--------------------------------------------------------------------------------
1 | package com.github.rschmitt.dynamicobject;
2 |
3 | import static com.github.rschmitt.dynamicobject.DynamicObject.deserialize;
4 | import static com.github.rschmitt.dynamicobject.DynamicObject.newInstance;
5 | import static com.github.rschmitt.dynamicobject.DynamicObject.serialize;
6 | import static org.junit.jupiter.api.Assertions.assertEquals;
7 |
8 | import java.time.Instant;
9 | import java.util.Date;
10 |
11 | import org.junit.jupiter.api.Test;
12 |
13 | public class InstantTest {
14 | @Test
15 | public void dateBuilder() {
16 | Date expected = Date.from(Instant.parse("1985-04-12T23:20:50.52Z"));
17 |
18 | TimeWrapper timeWrapper = newInstance(TimeWrapper.class).date(expected);
19 |
20 | assertEquals(expected, timeWrapper.date());
21 | assertEquals("{:date #inst \"1985-04-12T23:20:50.520-00:00\"}", serialize(timeWrapper));
22 | }
23 |
24 | @Test
25 | public void instantBuilder() {
26 | Instant expected = Instant.parse("1985-04-12T23:20:50.52Z");
27 |
28 | TimeWrapper timeWrapper = newInstance(TimeWrapper.class).instant(expected);
29 |
30 | assertEquals(expected, timeWrapper.instant());
31 | assertEquals("{:instant #inst \"1985-04-12T23:20:50.520-00:00\"}", serialize(timeWrapper));
32 | }
33 |
34 | @Test
35 | public void dateParser() {
36 | String edn = "{:date #inst \"1985-04-12T23:20:50.520-00:00\"}";
37 | Date expected = Date.from(Instant.parse("1985-04-12T23:20:50.52Z"));
38 |
39 | TimeWrapper timeWrapper = deserialize(edn, TimeWrapper.class);
40 |
41 | assertEquals(expected, timeWrapper.date());
42 | }
43 |
44 | @Test
45 | public void instantParser() {
46 | String edn = "{:instant #inst \"1985-04-12T23:20:50.520-00:00\"}";
47 | Instant expected = Instant.parse("1985-04-12T23:20:50.52Z");
48 |
49 | TimeWrapper timeWrapper = deserialize(edn, TimeWrapper.class);
50 |
51 | assertEquals(expected, timeWrapper.instant());
52 | assertEquals(edn, serialize(timeWrapper));
53 | }
54 |
55 | public interface TimeWrapper extends DynamicObject {
56 | Date date();
57 | Instant instant();
58 |
59 | TimeWrapper date(Date date);
60 | TimeWrapper instant(Instant instant);
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/src/test/java/com/github/rschmitt/dynamicobject/MapTest.java:
--------------------------------------------------------------------------------
1 | package com.github.rschmitt.dynamicobject;
2 |
3 | import clojure.lang.EdnReader;
4 | import clojure.lang.PersistentHashMap;
5 | import org.junit.jupiter.api.AfterEach;
6 | import org.junit.jupiter.api.BeforeEach;
7 | import org.junit.jupiter.api.Test;
8 |
9 | import static com.github.rschmitt.dynamicobject.DynamicObject.deserialize;
10 | import static com.github.rschmitt.dynamicobject.DynamicObject.serialize;
11 | import static java.lang.String.format;
12 | import static org.junit.jupiter.api.Assertions.assertEquals;
13 | import static org.junit.jupiter.api.Assertions.assertFalse;
14 |
15 | @SuppressWarnings("unchecked")
16 | public class MapTest {
17 | static final String SimpleEdn = "{:str \"expected value\", :i 4, :d 3.14}";
18 | static final String NestedEdn = format("{:version 1, :simple %s}", SimpleEdn);
19 | static final String TaggedEdn = format("#eo%s", NestedEdn);
20 |
21 | @BeforeEach
22 | public void setup() {
23 | DynamicObject.registerTag(EmptyObject.class, "eo");
24 | }
25 |
26 | @AfterEach
27 | public void teardown() {
28 | DynamicObject.deregisterTag(EmptyObject.class);
29 | }
30 |
31 | @Test
32 | public void getMapReturnsBackingMap() {
33 | EmptyObject object = deserialize(TaggedEdn, EmptyObject.class);
34 | Object map = EdnReader.readString(NestedEdn, PersistentHashMap.EMPTY);
35 | assertEquals(map, object.getMap());
36 | binaryRoundTrip(object);
37 | }
38 |
39 | @Test
40 | public void unknownFieldsAreConsideredForEquality() {
41 | EmptyObject obj1 = deserialize(SimpleEdn, EmptyObject.class);
42 | EmptyObject obj2 = deserialize(NestedEdn, EmptyObject.class);
43 | assertFalse(obj1.equals(obj2));
44 | binaryRoundTrip(obj1);
45 | binaryRoundTrip(obj2);
46 | }
47 |
48 | @Test
49 | public void unknownFieldsAreSerialized() {
50 | EmptyObject nestedObj = deserialize(TaggedEdn, EmptyObject.class);
51 | String actualEdn = serialize(nestedObj);
52 | assertEquals(TaggedEdn, actualEdn);
53 | }
54 |
55 | @Test
56 | public void mapDefaultMethodsAreUsable() throws Exception {
57 | EmptyObject object = DynamicObject.newInstance(EmptyObject.class);
58 |
59 | object.getOrDefault("some key", "some value");
60 | }
61 |
62 | private void binaryRoundTrip(Object expected) {
63 | Object actual = DynamicObject.fromFressianByteArray(DynamicObject.toFressianByteArray(expected));
64 | assertEquals(expected, actual);
65 | }
66 |
67 | public interface EmptyObject extends DynamicObject {
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/src/test/java/com/github/rschmitt/dynamicobject/MergeTest.java:
--------------------------------------------------------------------------------
1 | package com.github.rschmitt.dynamicobject;
2 |
3 | import static com.github.rschmitt.dynamicobject.TestUtils.assertEquivalent;
4 | import static org.junit.jupiter.api.Assertions.assertEquals;
5 |
6 | import org.junit.jupiter.api.AfterEach;
7 | import org.junit.jupiter.api.BeforeEach;
8 | import org.junit.jupiter.api.Test;
9 |
10 | public class MergeTest {
11 | @BeforeEach
12 | public void setup() {
13 | DynamicObject.registerTag(Mergeable.class, "M");
14 | }
15 |
16 | @AfterEach
17 | public void teardown() {
18 | DynamicObject.deregisterTag(Mergeable.class);
19 | }
20 |
21 | @Test
22 | public void twoEmptyObjects() {
23 | Mergeable a = DynamicObject.deserialize("#M{:a nil}", Mergeable.class);
24 | Mergeable b = DynamicObject.deserialize("#M{:a nil}", Mergeable.class);
25 |
26 | Mergeable c = a.merge(b);
27 |
28 | assertEquals("#M{:a nil}", DynamicObject.serialize(c));
29 | }
30 |
31 | @Test
32 | public void twoFullObjects() {
33 | Mergeable a = DynamicObject.deserialize("#M{:a \"first\"}", Mergeable.class);
34 | Mergeable b = DynamicObject.deserialize("#M{:a \"second\"}", Mergeable.class);
35 |
36 | Mergeable c = a.merge(b);
37 |
38 | assertEquals("#M{:a \"second\"}", DynamicObject.serialize(c));
39 | }
40 |
41 | @Test
42 | public void nullsDoNotReplaceNonNulls() {
43 | Mergeable a = DynamicObject.deserialize("#M{:a \"first\"}", Mergeable.class);
44 | Mergeable b = DynamicObject.deserialize("#M{:a nil}", Mergeable.class);
45 |
46 | Mergeable c = a.merge(b);
47 |
48 | assertEquals("#M{:a \"first\"}", DynamicObject.serialize(c));
49 | }
50 |
51 | @Test
52 | public void mergeOutputSerializesCorrectly() {
53 | Mergeable a = DynamicObject.deserialize("#M{:a \"outer\"}", Mergeable.class);
54 | Mergeable b = DynamicObject.deserialize("#M{:m #M{:a \"inner\"}}", Mergeable.class);
55 |
56 | Mergeable c = a.merge(b);
57 |
58 | assertEquivalent("#M{:m #M{:a \"inner\"}, :a \"outer\"}", DynamicObject.serialize(c));
59 | }
60 |
61 | public interface Mergeable extends DynamicObject {
62 | String a();
63 | Mergeable m();
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/src/test/java/com/github/rschmitt/dynamicobject/MetadataTest.java:
--------------------------------------------------------------------------------
1 | package com.github.rschmitt.dynamicobject;
2 |
3 | import static org.junit.jupiter.api.Assertions.assertEquals;
4 | import static org.junit.jupiter.api.Assertions.assertNull;
5 |
6 | import org.junit.jupiter.api.Test;
7 |
8 | public class MetadataTest {
9 | private static final AnnotatedData AnnotatedData = DynamicObject.deserialize("{:value \"regular data\"}", AnnotatedData.class);
10 |
11 | @Test
12 | public void noInitialMetadata() {
13 | assertNull(AnnotatedData.source());
14 | assertEquals("{:value \"regular data\"}", DynamicObject.serialize(AnnotatedData));
15 | }
16 |
17 | @Test
18 | public void metadataBuilders() {
19 | AnnotatedData annotatedData = AnnotatedData.source("SQS");
20 | assertEquals("SQS", annotatedData.source());
21 | }
22 |
23 | @Test
24 | public void buildersWithCustomNames() {
25 | AnnotatedData annotatedData = AnnotatedData.withSource("SQS");
26 | assertEquals("SQS", annotatedData.source());
27 | }
28 |
29 | @Test
30 | public void customKeys() {
31 | CustomAnnotatedData annotatedData = DynamicObject.newInstance(CustomAnnotatedData.class);
32 |
33 | annotatedData = annotatedData.setSource("Azure");
34 |
35 | assertEquals("{}", DynamicObject.serialize(annotatedData));
36 | assertEquals("Azure", annotatedData.getSource());
37 | }
38 |
39 | @Test
40 | public void metadataIsNotSerialized() {
41 | AnnotatedData annotatedData = AnnotatedData.source("DynamoDB");
42 | assertEquals("{:value \"regular data\"}", DynamicObject.serialize(annotatedData));
43 | }
44 |
45 | @Test
46 | public void metadataIsIgnoredForEquality() {
47 | AnnotatedData withMetadata = AnnotatedData.source("Datomic");
48 | assertEquals(AnnotatedData, withMetadata);
49 | }
50 |
51 | public interface AnnotatedData extends DynamicObject {
52 | @Meta String source();
53 | AnnotatedData source(String meta);
54 | @Meta @Key(":source") AnnotatedData withSource(String meta);
55 | }
56 |
57 | public interface CustomAnnotatedData extends DynamicObject {
58 | @Meta @Key(":source") String getSource();
59 | @Meta @Key(":source") CustomAnnotatedData setSource(String source);
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/src/test/java/com/github/rschmitt/dynamicobject/MethodHandleTest.java:
--------------------------------------------------------------------------------
1 | package com.github.rschmitt.dynamicobject;
2 |
3 | import static com.github.rschmitt.dynamicobject.DynamicObject.deserialize;
4 | import static org.junit.jupiter.api.Assertions.assertEquals;
5 |
6 | import java.util.UUID;
7 | import java.util.function.BiFunction;
8 |
9 | import org.junit.jupiter.api.Test;
10 |
11 | public class MethodHandleTest {
12 | private static final UUID ReceiptHandle = UUID.randomUUID();
13 |
14 | @Test
15 | public void buildPolymorphically() {
16 | String edn = "{:command \"start the reactor\"}";
17 |
18 | QueueMessage queueMessage = deserializeAndAttachMetadata(edn, QueueMessage::receiptHandle, QueueMessage.class);
19 |
20 | assertEquals(ReceiptHandle, queueMessage.receiptHandle());
21 | assertEquals("start the reactor", queueMessage.command());
22 | }
23 |
24 | private T deserializeAndAttachMetadata(String edn, BiFunction receiptHandleMetadataBuilder, Class type) {
25 | T instance = deserialize(edn, type);
26 | return receiptHandleMetadataBuilder.apply(instance, ReceiptHandle);
27 | }
28 |
29 | public interface QueueMessage extends DynamicObject {
30 | String command();
31 |
32 | @Meta UUID receiptHandle();
33 | QueueMessage receiptHandle(UUID receiptHandle);
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/src/test/java/com/github/rschmitt/dynamicobject/NestingTest.java:
--------------------------------------------------------------------------------
1 | package com.github.rschmitt.dynamicobject;
2 |
3 | import static com.github.rschmitt.dynamicobject.DynamicObject.serialize;
4 | import static org.junit.jupiter.api.Assertions.assertEquals;
5 |
6 | import java.util.ArrayList;
7 | import java.util.HashMap;
8 | import java.util.HashSet;
9 | import java.util.List;
10 | import java.util.Map;
11 | import java.util.Set;
12 |
13 | import org.junit.jupiter.api.Test;
14 |
15 | public class NestingTest {
16 | @Test
17 | public void nestedInts() {
18 | List innerList = new ArrayList<>();
19 | innerList.add(1);
20 | innerList.add(2);
21 | innerList.add(3);
22 | List> outerList = new ArrayList<>();
23 | outerList.add(innerList);
24 |
25 | Nested nested = DynamicObject.newInstance(Nested.class).nestedIntegers(outerList);
26 |
27 | assertEquals(outerList, nested.nestedIntegers());
28 | assertEquals("{:nestedIntegers [[1 2 3]]}", serialize(nested));
29 | }
30 |
31 | @Test
32 | public void nestedStrings() {
33 | List innerList = new ArrayList<>();
34 | innerList.add("str1");
35 | innerList.add("str2");
36 | innerList.add("str3");
37 | List> outerList = new ArrayList<>();
38 | outerList.add(innerList);
39 |
40 | Nested nested = DynamicObject.newInstance(Nested.class).nestedStrings(outerList);
41 |
42 | assertEquals(outerList, nested.nestedStrings());
43 | }
44 |
45 | @Test
46 | public void nestedShorts() {
47 | Set innerSet = new HashSet<>();
48 | innerSet.add((short) 1);
49 | innerSet.add((short) 2);
50 | innerSet.add((short) 3);
51 | List> outerList = new ArrayList<>();
52 | outerList.add(innerSet);
53 |
54 | Nested nested = DynamicObject.newInstance(Nested.class).nestedShorts(outerList);
55 |
56 | assertEquals(outerList, nested.nestedShorts());
57 | assertEquals("{:nestedShorts [#{1 3 2}]}", serialize(nested));
58 | }
59 |
60 | @Test
61 | public void nestedMaps() {
62 | Map innerMap = new HashMap<>();
63 | innerMap.put(1, 2);
64 | Map> outerMap = new HashMap<>();
65 | outerMap.put(1, innerMap);
66 |
67 | Nested nested = DynamicObject.newInstance(Nested.class).nestedMaps(outerMap);
68 |
69 | assertEquals(outerMap, nested.nestedMaps());
70 | assertEquals("{:nestedMaps {1 {1 2}}}", serialize(nested));
71 | }
72 |
73 | public interface Nested extends DynamicObject {
74 | List> nestedStrings();
75 | List> nestedIntegers();
76 | List> nestedShorts();
77 | Map> nestedMaps();
78 |
79 | Nested nestedStrings(List> strings);
80 | Nested nestedIntegers(List> integers);
81 | Nested nestedShorts(List> nestedShorts);
82 | Nested nestedMaps(Map> nestedMaps);
83 | }
84 | }
85 |
--------------------------------------------------------------------------------
/src/test/java/com/github/rschmitt/dynamicobject/NumberTest.java:
--------------------------------------------------------------------------------
1 | package com.github.rschmitt.dynamicobject;
2 |
3 | import static com.github.rschmitt.dynamicobject.DynamicObject.deserialize;
4 | import static com.github.rschmitt.dynamicobject.DynamicObject.fromFressianByteArray;
5 | import static com.github.rschmitt.dynamicobject.DynamicObject.newInstance;
6 | import static com.github.rschmitt.dynamicobject.DynamicObject.serialize;
7 | import static com.github.rschmitt.dynamicobject.DynamicObject.toFressianByteArray;
8 | import static org.junit.jupiter.api.Assertions.assertEquals;
9 |
10 | import java.math.BigDecimal;
11 | import java.math.BigInteger;
12 |
13 | import org.junit.jupiter.api.BeforeEach;
14 | import org.junit.jupiter.api.Test;
15 |
16 | public class NumberTest {
17 | @BeforeEach
18 | public void setup() {
19 | DynamicObject.registerTag(ArbitraryPrecision.class, "ap");
20 | }
21 |
22 | @Test
23 | public void BigDecimal() {
24 | String edn = "#ap{:bigDecimal 3.14159M}";
25 |
26 | ArbitraryPrecision arbitraryPrecision = deserialize(edn, ArbitraryPrecision.class);
27 |
28 | assertEquals(edn, serialize(arbitraryPrecision));
29 | assertEquals(newInstance(ArbitraryPrecision.class).bigDecimal(new BigDecimal("3.14159")), arbitraryPrecision);
30 | binaryRoundTrip(arbitraryPrecision);
31 | }
32 |
33 | @Test
34 | public void BigInteger() {
35 | String edn = "#ap{:bigInteger 9234812039419082756912384500123N}";
36 |
37 | ArbitraryPrecision arbitraryPrecision = deserialize(edn, ArbitraryPrecision.class);
38 |
39 | assertEquals(edn, serialize(arbitraryPrecision));
40 | assertEquals(newInstance(ArbitraryPrecision.class).bigInteger(new BigInteger("9234812039419082756912384500123")), arbitraryPrecision);
41 | binaryRoundTrip(arbitraryPrecision);
42 | }
43 |
44 | private void binaryRoundTrip(Object expected) {
45 | Object actual = fromFressianByteArray(toFressianByteArray(expected));
46 | assertEquals(expected, actual);
47 | }
48 |
49 | public interface ArbitraryPrecision extends DynamicObject {
50 | BigDecimal bigDecimal();
51 | BigInteger bigInteger();
52 |
53 | ArbitraryPrecision bigDecimal(BigDecimal bigDecimal);
54 | ArbitraryPrecision bigInteger(BigInteger bigInteger);
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/src/test/java/com/github/rschmitt/dynamicobject/ObjectMethodsTest.java:
--------------------------------------------------------------------------------
1 | package com.github.rschmitt.dynamicobject;
2 |
3 | import org.junit.jupiter.api.Test;
4 |
5 | import static org.junit.jupiter.api.Assertions.assertFalse;
6 |
7 | @SuppressWarnings("unchecked")
8 | public class ObjectMethodsTest {
9 | @Test
10 | public void equalsNullTest() throws Exception {
11 | assertFalse(DynamicObject.newInstance(DynamicObject.class).equals(null));
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/src/test/java/com/github/rschmitt/dynamicobject/OptionalTest.java:
--------------------------------------------------------------------------------
1 | package com.github.rschmitt.dynamicobject;
2 |
3 | import static com.github.rschmitt.dynamicobject.DynamicObject.deserialize;
4 | import static com.github.rschmitt.dynamicobject.DynamicObject.newInstance;
5 | import static com.github.rschmitt.dynamicobject.DynamicObject.serialize;
6 | import static java.util.Arrays.asList;
7 | import static org.junit.jupiter.api.Assertions.assertEquals;
8 | import static org.junit.jupiter.api.Assertions.assertFalse;
9 | import static org.junit.jupiter.api.Assertions.assertThrows;
10 |
11 | import java.time.Instant;
12 | import java.util.List;
13 | import java.util.Optional;
14 |
15 | import org.junit.jupiter.api.Test;
16 |
17 | public class OptionalTest {
18 | @Test
19 | public void valuePresent() {
20 | OptWrapper instance = deserialize("{:str \"value\"}", OptWrapper.class).validate();
21 | OptWrapper expected = newInstance(OptWrapper.class).str(Optional.of("value")).validate();
22 |
23 | assertEquals("value", instance.str().get());
24 | assertEquals(expected, instance);
25 | }
26 |
27 | @Test
28 | public void valueMissing() {
29 | OptWrapper instance = deserialize("{:str nil}", OptWrapper.class).validate();
30 | OptWrapper expected = newInstance(OptWrapper.class).str(Optional.empty()).validate();
31 |
32 | assertFalse(instance.str().isPresent());
33 | assertEquals(expected, instance);
34 | }
35 |
36 | @Test
37 | public void nonOptionalBuilder() {
38 | OptWrapper nothing = newInstance(OptWrapper.class).rawStr(null).validate();
39 | OptWrapper some = newInstance(OptWrapper.class).rawStr("some string").validate();
40 |
41 | assertEquals(some.rawStr(), Optional.of("some string"));
42 | assertEquals(nothing.rawStr(), Optional.empty());
43 | }
44 |
45 | @Test
46 | public void intPresent() {
47 | OptWrapper instance = deserialize("{:i 24601}", OptWrapper.class).validate();
48 | OptWrapper expected = newInstance(OptWrapper.class).i(Optional.of(24601)).validate();
49 |
50 | assertEquals(Integer.valueOf(24601), instance.i().get());
51 | assertEquals(expected, instance);
52 | }
53 |
54 | @Test
55 | public void listPresent() {
56 | OptWrapper instance = deserialize("{:ints [1 2 3]}", OptWrapper.class).validate();
57 | OptWrapper expected = newInstance(OptWrapper.class).ints(Optional.of(asList(1, 2, 3))).validate();
58 |
59 | assertEquals(asList(1, 2, 3), instance.ints().get());
60 | assertEquals(expected, instance);
61 | }
62 |
63 | @Test
64 | public void instantPresent() {
65 | String edn = "{:inst #inst \"1985-04-12T23:20:50.520-00:00\"}";
66 | Instant expected = Instant.parse("1985-04-12T23:20:50.52Z");
67 |
68 | OptWrapper instance = deserialize(edn, OptWrapper.class).validate();
69 |
70 | assertEquals(expected, instance.inst().get());
71 | assertEquals(edn, serialize(instance));
72 | }
73 |
74 | @Test
75 | public void dynamicObjectPresent() {
76 | DynamicObject.registerTag(OptWrapper.class, "OptWrapper");
77 |
78 | OptWrapper instance = deserialize("#OptWrapper{:wrapper #OptWrapper{:i 24}}", OptWrapper.class).validate();
79 | OptWrapper expected = newInstance(OptWrapper.class).wrapper(Optional.of(newInstance(OptWrapper.class).i(Optional.of(24)))).validate();
80 |
81 | assertEquals(expected.wrapper().get(), instance.wrapper().get());
82 | assertEquals(expected, instance);
83 |
84 | DynamicObject.deregisterTag(OptWrapper.class);
85 | }
86 |
87 | @Test
88 | public void optionalValidation() {
89 | deserialize("{}", OptWrapper.class).validate();
90 | deserialize("{:str \"value\"}", OptWrapper.class).validate();
91 |
92 | }
93 |
94 | @Test
95 | public void optionalValidationFailure() {
96 | assertThrows(IllegalStateException.class, () -> deserialize("{:str 4}", OptWrapper.class).validate());
97 | }
98 |
99 | public interface OptWrapper extends DynamicObject {
100 | Optional