GObjectMapping.vala 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368
  1. using Invercargill;
  2. using Invercargill.DataStructures;
  3. using Invercargill.Mapping;
  4. using InvercargillJson;
  5. namespace Statum {
  6. /**
  7. * Non-generic adapter so heterogeneous {@link PropertyMapper}s can be stored
  8. * in a single registry keyed by {@link Type}.
  9. */
  10. internal interface ObjectPropertyMapper : Object {
  11. public abstract Properties map_from_object(Object o) throws GLib.Error;
  12. public abstract Object materialise_object(JsonObject json) throws GLib.Error;
  13. }
  14. /**
  15. * Maps between GObject instances and {@link Properties} using GObject
  16. * property introspection, so an application can declare a strongly-typed
  17. * data model as an ordinary GObject property class (e.g.
  18. * `public string order_id { get; set; }`) and have the framework map it
  19. * with no per-model mapper.
  20. *
  21. * Scalar properties (`string`, `int`, `int64`, `long`, `bool`, `double`,
  22. * enums) serialise as type-correct JSON primitives on every Statum path —
  23. * the public snapshot path and the encrypted private path
  24. * ({@link Statum.Cryptography.EncryptionProvider.author_properties}) alike.
  25. * `Object`-typed properties whose {@link Type} is registered (see
  26. * {@link register_mapper}) are mapped recursively via their
  27. * {@link PropertyMapper} — this is how typed action-reference fields (e.g.
  28. * `public Model.ActionDto bump { get; set; }`) round-trip as nested JSON.
  29. * Array-shaped properties serialise as JSON arrays; see {@link to_properties}
  30. * for the supported shapes and {@link from_properties} for read-back.
  31. */
  32. public class GObjectMapping : Object {
  33. private static GLib.HashTable<Type, ObjectPropertyMapper> _mappers;
  34. private static GLib.HashTable<Type, ObjectPropertyMapper> mappers() {
  35. if (_mappers == null) {
  36. _mappers = new GLib.HashTable<Type, ObjectPropertyMapper>(GLib.direct_hash, GLib.direct_equal);
  37. }
  38. return _mappers;
  39. }
  40. /**
  41. * Registers a {@link PropertyMapper} for `T` so that `Object`-typed
  42. * properties of that type are mapped recursively. Called once at startup
  43. * for framework types (e.g. {@link Model.ActionDto}); apps may register
  44. * their own nested model mappers.
  45. */
  46. public static void register_mapper<T>(PropertyMapper<T> mapper) {
  47. mappers()[typeof(T)] = new MapperAdapter<T>(mapper);
  48. }
  49. /**
  50. * Maps a GObject instance to a {@link PropertyDictionary} by reading its
  51. * declared (readable) GObject properties, ready to be used directly as
  52. * slot public/private data (no cast required — see S-13).
  53. *
  54. * Scalars (`string`, `int`, `int64`, `long`, `bool`, `double`, enums)
  55. * serialise as JSON primitives of the matching type on every path,
  56. * {@link Statum.Cryptography.EncryptionProvider.author_properties}
  57. * included. Registered `Object`-typed properties map recursively as
  58. * nested JSON objects; unregistered `Object` properties are skipped
  59. * unless they hold a collection (below).
  60. *
  61. * Arrays serialise as JSON arrays for two shapes: `string[]`
  62. * properties, and `Object`-typed properties whose runtime value is an
  63. * Invercargill {@link Invercargill.Enumerable} (e.g. `Series<T>`,
  64. * `Lot<T>`, `Vector<T>`). Collection elements that are registered
  65. * {@link Object} types map recursively via their mapper; fundamental
  66. * elements (`string`, `int`, `int64`, `bool`, `double`) map as JSON
  67. * primitives.
  68. *
  69. * **Limitation:** collection-typed properties are write-side only — a
  70. * property's GType does not record the collection's element type, so
  71. * {@link from_properties} cannot repopulate them. Models carrying
  72. * collection fields must be rebuilt from their source of truth (e.g.
  73. * the database) rather than round-tripped;
  74. * {@link DirectiveBuilder.update_held} carries such fields forward
  75. * unchanged instead of dropping them.
  76. */
  77. public static PropertyDictionary to_properties(Object model) throws GLib.Error {
  78. var dict = new PropertyDictionary();
  79. var object_class = (ObjectClass) model.get_type().class_ref();
  80. foreach (var spec in object_class.list_properties()) {
  81. if ((spec.flags & ParamFlags.READABLE) == 0) {
  82. continue;
  83. }
  84. // GObject stores property names with dashes (Vala underscores →
  85. // dashes); use the underscored name on the wire so client-side
  86. // expressions like `auth.update_announcement` resolve correctly.
  87. var key = spec.name.replace("-", "_");
  88. var value = Value(spec.value_type);
  89. model.get_property(spec.name, ref value);
  90. if (mappers().lookup(spec.value_type) != null) {
  91. var obj = value.get_object();
  92. if (obj != null) {
  93. var sub_props = (!)(mappers().lookup(spec.value_type)).map_from_object((!)obj);
  94. dict[key] = new JsonElement.from_properties(sub_props);
  95. }
  96. continue;
  97. }
  98. if (spec.value_type.is_a(typeof(Object))) {
  99. var obj = value.get_object();
  100. if (obj != null && ((!)obj).get_type().is_a(typeof(Enumerable))) {
  101. dict[key] = enumerable_to_array_element((!)obj);
  102. }
  103. // Other unregistered Object properties do not serialise.
  104. continue;
  105. }
  106. if (spec.value_type == typeof(string[])) {
  107. var raw = value.get_boxed();
  108. if (raw != null) {
  109. dict[key] = strv_to_array_element(raw);
  110. } else {
  111. dict[key] = new ValueElement(value);
  112. }
  113. continue;
  114. }
  115. dict[key] = new ValueElement(value);
  116. }
  117. return dict;
  118. }
  119. /**
  120. * Builds a new instance of `type` and populates its (writable) GObject
  121. * properties from `props`. Registered `Object`-typed properties are
  122. * materialised recursively; `string[]` properties are populated from
  123. * JSON arrays; JSON numbers/booleans coerce back into `int`, `int64`,
  124. * `long`, `double`, enum and boolean properties (including `long` and
  125. * `ulong`, which {@link Element.to_value} cannot express on its own).
  126. *
  127. * Collection-typed properties are not repopulated — see the limitation
  128. * documented on {@link to_properties} for the rebuild-from-source
  129. * pattern that replaces round-tripping such models.
  130. */
  131. public static Object from_properties(Type type, Properties props) throws GLib.Error {
  132. var model = Object.new(type);
  133. var object_class = (ObjectClass) type.class_ref();
  134. foreach (var spec in object_class.list_properties()) {
  135. if ((spec.flags & ParamFlags.WRITABLE) == 0) {
  136. continue;
  137. }
  138. var key = spec.name.replace("-", "_");
  139. Element element;
  140. if (!props.try_get(key, out element) || element == null) {
  141. continue;
  142. }
  143. if (mappers().lookup(spec.value_type) != null) {
  144. try {
  145. var json_obj = ((!)element).as<JsonObject>();
  146. var obj = (!)(mappers().lookup(spec.value_type)).materialise_object(json_obj);
  147. model.set_property(spec.name, obj);
  148. } catch (GLib.Error e) {
  149. // Skip properties whose nested object cannot be materialised.
  150. }
  151. continue;
  152. }
  153. if (spec.value_type == typeof(string[])) {
  154. try {
  155. var value = Value(spec.value_type);
  156. value.set_boxed(read_string_array((!)element));
  157. model.set_property(spec.name, value);
  158. } catch (GLib.Error e) {
  159. // Skip properties whose array cannot be read.
  160. }
  161. continue;
  162. }
  163. try {
  164. var value = element_value(((!)element), spec.value_type);
  165. model.set_property(spec.name, value);
  166. } catch (GLib.Error e) {
  167. var coerced = coerce_numeric((!)element, spec.value_type);
  168. if (coerced != null) {
  169. model.set_property(spec.name, (!)coerced);
  170. }
  171. // Skip properties whose value cannot be coerced.
  172. }
  173. }
  174. return model;
  175. }
  176. /**
  177. * Convenience: builds and populates `type`, returning it cast to `T`.
  178. */
  179. public static T from_properties_typed<T>(Type type, Properties props) throws GLib.Error {
  180. return (T) from_properties(type, props);
  181. }
  182. /**
  183. * Returns `mapped` with the JSON-array values of `current` that
  184. * `mapped` no longer carries re-attached unchanged. A typed update
  185. * cannot re-emit collection fields it was unable to read back into the
  186. * model, so this keeps them instead of silently dropping them.
  187. */
  188. internal static PropertyDictionary preserving_arrays(Properties current, PropertyDictionary mapped) {
  189. foreach (var item in current) {
  190. Element existing;
  191. if (mapped.try_get(item.key, out existing)) {
  192. continue;
  193. }
  194. if (item.value != null && item.value.assignable_to<Elements>()) {
  195. mapped[item.key] = item.value;
  196. }
  197. }
  198. return mapped;
  199. }
  200. /**
  201. * Builds the JSON array {@link Element} for a `string[]` (`GStrv`)
  202. * property value.
  203. */
  204. private static Element strv_to_array_element(void* strv) {
  205. var items = new Series<Element>();
  206. var cursor = (string**) strv;
  207. for (var i = 0; cursor[i] != null; i++) {
  208. var value = Value(typeof(string));
  209. value.set_string((string) cursor[i]);
  210. items.add(new ValueElement(value));
  211. }
  212. return new NativeElement<Elements>(items.to_elements());
  213. }
  214. /**
  215. * Builds the JSON array {@link Element} for an Invercargill collection
  216. * property value: registered `Object` elements map recursively via
  217. * their mapper, fundamental elements pass through as primitives.
  218. */
  219. private static Element enumerable_to_array_element(Object enumerable) throws GLib.Error {
  220. var boxed = Value(typeof(Enumerable));
  221. boxed.set_object(enumerable);
  222. var items = new Series<Element>();
  223. foreach (var item in new ValueElement(boxed).as<Elements>()) {
  224. var item_type = item.type();
  225. if (item_type != null && mappers().lookup((!)item_type) != null) {
  226. var obj = item.as<Object>();
  227. if (obj != null) {
  228. var sub_props = (!)(mappers().lookup((!)item_type)).map_from_object((!)obj);
  229. items.add(new JsonElement.from_properties(sub_props));
  230. continue;
  231. }
  232. }
  233. items.add(item);
  234. }
  235. return new NativeElement<Elements>(items.to_elements());
  236. }
  237. /**
  238. * Reads a JSON array {@link Element} back into a null-terminated
  239. * `string[]` suitable for a `GStrv` property value.
  240. */
  241. private static string[] read_string_array(Element element) throws GLib.Error {
  242. string[] items = new string[0];
  243. foreach (var item in element.as<Elements>()) {
  244. items += item.as_string_or_null() ?? "";
  245. }
  246. return items;
  247. }
  248. /**
  249. * Coerces a JSON number into the numeric targets
  250. * {@link Element.to_value} cannot produce on its own (`long`, `ulong`),
  251. * or returns `null` when the target is not coercible.
  252. */
  253. private static Value? coerce_numeric(Element element, Type target) {
  254. try {
  255. if (target == typeof(long)) {
  256. var value = Value(target);
  257. value.set_long((long) element.as<int64?>());
  258. return value;
  259. }
  260. if (target == typeof(ulong)) {
  261. var value = Value(target);
  262. value.set_ulong((ulong) element.as<int64?>());
  263. return value;
  264. }
  265. } catch (GLib.Error e) {
  266. }
  267. return null;
  268. }
  269. /**
  270. * Converts `element` to a `Value` of `target`: a {@link ValueElement}
  271. * holding a transformable fundamental is converted through native
  272. * GValue transformation (its raw scalar never passes through the
  273. * generic element dispatch, whose int64/uint64 branches mis-handle
  274. * the boxed representation); everything else goes through
  275. * {@link Element.to_value}.
  276. */
  277. private static Value element_value(Element element, Type target) throws GLib.Error {
  278. var value_element = element as ValueElement;
  279. if (value_element != null) {
  280. var raw = ((!)value_element).get_value();
  281. if (Value.type_transformable(raw.type(), target)) {
  282. var converted = Value(target);
  283. if (raw.transform(ref converted)) {
  284. return converted;
  285. }
  286. }
  287. }
  288. return element.to_value(target);
  289. }
  290. // ------------------------------------------------------------------
  291. // Typed {@link Properties} getters — remove ad-hoc read boilerplate.
  292. // ------------------------------------------------------------------
  293. public static string? get_string(Properties props, string key) {
  294. Element element;
  295. if (props.try_get(key, out element) && element != null) {
  296. return ((!)element).as_string_or_null();
  297. }
  298. return null;
  299. }
  300. public static int? get_int(Properties props, string key) {
  301. Element element;
  302. if (props.try_get(key, out element) && element != null) {
  303. return ((!)element).as_int_or_null();
  304. }
  305. return null;
  306. }
  307. public static bool? get_bool(Properties props, string key) {
  308. Element element;
  309. if (props.try_get(key, out element) && element != null) {
  310. return ((!)element).as_bool_or_null();
  311. }
  312. return null;
  313. }
  314. public static double? get_double(Properties props, string key) {
  315. Element element;
  316. if (props.try_get(key, out element) && element != null) {
  317. return ((!)element).as_double_or_null();
  318. }
  319. return null;
  320. }
  321. /**
  322. * Generic adapter wrapping a {@link PropertyMapper} for the registry.
  323. */
  324. private class MapperAdapter<T> : Object, ObjectPropertyMapper {
  325. private PropertyMapper<T> mapper;
  326. public MapperAdapter(PropertyMapper<T> mapper) {
  327. this.mapper = mapper;
  328. }
  329. public Properties map_from_object(Object o) throws GLib.Error {
  330. return mapper.map_from((T) o);
  331. }
  332. public Object materialise_object(JsonObject json) throws GLib.Error {
  333. return (Object) mapper.materialise(json);
  334. }
  335. }
  336. }
  337. }