| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368 |
- 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<Type, ObjectPropertyMapper> _mappers;
- private static GLib.HashTable<Type, ObjectPropertyMapper> mappers() {
- if (_mappers == null) {
- _mappers = new GLib.HashTable<Type, ObjectPropertyMapper>(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<T>(PropertyMapper<T> mapper) {
- mappers()[typeof(T)] = new MapperAdapter<T>(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<T>`,
- * `Lot<T>`, `Vector<T>`). 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<JsonObject>();
- 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<T>(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<Elements>()) {
- 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<Element>();
- 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<Elements>(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<Element>();
- foreach (var item in new ValueElement(boxed).as<Elements>()) {
- var item_type = item.type();
- if (item_type != null && mappers().lookup((!)item_type) != null) {
- var obj = item.as<Object>();
- 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<Elements>(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<Elements>()) {
- 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<int64?>());
- return value;
- }
- if (target == typeof(ulong)) {
- var value = Value(target);
- value.set_ulong((ulong) element.as<int64?>());
- 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<T> : Object, ObjectPropertyMapper {
- private PropertyMapper<T> mapper;
- public MapperAdapter(PropertyMapper<T> 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);
- }
- }
- }
- }
|