Repository: SINTEF/dlite (language: python)

Issue:
Give instances a default uri 
When an instance haven't got a default uri, it will default to its metadata uri followed by "/" followed by its uuid, like

    http://onto-ns.com/meta/0.1/Energy/5580a9e5-6692-41f5-8f5d-3083aec76b8d

This require that we update getuuid() such that it recognises id's on the above form and returns the uuid part instead of generating an uuid version 5 hash from it. 




Discussion / hints:

Fix patch:
diff --git a/doc/concepts.md b/doc/concepts.md
index 4cb6ee7d4..0adda367e 100644
--- a/doc/concepts.md
+++ b/doc/concepts.md
@@ -1,36 +1,56 @@
-Concepts
-========
-dlite shares the [metadata model of SOFT][1] and generic data stored
-with SOFT can be read with dlite and vice verse.  However, apart from
-dlite being much smaller and less complete, there are also a few
-notable differences in the API and even conceptual, which are detailed
-below.
+Relation to SOFT
+================
+SOFT stands for SINTEF Open Framework and Tools and is a set of
+concepts for how to achieve semantic interoperability as well as
+implementations and tooling.  At the core of SOFT are the [SOFT data
+models], which provide a by design simplistic but powerful way to
+represent scientific data.
+
+DLite is one implementation of the [SOFT data models].  Originally it
+was a simplified pure C implementation of SOFT5, but has with time
+developed into a project of its own.  This document will highlight a
+few notable differences with SOFT5.
 
 
 Named data instances
 --------------------
-In SOFT all Entity instances are referred to by their UUID.  However,
-in some cases when you have unique and immutable data, e.g. default
+In SOFT all instances are referred to by their UUID.  All metadata are
+uniquely identified by their URI, which must be of the form
+'`namespace`/`version`/`name`', like:
+http://onto-ns.com/meta/0.3/EntitySchema.  This is also valid in
+DLite, but we allow the URI to optionally end with a hash (#) or slash
+(/), which will be ignored.  Since a metadata is an instance of its
+meta-metadata, it also has an UUID, which is calculated from the URI
+(as an UUID version 5 SHA-1 hash of the URI using the DNS namespace).
+
+In some cases when you have unique and immutable data, e.g. default
 input parameters to a given version of a software model, it may be
 more convenient to refer to an unique human understandable name (URI),
-like "mymodel-1.2.3_default_input", rather than a UUID on the form
-"8290318f-258e-54e2-9838-bb187881f996".  dlite supports this.  The
-tool `dlite-getuuid` can be used to manually convert URIs to their
+like "`mymodel-1.2.3-default_input`", rather than a UUID on the form
+"`8290318f-258e-54e2-9838-bb187881f996`".  DLite supports this.
+Currently DLite does not enforce that user-defined URIs must follow the
+[RFC 3986] standard for a [valid URI], but it is recommended to
+follow it.
+
+The tool `dlite-getuuid` can be used to manually convert URIs to their
 corresponding UUIDs.
 
-If the `id` provided to dlite_datamodel() or dlite_instance_create()
-is not `NULL` or a valid UUID, it is interpreted as an unique uri
-referring to the instance.  dlite will then generate a version 5
-UUID from the `id` (from the SHA-1 hash of `id` and the DNS namespace)
-that the instance will be stored under.  The original `id` will also be
-stored and can be retrieved with dlite_datamodel_get_meta_uri().
+DLite also allow to refer to instances using id's of the form
+'`namespace`/`version`/`name`/`uuid`' (e.g:
+http://onto-ns.com/meta/0.1/Collection/db6e092b-20f9-44c1-831b-bd597c96daae),
+where the '`namespace`/`version`/`name`' part is the URI of the
+metadata and '`uuid`' is the UUID of the instance.  This has the
+advantage that the the URI of an instance will be a valid [RDF]
+subject or object in a knowledge base.  In the Python bindings, the
+method `Instance.get_uri()` will return a string in this format if the
+instance has no URI.
 
 
 Simple unified access to all data types
 ---------------------------------------
 The datamodel API for accessing properties of an instance in SOFT, has
 separate getters and setters for each type and number of dimensions.
-dlite generalize and simplifies this by describing types and
+DLite generalize and simplifies this by describing types and
 dimensionality of properties with 4 parameters:
 
   - `type`: an enum defining the type of the data item (or items if
@@ -41,13 +61,13 @@ dimensionality of properties with 4 parameters:
   - `dims`: array of length `ndims` with the length of each dimension.
 
 By taking these parameters as arguments, the functions
-dlite_datamodel_get_property() and dlite_datamodel_set_property() can handle
+DLite_datamodel_get_property() and DLite_datamodel_set_property() can handle
 all supported property types.  No storage strategy is needed.
 
-The table below summarises the different dtypes defined in dlite.  For
+The table below summarises the different dtypes defined in DLite.  For
 more details, see dlite-type.h.  Also note that this supports arbitrary
 dimensional arrays.  All arrays are assumed to be continuous in memory
-in C-order.  dlite has currently no api for working with arrays as
+in C-order.  DLite has currently no api for working with arrays as
 pointers to pointers.
 
 type      | dtype          | sizes                  | description                      | examples
@@ -91,7 +111,7 @@ a special type of *metadata*.
 
 *Collections* are a special type of instances containing references to
 a set of set of instances and relationships between them.  They are
-currently not yet implemented in dlite.
+currently not yet implemented in DLite.
 
 Instances can be subdivided into:
 
@@ -163,5 +183,8 @@ actively used in metadata, they must not be used in data instances:
 
 ---
 
-[1]: https://github.com/NanoSim/Porto/blob/porto/Preview-Final-Release/doc/manual/02_soft_introduction.md#soft5-features
+[SOFT data models]: https://github.com/NanoSim/Porto/blob/porto/Preview-Final-Release/doc/manual/02_soft_introduction.md#soft5-features
+[RFC 3986]: https://datatracker.ietf.org/doc/html/rfc3986
+[valid URI]: https://en.wikipedia.org/wiki/Uniform_Resource_Identifier#syntax
+[RDF]: https://en.wikipedia.org/wiki/Semantic_triple
 [fig1]: SOFT-metadata-structure.png "Figure 1. Metadata structure."
diff --git a/src/dlite-entity.c b/src/dlite-entity.c
index 44bf45bab..dd92042c9 100644
--- a/src/dlite-entity.c
+++ b/src/dlite-entity.c
@@ -12,6 +12,7 @@
 #include "utils/infixcalc.h"
 #include "utils/sha3.h"
 
+#include "getuuid.h"
 #include "dlite.h"
 #include "dlite-macros.h"
 #include "dlite-type.h"
@@ -441,7 +442,7 @@ static DLiteInstance *_instance_create(const DLiteMeta *meta,
   /* Initialise header */
   if ((uuid_version = dlite_get_uuid(uuid, id)) < 0) goto fail;
   memcpy(inst->uuid, uuid, sizeof(uuid));
-  if (uuid_version == 5) inst->uri = strdup(id);
+  if (uuid_version == UUID_HASH) inst->uri = strdup(id);
   inst->meta = (DLiteMeta *)meta;
 
   /* Set dimensions */
@@ -897,7 +898,8 @@ DLiteInstance *_instance_load_casted(const DLiteStorage *s, const char *id,
   size_t i, *dims=NULL;
   const char *uri=NULL;
 
-  if (!s) FAILCODE(dliteStorageLoadError, "invalid storage, see previous errors");
+  if (!s) FAILCODE(dliteStorageLoadError,
+                   "invalid storage, see previous errors");
 
   /* check if id is already loaded */
   if (lookup && id && *id && (inst = _instance_store_get(id))) {
@@ -988,6 +990,8 @@ DLiteInstance *_instance_load_casted(const DLiteStorage *s, const char *id,
         FAILCODE2(dliteMissingMetadataError, "metadata %s loaded from %s has no name, version and namespace",
              id, s->location);
       }
+    } else {
+      inst->uri = dlite_instance_default_uri(inst);
     }
   }
 
@@ -2016,6 +2020,24 @@ int dlite_instance_assign_casted_property_by_index(const DLiteInstance *inst,
 }
 
 
+/*
+  Return a newly allocated default instance URI constructed from
+  the metadata URI and the UUID of the instance, as `<meta_uri>/<uuid>`.
+
+  Returns NULL on error.
+ */
+char *dlite_instance_default_uri(const DLiteInstance *inst)
+{
+  int n = strlen(inst->meta->uri);
+  char *buf = malloc(n + DLITE_UUID_LENGTH + 2);
+  memcpy(buf, inst->meta->uri, n);
+  buf[n] = '/';
+  memcpy(buf+n+1, inst->uuid, DLITE_UUID_LENGTH);
+  buf[n+DLITE_UUID_LENGTH+1] = '\0';
+  return buf;
+}
+
+
 /*
   Calculates a hash of instance `inst`.  The calculated hash is stored
   in `hash`, where `hashsize` is the size of `hash` in bytes.  It should
diff --git a/src/dlite-entity.h b/src/dlite-entity.h
index 8ce4c91e3..a362b9af1 100644
--- a/src/dlite-entity.h
+++ b/src/dlite-entity.h
@@ -1013,6 +1013,15 @@ int dlite_instance_assign_casted_property_by_index(const DLiteInstance *inst,
                                                    const void *src,
                                                    DLiteTypeCast castfun);
 
+/**
+  Return a newly allocated default instance URI constructed from
+  the metadata URI and the UUID of the instance, as `<meta_uri>/<uuid>`.
+
+  Returns NULL on error.
+ */
+char *dlite_instance_default_uri(const DLiteInstance *inst);
+
+
 /**
   Calculates a hash of instance `inst`.  The calculated hash is stored
   in `hash`, where `hashsize` is the size of `hash` in bytes.  It should
diff --git a/src/dlite-misc.c b/src/dlite-misc.c
index 3b91ede31..534c14fe1 100644
--- a/src/dlite-misc.c
+++ b/src/dlite-misc.c
@@ -168,7 +168,6 @@ int dlite_split_meta_uri(const char *uri, char **name, char **version,
 }
 
 
-
 /********************************************************************
  * Parsing options
  ********************************************************************/
diff --git a/src/getuuid.c b/src/getuuid.c
index 38ab290ed..6c9266ec0 100644
--- a/src/getuuid.c
+++ b/src/getuuid.c
@@ -1,3 +1,4 @@
+#include <assert.h>
 #include <stdlib.h>
 #include <string.h>
 #include <stdio.h>
@@ -5,23 +6,66 @@
 
 #include "config.h"
 
+#include "utils/strutils.h"
 #include "utils/uuid.h"
 #include "utils/uuid4.h"
 #include "getuuid.h"
 
 
+/* Returns non-zero if `s` is a valid UUID. */
+
+int isuuid(const char *s)
+{
+  int i;
+  for (i=0; i<8; i++) if (!isxdigit(*(s++))) return 0;
+  if (*(s++) != '-') return 0;
+  for (i=0; i<4; i++) if (!isxdigit(*(s++))) return 0;
+  if (*(s++) != '-') return 0;
+  for (i=0; i<4; i++) if (!isxdigit(*(s++))) return 0;
+  if (*(s++) != '-') return 0;
+  for (i=0; i<4; i++) if (!isxdigit(*(s++))) return 0;
+  if (*(s++) != '-') return 0;
+  for (i=0; i<12; i++) if (!isxdigit(*(s++))) return 0;
+  return 1;
+}
+
+/* Returns non-zero if `s` matches <URI>/<UUID>. `len` is the length of `s`.
+   An optional final hash or slash will be ignored.*/
+int isinstanceuri(const char *s, int len)
+{
+  if (!len) len = strlen(s);
+  if (len < UUID_LEN + 10) return 0;
+  if (strchr("#/", s[len-1])) len--;
+  len -= UUID_LEN;
+  if (!isuuid(s+len)) return 0;
+  if (s[--len] != '/') return 0;
+  if (strcatjspn(s, strcatPercent) < len) return 0;
+  if ((int)strcspn(s, ":") >= len) return 0;
+  return 1;
+}
+
+
 /*
- * Writes an UUID to `buff` based on `id`.
- *
- * If `id` is NULL or empty, a new random version 4 UUID is generated.
- * If `id` is an invalid UUID string, a new version 5 sha1-based UUID
- * is generated from `id` using the DNS namespace.
- * Otherwise `id` is copied to `buff`.
- *
- * Length of `buff` must at least 37 bytes (36 for UUID + NUL termination).
- *
- * Returns the UUID version, if a new UUID is generated or zero if
- * `id` is simply copied.  On error, -1 is returned.
+  Writes an UUID to `buff` based on `id`.
+
+  It follows from the following heuristics:
+  - If `id` is NULL or empty, a new random version 4 UUID is generated.
+    Return: UUID_RANDOM
+  - If `id` is a valid UUID, it is copied as-is to `buff`.
+    Return: UUID_COPY
+  - If `id` matches `<URI>/<UUID>` then it returns the <UUID> part.
+    `id` may optionally end with a final hash (#) or slash (/), which will
+    be ignored.
+    Return: UUID_EXTRACT
+  - Otherwise is `id` an invalid UUID string.  A new version 5 sha1-based
+    UUID is generated from `id` using the DNS namespace.  Any optional final
+    hash (#) or slash (/) will be stripped off.
+    Return: UUID_HASH
+
+  Length of `buff` must at least 37 bytes (36 for UUID + NUL termination).
+
+  Returns one of the codes UUID_RANDOM, UUID_COPY, UUID_EXTRACT or UUID_HASH.
+  On error -1 is returned.
  */
 int getuuid(char *buff, const char *id)
 {
@@ -41,15 +85,24 @@ int getuuidn(char *buff, const char *id, size_t len)
 
   if (!id || !*id) {
     int status = uuid4_generate(buff);
-    version = (status == 0) ? 4 : -1;
-  } else if (uuid_from_string(NULL, id, len)) {
-    uuid_create_sha1_from_name(&uuid, NameSpace_DNS, id, len);
-    uuid_as_string(&uuid, buff);
-    version = 5;
-  } else {
+    if (status) return -1;
+    version = UUID_RANDOM;
+  } else if (((len == UUID_LEN) ||
+              (len == UUID_LEN+1 && strchr("#/", id[len]))) &&
+             isuuid(id)) {
     strncpy(buff, id, UUID_LEN);
     buff[UUID_LEN] = '\0';
-    version = 0;
+    version = UUID_COPY;
+  } else if (isinstanceuri(id, len)) {
+    if (id[len-1] && strchr("#/", id[len-1])) len--;
+    len -= UUID_LEN;
+    assert(len > 0);
+    strncpy(buff, id+len, UUID_LEN);
+    version = UUID_EXTRACT;
+  } else {
+    uuid_create_sha1_from_name(&uuid, NameSpace_DNS, id, len);
+    uuid_as_string(&uuid, buff);
+    version = UUID_HASH;
   }
 
   /* For reprodusability, always convert to lower case */
diff --git a/src/getuuid.h b/src/getuuid.h
index 57c7b9b99..cec933ded 100644
--- a/src/getuuid.h
+++ b/src/getuuid.h
@@ -9,18 +9,35 @@
 /** Length of UUID, excluding terminating NUL. */
 #define UUID_LEN 36
 
+/** Return values */
+#define UUID_ERROR   -1  //!< error
+#define UUID_COPY     0  //!< copied UUID from input as-is
+#define UUID_RANDOM   4  //!< random version 4 UUID
+#define UUID_HASH     5  //!< version 5 sha1-based UUID using the DNS namespace
+#define UUID_EXTRACT 10  //!< UUID extracted input: <metadata URI>/<UUID>
+
+
 /**
- * Writes an UUID to \a buff based on \a id.
- *
- * If \a id is NULL or empty, a new random version 4 UUID is generated.
- * If \a id is an invalid UUID string, a new version 5 sha1-based UUID
- * is generated from \a id using the DNS namespace.
- * Otherwise \a id is copied to \a buff.
- *
- * Length of \a buff must at least 37 bytes (36 for UUID + NUL termination).
- *
- * Returns the UUID version if a new UUID is generated or zero if \a
- * id is simply copied.  On error, -1 is returned.
+  Writes an UUID to `buff` based on `id`.
+
+  It follow the follow heuristics:
+  - If `id` is NULL or empty, a new random version 4 UUID is generated.
+    Return: UUID_RANDOM
+  - If `id` is a valid UUID, it is copied as-is to `buff`.
+    Return: UUID_COPY
+  - If `id` matches `<URI>/<UUID>` then it returns the <UUID> part.
+    `id` may optionally end with a final hash (#) or slash (/), which will
+    be ignored.
+    Return: UUID_EXTRACT
+  - Otherwise is `id` an invalid UUID string.  A new version 5 sha1-based
+    UUID is generated from `id` using the DNS namespace.  Any optional final
+    hash (#) or slash (/) will be stripped off.
+    Return: UUID_HASH
+
+  Length of `buff` must at least 37 bytes (36 for UUID + NUL termination).
+
+  Returns one of the codes UUID_RANDOM, UUID_COPY, UUID_EXTRACT or UUID_HASH.
+  On error -1 is returned.
  */
 int getuuid(char *buff, const char *id);
 
@@ -30,4 +47,8 @@ int getuuid(char *buff, const char *id);
  */
 int getuuidn(char *buff, const char *id, size_t len);
 
+
+int isuuid(const char *s);
+int isinstanceuri(const char *s, int len);
+
 #endif /* _GETUUID_H */
diff --git a/src/utils/strutils.c b/src/utils/strutils.c
index 327b8cf5f..db090c20e 100644
--- a/src/utils/strutils.c
+++ b/src/utils/strutils.c
@@ -249,3 +249,153 @@ int strhex_decode(unsigned char *data, size_t size, const char *hex,
   }
   return hexsize / 2;
 }
+
+
+/*
+  Returns the category (from RFC 3986) of character `c`.
+ */
+StrCategory strcategory(int c)
+{
+  switch (c) {
+  case 'A':
+  case 'B':
+  case 'C':
+  case 'D':
+  case 'E':
+  case 'F':
+  case 'G':
+  case 'H':
+  case 'I':
+  case 'J':
+  case 'K':
+  case 'L':
+  case 'M':
+  case 'N':
+  case 'O':
+  case 'P':
+  case 'Q':
+  case 'R':
+  case 'S':
+  case 'T':
+  case 'U':
+  case 'V':
+  case 'W':
+  case 'X':
+  case 'Y':
+  case 'Z':
+    return strcatUpper;
+  case 'a':
+  case 'b':
+  case 'c':
+  case 'd':
+  case 'e':
+  case 'f':
+  case 'g':
+  case 'h':
+  case 'i':
+  case 'j':
+  case 'k':
+  case 'l':
+  case 'm':
+  case 'n':
+  case 'o':
+  case 'p':
+  case 'q':
+  case 'r':
+  case 's':
+  case 't':
+  case 'u':
+  case 'v':
+  case 'w':
+  case 'x':
+  case 'y':
+  case 'z':
+    return strcatLower;
+  case '0':
+  case '1':
+  case '2':
+  case '3':
+  case '4':
+  case '5':
+  case '6':
+  case '7':
+  case '8':
+  case '9':
+    return strcatDigit;
+  case '-':
+  case '.':
+  case '_':
+  case '~':
+    return strcatUnreserved;
+  case ':':
+  case '/':
+  case '?':
+  case '#':
+  case '[':
+  case ']':
+  case '@':
+    return strcatReserved;
+  case '!':
+  case '$':
+  case '&':
+  case '\'':
+  case '(':
+  case ')':
+  case '*':
+  case '+':
+  case ',':
+  case ';':
+  case '=':
+    return strcatSpecific;
+  case '%':
+    return strcatPercent;
+  case 0:
+    return strcatNul;
+  default:
+    return strcatOther;
+  }
+}
+
+/*
+  Returns the length of initial segment of `s` which concists entirely of
+  bytes in category `cat`.
+ */
+int strcatspn(const char *s, StrCategory cat)
+{
+  int n=0;
+  while (strcategory(s[n]) == cat) n++;
+  return n;
+}
+
+/*
+  Returns the length of initial segment of `s` which concists entirely of
+  bytes NOT in category `cat`.
+ */
+int strcatcspn(const char *s, StrCategory cat)
+{
+  int n=0;
+  while (strcategory(s[n]) != cat) n++;
+  return n;
+}
+
+/*
+  Returns the length of initial segment of `s` which concists entirely of
+  bytes in all categories less or equal to `cat`.
+ */
+int strcatjspn(const char *s, StrCategory cat)
+{
+  int n=0;
+  while (strcategory(s[n]) <= cat) n++;
+  return n;
+}
+
+/*
+  Returns the length of initial segment of `s` which concists entirely of
+  bytes NOT in all categories less or equal to `cat`.
+ */
+int strcatcjspn(const char *s, StrCategory cat)
+{
+  int n=0;
+  while (strcategory(s[n]) > cat) n++;
+  return n;
+}
diff --git a/src/utils/strutils.h b/src/utils/strutils.h
index 7ddfc397a..bf17fc503 100644
--- a/src/utils/strutils.h
+++ b/src/utils/strutils.h
@@ -25,6 +25,20 @@ typedef enum _StrquoteFlags {
 } StrquoteFlags;
 
 
+/** Character categories, from, RFC 3986 */
+typedef enum {
+  strcatUpper,       //!<  A-Z
+  strcatLower,       //!<  a-z
+  strcatDigit,       //!<  0-9
+  strcatUnreserved,  //!<  "-._~"  (in addition to upper + lower + digit)
+  strcatReserved,    //!<  ":/?#[]@"
+  strcatSpecific,    //!<  "!$&'()*+,;="
+  strcatPercent,     //!<  "%"
+  strcatOther,       //!<  anything else, except NUL
+  strcatNul,         //!<  NUL
+} StrCategory;
+
+
 /**
   A convinient variant of asnprintf() that returns the allocated string,
   or NULL on error.
@@ -140,4 +154,35 @@ int strhex_encode(char *hex, size_t hexsize, const unsigned char *data,
 int strhex_decode(unsigned char *data, size_t size, const char *hex,
                   int hexsize);
 
+
+/**
+  Returns the category of character `c`.
+ */
+StrCategory strcategory(int c);
+
+/**
+  Returns the length of initial segment of `s` which concists entirely of
+  bytes in category `cat`.
+ */
+int strcatspn(const char *s, StrCategory cat);
+
+/**
+  Returns the length of initial segment of `s` which concists entirely of
+  bytes NOT in category `cat`.
+ */
+int strcatcspn(const char *s, StrCategory cat);
+
+/**
+  Returns the length of initial segment of `s` which concists entirely of
+  bytes in all categories less or equal to `cat`.
+ */
+int strcatjspn(const char *s, StrCategory cat);
+
+/**
+  Returns the length of initial segment of `s` which concists entirely of
+  bytes NOT in all categories less or equal to `cat`.
+ */
+int strcatcjspn(const char *s, StrCategory cat);
+
+
 #endif  /* _STRUTILS_H */


Test patch:
diff --git a/src/tests/CMakeLists.txt b/src/tests/CMakeLists.txt
index d55126cc0..b1491d242 100644
--- a/src/tests/CMakeLists.txt
+++ b/src/tests/CMakeLists.txt
@@ -16,6 +16,7 @@ set(tests
   test_schemas
   test_arrays
   test_ref
+  test_getuuid
   )
 
 list(APPEND tests test_json_entity)
diff --git a/src/tests/test_getuuid.c b/src/tests/test_getuuid.c
new file mode 100644
index 000000000..7fd9cca4e
--- /dev/null
+++ b/src/tests/test_getuuid.c
@@ -0,0 +1,90 @@
+#include <stdlib.h>
+#include <stddef.h>
+#include <stdio.h>
+#include <string.h>
+
+#include "config.h"
+
+#include "minunit/minunit.h"
+#include "getuuid.h"
+
+
+MU_TEST(test_isuuid)
+{
+  mu_assert_int_eq(1, isuuid("a58d4302-c9be-416d-a36c-cb25524a5a17"));
+  mu_assert_int_eq(1, isuuid("a58d4302-c9be-416d-a36c-cb25524a5a17+"));
+  mu_assert_int_eq(0, isuuid("58d4302-c9be-416d-a36c-cb25524a5a17"));
+  mu_assert_int_eq(0, isuuid("_a58d4302-c9be-416d-a36c-cb25524a5a17"));
+}
+
+MU_TEST(test_isinstanceiri)
+{
+  mu_assert_int_eq(
+    1, isinstanceuri("http://onto-ns.com/meta/0.1/Entity/"
+                     "a58d4302-c9be-416d-a36c-cb25524a5a17", 0));
+  mu_assert_int_eq(
+    1, isinstanceuri("http://onto-ns.com/meta/0.1/Entity/"
+                     "a58d4302-c9be-416d-a36c-cb25524a5a17#", 0));
+  mu_assert_int_eq(
+    1, isinstanceuri("http://onto-ns.com/meta/0.1/Entity/"
+                     "a58d4302-c9be-416d-a36c-cb25524a5a17/", 0));
+  mu_assert_int_eq(  // no slash between URI and UUID
+    0, isinstanceuri("http://onto-ns.com/meta/0.1/Entity#"
+                     "a58d4302-c9be-416d-a36c-cb25524a5a17", 0));
+  mu_assert_int_eq(  // no colon in URI
+    0, isinstanceuri("http//onto-ns.com/meta/0.1/Entity/"
+                     "a58d4302-c9be-416d-a36c-cb25524a5a17", 0));
+  mu_assert_int_eq(  // characters after the hash
+    0, isinstanceuri("http://onto-ns.com/meta/0.1/Entity/"
+                     "a58d4302-c9be-416d-a36c-cb25524a5a17#x", 0));
+}
+
+MU_TEST(test_getuuid)
+{
+  char buf[UUID_LEN+1];
+
+  mu_assert_int_eq(UUID_RANDOM, getuuid(buf, NULL));
+  mu_assert_int_eq(1, isuuid(buf));
+  mu_assert_int_eq(UUID_RANDOM, getuuid(buf, ""));
+  mu_assert_int_eq(1, isuuid(buf));
+
+  mu_assert_int_eq(UUID_COPY,
+                   getuuid(buf, "d683cdda-4987-48a5-9e32-cb37adfe3db0"));
+  mu_assert_string_eq("d683cdda-4987-48a5-9e32-cb37adfe3db0", buf);
+
+  char *uri;
+  uri = "http://onto-ns.com/meta/0.1/Energy/"
+    "d683cdda-4987-48a5-9e32-cb37adfe3db0";
+  mu_assert_int_eq(UUID_EXTRACT, getuuid(buf, uri));
+  mu_assert_string_eq("d683cdda-4987-48a5-9e32-cb37adfe3db0", buf);
+  uri = "http://onto-ns.com/meta/0.1/Energy/"
+    "d683cdda-4987-48a5-9e32-cb37adfe3db0#";
+  mu_assert_int_eq(UUID_EXTRACT, getuuid(buf, uri));
+  mu_assert_string_eq("d683cdda-4987-48a5-9e32-cb37adfe3db0", buf);
+
+  mu_assert_int_eq(UUID_HASH,
+                   getuuid(buf, "?683cdda-4987-48a5-9e32-cb37adfe3db0"));
+  mu_assert_int_eq(1, isuuid(buf));
+  mu_assert_int_eq(UUID_HASH,
+                   getuuid(buf, "abc"));
+  mu_assert_int_eq(1, isuuid(buf));
+}
+
+
+/***********************************************************************/
+
+MU_TEST_SUITE(test_suite)
+{
+  MU_RUN_TEST(test_isuuid);
+  MU_RUN_TEST(test_isinstanceiri);
+  MU_RUN_TEST(test_getuuid);
+}
+
+
+
+int main()
+{
+  MU_RUN_SUITE(test_suite);
+  MU_REPORT();
+  return (minunit_fail) ? 1 : 0;
+}
diff --git a/src/utils/tests/test_strutils.c b/src/utils/tests/test_strutils.c
index d9178e72e..1dbf3ed5b 100644
--- a/src/utils/tests/test_strutils.c
+++ b/src/utils/tests/test_strutils.c
@@ -187,6 +187,81 @@ MU_TEST(test_strhex_decode)
 }
 
 
+MU_TEST(test_strcategory)
+{
+  mu_assert_int_eq(strcatUpper, strcategory('A'));
+  mu_assert_int_eq(strcatUpper, strcategory('Z'));
+  mu_assert_int_eq(strcatLower, strcategory('a'));
+  mu_assert_int_eq(strcatLower, strcategory('z'));
+  mu_assert_int_eq(strcatDigit, strcategory('0'));
+  mu_assert_int_eq(strcatDigit, strcategory('9'));
+  mu_assert_int_eq(strcatUnreserved, strcategory('-'));
+  mu_assert_int_eq(strcatUnreserved, strcategory('.'));
+  mu_assert_int_eq(strcatUnreserved, strcategory('_'));
+  mu_assert_int_eq(strcatUnreserved, strcategory('~'));
+  mu_assert_int_eq(strcatReserved, strcategory(':'));
+  mu_assert_int_eq(strcatReserved, strcategory('/'));
+  mu_assert_int_eq(strcatReserved, strcategory('?'));
+  mu_assert_int_eq(strcatReserved, strcategory('#'));
+  mu_assert_int_eq(strcatReserved, strcategory('['));
+  mu_assert_int_eq(strcatReserved, strcategory(']'));
+  mu_assert_int_eq(strcatReserved, strcategory('@'));
+  mu_assert_int_eq(strcatSpecific, strcategory('!'));
+  mu_assert_int_eq(strcatSpecific, strcategory('$'));
+  mu_assert_int_eq(strcatSpecific, strcategory('&'));
+  mu_assert_int_eq(strcatSpecific, strcategory('\''));
+  mu_assert_int_eq(strcatSpecific, strcategory('('));
+  mu_assert_int_eq(strcatSpecific, strcategory(')'));
+  mu_assert_int_eq(strcatSpecific, strcategory('*'));
+  mu_assert_int_eq(strcatSpecific, strcategory('+'));
+  mu_assert_int_eq(strcatSpecific, strcategory(','));
+  mu_assert_int_eq(strcatSpecific, strcategory(';'));
+  mu_assert_int_eq(strcatSpecific, strcategory('='));
+  mu_assert_int_eq(strcatPercent, strcategory('%'));
+  mu_assert_int_eq(strcatNul, strcategory('\0'));
+  mu_assert_int_eq(strcatOther, strcategory('"'));
+  mu_assert_int_eq(strcatOther, strcategory('`'));
+  mu_assert_int_eq(strcatOther, strcategory('<'));
+  mu_assert_int_eq(strcatOther, strcategory('>'));
+  mu_assert_int_eq(strcatOther, strcategory('\xf8'));  // ø
+}
+
+
+MU_TEST(test_strcatspn)
+{
+  char *s1 = "ABZabz019-.~:/!=%<>";
+  char *s2 = "<>%=!/:~.-910zbaZBA";
+
+  mu_assert_int_eq(3,  strcatspn(s1, strcatUpper));
+  mu_assert_int_eq(0,  strcatspn(s1, strcatLower));
+  mu_assert_int_eq(0,  strcatspn(s2, strcatUpper));
+  mu_assert_int_eq(2,  strcatspn(s2, strcatOther));
+
+  mu_assert_int_eq(0,  strcatcspn(s1, strcatUpper));
+  mu_assert_int_eq(3,  strcatcspn(s1, strcatLower));
+  mu_assert_int_eq(16, strcatcspn(s2, strcatUpper));
+  mu_assert_int_eq(0,  strcatcspn(s2, strcatOther));
+
+  mu_assert_int_eq(3,  strcatjspn(s1, strcatUpper));
+  mu_assert_int_eq(6,  strcatjspn(s1, strcatLower));
+  mu_assert_int_eq(9,  strcatjspn(s1, strcatDigit));
+  mu_assert_int_eq(12, strcatjspn(s1, strcatUnreserved));
+  mu_assert_int_eq(14, strcatjspn(s1, strcatReserved));
+  mu_assert_int_eq(16, strcatjspn(s1, strcatSpecific));
+  mu_assert_int_eq(17, strcatjspn(s1, strcatPercent));
+  mu_assert_int_eq(19, strcatjspn(s1, strcatOther));
+
+  mu_assert_int_eq(16, strcatcjspn(s2, strcatUpper));
+  mu_assert_int_eq(13, strcatcjspn(s2, strcatLower));
+  mu_assert_int_eq(10, strcatcjspn(s2, strcatDigit));
+  mu_assert_int_eq(7,  strcatcjspn(s2, strcatUnreserved));
+  mu_assert_int_eq(5,  strcatcjspn(s2, strcatReserved));
+  mu_assert_int_eq(3,  strcatcjspn(s2, strcatSpecific));
+  mu_assert_int_eq(2,  strcatcjspn(s2, strcatPercent));
+  mu_assert_int_eq(0,  strcatcjspn(s2, strcatOther));
+}
+
+
 
 
 /***********************************************************************/
@@ -199,6 +274,8 @@ MU_TEST_SUITE(test_suite)
   MU_RUN_TEST(test_strunquote);
   MU_RUN_TEST(test_strhex_encode);
   MU_RUN_TEST(test_strhex_decode);
+  MU_RUN_TEST(test_strcategory);
+  MU_RUN_TEST(test_strcatspn);
 }