Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion docs/implementation-requirements/02-models-and-errors.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ Rules:
- Missing `results` means empty rows.
- `rows()` must return an immutable list.
- Each row map must be immutable.
- Nested JSON maps and lists in rows must be recursively immutable and defensively copied.
- `firstRow()` must return an immutable map if present.
- `messages()` and `errors()` must return immutable lists.
- `rawBody()` must contain the raw HTTP response body.
Expand Down Expand Up @@ -99,6 +100,8 @@ timings = Optional.empty()
```

Unknown meta fields must be preserved in `additionalProperties()`.
Nested JSON maps and lists in `additionalProperties()` must be recursively immutable and
defensively copied.

## D1Timings

Expand Down Expand Up @@ -228,7 +231,7 @@ public Map<String, Object> row();

Rules:

- `row()` must be immutable.
- `row()` and its nested JSON maps and lists must be immutable and defensively copied.
- Default exception message must not include row contents.

## Error Mapping
Expand Down
5 changes: 2 additions & 3 deletions docs/implementation-requirements/05-json-and-mapping.md
Original file line number Diff line number Diff line change
Expand Up @@ -104,9 +104,8 @@ JSON array -> List

Rows may contain nested `Map` or `List` values.

The outer rows list and each row map must be immutable.

Deep immutability for nested values is not required in v0.1.0.
The outer rows list, each row map, and nested JSON maps and lists must be immutable and
defensively copied.

## Typed Mapping

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ Required cases:

- Rows list is immutable
- Row map is immutable
- Nested JSON maps and lists are immutable and defensively copied
- Messages list is immutable
- Errors list is immutable
- additionalProperties map is immutable
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package io.github.xxvw.cloudflare.d1;

import io.github.xxvw.cloudflare.d1.internal.D1ImmutableValues;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
Expand Down Expand Up @@ -30,7 +30,7 @@ public D1MappingException(Class<?> targetType, int rowIndex, Map<String, Object>
Collections.<D1ResponseInfo>emptyList(), Collections.<D1ResponseInfo>emptyList());
this.targetType = Objects.requireNonNull(targetType, "targetType must not be null");
this.rowIndex = rowIndex;
this.row = row == null ? Collections.<String, Object>emptyMap() : Collections.unmodifiableMap(new LinkedHashMap<>(row));
this.row = D1ImmutableValues.immutableMap(row);
}

/**
Expand Down
6 changes: 2 additions & 4 deletions src/main/java/io/github/xxvw/cloudflare/d1/D1Meta.java
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package io.github.xxvw.cloudflare.d1;

import io.github.xxvw.cloudflare.d1.internal.D1ImmutableValues;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Optional;
import java.util.OptionalLong;
Expand Down Expand Up @@ -66,9 +66,7 @@ public D1Meta(
this.servedByRegion = servedByRegion;
this.servedByPrimary = servedByPrimary;
this.timings = timings;
this.additionalProperties = additionalProperties == null
? Collections.<String, Object>emptyMap()
: Collections.unmodifiableMap(new LinkedHashMap<>(additionalProperties));
this.additionalProperties = D1ImmutableValues.immutableMap(additionalProperties);
}

/**
Expand Down
5 changes: 2 additions & 3 deletions src/main/java/io/github/xxvw/cloudflare/d1/D1RawResult.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package io.github.xxvw.cloudflare.d1;

import io.github.xxvw.cloudflare.d1.internal.D1ImmutableValues;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
Expand Down Expand Up @@ -139,9 +140,7 @@ private static List<List<Object>> immutableRows(List<List<Object>> rows) {
}
List<List<Object>> copy = new ArrayList<>(rows.size());
for (List<Object> row : rows) {
copy.add(row == null
? Collections.<Object>emptyList()
: Collections.unmodifiableList(new ArrayList<>(row)));
copy.add(D1ImmutableValues.immutableList(row));
}
return Collections.unmodifiableList(copy);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package io.github.xxvw.cloudflare.d1;

import java.util.Collections;
import java.util.LinkedHashMap;
import io.github.xxvw.cloudflare.d1.internal.D1ImmutableValues;
import java.util.Map;
import java.util.Optional;

Expand Down Expand Up @@ -34,9 +33,7 @@ public D1ResponseInfo(
this.message = message;
this.documentationUrl = documentationUrl;
this.source = source;
this.additionalProperties = additionalProperties == null
? Collections.<String, Object>emptyMap()
: Collections.unmodifiableMap(new LinkedHashMap<>(additionalProperties));
this.additionalProperties = D1ImmutableValues.immutableMap(additionalProperties);
}

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package io.github.xxvw.cloudflare.d1;

import java.util.Collections;
import java.util.LinkedHashMap;
import io.github.xxvw.cloudflare.d1.internal.D1ImmutableValues;
import java.util.Map;
import java.util.Optional;

Expand All @@ -20,9 +19,7 @@ public final class D1ResponseSource {
*/
public D1ResponseSource(String pointer, Map<String, Object> additionalProperties) {
this.pointer = pointer;
this.additionalProperties = additionalProperties == null
? Collections.<String, Object>emptyMap()
: Collections.unmodifiableMap(new LinkedHashMap<>(additionalProperties));
this.additionalProperties = D1ImmutableValues.immutableMap(additionalProperties);
}

/**
Expand Down
4 changes: 2 additions & 2 deletions src/main/java/io/github/xxvw/cloudflare/d1/D1Result.java
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
package io.github.xxvw.cloudflare.d1;

import io.github.xxvw.cloudflare.d1.internal.D1ImmutableValues;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
Expand Down Expand Up @@ -134,7 +134,7 @@ private static List<Map<String, Object>> immutableRows(List<Map<String, Object>>
}
List<Map<String, Object>> copy = new ArrayList<>(rows.size());
for (Map<String, Object> row : rows) {
copy.add(Collections.unmodifiableMap(new LinkedHashMap<>(row)));
copy.add(D1ImmutableValues.immutableMap(row));
}
return Collections.unmodifiableList(copy);
}
Expand Down
7 changes: 2 additions & 5 deletions src/main/java/io/github/xxvw/cloudflare/d1/D1Timings.java
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package io.github.xxvw.cloudflare.d1;

import java.util.Collections;
import java.util.LinkedHashMap;
import io.github.xxvw.cloudflare.d1.internal.D1ImmutableValues;
import java.util.Map;

/**
Expand All @@ -19,9 +18,7 @@ public final class D1Timings {
*/
public D1Timings(double sqlDurationMs, Map<String, Object> additionalProperties) {
this.sqlDurationMs = sqlDurationMs;
this.additionalProperties = additionalProperties == null
? Collections.<String, Object>emptyMap()
: Collections.unmodifiableMap(new LinkedHashMap<>(additionalProperties));
this.additionalProperties = D1ImmutableValues.immutableMap(additionalProperties);
}

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
package io.github.xxvw.cloudflare.d1.internal;

import java.util.ArrayList;
import java.util.Collections;
import java.util.IdentityHashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

/**
* Creates immutable defensive copies of JSON-shaped maps and lists.
*/
public final class D1ImmutableValues {
private static final String CYCLIC_VALUES_MESSAGE =
"cyclic map/list values are not supported";

private D1ImmutableValues() {}

public static Map<String, Object> immutableMap(Map<String, ?> values) {
if (values == null || values.isEmpty()) {
return Collections.emptyMap();
}
return immutableStringMap(values, new IdentityHashMap<Object, Boolean>());
}

public static List<Object> immutableList(List<?> values) {
if (values == null || values.isEmpty()) {
return Collections.emptyList();
}
return immutableObjectList(values, new IdentityHashMap<Object, Boolean>());
}

private static Map<String, Object> immutableStringMap(
Map<String, ?> values, IdentityHashMap<Object, Boolean> visiting) {
enter(values, visiting);
try {
Map<String, Object> copy = new LinkedHashMap<>();
for (Map.Entry<String, ?> entry : values.entrySet()) {
copy.put(entry.getKey(), immutableValue(entry.getValue(), visiting));
}
return Collections.unmodifiableMap(copy);
} finally {
visiting.remove(values);
}
}

private static Map<Object, Object> immutableObjectMap(
Map<?, ?> values, IdentityHashMap<Object, Boolean> visiting) {
enter(values, visiting);
try {
Map<Object, Object> copy = new LinkedHashMap<>();
for (Map.Entry<?, ?> entry : values.entrySet()) {
copy.put(entry.getKey(), immutableValue(entry.getValue(), visiting));
}
return Collections.unmodifiableMap(copy);
} finally {
visiting.remove(values);
}
}

private static List<Object> immutableObjectList(
List<?> values, IdentityHashMap<Object, Boolean> visiting) {
enter(values, visiting);
try {
List<Object> copy = new ArrayList<>(values.size());
for (Object value : values) {
copy.add(immutableValue(value, visiting));
}
return Collections.unmodifiableList(copy);
} finally {
visiting.remove(values);
}
}

private static Object immutableValue(
Object value, IdentityHashMap<Object, Boolean> visiting) {
if (value instanceof Map<?, ?>) {
return immutableObjectMap((Map<?, ?>) value, visiting);
}
if (value instanceof List<?>) {
return immutableObjectList((List<?>) value, visiting);
}
return value;
}

private static void enter(Object value, IdentityHashMap<Object, Boolean> visiting) {
if (visiting.put(value, Boolean.TRUE) != null) {
throw new IllegalArgumentException(CYCLIC_VALUES_MESSAGE);
}
}
}
Loading