-
Notifications
You must be signed in to change notification settings - Fork 1
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Feature/concurrency tests #6
Conversation
frankwis
commented
Nov 17, 2016
- Added custom types and conversion for UUID, Date, BigInteger and BigDecimal
- Fixed initialization of custom type converters
- Added types for custom values and operations, fixed conversion
- CustomTypeSerializer and corresponding Builder
- Added headers to Patch operation
- Concurrency tests for serialization
private final List<Operation> operations; | ||
|
||
public Patch() { | ||
operations = new ArrayList<>(); | ||
headers = new HashMap<>(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Perhaps we should align on whether to use this.
....
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
btw why don't we use
this(new ArrayList<>()):
?
* This interface holds the schema for all valid value types. | ||
*/ | ||
public interface DefaultSchema { | ||
String VALUE = |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Eventually move to file and load with getClass().getResource(...)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would probably make it more readable?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This would not compile since the value is used in an annotation (only constants allowed there). Would need an indirection for compilation. I am adding a card to the backlog
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This would not compile per se since the value is used in an annotation (only constants allowed there). Would need an indirection for compilation. I am adding a card to the backlog
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In that case generating that file from a json is probably the easiest?
} | ||
|
||
public static class Builder { | ||
final List<Schema> types = new ArrayList<>(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should be private
import java.math.BigInteger; | ||
|
||
/** | ||
* This class converts an {@link BigDecimal} value into am Avro {@link IndexedRecord} and back. It depends on |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
s/am/an/g
public void serializesDefaultValueTypes() throws IOException { | ||
Date date = new Date(); | ||
UUID uuid = UUID.randomUUID(); | ||
Patch patch = new Patch(ImmutableList.of(new Add<>(Path.of("some", "value"), "John Doe"), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Perhaps insert line break before new Add
...
|
||
@Override | ||
public int hashCode() { | ||
return Objects.hash(name, number, id, bommel); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice!
futures.stream().forEach(future -> { | ||
try { | ||
verify(PATCHES, future.get()); | ||
} catch (InterruptedException ex) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Use Java 8 multicatch?
|
||
public Operation generateOperation(Type type) { | ||
switch (type) { | ||
case ADD: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Possibly refactor to enum strategy pattern?
} | ||
|
||
public Object generate(Type type) { | ||
switch (type) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Refactor to enum strategy pattern.
.withSchemata(Schema.createArray(BigDecimalType.SCHEMA)) | ||
.withConverters(new BigIntegerConversion(), new BigDecimalConversion()) | ||
.reserializeAndAssert(ImmutableList.of(BigDecimal.valueOf(1234567890L), | ||
new BigDecimal("12345678901234567.8901234567890123456789012345678901234567890"), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Extract constants.
|
||
interface PatchType { | ||
String NAME = Patch.class.getSimpleName(); | ||
String DOC = "This record represents a JSON PATCH operation holding a sequence of operations to apply to a" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
JSON PATCH - does this description fit?
} | ||
|
||
public Patch(List<Operation> operations) { | ||
this.operations = new ArrayList<>(operations); | ||
this.headers = new HashMap<>(); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why not call the two-argument constructor here? This would follow the same principle as the no-argument constructor...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Correct :-)
new DateConversion(), | ||
new BigIntegerConversion(), | ||
new BigDecimalConversion(), | ||
new UUIDConversion()); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd say according to K&R style we would need to insert a line break before the last closing brace...
|
||
return allOf(all); | ||
return allOf(items.stream() | ||
.map(item -> hasItem(item)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
.map(this::hasItem)
} | ||
|
||
return allOf(all); | ||
return allOf(items.stream() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Insert line break before items.stream()
ImmutableMap.of("header", UUID.randomUUID())); | ||
|
||
@Test | ||
public void equality() throws IOException { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Split the two asserts into two tests. Each assertion should be in a separate test to avoid failing assertions being hidden by assertions already failing before.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Best rule would be one assertion per test method.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Perhaps you could elaborate a bit on the test methods by naming them so they explain what is tested exactly.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since we're considering to get rid of these classes (see #7), I'll ignore it for now.
|
||
@Test | ||
public void equality() throws IOException { | ||
assertThat(copyMovePatch, is(equalTo(copyMovePatchSibling))); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
BTW: this equalTo()
is an own matcher that would have been unnecessary if equals
had been overridden in the first place. Then you would have been able to just use is
as that is equivalent to is(equalTo(...)
using the default hamcrest matcher
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I am going to add a card to the backlog in order to remove the boilerplate test code
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I created issue #7 for future discussions and reference on this topic
MOVE(() -> new Move(generatePath(), generatePath())), | ||
REMOVE(() -> new Remove(generatePath())), | ||
REPLACE(() -> new Replace<>(generatePath(), generateValue())), | ||
@SuppressWarnings("unchecked") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Add comment why this was necessary?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I dug a little bit deeper and figured out it's not necessary 👍
this.operationSupplier = operationSupplier; | ||
} | ||
|
||
public static Operation generate() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Rename method to reflect its random nature.
} | ||
|
||
public static Operation generate() { | ||
OperationGenerator operationType = values()[random.nextInt(values().length)]; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Inline variable as it's only used once.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
IMHO a variable should only be declared if it's used more than once.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Agreed. It just became a one liner during the last refactorization :-)
} | ||
|
||
private static List<?> generateList(ValueType type) { | ||
List<Object> data = new ArrayList<>(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe replace with
return IntStream
.range(0, MAX_LIST_SIZE)
.mapToObject(i -> type.generate())
.collect(toList());
} | ||
|
||
private static BigDecimal randomBigDecimal() { | ||
BigInteger unscaledValue= new BigInteger(random.nextInt(256), random); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Space missing before =
} | ||
|
||
public Builder withCustomTypes() { | ||
types.addAll(Arrays.asList(BigDecimalType.SCHEMA, BigIntegerType.SCHEMA, DateType.SCHEMA, UuidType.SCHEMA)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm just wondering .. we have been using a special formatter with the ISO8601 format for dates. Would we need such a format as well for the DateTypes here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No. Date is just converted to a long value and is defined accurate as a timestamp representing the number of milliseconds since January 1, 1970, 00:00:00 GMT. (See io.datanerds.avropatch.value.conversion.DateConversion)
import org.apache.avro.generic.GenericFixed; | ||
|
||
import java.nio.ByteBuffer; | ||
import java.util.Date; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hm .. just a general question .. could we built a DateConversion for the yodatime DateTime?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sure, but do we need it? ;-)
public static Operation generate() { | ||
OperationGenerator operationType = values()[random.nextInt(values().length)]; | ||
return operationType.generateOperation(); | ||
public static Operation generateOperation() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
random? :)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please fix the typos.
Otherwise only formatting / minor refactoring suggestions.
this(new ArrayList<>(operations), new HashMap<>()); | ||
} | ||
|
||
public Patch(List<Operation> operations, Map<String, ?> headers) { | ||
this.operations = new ArrayList<>(operations); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Following the pattern above, this(operations);
|
||
interface BigDecimalType { | ||
String NAME = "big-decimal"; | ||
String DOC = "BigDecimal value represented via it's scale and unscaled value."; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
"its" (typo)
|
||
interface UuidType { | ||
String NAME = LogicalTypes.uuid().getName(); | ||
String DOC = "UUID serialized via two long values: It's most significant and least significant 64 bits."; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
"Its" (see above)
static Schema create(Schema valueSchema) { | ||
Field path = new Field("path", Path.SCHEMA, "Path pointing out where the JSON value should be replaced", | ||
NO_DEFAULT); | ||
Field value = new Field("value", valueSchema, "Actual value to replaced in patched object", NO_DEFAULT); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
"to be replaced"
static Schema create(Schema valueSchema) { | ||
Field path = new Field("path", Path.SCHEMA, "Path pointing out which JSON value should be tested against", | ||
NO_DEFAULT); | ||
Field value = new Field("value", valueSchema, "Actual value to test< against", NO_DEFAULT); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
remove "<"
|
||
List<Future<List<Patch>>> futures = new ArrayList<>(); | ||
for (int i = 0; i < NUMBER_OF_THREADS; i++) { | ||
futures.add(executorService.submit(() -> { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It might improve readability to give this function a name so we can only do something like executorService.submit(this::serialize)
or whatever name is appropriate.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Have only reviewed a few files. Plan to continue reviewing. Please take your time to address existing comments till then! :)
} | ||
|
||
public Patch(List<Operation> operations) { | ||
this(new ArrayList<>(operations), new HashMap<>()); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This would potentially result in copying the list twice. Perhaps, makes more sense to just pass "operations" as is to the other constructor where this list would be copied again into a new list.
this.operations = new ArrayList<>(operations); | ||
this.headers = new HashMap<>(headers); | ||
} | ||
|
||
public boolean addOperation(Operation operation) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thread safety will become an issue here. You are using a thread unsafe list for "operations". If there are 2 competing threads one of which does a getOperations and tries to iterate on it where as the other tries to add an operation at the same time, it will result in a ConcurrentModificationException. Would be better to use either a CopyOnWriteArrayList if operations are not expected to be added too frequently or synchronize getOperations and create a copy of the list before returning. AFAIK Collections.unmodifiableList is only a shallow copy and not a deep copy, so that would not handle it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Or explicitly point out the Thread unsafety in the class documentation. This might well be desired in favor of performance as a tradeoff. I think Effective Concurrency In Java notes mentioning something like
<b>This method is not thread-safe</b>
in the java docs of a class.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
See here "Document Thread-Safety"
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Originally the class wasn't designed to be thread-safe, because there would only be one thread processing a PATCH call, but since removing this convenient method results in thread-safety I will do that.
@@ -35,16 +45,29 @@ public boolean addOperation(Operation operation) { | |||
return Collections.unmodifiableList(operations); | |||
} | |||
|
|||
public Map<String, ?> getHeaders() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not a concurrency issue with the map as long as the map is not open to modification. It might not be the case right now but leaves the code open to errors in case there is a write interface exposed to the map erroneously in the future. Would suggest initializing the map as immutable during initialization in the constructor.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
the map should not be open to modifications
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's not:
public Map<String, ?> getHeaders() {
return Collections.unmodifiableMap(headers);
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Agreed that it is not open to modifications to external modifiers but from a code extensibility perspective, if there is a method in this class which tries to modify this map, extra care would need to be taken to ensure that the map is made immutable then. By making it immutable now, we can future proof it so that we don't run the risk of somebody not making it immutable/thread safe while adding code modifying it in future.
private final List<Operation> operations; | ||
|
||
public Patch() { | ||
operations = new ArrayList<>(); | ||
this(new ArrayList<>(), new HashMap<>()); | ||
} | ||
|
||
public Patch(List<Operation> operations) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Always a good idea to declare method arguments as final (especially in case of collections) which makes it explicit to the calling function that the list would not be modified in the called method.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Disagree. The final
only states that the reference can't be reassigned within the block. The list is still mutable.
Agree though on parameter declarations being implicitly final
would be a good thing :) Though I think that the boilerplate doesn't satisfy the improvement, would support a source code check rather for a variable being reassigned, as i.e. Findbugs supports AFAIR.
return outputStream.toByteArray(); | ||
} | ||
|
||
public static Patch of(byte[] bytes) throws IOException { | ||
DatumReader<Patch> reader = new ReflectDatumReader<>(SCHEMA); | ||
return reader.read(null, DecoderFactory.get().binaryDecoder(bytes, null)); | ||
return SERIALIZER.reader.read(null, DecoderFactory.get().binaryDecoder(bytes, null)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I assume we are not reusing the read "Patch" object here because it is being returned to an external method as it is, correct?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes and we'd like to have thread-safety
CONVERTERS.forEach(this::addLogicalTypeConversion); | ||
} | ||
|
||
public static AvroData get() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
getInstance() sounds like a more intuitive method name to me. Just a good to have. We can agree or disagree. :)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That is a matter of taste IMHO. There are variations of this like instance()
or getSingleton()
or whatever :) As far as we agree on what it means this should be OK. Although I generally would favor a more descriptive method name to support static import of the method...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It hides ReflectData::get
. Since it's static I cannot annotate with @Override
. Will add javadoc instead.
interface PatchType { | ||
String NAME = Patch.class.getSimpleName(); | ||
String DOC = "This record represents a PATCH holding a sequence of operations to apply to a given object."; | ||
static Schema create(Schema valueSchema) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Defining an interface method. This is new to me. Would love to understand more.
String NAME = Patch.class.getSimpleName(); | ||
String DOC = "This record represents a PATCH holding a sequence of operations to apply to a given object."; | ||
static Schema create(Schema valueSchema) { | ||
Schema.Field operations = new Schema.Field("operations", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Better to move the literal ("operations") to a constant before using it here and other places.
String NAME = "big-decimal"; | ||
String DOC = "BigDecimal value represented via it's scale and unscaled value."; | ||
LogicalType LOGICAL_TYPE = new LogicalType(NAME); | ||
Schema RECORD = SchemaBuilder.record("decimal").doc(DOC).fields() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Better to move the literal ("decimal") to a constant before using it here and other places.
String DOC = "BigDecimal value represented via it's scale and unscaled value."; | ||
LogicalType LOGICAL_TYPE = new LogicalType(NAME); | ||
Schema RECORD = SchemaBuilder.record("decimal").doc(DOC).fields() | ||
.name("unscaledValue").type(BigIntegerType.SCHEMA).noDefault() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The same comment for all literals. I guess these are the same literals used in the DefaultSchema class. It would be better for code coupling to put them in a common class of constants and use the constants at both places.
|
@@ -16,8 +16,9 @@ project.ext { | |||
dependencies { | |||
|
|||
compile([ | |||
"org.apache.avro:avro:1.8.0", | |||
"org.slf4j:slf4j-api:1.7.18" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
slf4j version should become a variable since it is also used in testCompile
* This interface holds the schema for all valid value types. | ||
*/ | ||
public interface DefaultSchema { | ||
String VALUE = |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would probably make it more readable?
public class Patch { | ||
|
||
public static final Schema SCHEMA = ReflectData.get().getSchema(Patch.class); | ||
private static final DatumWriter<Patch> writer = new ReflectDatumWriter<>(SCHEMA); | ||
private static final PatchSerializer SERIALIZER = new PatchSerializer(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why is domain and (de)-serialization combined?
private static final PatchSerializer SERIALIZER = new PatchSerializer(); | ||
|
||
@AvroSchema(DefaultSchema.HEADERS) | ||
private final Map<String, ?> headers; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Shouldn't headers be a multimap?
* | ||
* @see <a href="https://tools.ietf.org/html/rfc6902">https://tools.ietf.org/html/rfc6902</a> | ||
*/ | ||
@ThreadSafe |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think helper methods for getFirstOperation
and getSingle/FirstHeader(somekey)
would be nice
ByteBuffer buffer = ByteBuffer.wrap(value.bytes()); | ||
long leastSignificantBits = buffer.getLong(); | ||
long mostSignificantBits = buffer.getLong(); | ||
return new UUID(mostSignificantBits, leastSignificantBits); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nice, just the two longs! 👍
} | ||
|
||
@Test | ||
public void serializesCopy() throws IOException { | ||
Patch patch = new Patch(); | ||
patch.addOperation(new Copy(Path.parse("/person/firstName"), Path.parse("/person/lastName"))); | ||
Patch patch = patchOf(new Copy(Path.parse("/person/firstName"), Path.parse("/person/lastName"))); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
maybe copy,move etc need a method where you can pass a string which will be parsed?
} | ||
|
||
public boolean addOperation(Operation operation) { | ||
return operations.add(operation); | ||
public Patch(List<Operation> operations, Map<String, ?> headers) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should we do a sanity check that the value instances are of a supported type?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I created issue #10 for this feature
* This interface holds the schema for all valid value types. | ||
*/ | ||
public interface DefaultSchema { | ||
String VALUE = |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In that case generating that file from a json is probably the easiest?
.endRecord(); | ||
} | ||
|
||
Schema VALUE_TYPE_UNION = createUnion(BOOLEAN, DOUBLE, FLOAT, INTEGER, LONG, NULL, STRING, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would appreciate a comment what it does :D
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would be great if we could do something about the withArray() endArray() stuff. Does not seem like the best solution for what we want to solve there.
return this; | ||
} | ||
|
||
public Builder.ArrayBuilder withArray() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not a particularly good choice regarding naming IMO. Not obvious that you are starting a subbuilder here that requires endArray to be called to close it. Same goes for endArray. It will be very hard looking at code using this that there is a nested structure in there.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How about something like public Builder withArray(Builder.ArrayBuilder arrayBuilder)
and then go from there? Having the ArrayBuilder explicitly as a method parameter would emphasise the nesting.
this.parentBuilder = parentBuilder; | ||
} | ||
|
||
public Builder endArray() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
See comment regarding withArray
} | ||
|
||
interface DateType { | ||
String NAME = "timestamp"; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Date or timestamp? What is it? :D
Date date = new Date(); | ||
UUID uuid = UUID.randomUUID(); | ||
Patch patch = new Patch(ImmutableList.of( | ||
new Add<>(Path.of("some", "value"), "John Doe"), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
API wise this seems to be strange and very cumbersome. Path.of seems to be a looooot of boilerplate. Especially when it is not possible (maybe I'm missing something?) to create a base path and then just add a child to it.
IMO Path either needs a possibility to have a base path and then add children or otherwise people will likely start using it like this which does not seem to be right:
String base = "/some/namespace/here/";
[...]
new Add<>(Path.parse(base+"foobar"), "Knackwurst");
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You could do new Add<>(Path.of(base, "foobar")
, but it needs documentation. A method append(...)
makes sense either way :-)
} | ||
|
||
private static Matcher<Patch> operationsEqualTo(Patch expected) { | ||
return new CustomTypeSafeMatcher<Patch>(Joiner.on(",").join(expected.getOperations())) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Java 8 has String.join() in case you haven't seen that one yet :)
.withCustomTypes() | ||
.nullable() | ||
.with(Bimmel.class) | ||
.endArray() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is what I meant with withArray() and endArray(). Very hard to see whats going on here, especially with the next line being indented one more level :)
return this; | ||
} | ||
|
||
public Builder with(Schema schema) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
withSchema()?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Schema seems to be the most simple building block of the List this builder is creating, so having a simple method name for this case might be okay.
return this; | ||
} | ||
|
||
public Builder with(Type type) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
withType()?
} | ||
|
||
public static class Builder { | ||
protected final List<Schema> types; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Considering that both a method for adding a type and a schema exist, the combination of declaration and variable name is a little confusing.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh... one can set this one here. 😃
private final List<Operation> operations; | ||
|
||
public Patch() { | ||
operations = new ArrayList<>(); | ||
@SuppressWarnings("unused") // no arg constructor needed by Avro |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
rename to: no-arg constructor, otherwise the meaning is that an arg constructor is not needed. Change everywhere else, too.
@@ -13,9 +13,9 @@ | |||
public final Path from; | |||
public final Path path; | |||
|
|||
@SuppressWarnings("unused") // no arg constructor needed by Avro |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
no-arg
@@ -12,20 +12,20 @@ | |||
@AvroIgnore | |||
public static final String op = "replace"; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why/where is this string needed anyway? Is it not redundant information?
"header 2", new Date(), | ||
"header 3", 1234L, | ||
"header 4", new BigDecimal("3214123453.123512345"))); | ||
byte[] bytes = patch.toBytes();; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
remove ;
public Map<String, ?> getHeaders() { | ||
return Collections.unmodifiableMap(headers); | ||
} | ||
|
||
public byte[] toBytes() throws IOException { | ||
ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); | ||
Encoder binaryEncoder = EncoderFactory.get().directBinaryEncoder(outputStream, null); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this local variables binaryEncoder and outputStream can probably shortened for readability, as they are used only once and right after declaration. (see heuristic N5 in Clean Code).
String NAME = io.datanerds.avropatch.operation.Add.class.getSimpleName(); | ||
String DOC = "This record represents the add operation of RFC 6902 for JSON Patch'."; | ||
|
||
static Schema create(Schema valueSchema) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
IMHO, the name valueSchema is not concise enough. what is it really, the allowed types of a field in the schema?
is the name valueSchema only used because value is used already below?
.nullable() | ||
.with(Bimmel.class) | ||
.endArray() | ||
.withPrimitives() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
is the indentation correct here?
.withSchemata(UuidType.SCHEMA) | ||
.withConverters(new UUIDConversion()) | ||
.reserializeAndAssert(UUID.randomUUID()) | ||
.reserializeAndAssert(UUID.randomUUID()); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
is testing one randomUUID not enough?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It should be ;-)
} | ||
|
||
public byte[] toBytes(Patch value) throws IOException { | ||
ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
shorten local variable names (see comment somewhere above).