Json.vala 8.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200
  1. using Invercargill;
  2. using Invercargill.DataStructures;
  3. using InvercargillJson;
  4. using Statum;
  5. namespace ClankerSaddle {
  6. /**
  7. * Helpers for reading the JSON objects that travel the wire —
  8. * pushed results/chunks entries, block payloads and request
  9. * bodies. One implementation of each member shape (string,
  10. * nullable string, string array — native or JSON-encoded — and
  11. * ISO-8601 stamps), shared by SaddleService, SaddleApi and
  12. * TaskService instead of three private copies with drifting
  13. * edge cases.
  14. */
  15. public class EntryJson : Object {
  16. /** A string member: "" when absent or not a string. */
  17. public static string string_member(Json.Object obj, string name) {
  18. return obj.has_member(name) && obj.get_member(name).get_string() != null
  19. ? obj.get_string_member(name) : "";
  20. }
  21. /** A nullable string member: null when absent or empty. */
  22. public static string? optional_string_member(Json.Object obj, string name) {
  23. string value = string_member(obj, name);
  24. return value.length > 0 ? value : null;
  25. }
  26. /** A string member, or `fallback` when absent or not a string. */
  27. public static string get_string_or(Json.Object obj, string name, string? fallback) {
  28. if (obj.has_member(name) && obj.get_member(name).get_string() != null) {
  29. return obj.get_string_member(name);
  30. }
  31. return fallback;
  32. }
  33. /**
  34. * An ISO-8601 string member as a DateTime (null when absent or
  35. * not a string; a bare local time reads as UTC, the daemon's
  36. * clock domain).
  37. */
  38. public static DateTime? parse_iso(Json.Object obj, string name) {
  39. if (!obj.has_member(name)) {
  40. return null;
  41. }
  42. var value = obj.get_member(name);
  43. if (value.get_node_type() != Json.NodeType.VALUE || value.get_string() == null) {
  44. return null;
  45. }
  46. return new DateTime.from_iso8601(value.get_string(), new TimeZone.utc());
  47. }
  48. /**
  49. * A string-array member: native JSON arrays are read
  50. * element-wise; a JSON-encoded string is parsed; anything else
  51. * is empty. `what` names the member in the malformed-payload
  52. * warning.
  53. */
  54. public static string[] string_array_member(Json.Object obj, string name,
  55. string what = "options array") {
  56. if (!obj.has_member(name)) {
  57. return new string[0];
  58. }
  59. var member = obj.get_member(name);
  60. if (member.get_node_type() == Json.NodeType.ARRAY) {
  61. return elements_of(member.get_array());
  62. }
  63. string raw = string_member(obj, name);
  64. if (raw.length == 0) {
  65. return new string[0];
  66. }
  67. try {
  68. var parser = new Json.Parser();
  69. parser.load_from_data(raw);
  70. var root = parser.get_root();
  71. if (root != null && root.get_node_type() == Json.NodeType.ARRAY) {
  72. return elements_of(root.get_array());
  73. }
  74. } catch (Error e) {
  75. warning("[ClankerSaddle] Ignoring malformed %s: %s", what, e.message);
  76. }
  77. return new string[0];
  78. }
  79. /**
  80. * A stored JSON array-of-strings text (a block's options
  81. * column) back into a Vala array; null, empty, non-array or
  82. * malformed input is empty.
  83. */
  84. public static string[] parse_string_array(string? raw) {
  85. if (raw == null || ((!)raw).length == 0) {
  86. return new string[0];
  87. }
  88. try {
  89. var parser = new Json.Parser();
  90. parser.load_from_data((!)raw);
  91. var root = parser.get_root();
  92. if (root == null || root.get_node_type() != Json.NodeType.ARRAY) {
  93. return new string[0];
  94. }
  95. return elements_of(root.get_array());
  96. } catch (Error e) {
  97. return new string[0];
  98. }
  99. }
  100. /** A JSON array's string elements (non-strings are skipped). */
  101. private static string[] elements_of(Json.Array array) {
  102. var values = new Vector<string>();
  103. foreach (var element in array.get_elements()) {
  104. if (element.get_node_type() == Json.NodeType.VALUE
  105. && element.get_string() != null) {
  106. values.add(element.get_string());
  107. }
  108. }
  109. return values.to_array();
  110. }
  111. }
  112. /**
  113. * Helpers for shaping Statum slot state. Because Statum's GObjectMapping
  114. * cannot serialise array/list fields, lists are built as a JsonArray and set
  115. * on a PropertyDictionary under a key, where stm-for-{x}-in can iterate them.
  116. */
  117. public class StateJson : Object {
  118. /**
  119. * Builds a JSON array element from a lot of GObject row models, mapping
  120. * each row via GObjectMapping. Use the result as a slot public-data value:
  121. * `dict["tasks"] = StateJson.list<TaskView>(rows);`
  122. */
  123. public static JsonElement list<T>(Lot<T> items) throws GLib.Error {
  124. var arr = new JsonArray();
  125. foreach (var item in items) {
  126. var props = GObjectMapping.to_properties((Object) item);
  127. arr.add(new JsonElement.from_properties(props));
  128. }
  129. return new JsonElement.from_elements(arr);
  130. }
  131. /** Same as {@link list} but accepts a Vala array. */
  132. public static JsonElement list_array<T>(T[] items) throws GLib.Error {
  133. var arr = new JsonArray();
  134. foreach (var item in items) {
  135. var props = GObjectMapping.to_properties((Object) item);
  136. arr.add(new JsonElement.from_properties(props));
  137. }
  138. return new JsonElement.from_elements(arr);
  139. }
  140. /**
  141. * A single row model as a slot public-data value (the object
  142. * twin of {@link list}): `dict[\"current\"] = StateJson.element(row);`
  143. * — null-safe, a null row maps to a JSON null.
  144. */
  145. public static JsonElement? element(Object? item) throws GLib.Error {
  146. if (item == null) {
  147. return null;
  148. }
  149. var props = GObjectMapping.to_properties((!)item);
  150. return new JsonElement.from_properties(props);
  151. }
  152. /**
  153. * A JSON array of plain strings (e.g. a block's multiple-choice
  154. * options).
  155. */
  156. public static JsonElement strings(string[] values) throws GLib.Error {
  157. var arr = new JsonArray();
  158. foreach (var value in values) {
  159. arr.add(new JsonElement.from_element(new NativeElement<string>(value)));
  160. }
  161. return new JsonElement.from_elements(arr);
  162. }
  163. /**
  164. * Per-row enrichment hook for {@link list_rich}: after each item
  165. * is mapped, the enricher may add array/complex values the plain
  166. * GObject mapping cannot serialise (e.g. a block's option list).
  167. */
  168. public delegate void PropertyEnricher(Object item, PropertyDictionary props) throws GLib.Error;
  169. /**
  170. * Like {@link list}, but every row's property dictionary passes
  171. * through `enrich` before serialisation so callers can attach
  172. * fields GObjectMapping drops (arrays, nested objects).
  173. */
  174. public static JsonElement list_rich<T>(Lot<T> items, owned PropertyEnricher enrich) throws GLib.Error {
  175. var arr = new JsonArray();
  176. foreach (var item in items) {
  177. var props = (PropertyDictionary) GObjectMapping.to_properties((Object) item);
  178. enrich((Object) item, props);
  179. arr.add(new JsonElement.from_properties(props));
  180. }
  181. return new JsonElement.from_elements(arr);
  182. }
  183. }
  184. }