using Invercargill; using Invercargill.DataStructures; using Invercargill.Mapping; using InvercargillJson; namespace Statum { /** * Non-generic adapter so heterogeneous {@link PropertyMapper}s can be stored * in a single registry keyed by {@link Type}. */ internal interface ObjectPropertyMapper : Object { public abstract Properties map_from_object(Object o) throws GLib.Error; public abstract Object materialise_object(JsonObject json) throws GLib.Error; } /** * Maps between GObject instances and {@link Properties} using GObject * property introspection, so an application can declare a strongly-typed * data model as an ordinary GObject property class (e.g. * `public string order_id { get; set; }`) and have the framework map it * with no per-model mapper. * * Scalar properties (`string`, `int`, `int64`, `long`, `bool`, `double`, * enums) serialise as type-correct JSON primitives on every Statum path — * the public snapshot path and the encrypted private path * ({@link Statum.Cryptography.EncryptionProvider.author_properties}) alike. * `Object`-typed properties whose {@link Type} is registered (see * {@link register_mapper}) are mapped recursively via their * {@link PropertyMapper} — this is how typed action-reference fields (e.g. * `public Model.ActionDto bump { get; set; }`) round-trip as nested JSON. * Array-shaped properties serialise as JSON arrays; see {@link to_properties} * for the supported shapes and {@link from_properties} for read-back. */ public class GObjectMapping : Object { private static GLib.HashTable _mappers; private static GLib.HashTable mappers() { if (_mappers == null) { _mappers = new GLib.HashTable(GLib.direct_hash, GLib.direct_equal); } return _mappers; } /** * Registers a {@link PropertyMapper} for `T` so that `Object`-typed * properties of that type are mapped recursively. Called once at startup * for framework types (e.g. {@link Model.ActionDto}); apps may register * their own nested model mappers. */ public static void register_mapper(PropertyMapper mapper) { mappers()[typeof(T)] = new MapperAdapter(mapper); } /** * Maps a GObject instance to a {@link PropertyDictionary} by reading its * declared (readable) GObject properties, ready to be used directly as * slot public/private data (no cast required — see S-13). * * Scalars (`string`, `int`, `int64`, `long`, `bool`, `double`, enums) * serialise as JSON primitives of the matching type on every path, * {@link Statum.Cryptography.EncryptionProvider.author_properties} * included. Registered `Object`-typed properties map recursively as * nested JSON objects; unregistered `Object` properties are skipped * unless they hold a collection (below). * * Arrays serialise as JSON arrays for two shapes: `string[]` * properties, and `Object`-typed properties whose runtime value is an * Invercargill {@link Invercargill.Enumerable} (e.g. `Series`, * `Lot`, `Vector`). Collection elements that are registered * {@link Object} types map recursively via their mapper; fundamental * elements (`string`, `int`, `int64`, `bool`, `double`) map as JSON * primitives. * * **Limitation:** collection-typed properties are write-side only — a * property's GType does not record the collection's element type, so * {@link from_properties} cannot repopulate them. Models carrying * collection fields must be rebuilt from their source of truth (e.g. * the database) rather than round-tripped; * {@link DirectiveBuilder.update_held} carries such fields forward * unchanged instead of dropping them. */ public static PropertyDictionary to_properties(Object model) throws GLib.Error { var dict = new PropertyDictionary(); var object_class = (ObjectClass) model.get_type().class_ref(); foreach (var spec in object_class.list_properties()) { if ((spec.flags & ParamFlags.READABLE) == 0) { continue; } // GObject stores property names with dashes (Vala underscores → // dashes); use the underscored name on the wire so client-side // expressions like `auth.update_announcement` resolve correctly. var key = spec.name.replace("-", "_"); var value = Value(spec.value_type); model.get_property(spec.name, ref value); if (mappers().lookup(spec.value_type) != null) { var obj = value.get_object(); if (obj != null) { var sub_props = (!)(mappers().lookup(spec.value_type)).map_from_object((!)obj); dict[key] = new JsonElement.from_properties(sub_props); } continue; } if (spec.value_type.is_a(typeof(Object))) { var obj = value.get_object(); if (obj != null && ((!)obj).get_type().is_a(typeof(Enumerable))) { dict[key] = enumerable_to_array_element((!)obj); } // Other unregistered Object properties do not serialise. continue; } if (spec.value_type == typeof(string[])) { var raw = value.get_boxed(); if (raw != null) { dict[key] = strv_to_array_element(raw); } else { dict[key] = new ValueElement(value); } continue; } dict[key] = new ValueElement(value); } return dict; } /** * Builds a new instance of `type` and populates its (writable) GObject * properties from `props`. Registered `Object`-typed properties are * materialised recursively; `string[]` properties are populated from * JSON arrays; JSON numbers/booleans coerce back into `int`, `int64`, * `long`, `double`, enum and boolean properties (including `long` and * `ulong`, which {@link Element.to_value} cannot express on its own). * * Collection-typed properties are not repopulated — see the limitation * documented on {@link to_properties} for the rebuild-from-source * pattern that replaces round-tripping such models. */ public static Object from_properties(Type type, Properties props) throws GLib.Error { var model = Object.new(type); var object_class = (ObjectClass) type.class_ref(); foreach (var spec in object_class.list_properties()) { if ((spec.flags & ParamFlags.WRITABLE) == 0) { continue; } var key = spec.name.replace("-", "_"); Element element; if (!props.try_get(key, out element) || element == null) { continue; } if (mappers().lookup(spec.value_type) != null) { try { var json_obj = ((!)element).as(); var obj = (!)(mappers().lookup(spec.value_type)).materialise_object(json_obj); model.set_property(spec.name, obj); } catch (GLib.Error e) { // Skip properties whose nested object cannot be materialised. } continue; } if (spec.value_type == typeof(string[])) { try { var value = Value(spec.value_type); value.set_boxed(read_string_array((!)element)); model.set_property(spec.name, value); } catch (GLib.Error e) { // Skip properties whose array cannot be read. } continue; } try { var value = element_value(((!)element), spec.value_type); model.set_property(spec.name, value); } catch (GLib.Error e) { var coerced = coerce_numeric((!)element, spec.value_type); if (coerced != null) { model.set_property(spec.name, (!)coerced); } // Skip properties whose value cannot be coerced. } } return model; } /** * Convenience: builds and populates `type`, returning it cast to `T`. */ public static T from_properties_typed(Type type, Properties props) throws GLib.Error { return (T) from_properties(type, props); } /** * Returns `mapped` with the JSON-array values of `current` that * `mapped` no longer carries re-attached unchanged. A typed update * cannot re-emit collection fields it was unable to read back into the * model, so this keeps them instead of silently dropping them. */ internal static PropertyDictionary preserving_arrays(Properties current, PropertyDictionary mapped) { foreach (var item in current) { Element existing; if (mapped.try_get(item.key, out existing)) { continue; } if (item.value != null && item.value.assignable_to()) { mapped[item.key] = item.value; } } return mapped; } /** * Builds the JSON array {@link Element} for a `string[]` (`GStrv`) * property value. */ private static Element strv_to_array_element(void* strv) { var items = new Series(); var cursor = (string**) strv; for (var i = 0; cursor[i] != null; i++) { var value = Value(typeof(string)); value.set_string((string) cursor[i]); items.add(new ValueElement(value)); } return new NativeElement(items.to_elements()); } /** * Builds the JSON array {@link Element} for an Invercargill collection * property value: registered `Object` elements map recursively via * their mapper, fundamental elements pass through as primitives. */ private static Element enumerable_to_array_element(Object enumerable) throws GLib.Error { var boxed = Value(typeof(Enumerable)); boxed.set_object(enumerable); var items = new Series(); foreach (var item in new ValueElement(boxed).as()) { var item_type = item.type(); if (item_type != null && mappers().lookup((!)item_type) != null) { var obj = item.as(); if (obj != null) { var sub_props = (!)(mappers().lookup((!)item_type)).map_from_object((!)obj); items.add(new JsonElement.from_properties(sub_props)); continue; } } items.add(item); } return new NativeElement(items.to_elements()); } /** * Reads a JSON array {@link Element} back into a null-terminated * `string[]` suitable for a `GStrv` property value. */ private static string[] read_string_array(Element element) throws GLib.Error { string[] items = new string[0]; foreach (var item in element.as()) { items += item.as_string_or_null() ?? ""; } return items; } /** * Coerces a JSON number into the numeric targets * {@link Element.to_value} cannot produce on its own (`long`, `ulong`), * or returns `null` when the target is not coercible. */ private static Value? coerce_numeric(Element element, Type target) { try { if (target == typeof(long)) { var value = Value(target); value.set_long((long) element.as()); return value; } if (target == typeof(ulong)) { var value = Value(target); value.set_ulong((ulong) element.as()); return value; } } catch (GLib.Error e) { } return null; } /** * Converts `element` to a `Value` of `target`: a {@link ValueElement} * holding a transformable fundamental is converted through native * GValue transformation (its raw scalar never passes through the * generic element dispatch, whose int64/uint64 branches mis-handle * the boxed representation); everything else goes through * {@link Element.to_value}. */ private static Value element_value(Element element, Type target) throws GLib.Error { var value_element = element as ValueElement; if (value_element != null) { var raw = ((!)value_element).get_value(); if (Value.type_transformable(raw.type(), target)) { var converted = Value(target); if (raw.transform(ref converted)) { return converted; } } } return element.to_value(target); } // ------------------------------------------------------------------ // Typed {@link Properties} getters — remove ad-hoc read boilerplate. // ------------------------------------------------------------------ public static string? get_string(Properties props, string key) { Element element; if (props.try_get(key, out element) && element != null) { return ((!)element).as_string_or_null(); } return null; } public static int? get_int(Properties props, string key) { Element element; if (props.try_get(key, out element) && element != null) { return ((!)element).as_int_or_null(); } return null; } public static bool? get_bool(Properties props, string key) { Element element; if (props.try_get(key, out element) && element != null) { return ((!)element).as_bool_or_null(); } return null; } public static double? get_double(Properties props, string key) { Element element; if (props.try_get(key, out element) && element != null) { return ((!)element).as_double_or_null(); } return null; } /** * Generic adapter wrapping a {@link PropertyMapper} for the registry. */ private class MapperAdapter : Object, ObjectPropertyMapper { private PropertyMapper mapper; public MapperAdapter(PropertyMapper mapper) { this.mapper = mapper; } public Properties map_from_object(Object o) throws GLib.Error { return mapper.map_from((T) o); } public Object materialise_object(JsonObject json) throws GLib.Error { return (Object) mapper.materialise(json); } } } }