You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
After GH-130 / commit 169bd95 made @Computed properties lazy by
default, bare Record#map(), Record#json(), and Audience#frame()
exclude computed properties unless the caller either positively names
them or opts in via SerializationOptions.builder().includeComputedValuesByDefault(true).
The positive-naming escape hatch is awkward when a Record has many
intrinsic/derived properties: to layer a single computed property
onto the default payload, the caller must enumerate every other key
they want. The serialization options flag is the opposite problem
— it opts in to all computed properties at once, defeating
the laziness guarantee for any expensive ones.
This issue proposes a third selection mode that closes the gap: a + prefix that means "include this on top of the defaults."
Record#map and friends already recognize - for negative
filtering. We add + as a default-augmenting counterpart, governed
by a clean three-mode dispatch on the shape of the key list.
The trigger is the presence of a bare positive key, not +. A
bare positive key signals whitelist intent — "I want only what
I'm listing, defaults do not apply." A + prefix is the escape
hatch: it ensures a key is included regardless of whether defaults
would have it.
additive = every +-prefixed key (with the + stripped)
bare = every positive key without a prefix
exclude = every --prefixed key (with the - stripped)
Confirmed behaviors
Bare keys are never mode-dependent. A bare key always means
"include only this." map("name") returns {name} whether or
not +other is also in the call. The bare key forces whitelist
mode and defaults drop out.
+ in whitelist mode is a redundant annotation. When at
least one bare positive is present, +foo and foo resolve to
the same thing — both are members of the whitelist. The + does no harm; it just doesn't unlock defaults once a bare
key has already locked them out.
+ on a key already in defaults is a no-op in additive mode. map("+name") returns the defaults exactly — same keys,
same values, no duplication for collection-typed entries.
Exclusion wins over addition. When the same key appears with
both + and -, the - excludes it. In additive mode this
means defaults minus the key; in whitelist mode it drops the
key from the whitelist.
Backward compatibility
Every legacy call shape is preserved by the trigger rule, because
no legacy call ever uses +.
Call
Today
After
map()
defaults
defaults (defaults mode)
map("a", "b")
only a, b
only a, b (whitelist)
map("-a", "-b")
defaults minus a, b
defaults minus a, b (defaults)
map("-a", "b", "-c")
only b (existing oddity tested at RecordDataAccessTest#testGetNegativeAndPositiveFiltering)
only b (whitelist; bare b triggers, -a/-c are no-ops since they were never in the whitelist) — unchanged
Pool selection grows a third branch. Extract the per-key
resolver (currently inline at lines 1501–1532) as a
private helper resolveEntry(String, SerializationOptions)
so all three branches can share it.
if(mode == Mode.DEFAULTS) {
// existing bare-defaults branchpool = data().entrySet().stream();
if(!options.includeComputedValuesByDefault()) {
pool = pool.filter(e -> !(einstanceofComputedEntry));
}
}
elseif(mode == Mode.ADDITIVE) {
// Skip baseline entries only when an additive key is a// bare root (no nav stops). For navigation additives// like "+company.name", let MergeStrategies::upsert// deep-merge the slice into the baseline's nested map.Set<String> bareAdditiveRoots = additive.stream()
.filter(k -> !k.contains("."))
.collect(Collectors.toSet());
Stream<Entry<String, Object>> baseline = data().entrySet()
.stream();
if(!options.includeComputedValuesByDefault()) {
baseline = baseline
.filter(e -> !(einstanceofComputedEntry));
}
baseline = baseline
.filter(e -> !bareAdditiveRoots.contains(e.getKey()));
Stream<Entry<String, Object>> added = additive.stream()
.map(key -> resolveEntry(key, options));
pool = Stream.concat(baseline, added);
}
else {
// WHITELIST: resolve bare ∪ additive (treat + identically// to bare for resolution purposes). Exclude filter runs// downstream as it does today.List<String> whitelist = Lists.newArrayList(bare);
whitelist.addAll(additive);
pool = whitelist.stream()
.map(key -> resolveEntry(key, options));
}
The narrower skip predicate (bare-root additives only)
resolves a bug in an earlier draft of this plan: a broader
skip would have caused map("+company.name") to lose every
non-name field of the company baseline. With the narrower
predicate, the navigation slice merges into the full company baseline via upsert, preserving the other fields.
Javadoc on both map overloads (and the matching json
overloads, which delegate) gains an "Additive mode" paragraph
and a "Whitelist mode" paragraph describing the trigger rule.
The existing "negative filtering" paragraph is preserved
verbatim.
Current location: src/main/java/com/cinchapi/runway/access/Audience.java,
around line 324.
Today the parser splits each user-provided key on . and uses toks[0] (the root) as the access-control matching key. A +-
or --prefixed root will not match the audience's readable set
(which contains bare field names), so the prefixed key would be
silently dropped during the intersection check around line 400.
Fix: strip the + or - prefix before matching against readable, then re-attach it when building the visible array
that is passed to subject.map(options, visible). The existing
handling of - entries inside the readable set (line 393,
where - in the access matrix means "denied") is unrelated and
unchanged.
This fix also closes a latent bug: today audience.frame(ImmutableSet.of("-name"), record) only works when
the audience's readable set is ALL_KEYS; with any non-trivial
access control, -name is dropped at the intersection and the
caller silently gets an empty result. After the fix, both + and - survive the access check.
Tests
New file: src/test/java/com/cinchapi/runway/RecordAdditiveKeysTest.java
Modeled on RecordIncludeComputedValuesTest.java. The fixture
includes an intrinsic field, an intrinsic Set<String> field (to
exercise the no-duplicate guarantee), a counter-instrumented @Computed property, and a linked-record field (to exercise
navigation merging).
testAdditiveAndNegativeMix — map("+label", "-name")
returns defaults minus name plus label; supplier fires once.
testAdditiveOnIntrinsicIsNoOp — map("+name") equals
the bare-defaults result; computed supplier does not fire.
testAdditiveOnIntrinsicCollectionDoesNotDuplicate — map("+tags") returns the tags set with its original size,
not concatenated. Locks in the no-double-merge guarantee
that motivates the bare-root skip in the implementation.
testNegativeWinsOverAdditiveForSameKey — map("+label", "-label") excludes label; supplier does not fire.
testBareKeyWithPlusKeyIsWhitelistOnly — map("+label", "name") returns only {name, label}; other
default keys (e.g., tags) are not in the result. This is
the explicit refutation of the earlier "modifier mode
promotes bare keys to additive" reading.
testLegacyOddityPreservedInWhitelistMode — map("-name", "tagsAlt", "-other") still returns only the
bare positive (whitelist mode; - keys are no-ops since
they were never on the whitelist). Mirrors RecordDataAccessTest#testGetNegativeAndPositiveFiltering.
testAdditiveNavigationKeyMergesIntoBaseline — map("+linkedGadget.name") keeps the full linkedGadget
defaults and ensures the navigation slice resolves
correctly. Locks in the narrower-skip-predicate fix.
testAdditiveWithJson — json("+label") produces JSON
that contains the computed value alongside defaults.
testNoPositiveKeysIsDefaultsBranch — sanity check
that map() and map("-name") are unchanged.
New file: src/test/java/com/cinchapi/runway/access/AudienceFrameAdditiveKeyAccessControlTest.java
Modeled on AudienceFrameOptionsTest.java. Uses a fixture with
restricted readable rules to exercise the access-control
intersection.
testFrameWithAdditiveKeyOnReadableField —
restricted readable that includes the additive's bare
root; the + survives the intersection (prefix strip) and
reaches subject.map.
testFrameDropsAdditiveKeyOnUnreadableField —
restricted readable that excludes the additive's bare
root; the +key is dropped at the access check so the
caller cannot use + to bypass access control.
Existing regression test (already in tree)
src/test/java/com/cinchapi/runway/access/AudienceFrameNegativeKeyAccessControlTest.java
demonstrates the latent --prefix bug in frame against a
restricted readable set. Currently failing; expected to pass
once the prefix-strip-before-intersection fix lands.
Tests that must remain green
src/test/java/com/cinchapi/runway/RecordDataAccessTest.java and src/test/java/com/cinchapi/runway/RecordIncludeComputedValuesTest.java
must continue to pass unmodified.
Out of scope
No new SerializationOptions field. + is purely a parsing
concern at the call site.
Summary
After GH-130 / commit 169bd95 made
@Computedproperties lazy bydefault, bare
Record#map(),Record#json(), andAudience#frame()exclude computed properties unless the caller either positively names
them or opts in via
SerializationOptions.builder().includeComputedValuesByDefault(true).The positive-naming escape hatch is awkward when a Record has many
intrinsic/derived properties: to layer a single computed property
onto the default payload, the caller must enumerate every other key
they want. The serialization options flag is the opposite problem
— it opts in to all computed properties at once, defeating
the laziness guarantee for any expensive ones.
This issue proposes a third selection mode that closes the gap: a
+prefix that means "include this on top of the defaults."Semantics
Record#mapand friends already recognize-for negativefiltering. We add
+as a default-augmenting counterpart, governedby a clean three-mode dispatch on the shape of the key list.
The trigger is the presence of a bare positive key, not
+. Abare positive key signals whitelist intent — "I want only what
I'm listing, defaults do not apply." A
+prefix is the escapehatch: it ensures a key is included regardless of whether defaults
would have it.
Three modes
-keys)defaults − exclude+-prefixed positives(defaults ∪ additive) − exclude(bare ∪ additive) − excludeWhere:
defaults= intrinsic + derived (+ computed iffincludeComputedValuesByDefaultistrue)additive= every+-prefixed key (with the+stripped)bare= every positive key without a prefixexclude= every--prefixed key (with the-stripped)Confirmed behaviors
"include only this."
map("name")returns{name}whether ornot
+otheris also in the call. The bare key forces whitelistmode and defaults drop out.
+in whitelist mode is a redundant annotation. When atleast one bare positive is present,
+fooandfooresolve tothe same thing — both are members of the whitelist. The
+does no harm; it just doesn't unlock defaults once a barekey has already locked them out.
+on a key already in defaults is a no-op in additive mode.map("+name")returns the defaults exactly — same keys,same values, no duplication for collection-typed entries.
both
+and-, the-excludes it. In additive mode thismeans defaults minus the key; in whitelist mode it drops the
key from the whitelist.
Backward compatibility
Every legacy call shape is preserved by the trigger rule, because
no legacy call ever uses
+.map()map("a", "b")a,ba,b(whitelist)map("-a", "-b")a,ba,b(defaults)map("-a", "b", "-c")b(existing oddity tested atRecordDataAccessTest#testGetNegativeAndPositiveFiltering)b(whitelist; barebtriggers,-a/-care no-ops since they were never in the whitelist) — unchangedImplementation outline
Record.java—map(SerializationOptions, String...)Current location:
src/main/java/com/cinchapi/runway/Record.java,the
map(SerializationOptions, String...)overload (around line1455).
Parsing block fans into three buckets and resolves the mode
from whether any bare positive was supplied.
Pool selection grows a third branch. Extract the per-key
resolver (currently inline at lines 1501–1532) as a
private helper
resolveEntry(String, SerializationOptions)so all three branches can share it.
The narrower skip predicate (bare-root additives only)
resolves a bug in an earlier draft of this plan: a broader
skip would have caused
map("+company.name")to lose everynon-
namefield of thecompanybaseline. With the narrowerpredicate, the navigation slice merges into the full
companybaseline viaupsert, preserving the other fields.Javadoc on both
mapoverloads (and the matchingjsonoverloads, which delegate) gains an "Additive mode" paragraph
and a "Whitelist mode" paragraph describing the trigger rule.
The existing "negative filtering" paragraph is preserved
verbatim.
Audience.java—frame(SerializationOptions, Collection<String>, T)Current location:
src/main/java/com/cinchapi/runway/access/Audience.java,around line 324.
Today the parser splits each user-provided key on
.and usestoks[0](the root) as the access-control matching key. A+-or
--prefixed root will not match the audience'sreadableset(which contains bare field names), so the prefixed key would be
silently dropped during the intersection check around line 400.
Fix: strip the
+or-prefix before matching againstreadable, then re-attach it when building thevisiblearraythat is passed to
subject.map(options, visible). The existinghandling of
-entries inside thereadableset (line 393,where
-in the access matrix means "denied") is unrelated andunchanged.
This fix also closes a latent bug: today
audience.frame(ImmutableSet.of("-name"), record)only works whenthe audience's readable set is
ALL_KEYS; with any non-trivialaccess control,
-nameis dropped at the intersection and thecaller silently gets an empty result. After the fix, both
+and-survive the access check.Tests
New file:
src/test/java/com/cinchapi/runway/RecordAdditiveKeysTest.javaModeled on
RecordIncludeComputedValuesTest.java. The fixtureincludes an intrinsic field, an intrinsic
Set<String>field (toexercise the no-duplicate guarantee), a counter-instrumented
@Computedproperty, and a linked-record field (to exercisenavigation merging).
testAdditivePrefixIncludesComputedOnTopOfDefaults—map("+label")returns defaults plus label; supplier firesexactly once.
testAdditiveAndNegativeMix—map("+label", "-name")returns defaults minus name plus label; supplier fires once.
testAdditiveOnIntrinsicIsNoOp—map("+name")equalsthe bare-defaults result; computed supplier does not fire.
testAdditiveOnIntrinsicCollectionDoesNotDuplicate—map("+tags")returns the tags set with its original size,not concatenated. Locks in the no-double-merge guarantee
that motivates the bare-root skip in the implementation.
testNegativeWinsOverAdditiveForSameKey—map("+label", "-label")excludes label; supplier doesnot fire.
testBareKeyWithPlusKeyIsWhitelistOnly—map("+label", "name")returns only{name, label}; otherdefault keys (e.g.,
tags) are not in the result. This isthe explicit refutation of the earlier "modifier mode
promotes bare keys to additive" reading.
testLegacyOddityPreservedInWhitelistMode—map("-name", "tagsAlt", "-other")still returns only thebare positive (whitelist mode;
-keys are no-ops sincethey were never on the whitelist). Mirrors
RecordDataAccessTest#testGetNegativeAndPositiveFiltering.testAdditiveNavigationKeyMergesIntoBaseline—map("+linkedGadget.name")keeps the fulllinkedGadgetdefaults and ensures the navigation slice resolves
correctly. Locks in the narrower-skip-predicate fix.
testAdditiveWithJson—json("+label")produces JSONthat contains the computed value alongside defaults.
testNoPositiveKeysIsDefaultsBranch— sanity checkthat
map()andmap("-name")are unchanged.New file:
src/test/java/com/cinchapi/runway/access/AudienceFrameAdditiveKeyAccessControlTest.javaModeled on
AudienceFrameOptionsTest.java. Uses a fixture withrestricted readable rules to exercise the access-control
intersection.
testFrameWithAdditiveKeyUnderAllKeysReadable—audience.frame(ImmutableSet.of("+computedKey"), subject)under
ALL_KEYSreadable returns defaults + computed.testFrameWithAdditiveKeyOnReadableField—restricted readable that includes the additive's bare
root; the
+survives the intersection (prefix strip) andreaches
subject.map.testFrameDropsAdditiveKeyOnUnreadableField—restricted readable that excludes the additive's bare
root; the
+keyis dropped at the access check so thecaller cannot use
+to bypass access control.Existing regression test (already in tree)
src/test/java/com/cinchapi/runway/access/AudienceFrameNegativeKeyAccessControlTest.javademonstrates the latent
--prefix bug inframeagainst arestricted readable set. Currently failing; expected to pass
once the prefix-strip-before-intersection fix lands.
Tests that must remain green
src/test/java/com/cinchapi/runway/RecordDataAccessTest.javaandsrc/test/java/com/cinchapi/runway/RecordIncludeComputedValuesTest.javamust continue to pass unmodified.
Out of scope
SerializationOptionsfield.+is purely a parsingconcern at the call site.
@Computed,$computed(), or the lazy-supplierdiscovery added in GH-128: SerializationOptions.includeComputedValuesByDefault #130 / commit 169bd95.
change that propagates through
map,json, andframe.+prefix does not introduce a new sort/order semanticelsewhere; existing
</>sort prefixes oncompareTokeys are unrelated and untouched.