tagged-urn 0.40.107 → 0.41.110
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/tagged-urn.js +109 -0
- package/tagged-urn.test.js +32 -0
package/package.json
CHANGED
package/tagged-urn.js
CHANGED
|
@@ -110,6 +110,25 @@ const Form = {
|
|
|
110
110
|
MUST_NOT_HAVE: 6, // "!"
|
|
111
111
|
};
|
|
112
112
|
|
|
113
|
+
const TaggedUrnRelationKind = Object.freeze({
|
|
114
|
+
EQUIVALENT: 'equivalent',
|
|
115
|
+
COMPARABLE: 'comparable',
|
|
116
|
+
INCOMPARABLE: 'incomparable',
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
class TaggedUrnCoordinateDelta {
|
|
120
|
+
constructor(prefix, removed = {}, added = {}, relationKind = TaggedUrnRelationKind.EQUIVALENT) {
|
|
121
|
+
this.prefix = prefix.toLowerCase();
|
|
122
|
+
this.removed = { ...removed };
|
|
123
|
+
this.added = { ...added };
|
|
124
|
+
this.relationKind = relationKind;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
isEmpty() {
|
|
128
|
+
return Object.keys(this.removed).length === 0 && Object.keys(this.added).length === 0;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
113
132
|
// Classify a stored value. Returns { kind, raw } — raw is the inner
|
|
114
133
|
// v for ?=v and !=v, the literal value for exact, and '' otherwise.
|
|
115
134
|
function classifyForm(value) {
|
|
@@ -828,6 +847,94 @@ class TaggedUrn {
|
|
|
828
847
|
return this.isComparable(other);
|
|
829
848
|
}
|
|
830
849
|
|
|
850
|
+
/**
|
|
851
|
+
* Compute the coordinate-space delta from `base` to `this`.
|
|
852
|
+
*
|
|
853
|
+
* Delta is defined over explicit canonical coordinates, not semantic
|
|
854
|
+
* equivalence classes. Equivalent URNs may still yield a non-empty delta if
|
|
855
|
+
* one side explicitly authors no-op coordinates.
|
|
856
|
+
*
|
|
857
|
+
* @param {TaggedUrn} base
|
|
858
|
+
* @returns {TaggedUrnCoordinateDelta}
|
|
859
|
+
* @throws {TaggedUrnError} If prefixes do not match
|
|
860
|
+
*/
|
|
861
|
+
deltaFrom(base) {
|
|
862
|
+
if (!base) {
|
|
863
|
+
throw new TaggedUrnError(ErrorCodes.INVALID_FORMAT, 'cannot derive delta from null URN');
|
|
864
|
+
}
|
|
865
|
+
if (this.prefix !== base.prefix) {
|
|
866
|
+
throw new TaggedUrnError(
|
|
867
|
+
ErrorCodes.PREFIX_MISMATCH,
|
|
868
|
+
`Cannot compare URNs with different prefixes: '${base.prefix}' vs '${this.prefix}'`
|
|
869
|
+
);
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
const relationKind = this.isEquivalent(base)
|
|
873
|
+
? TaggedUrnRelationKind.EQUIVALENT
|
|
874
|
+
: (this.isComparable(base)
|
|
875
|
+
? TaggedUrnRelationKind.COMPARABLE
|
|
876
|
+
: TaggedUrnRelationKind.INCOMPARABLE);
|
|
877
|
+
|
|
878
|
+
const removed = {};
|
|
879
|
+
const added = {};
|
|
880
|
+
const allKeys = new Set([
|
|
881
|
+
...Object.keys(base.tags),
|
|
882
|
+
...Object.keys(this.tags),
|
|
883
|
+
]);
|
|
884
|
+
|
|
885
|
+
for (const key of allKeys) {
|
|
886
|
+
const baseValue = Object.prototype.hasOwnProperty.call(base.tags, key)
|
|
887
|
+
? base.tags[key]
|
|
888
|
+
: undefined;
|
|
889
|
+
const targetValue = Object.prototype.hasOwnProperty.call(this.tags, key)
|
|
890
|
+
? this.tags[key]
|
|
891
|
+
: undefined;
|
|
892
|
+
if (baseValue === targetValue) {
|
|
893
|
+
continue;
|
|
894
|
+
}
|
|
895
|
+
if (baseValue !== undefined) {
|
|
896
|
+
removed[key] = baseValue;
|
|
897
|
+
}
|
|
898
|
+
if (targetValue !== undefined) {
|
|
899
|
+
added[key] = targetValue;
|
|
900
|
+
}
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
return new TaggedUrnCoordinateDelta(this.prefix, removed, added, relationKind);
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
/**
|
|
907
|
+
* Apply a coordinate-space delta to this tagged URN.
|
|
908
|
+
*
|
|
909
|
+
* Keys named in `removed` are deleted regardless of their current value,
|
|
910
|
+
* then keys named in `added` are inserted. Unrelated coordinates are
|
|
911
|
+
* preserved.
|
|
912
|
+
*
|
|
913
|
+
* @param {TaggedUrnCoordinateDelta} delta
|
|
914
|
+
* @returns {TaggedUrn}
|
|
915
|
+
* @throws {TaggedUrnError} If prefixes do not match
|
|
916
|
+
*/
|
|
917
|
+
applyDelta(delta) {
|
|
918
|
+
if (!delta) {
|
|
919
|
+
throw new TaggedUrnError(ErrorCodes.INVALID_FORMAT, 'cannot apply null delta');
|
|
920
|
+
}
|
|
921
|
+
if (this.prefix !== delta.prefix) {
|
|
922
|
+
throw new TaggedUrnError(
|
|
923
|
+
ErrorCodes.PREFIX_MISMATCH,
|
|
924
|
+
`Cannot apply delta with different prefix: '${delta.prefix}' vs '${this.prefix}'`
|
|
925
|
+
);
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
const nextTags = { ...this.tags };
|
|
929
|
+
for (const key of Object.keys(delta.removed)) {
|
|
930
|
+
delete nextTags[key];
|
|
931
|
+
}
|
|
932
|
+
for (const [key, value] of Object.entries(delta.added)) {
|
|
933
|
+
nextTags[key] = value;
|
|
934
|
+
}
|
|
935
|
+
return new TaggedUrn(this.prefix, nextTags, true);
|
|
936
|
+
}
|
|
937
|
+
|
|
831
938
|
/**
|
|
832
939
|
* Create a new URN with a specific tag set to wildcard
|
|
833
940
|
*
|
|
@@ -1075,6 +1182,8 @@ class UrnMatcher {
|
|
|
1075
1182
|
// Export for CommonJS
|
|
1076
1183
|
module.exports = {
|
|
1077
1184
|
TaggedUrn,
|
|
1185
|
+
TaggedUrnCoordinateDelta,
|
|
1186
|
+
TaggedUrnRelationKind,
|
|
1078
1187
|
TaggedUrnBuilder,
|
|
1079
1188
|
UrnMatcher,
|
|
1080
1189
|
TaggedUrnError,
|
package/tagged-urn.test.js
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
const {
|
|
6
6
|
TaggedUrn,
|
|
7
|
+
TaggedUrnRelationKind,
|
|
7
8
|
TaggedUrnBuilder,
|
|
8
9
|
UrnMatcher,
|
|
9
10
|
TaggedUrnError,
|
|
@@ -1495,4 +1496,35 @@ module.exports = { runTests };
|
|
|
1495
1496
|
);
|
|
1496
1497
|
})();
|
|
1497
1498
|
|
|
1499
|
+
// TEST596: Empty delta preserves a more specific concrete instance.
|
|
1500
|
+
(() => {
|
|
1501
|
+
const base = TaggedUrn.fromString('media:');
|
|
1502
|
+
const target = TaggedUrn.fromString('media:');
|
|
1503
|
+
const runtime = TaggedUrn.fromString('media:image;png');
|
|
1504
|
+
const delta = target.deltaFrom(base);
|
|
1505
|
+
assertEqual(delta.relationKind, TaggedUrnRelationKind.EQUIVALENT, 'TEST596: empty delta should be equivalent');
|
|
1506
|
+
assert(delta.isEmpty(), 'TEST596: empty delta should have no edits');
|
|
1507
|
+
assertEqual(runtime.applyDelta(delta).toString(), 'media:image;png', 'TEST596: empty delta should preserve runtime refinement');
|
|
1508
|
+
})();
|
|
1509
|
+
|
|
1510
|
+
// TEST597: Incomparable replacement rewrites only declared coordinates.
|
|
1511
|
+
(() => {
|
|
1512
|
+
const base = TaggedUrn.fromString('media:markdown;textable');
|
|
1513
|
+
const target = TaggedUrn.fromString('media:html;textable');
|
|
1514
|
+
const runtime = TaggedUrn.fromString('media:lang=en;markdown;textable');
|
|
1515
|
+
const delta = target.deltaFrom(base);
|
|
1516
|
+
assertEqual(delta.relationKind, TaggedUrnRelationKind.INCOMPARABLE, 'TEST597: markdown -> html should be incomparable');
|
|
1517
|
+
assertEqual(runtime.applyDelta(delta).toString(), 'media:html;lang=en;textable', 'TEST597: unrelated runtime refinements must be preserved');
|
|
1518
|
+
})();
|
|
1519
|
+
|
|
1520
|
+
// TEST598: Comparable removal drops only the declared key.
|
|
1521
|
+
(() => {
|
|
1522
|
+
const base = TaggedUrn.fromString('media:image;png');
|
|
1523
|
+
const target = TaggedUrn.fromString('media:image');
|
|
1524
|
+
const runtime = TaggedUrn.fromString('media:author=me;image;png');
|
|
1525
|
+
const delta = target.deltaFrom(base);
|
|
1526
|
+
assertEqual(delta.relationKind, TaggedUrnRelationKind.COMPARABLE, 'TEST598: image;png -> image should be comparable');
|
|
1527
|
+
assertEqual(runtime.applyDelta(delta).toString(), 'media:author=me;image', 'TEST598: only the declared key should be removed');
|
|
1528
|
+
})();
|
|
1529
|
+
|
|
1498
1530
|
console.log('All tests passed!');
|