Add compact test and update to latest jsonld.js.
authorDave Longley <dlongley@digitalbazaar.com>
Sat, 30 Jun 2012 21:37:44 -0400
changeset 768 cedc42e247d2
parent 767 b53e28df4bfd
child 769 d340229df708
Add compact test and update to latest jsonld.js.

- Add compaction test for the case where an @id could be
compacted but the term is mapped to a @list @container.
- Fix duplicate output in compact 15.
playground/jsonld.js
test-suite/tests/compact-0015-out.jsonld
test-suite/tests/compact-0020-context.jsonld
test-suite/tests/compact-0020-in.jsonld
test-suite/tests/compact-0020-out.jsonld
test-suite/tests/compact-manifest.jsonld
--- a/playground/jsonld.js	Tue Jun 26 23:49:05 2012 -0400
+++ b/playground/jsonld.js	Sat Jun 30 21:37:44 2012 -0400
@@ -473,6 +473,12 @@
         return collateCallback(err);
       }
       if(statement !== null) {
+        // do not allow duplicate statements
+        for(var i in statements) {
+          if(_compareRdfStatements(statements[i], statement)) {
+            return;
+          }
+        }
         statements.push(statement);
       }
       else {
@@ -746,44 +752,44 @@
 };
 
 /**
- * Adds a value to a subject. If the subject already has the value, it will
- * not be added. If the value is an array, all values in the array will be
- * added.
- *
- * Note: If the value is a subject that already exists as a property of the
- * given subject, this method makes no attempt to deeply merge properties.
- * Instead, the value will not be added.
+ * Adds a value to a subject. If the value is an array, all values in the
+ * array will be added.
  *
  * @param subject the subject to add the value to.
  * @param property the property that relates the value to the subject.
  * @param value the value to add.
- * @param [propertyIsArray] true if the property is always an array, false
+ * @param [options] the options to use:
+ *        [propertyIsArray] true if the property is always an array, false
  *          if not (default: false).
- * @param [propertyIsList] true if the property is a @list, false
- *          if not (default: false).
+ *        [allowDuplicate] true to allow duplicates, false not to (uses a
+ *          simple shallow comparison of subject ID or value) (default: true).
  */
-jsonld.addValue = function(
-  subject, property, value, propertyIsArray, propertyIsList) {
-  propertyIsArray = _isUndefined(propertyIsArray) ? false : propertyIsArray;
-  propertyIsList = (_isUndefined(propertyIsList) ?
-    (property === '@list') : propertyIsList);
+jsonld.addValue = function(subject, property, value, options) {
+  options = options || {};
+  if(!('propertyIsArray' in options)) {
+    options.propertyIsArray = false;
+  }
+  if(!('allowDuplicate' in options)) {
+    options.allowDuplicate = true;
+  }
 
   if(_isArray(value)) {
-    if(value.length === 0 && propertyIsArray && !(property in subject)) {
+    if(value.length === 0 && options.propertyIsArray &&
+      !(property in subject)) {
       subject[property] = [];
     }
     for(var i in value) {
-      jsonld.addValue(
-        subject, property, value[i], propertyIsArray, propertyIsList);
+      jsonld.addValue(subject, property, value[i], options);
     }
   }
   else if(property in subject) {
-    // check if subject already has value unless property is list
-    var hasValue = (!propertyIsList &&
+    // check if subject already has value if duplicates not allowed
+    var hasValue = (!options.allowDuplicate &&
       jsonld.hasValue(subject, property, value));
 
     // make property an array if value not present or always an array
-    if(!_isArray(subject[property]) && (!hasValue || propertyIsArray)) {
+    if(!_isArray(subject[property]) &&
+      (!hasValue || options.propertyIsArray)) {
       subject[property] = [subject[property]];
     }
 
@@ -794,7 +800,7 @@
   }
   else {
     // add new value as set or single value
-    subject[property] = propertyIsArray ? [value] : value;
+    subject[property] = options.propertyIsArray ? [value] : value;
   }
 };
 
@@ -830,11 +836,15 @@
  * @param subject the subject.
  * @param property the property that relates the value to the subject.
  * @param value the value to remove.
- * @param [propertyIsArray] true if the property is always an array, false
- *          if not (default: false).
+ * @param [options] the options to use:
+ *          [propertyIsArray] true if the property is always an array, false
+ *            if not (default: false).
  */
-jsonld.removeValue = function(subject, property, value, propertyIsArray) {
-  propertyIsArray = _isUndefined(propertyIsArray) ? false : propertyIsArray;
+jsonld.removeValue = function(subject, property, value, options) {
+  options = options || {};
+  if(!('propertyIsArray' in options)) {
+    options.propertyIsArray = false;
+  }
 
   // filter out value
   var values = jsonld.getValues(subject, property).filter(function(e) {
@@ -844,7 +854,7 @@
   if(values.length === 0) {
     jsonld.removeProperty(subject, property);
   }
-  else if(values.length === 1 && !propertyIsArray) {
+  else if(values.length === 1 && !options.propertyIsArray) {
     subject[property] = values[0];
   }
   else {
@@ -1068,8 +1078,9 @@
     if(_isValue(element)) {
       // if @value is the only key
       if(Object.keys(element).length === 1) {
-        // if there is no default language, return value of @value
-        if(!('@language' in ctx)) {
+        // if there is no default language or @value is not a string,
+        // return value of @value
+        if(!('@language' in ctx) || !_isString(element['@value'])) {
           return element['@value'];
         }
         // return full element, alias @value
@@ -1139,7 +1150,7 @@
         // compact property and add value
         var prop = _compactIri(ctx, key);
         var isArray = (_isArray(value) && value.length === 0);
-        jsonld.addValue(rval, prop, value, isArray);
+        jsonld.addValue(rval, prop, value, {propertyIsArray: isArray});
         continue;
       }
 
@@ -1148,7 +1159,7 @@
       // preserve empty arrays
       if(value.length === 0) {
         var prop = _compactIri(ctx, key);
-        jsonld.addValue(rval, prop, [], true);
+        jsonld.addValue(rval, prop, [], {propertyIsArray: true});
       }
 
       // recusively process array values
@@ -1194,7 +1205,7 @@
           (_isArray(v) && v.length === 0));
 
         // add compact value
-        jsonld.addValue(rval, prop, v, isArray, (container === '@list'));
+        jsonld.addValue(rval, prop, v, {propertyIsArray: isArray});
       }
     }
     return rval;
@@ -1349,7 +1360,7 @@
         // add value, use an array if not @id, @type, @value, or @language
         var useArray = !(prop === '@id' || prop === '@type' ||
           prop === '@value' || prop === '@language');
-        jsonld.addValue(rval, prop, value, useArray);
+        jsonld.addValue(rval, prop, value, {propertyIsArray: useArray});
       }
     }
 
@@ -1461,6 +1472,11 @@
       return hashBlankNodes(Object.keys(bnodes));
     }
     // add statement and do mapping
+    for(var i in statements) {
+      if(_compareRdfStatements(statements[i], statement)) {
+        return;
+      }
+    }
     statements.push(statement);
     var nodes = ['subject', 'object'];
     for(var n in nodes) {
@@ -1652,22 +1668,24 @@
     var name = ('name' in statement) ? statement.name.nominalValue : '';
 
     // create a graph entry as needed
+    var graph;
     if(!(name in graphs)) {
-      var graph = graphs[name] = {subjects: {}, listMap: {}};
+      graph = graphs[name] = {subjects: {}, listMap: {}};
     }
     else {
-      var graph = graphs[name];
+      graph = graphs[name];
     }
 
     // handle element in @list
     if(p === RDF_FIRST) {
       // create list entry as needed
       var listMap = graph.listMap;
+      var entry;
       if(!(s in listMap)) {
-        var entry = listMap[s] = {};
+        entry = listMap[s] = {};
       }
       else {
-        var entry = listMap[s];
+        entry = listMap[s];
       }
       // set object value
       entry.first = _rdfToObject(o);
@@ -1680,11 +1698,12 @@
       if(o.interfaceName === 'BlankNode') {
         // create list entry as needed
         var listMap = graph.listMap;
+        var entry;
         if(!(s in listMap)) {
-          var entry = listMap[s] = {};
+          entry = listMap[s] = {};
         }
         else {
-          var entry = listMap[s];
+          entry = listMap[s];
         }
         entry.rest = o.nominalValue;
       }
@@ -1698,33 +1717,35 @@
 
     // add subject to graph as needed
     var subjects = graph.subjects;
+    var value;
     if(!(s in subjects)) {
-      var value = subjects[s] = {'@id': s};
+      value = subjects[s] = {'@id': s};
     }
     // use existing subject value
     else {
-      var value = subjects[s];
+      value = subjects[s];
     }
 
     // convert to @type unless options indicate to treat rdf:type as property
     if(p === RDF_TYPE && !options.notType) {
       // add value of object as @type
-      jsonld.addValue(value, '@type', o.nominalValue, true);
+      jsonld.addValue(value, '@type', o.nominalValue, {propertyIsArray: true});
     }
     else {
       // add property to value as needed
       var object = _rdfToObject(o);
-      jsonld.addValue(value, p, object, true);
+      jsonld.addValue(value, p, object, {propertyIsArray: true});
 
       // a bnode might be the beginning of a list, so add it to the list map
       if(o.interfaceName === 'BlankNode') {
         var id = object['@id'];
         var listMap = graph.listMap;
+        var entry;
         if(!(id in listMap)) {
-          var entry = listMap[id] = {};
+          entry = listMap[id] = {};
         }
         else {
-          var entry = listMap[id];
+          entry = listMap[id];
         }
         entry.head = object;
       }
@@ -2112,6 +2133,45 @@
 }
 
 /**
+ * Compares two RDF statements for equality.
+ *
+ * @param s1 the first statement.
+ * @param s2 the second statement.
+ *
+ * @return true if the statements are the same, false if not.
+ */
+function _compareRdfStatements(s1, s2) {
+  if(_isString(s1) || _isString(s2)) {
+    return s1 === s2;
+  }
+
+  var attrs = ['subject', 'property', 'object'];
+  for(var i in attrs) {
+    var attr = attrs[i];
+    if(s1[attr].interfaceName !== s2[attr].interfaceName ||
+      s1[attr].nominalValue !== s2[attr].nominalValue) {
+      return false;
+    }
+  }
+  if(s1.object.language !== s2.object.language) {
+    return false;
+  }
+  if(('datatype' in s1.object) !== ('datatype' in s2.object)) {
+    return false;
+  }
+  if('datatype' in s1.object) {
+    if(s1.object.datatype.interfaceName !== s2.object.datatype.interfaceName ||
+      s1.object.datatype.nominalValue !== s2.object.datatype.nominalValue) {
+      return false;
+    }
+  }
+  if(s1.name !== s2.name) {
+    return false;
+  }
+  return true;
+}
+
+/**
  * Converts a @list value into an embedded linked list of blank nodes in
  * expanded form. The resulting array can be used as an RDF-replacement for
  * a property that used a @list.
@@ -2430,7 +2490,7 @@
         var id = _isBlankNode(o) ? namer.getName(o['@id']) : o['@id'];
 
         // add reference and recurse
-        jsonld.addValue(subject, prop, {'@id': id}, true);
+        jsonld.addValue(subject, prop, {'@id': id}, {propertyIsArray: true});
         _flatten(o, graphs, graph, namer, id);
       }
       else {
@@ -2446,7 +2506,7 @@
         }
 
         // add non-subject
-        jsonld.addValue(subject, prop, o, true);
+        jsonld.addValue(subject, prop, o, {propertyIsArray: true});
       }
     }
   }
@@ -2781,8 +2841,8 @@
   else {
     // replace subject with reference
     var useArray = _isArray(parent[property]);
-    jsonld.removeValue(parent, property, subject, useArray);
-    jsonld.addValue(parent, property, subject, useArray);
+    jsonld.removeValue(parent, property, subject, {propertyIsArray: useArray});
+    jsonld.addValue(parent, property, subject, {propertyIsArray: useArray});
   }
 
   // recursively remove dependent dangling embeds
@@ -2811,7 +2871,7 @@
  */
 function _addFrameOutput(state, parent, property, output) {
   if(_isObject(parent)) {
-    jsonld.addValue(parent, property, output, true);
+    jsonld.addValue(parent, property, output, {propertyIsArray: true});
   }
   else {
     parent.push(output);
@@ -3021,7 +3081,7 @@
       continue;
     }
     // skip @list containers for non-@lists
-    if(!isList && entry['@container'] === '@list') {
+    if(!isList && entry['@container'] === '@list' && value !== null) {
       continue;
     }
     // for @lists, if listContainer is set, skip non-list containers
@@ -3285,7 +3345,7 @@
   // merge onto parent mapping if one exists for a prefix
   if(prefix !== null && prefix in activeCtx.mappings) {
     var child = mapping;
-    var mapping = _clone(activeCtx.mappings[prefix]);
+    mapping = _clone(activeCtx.mappings[prefix]);
     for(var k in child) {
       mapping[k] = child[k];
     }
@@ -4069,7 +4129,12 @@
       s.name = {nominalValue: match[10], interfaceName: 'BlankNode'};
     }
 
-    // add statement
+    // add unique statement
+    for(var si in statements) {
+      if(_compareRdfStatements(statements[si], s)) {
+        continue;
+      }
+    }
     statements.push(s);
   }
 
--- a/test-suite/tests/compact-0015-out.jsonld	Tue Jun 26 23:49:05 2012 -0400
+++ b/test-suite/tests/compact-0015-out.jsonld	Sat Jun 30 21:37:44 2012 -0400
@@ -16,5 +16,5 @@
   "term2": "v2",
   "term3": "v3",
   "term4": [ 1, 2 ],
-  "term5": [ "v5", "plain literal" ]
+  "term5": [ {"@value": "v5"}, {"@value": "plain literal"} ]
 }
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/test-suite/tests/compact-0020-context.jsonld	Sat Jun 30 21:37:44 2012 -0400
@@ -0,0 +1,6 @@
+{
+  "@context": {
+    "ex": "http://example.org/ns#",
+    "ex:property": {"@container": "@list"}
+  }
+}
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/test-suite/tests/compact-0020-in.jsonld	Sat Jun 30 21:37:44 2012 -0400
@@ -0,0 +1,9 @@
+{
+  "@context": {
+    "ex": "http://example.org/ns#"
+  },
+  "@id": "ex:property",
+  "ex:property": {
+    "@list": [1, 2]
+  }
+}
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/test-suite/tests/compact-0020-out.jsonld	Sat Jun 30 21:37:44 2012 -0400
@@ -0,0 +1,10 @@
+{
+  "@context": {
+    "ex": "http://example.org/ns#",
+    "ex:property": {
+      "@container": "@list"
+    }
+  },
+  "@id": "ex:property",
+  "ex:property": [1, 2]
+}
\ No newline at end of file
--- a/test-suite/tests/compact-manifest.jsonld	Tue Jun 26 23:49:05 2012 -0400
+++ b/test-suite/tests/compact-manifest.jsonld	Sat Jun 30 21:37:44 2012 -0400
@@ -119,5 +119,11 @@
     "input": "compact-0019-in.jsonld",
     "context": "compact-0019-context.jsonld",
     "expect": "compact-0019-out.jsonld"
+  }, {
+    "@type": ["test:TestCase", "jld:CompactTest"],
+    "name": "Compact @id that is a property IRI when @container is @list",
+    "input": "compact-0020-in.jsonld",
+    "context": "compact-0020-context.jsonld",
+    "expect": "compact-0020-out.jsonld"
   }]
 }