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
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ public void onGeometry(ModifiableContext<FeatureSchema, SchemaMapping> context)
}

if (Objects.nonNull(context.geometry())
|| context.schema().get().isRequired()
|| isEffectivelyRequired(context)
|| !removeNullValues.getOrDefault(context.type(), true)) {
openIfNecessary(context);

Expand All @@ -122,14 +122,35 @@ public void onValue(ModifiableContext<FeatureSchema, SchemaMapping> context) {
}

if (Objects.nonNull(context.value())
|| context.schema().get().isRequired()
|| isEffectivelyRequired(context)
|| !removeNullValues.getOrDefault(context.type(), true)) {
openIfNecessary(context);

super.onValue(context);
}
}

/**
* A null value is kept only if its property is effectively required, i.e. the property itself and
* every wrapping property up to (but excluding) the feature are required. A required property
* inside an optional object must not keep that object alive when all of its values are null: in
* that case the optional wrapper, together with the null value, is removed. Without this check
* the wrapping property is only ever looked at indirectly, so an optional object whose required
* sub-properties are all null is emitted as an empty object instead of being omitted.
*/
private boolean isEffectivelyRequired(ModifiableContext<FeatureSchema, SchemaMapping> context) {
if (context.schema().isEmpty() || !context.schema().get().isRequired()) {
return false;
}

List<FeatureSchema> parentSchemas = context.parentSchemas();

return parentSchemas.size() <= 1
|| parentSchemas.stream()
.limit(parentSchemas.size() - 1L)
.allMatch(FeatureSchema::isRequired);
}

private void openIfNecessary(ModifiableContext<FeatureSchema, SchemaMapping> context) {
if (!schemaStack.isEmpty()) {
List<String> previousPath = context.path();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,34 @@ class FeatureTokenTransformerRemoveEmptyOptionalsSpec extends Specification {

}

def 'single feature optional object with only null required sub-properties'() {

given:

when:

FeatureTokenFixtures.SINGLE_FEATURE_NESTED_OBJECT_REQUIRED_NULLS.forEach(token -> tokenReader.onToken(token))

then:

tokens == FeatureTokenFixtures.SINGLE_FEATURE

}

def 'single feature optional object with a non-null and a null required sub-property'() {

given:

when:

FeatureTokenFixtures.SINGLE_FEATURE_NESTED_OBJECT_REQUIRED_NULLS_PARTIAL.forEach(token -> tokenReader.onToken(token))

then:

tokens == FeatureTokenFixtures.SINGLE_FEATURE_NESTED_OBJECT_REQUIRED_NULLS_PARTIAL_REDUCED

}

def 'single feature value array'() {

given:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -429,6 +429,23 @@ class FeatureSchemaFixtures {
.required(true)
.build())
.sourcePath("name"))
.putProperties2("optional_with_required",
new ImmutableFeatureSchema.Builder()
.type(Type.OBJECT)
.putProperties2("numerator",
new ImmutableFeatureSchema.Builder()
.type(Type.FLOAT)
.constraints(new ImmutableSchemaConstraints.Builder()
.required(true)
.build())
.sourcePath("numerator"))
.putProperties2("denominator",
new ImmutableFeatureSchema.Builder()
.type(Type.FLOAT)
.constraints(new ImmutableSchemaConstraints.Builder()
.required(true)
.build())
.sourcePath("denominator")))
.build()

public static final SchemaMapping BIOTOP_MAPPING = new ImmutableSchemaMapping.Builder()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,86 @@ class FeatureTokenFixtures {
FeatureTokenType.INPUT_END
]

public static final List<Object> SINGLE_FEATURE_NESTED_OBJECT_REQUIRED_NULLS = [
FeatureTokenType.INPUT,
true,
FeatureTokenType.FEATURE,
FeatureTokenType.VALUE,
["id"],
"24",
Type.STRING,
FeatureTokenType.OBJECT,
["optional_with_required"],
FeatureTokenType.VALUE,
["optional_with_required", "numerator"],
null,
Type.FLOAT,
FeatureTokenType.VALUE,
["optional_with_required", "denominator"],
null,
Type.FLOAT,
FeatureTokenType.OBJECT_END,
["optional_with_required"],
FeatureTokenType.VALUE,
["kennung"],
"611320001-1",
Type.STRING,
FeatureTokenType.FEATURE_END,
FeatureTokenType.INPUT_END
]

public static final List<Object> SINGLE_FEATURE_NESTED_OBJECT_REQUIRED_NULLS_PARTIAL = [
FeatureTokenType.INPUT,
true,
FeatureTokenType.FEATURE,
FeatureTokenType.VALUE,
["id"],
"24",
Type.STRING,
FeatureTokenType.OBJECT,
["optional_with_required"],
FeatureTokenType.VALUE,
["optional_with_required", "numerator"],
"0.5",
Type.FLOAT,
FeatureTokenType.VALUE,
["optional_with_required", "denominator"],
null,
Type.FLOAT,
FeatureTokenType.OBJECT_END,
["optional_with_required"],
FeatureTokenType.VALUE,
["kennung"],
"611320001-1",
Type.STRING,
FeatureTokenType.FEATURE_END,
FeatureTokenType.INPUT_END
]

public static final List<Object> SINGLE_FEATURE_NESTED_OBJECT_REQUIRED_NULLS_PARTIAL_REDUCED = [
FeatureTokenType.INPUT,
true,
FeatureTokenType.FEATURE,
FeatureTokenType.VALUE,
["id"],
"24",
Type.STRING,
FeatureTokenType.OBJECT,
["optional_with_required"],
FeatureTokenType.VALUE,
["optional_with_required", "numerator"],
"0.5",
Type.FLOAT,
FeatureTokenType.OBJECT_END,
["optional_with_required"],
FeatureTokenType.VALUE,
["kennung"],
"611320001-1",
Type.STRING,
FeatureTokenType.FEATURE_END,
FeatureTokenType.INPUT_END
]

public static final List<Object> SINGLE_FEATURE_VALUE_ARRAY = [
FeatureTokenType.INPUT,
true,
Expand Down
Loading