[json-glib] core: Add immutability support to core objects



commit 58f479b60eb2db4c73605d469d68a8ffd8679327
Author: Philip Withnall <philip withnall collabora co uk>
Date:   Sat Nov 7 14:17:31 2015 +0100

    core: Add immutability support to core objects
    
    Add an immutable mode to JsonNode, JsonObject, JsonArray and JsonValue.
    This is an optional mode which objects enter by calling json_*_seal().
    It is a one-way transition, which means that we can build and manipulate
    objects as much as desired, before sealing them and enjoying the
    benefits of immutable objects: no need to take copies when handling
    them, persistent hash values (still to be implemented).
    
    https://bugzilla.gnome.org/show_bug.cgi?id=756121

 doc/reference/json-glib-sections.txt |    8 +
 json-glib/json-array.c               |   48 +++++
 json-glib/json-builder.c             |  106 +++++++++++
 json-glib/json-builder.h             |    2 +
 json-glib/json-node.c                |  127 +++++++++++++-
 json-glib/json-object.c              |   51 ++++++
 json-glib/json-parser.c              |  108 ++++++++++++
 json-glib/json-parser.h              |    2 +
 json-glib/json-types-private.h       |    7 +
 json-glib/json-types.h               |   13 ++
 json-glib/json-value.c               |   23 +++
 json-glib/tests/node.c               |  320 ++++++++++++++++++++++++++++++++++
 12 files changed, 810 insertions(+), 5 deletions(-)
---
diff --git a/doc/reference/json-glib-sections.txt b/doc/reference/json-glib-sections.txt
index 015a442..2dccde9 100644
--- a/doc/reference/json-glib-sections.txt
+++ b/doc/reference/json-glib-sections.txt
@@ -5,6 +5,8 @@ JsonObject
 json_object_new
 json_object_ref
 json_object_unref
+json_object_seal
+json_object_is_immutable
 
 <SUBSECTION>
 json_object_add_member
@@ -51,6 +53,8 @@ json_array_new
 json_array_sized_new
 json_array_ref
 json_array_unref
+json_array_seal
+json_array_is_immutable
 
 <SUBSECTION>
 json_array_add_element
@@ -106,6 +110,8 @@ json_node_init_array
 json_node_new
 json_node_copy
 json_node_free
+json_node_is_immutable
+json_node_seal
 
 <SUBSECTION>
 json_node_set_array
@@ -156,6 +162,7 @@ JsonParserError
 JsonParser
 JsonParserClass
 json_parser_new
+json_parser_new_immutable
 json_parser_load_from_file
 json_parser_load_from_data
 json_parser_load_from_stream
@@ -325,6 +332,7 @@ _JSON_EXTERN
 JsonBuilder
 JsonBuilderClass
 json_builder_new
+json_builder_new_immutable
 json_builder_get_root
 json_builder_reset
 <SUBSECTION>
diff --git a/json-glib/json-array.c b/json-glib/json-array.c
index af67b4a..05cde4a 100644
--- a/json-glib/json-array.c
+++ b/json-glib/json-array.c
@@ -134,6 +134,54 @@ json_array_unref (JsonArray *array)
 }
 
 /**
+ * json_array_seal:
+ * @array: a #JsonArray
+ *
+ * Seals the #JsonArray, making it immutable to further changes. This will
+ * recursively seal all elements in the array too.
+ *
+ * If the @array is already immutable, this is a no-op.
+ *
+ * Since: UNRELEASED
+ */
+void
+json_array_seal (JsonArray *array)
+{
+  guint i;
+
+  g_return_if_fail (array != NULL);
+  g_return_if_fail (array->ref_count > 0);
+
+  if (array->immutable)
+    return;
+
+  /* Propagate to all members. */
+  for (i = 0; i < array->elements->len; i++)
+    json_node_seal (g_ptr_array_index (array->elements, i));
+
+  array->immutable = TRUE;
+}
+
+/**
+ * json_array_is_immutable:
+ * @array: a #JsonArray
+ *
+ * Check whether the given @array has been marked as immutable by calling
+ * json_array_seal() on it.
+ *
+ * Since: UNRELEASED
+ * Returns: %TRUE if the @array is immutable
+ */
+gboolean
+json_array_is_immutable (JsonArray *array)
+{
+  g_return_val_if_fail (array != NULL, FALSE);
+  g_return_val_if_fail (array->ref_count > 0, FALSE);
+
+  return array->immutable;
+}
+
+/**
  * json_array_get_elements:
  * @array: a #JsonArray
  *
diff --git a/json-glib/json-builder.c b/json-glib/json-builder.c
index d3a5cef..6531b0c 100644
--- a/json-glib/json-builder.c
+++ b/json-glib/json-builder.c
@@ -2,6 +2,7 @@
  *
  * This file is part of JSON-GLib
  * Copyright (C) 2010  Luca Bruno <lethalman88 gmail com>
+ * Copyright (C) 2015  Collabora Ltd.
  *
  * This library is free software; you can redistribute it and/or
  * modify it under the terms of the GNU Lesser General Public
@@ -18,6 +19,7 @@
  *
  * Author:
  *   Luca Bruno  <lethalman88 gmail com>
+ *   Philip Withnall  <philip withnall collabora co uk>
  */
 
 /**
@@ -50,8 +52,17 @@ struct _JsonBuilderPrivate
 {
   GQueue *stack;
   JsonNode *root;
+  gboolean immutable;
 };
 
+enum
+{
+  PROP_IMMUTABLE = 1,
+  PROP_LAST
+};
+
+static GParamSpec *builder_props[PROP_LAST] = { NULL, };
+
 typedef enum
 {
   JSON_BUILDER_MODE_OBJECT,
@@ -133,11 +144,70 @@ json_builder_finalize (GObject *gobject)
 }
 
 static void
+json_builder_set_property (GObject      *gobject,
+                           guint         prop_id,
+                           const GValue *value,
+                           GParamSpec   *pspec)
+{
+  JsonBuilderPrivate *priv = JSON_BUILDER (gobject)->priv;
+
+  switch (prop_id)
+    {
+    case PROP_IMMUTABLE:
+      /* Construct-only. */
+      priv->immutable = g_value_get_boolean (value);
+      break;
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (gobject, prop_id, pspec);
+      break;
+    }
+}
+
+static void
+json_builder_get_property (GObject    *gobject,
+                           guint       prop_id,
+                           GValue     *value,
+                           GParamSpec *pspec)
+{
+  JsonBuilderPrivate *priv = JSON_BUILDER (gobject)->priv;
+
+  switch (prop_id)
+    {
+    case PROP_IMMUTABLE:
+      g_value_set_boolean (value, priv->immutable);
+      break;
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (gobject, prop_id, pspec);
+      break;
+    }
+}
+
+static void
 json_builder_class_init (JsonBuilderClass *klass)
 {
   GObjectClass *gobject_class = G_OBJECT_CLASS (klass);
 
+  /**
+   * JsonBuilder:immutable:
+   *
+   * Whether the #JsonNode tree built by the #JsonBuilder should be immutable
+   * when created. Making the output immutable on creation avoids the expense
+   * of traversing it to make it immutable later.
+   *
+   * Since: UNRELEASED
+   */
+  builder_props[PROP_IMMUTABLE] =
+    g_param_spec_boolean ("immutable",
+                          "Immutable Output",
+                          "Whether the builder output is immutable.",
+                          FALSE,
+                          G_PARAM_CONSTRUCT_ONLY | G_PARAM_READWRITE);
+
+  gobject_class->set_property = json_builder_set_property;
+  gobject_class->get_property = json_builder_get_property;
   gobject_class->finalize = json_builder_finalize;
+
+  g_object_class_install_properties (gobject_class, PROP_LAST, builder_props);
 }
 
 static void
@@ -180,6 +250,21 @@ json_builder_new (void)
 }
 
 /**
+ * json_builder_new_immutable:
+ *
+ * Creates a new #JsonBuilder instance with its #JsonBuilder:immutable property
+ * set to %TRUE to create immutable output trees.
+ *
+ * Since: UNRELEASED
+ * Returns: (transfer full): a new #JsonBuilder
+ */
+JsonBuilder *
+json_builder_new_immutable (void)
+{
+  return g_object_new (JSON_TYPE_BUILDER, "immutable", TRUE, NULL);
+}
+
+/**
  * json_builder_get_root:
  * @builder: a #JsonBuilder
  *
@@ -199,6 +284,11 @@ json_builder_get_root (JsonBuilder *builder)
   if (builder->priv->root)
     root = json_node_copy (builder->priv->root);
 
+  /* Sanity check. */
+  g_return_val_if_fail (!builder->priv->immutable ||
+                        root == NULL ||
+                        json_node_is_immutable (root), NULL);
+
   return root;
 }
 
@@ -292,10 +382,16 @@ json_builder_end_object (JsonBuilder *builder)
 
   state = g_queue_pop_head (builder->priv->stack);
 
+  if (builder->priv->immutable)
+    json_object_seal (state->data.object);
+
   if (g_queue_is_empty (builder->priv->stack))
     {
       builder->priv->root = json_node_new (JSON_NODE_OBJECT);
       json_node_take_object (builder->priv->root, json_object_ref (state->data.object));
+
+      if (builder->priv->immutable)
+        json_node_seal (builder->priv->root);
     }
 
   json_builder_state_free (state);
@@ -378,10 +474,16 @@ json_builder_end_array (JsonBuilder *builder)
 
   state = g_queue_pop_head (builder->priv->stack);
 
+  if (builder->priv->immutable)
+    json_array_seal (state->data.array);
+
   if (g_queue_is_empty (builder->priv->stack))
     {
       builder->priv->root = json_node_new (JSON_NODE_ARRAY);
       json_node_take_array (builder->priv->root, json_array_ref (state->data.array));
+
+      if (builder->priv->immutable)
+        json_node_seal (builder->priv->root);
     }
 
   json_builder_state_free (state);
@@ -444,6 +546,10 @@ json_builder_add_value (JsonBuilder *builder,
   g_return_val_if_fail (json_builder_is_valid_add_mode (builder), NULL);
 
   state = g_queue_peek_head (builder->priv->stack);
+
+  if (builder->priv->immutable)
+    json_node_seal (node);
+
   switch (state->mode)
     {
     case JSON_BUILDER_MODE_MEMBER:
diff --git a/json-glib/json-builder.h b/json-glib/json-builder.h
index 45d4b5d..b35230e 100644
--- a/json-glib/json-builder.h
+++ b/json-glib/json-builder.h
@@ -80,6 +80,8 @@ GType json_builder_get_type (void) G_GNUC_CONST;
 
 JSON_AVAILABLE_IN_1_0
 JsonBuilder *json_builder_new                (void);
+JSON_AVAILABLE_IN_1_2
+JsonBuilder *json_builder_new_immutable      (void);
 JSON_AVAILABLE_IN_1_0
 JsonNode    *json_builder_get_root           (JsonBuilder  *builder);
 JSON_AVAILABLE_IN_1_0
diff --git a/json-glib/json-node.c b/json-glib/json-node.c
index fb95f42..092a27f 100644
--- a/json-glib/json-node.c
+++ b/json-glib/json-node.c
@@ -3,6 +3,7 @@
  * This file is part of JSON-GLib
  * Copyright (C) 2007  OpenedHand Ltd.
  * Copyright (C) 2009  Intel Corp.
+ * Copyright (C) 2015  Collabora Ltd.
  *
  * This library is free software; you can redistribute it and/or
  * modify it under the terms of the GNU Lesser General Public
@@ -19,6 +20,7 @@
  *
  * Author:
  *   Emmanuele Bassi  <ebassi linux intel com>
+ *   Philip Withnall  <philip withnall collabora co uk>
  */
 
 #include "config.h"
@@ -44,6 +46,15 @@
  * #JsonObject or the #JsonArray using json_node_get_object() or
  * json_node_get_array() respectively, and then retrieve the nodes
  * they contain.
+ *
+ * A #JsonNode may be marked as immutable using json_node_seal(). This marks the
+ * node and all its descendents as read-only, and means that subsequent calls to
+ * setter functions (such as json_node_set_array()) on them will abort as a
+ * programmer error. By marking a node tree as immutable, it may be referenced
+ * in multiple places and its hash value cached for fast lookups, without the
+ * possibility of a value deep within the tree changing and affecting hash
+ * values. Immutable #JsonNodes may be passed to functions which retain a
+ * reference to them without needing to take a copy.
  */
 
 G_DEFINE_BOXED_TYPE (JsonNode, json_node, json_node_copy, json_node_free);
@@ -367,8 +378,12 @@ json_node_new (JsonNodeType type)
  * json_node_copy:
  * @node: a #JsonNode
  *
- * Copies @node. If the node contains complex data types then the reference
- * count of the objects is increased.
+ * Copies @node. If the node contains complex data types, their reference
+ * counts are increased, regardless of whether the node is mutable or
+ * immutable.
+ *
+ * The copy will be immutable if, and only if, @node is immutable. However,
+ * there should be no need to copy an immutable node.
  *
  * Return value: (transfer full): the copied #JsonNode
  */
@@ -381,6 +396,10 @@ json_node_copy (JsonNode *node)
 
   copy = g_slice_new0 (JsonNode);
   copy->type = node->type;
+  copy->immutable = node->immutable;
+
+  if (node->immutable)
+    g_debug ("Copying immutable JsonNode %p", node);
 
   switch (copy->type)
     {
@@ -415,6 +434,8 @@ json_node_copy (JsonNode *node)
  * Sets @objects inside @node. The reference count of @object is increased.
  *
  * If @object is %NULL, the node’s existing object is cleared.
+ *
+ * It is an error to call this on an immutable node.
  */
 void
 json_node_set_object (JsonNode   *node,
@@ -422,6 +443,7 @@ json_node_set_object (JsonNode   *node,
 {
   g_return_if_fail (node != NULL);
   g_return_if_fail (JSON_NODE_TYPE (node) == JSON_NODE_OBJECT);
+  g_return_if_fail (!node->immutable);
 
   if (node->data.object != NULL)
     json_object_unref (node->data.object);
@@ -438,6 +460,8 @@ json_node_set_object (JsonNode   *node,
  * @object: (transfer full): a #JsonObject
  *
  * Sets @object inside @node. The reference count of @object is not increased.
+ *
+ * It is an error to call this on an immutable node.
  */
 void
 json_node_take_object (JsonNode   *node,
@@ -445,6 +469,7 @@ json_node_take_object (JsonNode   *node,
 {
   g_return_if_fail (node != NULL);
   g_return_if_fail (JSON_NODE_TYPE (node) == JSON_NODE_OBJECT);
+  g_return_if_fail (!node->immutable);
 
   if (node->data.object)
     {
@@ -499,7 +524,9 @@ json_node_dup_object (JsonNode *node)
  * @node: a #JsonNode initialized to %JSON_NODE_ARRAY
  * @array: a #JsonArray
  *
- * Sets @array inside @node and increases the #JsonArray reference count
+ * Sets @array inside @node and increases the #JsonArray reference count.
+ *
+ * It is an error to call this on an immutable node.
  */
 void
 json_node_set_array (JsonNode  *node,
@@ -507,6 +534,7 @@ json_node_set_array (JsonNode  *node,
 {
   g_return_if_fail (node != NULL);
   g_return_if_fail (JSON_NODE_TYPE (node) == JSON_NODE_ARRAY);
+  g_return_if_fail (!node->immutable);
 
   if (node->data.array)
     json_array_unref (node->data.array);
@@ -523,6 +551,8 @@ json_node_set_array (JsonNode  *node,
  * @array: (transfer full): a #JsonArray
  *
  * Sets @array into @node without increasing the #JsonArray reference count.
+ *
+ * It is an error to call this on an immutable node.
  */
 void
 json_node_take_array (JsonNode  *node,
@@ -530,6 +560,7 @@ json_node_take_array (JsonNode  *node,
 {
   g_return_if_fail (node != NULL);
   g_return_if_fail (JSON_NODE_TYPE (node) == JSON_NODE_ARRAY);
+  g_return_if_fail (!node->immutable);
 
   if (node->data.array)
     {
@@ -627,7 +658,9 @@ json_node_get_value (JsonNode *node,
  * @node: a #JsonNode initialized to %JSON_NODE_VALUE
  * @value: the #GValue to set
  *
- * Sets @value inside @node. The passed #GValue is copied into the #JsonNode
+ * Sets @value inside @node. The passed #GValue is copied into the #JsonNode.
+ *
+ * It is an error to call this on an immutable node.
  */
 void
 json_node_set_value (JsonNode     *node,
@@ -636,6 +669,7 @@ json_node_set_value (JsonNode     *node,
   g_return_if_fail (node != NULL);
   g_return_if_fail (JSON_NODE_TYPE (node) == JSON_NODE_VALUE);
   g_return_if_fail (G_VALUE_TYPE (value) != G_TYPE_INVALID);
+  g_return_if_fail (!node->immutable);
 
   if (node->data.value == NULL)
     node->data.value = json_value_alloc ();
@@ -697,6 +731,72 @@ json_node_free (JsonNode *node)
 }
 
 /**
+ * json_node_seal:
+ * @node: a #JsonNode
+ *
+ * Seals the #JsonNode, making it immutable to further changes. In order to be
+ * sealed, the @node must have a type and value set. The value will be
+ * recursively sealed — if the node holds an object, that #JsonObject will be
+ * sealed, etc.
+ *
+ * If the @node is already immutable, this is a no-op.
+ *
+ * Since: UNRELEASED
+ */
+void
+json_node_seal (JsonNode *node)
+{
+  g_return_if_fail (node != NULL);
+  g_return_if_fail (node->type >= JSON_NODE_OBJECT &&
+                    node->type <= JSON_NODE_NULL);
+
+  if (node->immutable)
+    return;
+
+  switch (node->type)
+    {
+    case JSON_NODE_OBJECT:
+      g_return_if_fail (node->data.object != NULL);
+      json_object_seal (node->data.object);
+      break;
+    case JSON_NODE_ARRAY:
+      g_return_if_fail (node->data.array != NULL);
+      json_array_seal (node->data.array);
+      break;
+    case JSON_NODE_NULL:
+      break;
+    case JSON_NODE_VALUE:
+      g_return_if_fail (node->data.value != NULL);
+      json_value_seal (node->data.value);
+      break;
+    default:
+      g_assert_not_reached ();
+    }
+
+  node->immutable = TRUE;
+}
+
+/**
+ * json_node_is_immutable:
+ * @node: a #JsonNode
+ *
+ * Check whether the given @node has been marked as immutable by calling
+ * json_node_seal() on it.
+ *
+ * Since: UNRELEASED
+ * Returns: %TRUE if the @node is immutable
+ */
+gboolean
+json_node_is_immutable (JsonNode *node)
+{
+  g_return_val_if_fail (node != NULL, FALSE);
+  g_return_val_if_fail (node->type >= JSON_NODE_OBJECT &&
+                        node->type <= JSON_NODE_NULL, FALSE);
+
+  return node->immutable;
+}
+
+/**
  * json_node_type_name:
  * @node: a #JsonNode
  *
@@ -755,7 +855,10 @@ json_node_type_get_name (JsonNodeType node_type)
  * @node: a #JsonNode
  * @parent: (transfer none): the parent #JsonNode of @node
  *
- * Sets the parent #JsonNode of @node
+ * Sets the parent #JsonNode of @node.
+ *
+ * It is an error to call this with an immutable @parent. @node may be
+ * immutable.
  *
  * Since: 0.8
  */
@@ -764,6 +867,8 @@ json_node_set_parent (JsonNode *node,
                       JsonNode *parent)
 {
   g_return_if_fail (node != NULL);
+  g_return_if_fail (parent == NULL ||
+                    !json_node_is_immutable (parent));
 
   node->parent = parent;
 }
@@ -792,6 +897,8 @@ json_node_get_parent (JsonNode *node)
  *
  * Sets @value as the string content of the @node, replacing any existing
  * content.
+ *
+ * It is an error to call this on an immutable node.
  */
 void
 json_node_set_string (JsonNode    *node,
@@ -799,6 +906,7 @@ json_node_set_string (JsonNode    *node,
 {
   g_return_if_fail (node != NULL);
   g_return_if_fail (JSON_NODE_TYPE (node) == JSON_NODE_VALUE);
+  g_return_if_fail (!node->immutable);
 
   if (node->data.value == NULL)
     node->data.value = json_value_init (json_value_alloc (), JSON_VALUE_STRING);
@@ -854,6 +962,8 @@ json_node_dup_string (JsonNode *node)
  *
  * Sets @value as the integer content of the @node, replacing any existing
  * content.
+ *
+ * It is an error to call this on an immutable node.
  */
 void
 json_node_set_int (JsonNode *node,
@@ -861,6 +971,7 @@ json_node_set_int (JsonNode *node,
 {
   g_return_if_fail (node != NULL);
   g_return_if_fail (JSON_NODE_TYPE (node) == JSON_NODE_VALUE);
+  g_return_if_fail (!node->immutable);
 
   if (node->data.value == NULL)
     node->data.value = json_value_init (json_value_alloc (), JSON_VALUE_INT);
@@ -905,6 +1016,8 @@ json_node_get_int (JsonNode *node)
  *
  * Sets @value as the double content of the @node, replacing any existing
  * content.
+ *
+ * It is an error to call this on an immutable node.
  */
 void
 json_node_set_double (JsonNode *node,
@@ -912,6 +1025,7 @@ json_node_set_double (JsonNode *node,
 {
   g_return_if_fail (node != NULL);
   g_return_if_fail (JSON_NODE_TYPE (node) == JSON_NODE_VALUE);
+  g_return_if_fail (!node->immutable);
 
   if (node->data.value == NULL)
     node->data.value = json_value_init (json_value_alloc (), JSON_VALUE_DOUBLE);
@@ -956,6 +1070,8 @@ json_node_get_double (JsonNode *node)
  *
  * Sets @value as the boolean content of the @node, replacing any existing
  * content.
+ *
+ * It is an error to call this on an immutable node.
  */
 void
 json_node_set_boolean (JsonNode *node,
@@ -963,6 +1079,7 @@ json_node_set_boolean (JsonNode *node,
 {
   g_return_if_fail (node != NULL);
   g_return_if_fail (JSON_NODE_TYPE (node) == JSON_NODE_VALUE);
+  g_return_if_fail (!node->immutable);
 
   if (node->data.value == NULL)
     node->data.value = json_value_init (json_value_alloc (), JSON_VALUE_BOOLEAN);
diff --git a/json-glib/json-object.c b/json-glib/json-object.c
index 214ffef..574d04d 100644
--- a/json-glib/json-object.c
+++ b/json-glib/json-object.c
@@ -117,6 +117,57 @@ json_object_unref (JsonObject *object)
     }
 }
 
+/**
+ * json_object_seal:
+ * @object: a #JsonObject
+ *
+ * Seals the #JsonObject, making it immutable to further changes. This will
+ * recursively seal all members of the object too.
+ *
+ * If the @object is already immutable, this is a no-op.
+ *
+ * Since: UNRELEASED
+ */
+void
+json_object_seal (JsonObject *object)
+{
+  JsonObjectIter iter;
+  JsonNode *node;
+
+  g_return_if_fail (object != NULL);
+  g_return_if_fail (object->ref_count > 0);
+
+  if (object->immutable)
+    return;
+
+  /* Propagate to all members. */
+  json_object_iter_init (&iter, object);
+
+  while (json_object_iter_next (&iter, NULL, &node))
+    json_node_seal (node);
+
+  object->immutable = TRUE;
+}
+
+/**
+ * json_object_is_immutable:
+ * @object: a #JsonObject
+ *
+ * Check whether the given @object has been marked as immutable by calling
+ * json_object_seal() on it.
+ *
+ * Since: UNRELEASED
+ * Returns: %TRUE if the @object is immutable
+ */
+gboolean
+json_object_is_immutable (JsonObject *object)
+{
+  g_return_val_if_fail (object != NULL, FALSE);
+  g_return_val_if_fail (object->ref_count > 0, FALSE);
+
+  return object->immutable;
+}
+
 static inline void
 object_set_member_internal (JsonObject  *object,
                             const gchar *member_name,
diff --git a/json-glib/json-parser.c b/json-glib/json-parser.c
index 812f038..8f1c40c 100644
--- a/json-glib/json-parser.c
+++ b/json-glib/json-parser.c
@@ -4,6 +4,7 @@
  *
  * Copyright © 2007, 2008, 2009 OpenedHand Ltd
  * Copyright © 2009, 2010 Intel Corp.
+ * Copyright © 2015 Collabora Ltd.
  *
  * This library is free software; you can redistribute it and/or
  * modify it under the terms of the GNU Lesser General Public
@@ -20,6 +21,7 @@
  *
  * Author:
  *   Emmanuele Bassi  <ebassi linux intel com>
+ *   Philip Withnall  <philip withnall collabora co uk>
  */
 
 /**
@@ -58,6 +60,7 @@ struct _JsonParserPrivate
 
   guint has_assignment : 1;
   guint is_filename    : 1;
+  gboolean immutable   : 1;
 };
 
 static const gchar symbol_names[] =
@@ -96,6 +99,14 @@ enum
 
 static guint parser_signals[LAST_SIGNAL] = { 0, };
 
+enum
+{
+  PROP_IMMUTABLE = 1,
+  PROP_LAST
+};
+
+static GParamSpec *parser_props[PROP_LAST] = { NULL, };
+
 G_DEFINE_QUARK (json-parser-error-quark, json_parser_error)
 
 G_DEFINE_TYPE_WITH_PRIVATE (JsonParser, json_parser, G_TYPE_OBJECT)
@@ -148,14 +159,73 @@ json_parser_finalize (GObject *gobject)
 }
 
 static void
+json_parser_set_property (GObject      *gobject,
+                          guint         prop_id,
+                          const GValue *value,
+                          GParamSpec   *pspec)
+{
+  JsonParserPrivate *priv = JSON_PARSER (gobject)->priv;
+
+  switch (prop_id)
+    {
+    case PROP_IMMUTABLE:
+      /* Construct-only. */
+      priv->immutable = g_value_get_boolean (value);
+      break;
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (gobject, prop_id, pspec);
+      break;
+    }
+}
+
+static void
+json_parser_get_property (GObject    *gobject,
+                          guint       prop_id,
+                          GValue     *value,
+                          GParamSpec *pspec)
+{
+  JsonParserPrivate *priv = JSON_PARSER (gobject)->priv;
+
+  switch (prop_id)
+    {
+    case PROP_IMMUTABLE:
+      g_value_set_boolean (value, priv->immutable);
+      break;
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (gobject, prop_id, pspec);
+      break;
+    }
+}
+
+static void
 json_parser_class_init (JsonParserClass *klass)
 {
   GObjectClass *gobject_class = G_OBJECT_CLASS (klass);
 
+  gobject_class->set_property = json_parser_set_property;
+  gobject_class->get_property = json_parser_get_property;
   gobject_class->dispose = json_parser_dispose;
   gobject_class->finalize = json_parser_finalize;
 
   /**
+   * JsonParser:immutable:
+   *
+   * Whether the #JsonNode tree built by the #JsonParser should be immutable
+   * when created. Making the output immutable on creation avoids the expense
+   * of traversing it to make it immutable later.
+   *
+   * Since: UNRELEASED
+   */
+  parser_props[PROP_IMMUTABLE] =
+    g_param_spec_boolean ("immutable",
+                          "Immutable Output",
+                          "Whether the parser output is immutable.",
+                          FALSE,
+                          G_PARAM_CONSTRUCT_ONLY | G_PARAM_READWRITE);
+
+  g_object_class_install_properties (gobject_class, PROP_LAST, parser_props);
+
+  /**
    * JsonParser::parse-start:
    * @parser: the #JsonParser that received the signal
    * 
@@ -424,6 +494,9 @@ json_parse_value (JsonParser   *parser,
       break;
     }
 
+  if (priv->immutable && *node != NULL)
+    json_node_seal (*node);
+
   return G_TOKEN_NONE;
 }
 
@@ -522,6 +595,8 @@ json_parse_array (JsonParser   *parser,
 
       JSON_NOTE (PARSER, "Array element %d completed", idx);
       json_node_set_parent (element, priv->current_node);
+      if (priv->immutable)
+        json_node_seal (element);
       json_array_add_element (array, element);
 
       g_signal_emit (parser, parser_signals[ARRAY_ELEMENT], 0,
@@ -535,7 +610,13 @@ json_parse_array (JsonParser   *parser,
 array_done:
   json_scanner_get_next_token (scanner);
 
+  /* We can guarantee that all the array elements are immutable, so we
+   * can skip the formal loop over them to seal them again. */
+  array->immutable = TRUE;
+
   json_node_take_array (priv->current_node, array);
+  if (priv->immutable)
+    json_node_seal (priv->current_node);
   json_node_set_parent (priv->current_node, old_current);
 
   g_signal_emit (parser, parser_signals[ARRAY_END], 0, array);
@@ -697,6 +778,8 @@ json_parse_object (JsonParser   *parser,
 
       JSON_NOTE (PARSER, "Object member '%s' completed", name);
       json_node_set_parent (member, priv->current_node);
+      if (priv->immutable)
+        json_node_seal (member);
       json_object_set_member (object, name, member);
 
       g_signal_emit (parser, parser_signals[OBJECT_MEMBER], 0,
@@ -710,7 +793,13 @@ json_parse_object (JsonParser   *parser,
 
   json_scanner_get_next_token (scanner);
 
+  /* We can guarantee that all the object members are immutable, so we
+   * can skip the formal loop over them to seal them again. */
+  object->immutable = TRUE;
+
   json_node_take_object (priv->current_node, object);
+  if (priv->immutable)
+    json_node_seal (priv->current_node);
   json_node_set_parent (priv->current_node, old_current);
 
   g_signal_emit (parser, parser_signals[OBJECT_END], 0, object);
@@ -876,6 +965,21 @@ json_parser_new (void)
   return g_object_new (JSON_TYPE_PARSER, NULL);
 }
 
+/**
+ * json_parser_new_immutable:
+ *
+ * Creates a new #JsonParser instance with its #JsonParser:immutable property
+ * set to %TRUE to create immutable output trees.
+ *
+ * Since: UNRELEASED
+ * Returns: (transfer full): a new #JsonParser
+ */
+JsonParser *
+json_parser_new_immutable (void)
+{
+  return g_object_new (JSON_TYPE_PARSER, "immutable", TRUE, NULL);
+}
+
 static gboolean
 json_parser_load (JsonParser   *parser,
                   const gchar  *data,
@@ -1097,6 +1201,10 @@ json_parser_get_root (JsonParser *parser)
 {
   g_return_val_if_fail (JSON_IS_PARSER (parser), NULL);
 
+  /* Sanity check. */
+  g_return_val_if_fail (!parser->priv->immutable ||
+                        json_node_is_immutable (parser->priv->root), NULL);
+
   return parser->priv->root;
 }
 
diff --git a/json-glib/json-parser.h b/json-glib/json-parser.h
index a65558e..c7223d2 100644
--- a/json-glib/json-parser.h
+++ b/json-glib/json-parser.h
@@ -147,6 +147,8 @@ GType json_parser_get_type (void) G_GNUC_CONST;
 
 JSON_AVAILABLE_IN_1_0
 JsonParser *json_parser_new                     (void);
+JSON_AVAILABLE_IN_1_2
+JsonParser *json_parser_new_immutable           (void);
 JSON_AVAILABLE_IN_1_0
 gboolean    json_parser_load_from_file          (JsonParser           *parser,
                                                  const gchar          *filename,
diff --git a/json-glib/json-types-private.h b/json-glib/json-types-private.h
index f9ac064..34a3160 100644
--- a/json-glib/json-types-private.h
+++ b/json-glib/json-types-private.h
@@ -43,6 +43,7 @@ struct _JsonNode
 {
   /*< private >*/
   JsonNodeType type;
+  gboolean immutable : 1;
 
   union {
     JsonObject *object;
@@ -69,6 +70,7 @@ struct _JsonValue
   JsonValueType type;
 
   volatile gint ref_count;
+  gboolean immutable : 1;
 
   union {
     gint64 v_int;
@@ -83,6 +85,7 @@ struct _JsonArray
   GPtrArray *elements;
 
   volatile gint ref_count;
+  gboolean immutable : 1;
 };
 
 struct _JsonObject
@@ -93,6 +96,7 @@ struct _JsonObject
   GList *members_ordered;
 
   volatile gint ref_count;
+  gboolean immutable : 1;
 };
 
 typedef struct
@@ -146,6 +150,9 @@ void            json_value_set_string           (JsonValue       *value,
 G_GNUC_INTERNAL
 const gchar *   json_value_get_string           (const JsonValue *value);
 
+G_GNUC_INTERNAL
+void            json_value_seal                 (JsonValue       *value);
+
 G_END_DECLS
 
 #endif /* __JSON_TYPES_PRIVATE_H__ */
diff --git a/json-glib/json-types.h b/json-glib/json-types.h
index 1ee84ff..5f2a084 100644
--- a/json-glib/json-types.h
+++ b/json-glib/json-types.h
@@ -276,6 +276,10 @@ gboolean              json_node_get_boolean     (JsonNode     *node);
 JSON_AVAILABLE_IN_1_0
 gboolean              json_node_is_null         (JsonNode     *node);
 
+JSON_AVAILABLE_IN_1_2
+void                  json_node_seal            (JsonNode     *node);
+gboolean              json_node_is_immutable    (JsonNode     *node);
+
 /*
  * JsonObject
  */
@@ -368,6 +372,11 @@ void                  json_object_foreach_member     (JsonObject  *object,
                                                       JsonObjectForeach func,
                                                       gpointer     data);
 
+JSON_AVAILABLE_IN_1_2
+void                  json_object_seal               (JsonObject  *object);
+JSON_AVAILABLE_IN_1_2
+gboolean              json_object_is_immutable       (JsonObject  *object);
+
 /**
  * JsonObjectIter:
  *
@@ -467,6 +476,10 @@ JSON_AVAILABLE_IN_1_0
 void                  json_array_foreach_element     (JsonArray   *array,
                                                       JsonArrayForeach func,
                                                       gpointer     data);
+JSON_AVAILABLE_IN_1_2
+void                  json_array_seal                (JsonArray   *array);
+JSON_AVAILABLE_IN_1_2
+gboolean              json_array_is_immutable        (JsonArray   *array);
 
 #ifdef G_DEFINE_AUTOPTR_CLEANUP_FUNC
 G_DEFINE_AUTOPTR_CLEANUP_FUNC (JsonArray, json_array_unref)
diff --git a/json-glib/json-value.c b/json-glib/json-value.c
index b6d47e1..148cf8d 100644
--- a/json-glib/json-value.c
+++ b/json-glib/json-value.c
@@ -2,6 +2,7 @@
  * 
  * This file is part of JSON-GLib
  * Copyright (C) 2012  Emmanuele Bassi <ebassi gnome org>
+ * Copyright (C) 2015 Collabora Ltd.
  *
  * This library is free software; you can redistribute it and/or
  * modify it under the terms of the GNU Lesser General Public
@@ -18,6 +19,7 @@
  *
  * Author:
  *   Emmanuele Bassi  <ebassi linux intel com>
+ *   Philip Withnall  <philip withnall collabora co uk>
  */
 
 #include "config.h"
@@ -165,12 +167,32 @@ json_value_free (JsonValue *value)
     }
 }
 
+/**
+ * json_value_seal:
+ * @value: a #JsonValue
+ *
+ * Seals the #JsonValue, making it immutable to further changes.
+ *
+ * If the @value is already immutable, this is a no-op.
+ *
+ * Since: UNRELEASED
+ */
+void
+json_value_seal (JsonValue *value)
+{
+  g_return_if_fail (JSON_VALUE_IS_VALID (value));
+  g_return_if_fail (value->ref_count > 0);
+
+  value->immutable = TRUE;
+}
+
 #define _JSON_VALUE_DEFINE_SET(Type,EType,CType,VField) \
 void \
 json_value_set_##Type (JsonValue *value, CType VField) \
 { \
   g_return_if_fail (JSON_VALUE_IS_VALID (value)); \
   g_return_if_fail (JSON_VALUE_HOLDS (value, JSON_VALUE_##EType)); \
+  g_return_if_fail (!value->immutable); \
 \
   value->data.VField = VField; \
 \
@@ -202,6 +224,7 @@ json_value_set_string (JsonValue *value,
 {
   g_return_if_fail (JSON_VALUE_IS_VALID (value));
   g_return_if_fail (JSON_VALUE_HOLDS_STRING (value));
+  g_return_if_fail (!value->immutable);
 
   g_free (value->data.v_str);
   value->data.v_str = g_strdup (v_str);
diff --git a/json-glib/tests/node.c b/json-glib/tests/node.c
index 6aad5bc..23bda63 100644
--- a/json-glib/tests/node.c
+++ b/json-glib/tests/node.c
@@ -242,6 +242,311 @@ test_gvalue_autopromotion (void)
   json_node_free (node);
 }
 
+/* Test that creating then sealing a node containing an int causes it to be
+ * immutable. */
+static void
+test_seal_int (void)
+{
+  JsonNode *node = NULL;
+
+  node = json_node_init_int (json_node_alloc (), 1);
+
+  g_assert_false (json_node_is_immutable (node));
+  json_node_seal (node);
+  g_assert_true (json_node_is_immutable (node));
+  json_node_free (node);
+}
+
+/* Test that creating then sealing a node containing a double causes it to be
+ * immutable. */
+static void
+test_seal_double (void)
+{
+  JsonNode *node = NULL;
+
+  node = json_node_init_double (json_node_alloc (), 15.2);
+  g_assert_false (json_node_is_immutable (node));
+  json_node_seal (node);
+  g_assert_true (json_node_is_immutable (node));
+  json_node_free (node);
+}
+
+/* Test that creating then sealing a node containing a boolean causes it to be
+ * immutable. */
+static void
+test_seal_boolean (void)
+{
+  JsonNode *node = NULL;
+
+  node = json_node_init_boolean (json_node_alloc (), TRUE);
+  g_assert_false (json_node_is_immutable (node));
+  json_node_seal (node);
+  g_assert_true (json_node_is_immutable (node));
+  json_node_free (node);
+}
+
+/* Test that creating then sealing a node containing a string causes it to be
+ * immutable. */
+static void
+test_seal_string (void)
+{
+  JsonNode *node = NULL;
+
+  node = json_node_init_string (json_node_alloc (), "hi there");
+  g_assert_false (json_node_is_immutable (node));
+  json_node_seal (node);
+  g_assert_true (json_node_is_immutable (node));
+  json_node_free (node);
+}
+
+/* Test that creating then sealing a node containing a null causes it to be
+ * immutable. */
+static void
+test_seal_null (void)
+{
+  JsonNode *node = NULL;
+
+  node = json_node_init_null (json_node_alloc ());
+  g_assert_false (json_node_is_immutable (node));
+  json_node_seal (node);
+  g_assert_true (json_node_is_immutable (node));
+  json_node_free (node);
+}
+
+/* Test that creating then sealing a node containing an object causes it to be
+ * immutable. */
+static void
+test_seal_object (void)
+{
+  JsonNode *node = NULL;
+  JsonObject *object = NULL;
+
+  object = json_object_new ();
+  node = json_node_init_object (json_node_alloc (), object);
+
+  g_assert_false (json_object_is_immutable (object));
+  g_assert_false (json_node_is_immutable (node));
+  json_node_seal (node);
+  g_assert_true (json_node_is_immutable (node));
+  g_assert_true (json_object_is_immutable (object));
+
+  json_node_free (node);
+  json_object_unref (object);
+}
+
+/* Test that creating then sealing a node containing an array causes it to be
+ * immutable. */
+static void
+test_seal_array (void)
+{
+  JsonNode *node = NULL;
+  JsonArray *array = NULL;
+
+  array = json_array_new ();
+  node = json_node_init_array (json_node_alloc (), array);
+
+  g_assert_false (json_array_is_immutable (array));
+  g_assert_false (json_node_is_immutable (node));
+  json_node_seal (node);
+  g_assert_true (json_node_is_immutable (node));
+  g_assert_true (json_array_is_immutable (array));
+
+  json_node_free (node);
+  json_array_unref (array);
+}
+
+/* Test that an immutable node containing an int cannot be modified. */
+static void
+test_immutable_int (void)
+{
+  if (g_test_subprocess ())
+    {
+      JsonNode *node = NULL;
+
+      node = json_node_init_int (json_node_alloc (), 5);
+      json_node_seal (node);
+
+      /* Boom. */
+      json_node_set_int (node, 1);
+    }
+
+  g_test_trap_subprocess (NULL, 0, 0);
+  g_test_trap_assert_failed ();
+  g_test_trap_assert_stderr ("*Json-CRITICAL **: json_node_set_int: "
+                             "assertion '!node->immutable' failed*");
+}
+
+/* Test that an immutable node containing a double cannot be modified. */
+static void
+test_immutable_double (void)
+{
+  if (g_test_subprocess ())
+    {
+      JsonNode *node = NULL;
+
+      node = json_node_init_double (json_node_alloc (), 5.6);
+      json_node_seal (node);
+
+      /* Boom. */
+      json_node_set_double (node, 1.1);
+    }
+
+  g_test_trap_subprocess (NULL, 0, 0);
+  g_test_trap_assert_failed ();
+  g_test_trap_assert_stderr ("*Json-CRITICAL **: json_node_set_double: "
+                             "assertion '!node->immutable' failed*");
+}
+
+/* Test that an immutable node containing a boolean cannot be modified. */
+static void
+test_immutable_boolean (void)
+{
+  if (g_test_subprocess ())
+    {
+      JsonNode *node = NULL;
+
+      node = json_node_init_boolean (json_node_alloc (), TRUE);
+      json_node_seal (node);
+
+      /* Boom. */
+      json_node_set_boolean (node, FALSE);
+    }
+
+  g_test_trap_subprocess (NULL, 0, 0);
+  g_test_trap_assert_failed ();
+  g_test_trap_assert_stderr ("*Json-CRITICAL **: json_node_set_boolean: "
+                             "assertion '!node->immutable' failed*");
+}
+
+/* Test that an immutable node containing a string cannot be modified. */
+static void
+test_immutable_string (void)
+{
+  if (g_test_subprocess ())
+    {
+      JsonNode *node = NULL;
+
+      node = json_node_init_string (json_node_alloc (), "bonghits");
+      json_node_seal (node);
+
+      /* Boom. */
+      json_node_set_string (node, "asdasd");
+    }
+
+  g_test_trap_subprocess (NULL, 0, 0);
+  g_test_trap_assert_failed ();
+  g_test_trap_assert_stderr ("*Json-CRITICAL **: json_node_set_string: "
+                             "assertion '!node->immutable' failed*");
+}
+
+/* Test that an immutable node containing an object cannot be modified. */
+static void
+test_immutable_object (void)
+{
+  if (g_test_subprocess ())
+    {
+      JsonNode *node = NULL;
+
+      node = json_node_init_object (json_node_alloc (), json_object_new ());
+      json_node_seal (node);
+
+      /* Boom. */
+      json_node_set_object (node, json_object_new ());
+    }
+
+  g_test_trap_subprocess (NULL, 0, 0);
+  g_test_trap_assert_failed ();
+  g_test_trap_assert_stderr ("*Json-CRITICAL **: json_node_set_object: "
+                             "assertion '!node->immutable' failed*");
+}
+
+/* Test that an immutable node containing an array cannot be modified. */
+static void
+test_immutable_array (void)
+{
+  if (g_test_subprocess ())
+    {
+      JsonNode *node = NULL;
+
+      node = json_node_init_array (json_node_alloc (), json_array_new ());
+      json_node_seal (node);
+
+      /* Boom. */
+      json_node_set_array (node, json_array_new ());
+    }
+
+  g_test_trap_subprocess (NULL, 0, 0);
+  g_test_trap_assert_failed ();
+  g_test_trap_assert_stderr ("*Json-CRITICAL **: json_node_set_array: "
+                             "assertion '!node->immutable' failed*");
+}
+
+/* Test that an immutable node containing a value cannot be modified. */
+static void
+test_immutable_value (void)
+{
+  if (g_test_subprocess ())
+    {
+      JsonNode *node = NULL;
+      GValue val = G_VALUE_INIT;
+
+      node = json_node_init_int (json_node_alloc (), 5);
+      json_node_seal (node);
+
+      /* Boom. */
+      g_value_init (&val, G_TYPE_INT);
+      g_value_set_int (&val, 50);
+      json_node_set_value (node, &val);
+    }
+
+  g_test_trap_subprocess (NULL, 0, 0);
+  g_test_trap_assert_failed ();
+  g_test_trap_assert_stderr ("*Json-CRITICAL **: json_node_set_value: "
+                             "assertion '!node->immutable' failed*");
+}
+
+/* Test that an immutable node can be reparented but not to an immutable
+ * parent. */
+static void
+test_immutable_parent (void)
+{
+  if (g_test_subprocess ())
+    {
+      JsonNode *node = NULL;
+      JsonNode *parent_mutable = NULL;
+      JsonNode *parent_immutable = NULL;
+      JsonObject *object_mutable = NULL;
+      JsonObject *object_immutable = NULL;
+
+      node = json_node_init_int (json_node_alloc (), 5);
+      json_node_seal (node);
+
+      object_mutable = json_object_new ();
+      object_immutable = json_object_new ();
+
+      parent_mutable = json_node_init_object (json_node_alloc (),
+                                              object_mutable);
+      parent_immutable = json_node_init_object (json_node_alloc (),
+                                                object_immutable);
+
+      json_node_seal (parent_immutable);
+
+      /* Can we reparent the immutable node? */
+      json_object_set_member (object_mutable, "test", node);
+      json_node_set_parent (node, parent_mutable);
+
+      json_object_remove_member (object_mutable, "test");
+      json_node_set_parent (node, NULL);
+
+      /* Boom. */
+      json_node_set_parent (node, parent_immutable);
+    }
+
+  g_test_trap_subprocess (NULL, 0, 0);
+  g_test_trap_assert_failed ();
+  g_test_trap_assert_stderr ("*Json-CRITICAL **: json_node_set_parent: *");
+}
+
 int
 main (int   argc,
       char *argv[])
@@ -260,6 +565,21 @@ main (int   argc,
   g_test_add_func ("/nodes/get/double", test_get_double);
   g_test_add_func ("/nodes/gvalue", test_gvalue);
   g_test_add_func ("/nodes/gvalue/autopromotion", test_gvalue_autopromotion);
+  g_test_add_func ("/nodes/seal/int", test_seal_int);
+  g_test_add_func ("/nodes/seal/double", test_seal_double);
+  g_test_add_func ("/nodes/seal/boolean", test_seal_boolean);
+  g_test_add_func ("/nodes/seal/string", test_seal_string);
+  g_test_add_func ("/nodes/seal/null", test_seal_null);
+  g_test_add_func ("/nodes/seal/object", test_seal_object);
+  g_test_add_func ("/nodes/seal/array", test_seal_array);
+  g_test_add_func ("/nodes/immutable/int", test_immutable_int);
+  g_test_add_func ("/nodes/immutable/double", test_immutable_double);
+  g_test_add_func ("/nodes/immutable/boolean", test_immutable_boolean);
+  g_test_add_func ("/nodes/immutable/string", test_immutable_string);
+  g_test_add_func ("/nodes/immutable/object", test_immutable_object);
+  g_test_add_func ("/nodes/immutable/array", test_immutable_array);
+  g_test_add_func ("/nodes/immutable/value", test_immutable_value);
+  g_test_add_func ("/nodes/immutable/parent", test_immutable_parent);
 
   return g_test_run ();
 }



[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]