using Invercargill; using Invercargill.DataStructures; using Invercargill.Expressions; using InvercargillSql.Orm; using Inversion; using ClankerSaddle.Data; namespace ClankerSaddle.Services { /** * Capability storage: CRUD over `capabilities` plus their saddle and * task attachments, and the tag-matching predicate the dispatcher * routes by — a saddle is eligible for a task when its capability * set is a superset of the task's required set (an empty task set * matches any saddle). */ public class CapabilityService : Object { private OrmSession db = inject(); /** * Inserts a capability; names are unique. Returns the stored row * (with its id). */ public async Capability create_capability(string name) throws Error { if (name.strip().length == 0) { throw new Error(Quark.from_string("clanker-saddle"), 40, "A capability name is required"); } var existing = yield db.query() .where(expr("name == $0", elem(name.strip()))) .first_async(); if (existing != null) { throw new Error(Quark.from_string("clanker-saddle"), 41, @"A capability named '$(name)' already exists"); } var capability = new Capability() { name = name.strip(), created_at = new DateTime.now_local() }; yield db.insert_async(capability); return capability; } /** Loads the capability identified by `id`, or null. */ public async Capability? get_capability(int64 id) throws Error { return yield db.query() .where(expr("id == $0", elem(id))) .first_async(); } /** * The capability named exactly `name`, or null — the subtask * callback's tag validation lookup (names are unique). */ public async Capability? find_by_name(string name) throws Error { return yield db.query() .where(expr("name == $0", elem(name))) .first_async(); } /** Every capability ordered by name. */ public async ImmutableLot list_capabilities() throws Error { return yield db.query() .order_by(expr("name")) .materialise_async(); } /** * Deletes a capability and every attachment referencing it (a * saddle stops carrying it, a task stops requiring it). */ public async void delete_capability(int64 id) throws Error { foreach (var link in yield db.query() .where(expr("capability_id == $0", elem(id))) .materialise_async()) { yield db.delete_async(link); } foreach (var link in yield db.query() .where(expr("capability_id == $0", elem(id))) .materialise_async()) { yield db.delete_async(link); } var capability = yield get_capability(id); if (capability != null) { yield db.delete_async((!)capability); } } /** * Attaches `capability_id` to `saddle_id`; duplicates are a no-op * (the pair is unique). */ public async void attach_to_saddle(int64 capability_id, int64 saddle_id) throws Error { var capability = yield get_capability(capability_id); if (capability == null) { throw new Error(Quark.from_string("clanker-saddle"), 42, @"No capability exists with id $(capability_id)"); } var existing = yield db.query() .where(expr("capability_id == $0 && saddle_id == $1", elem(capability_id), elem(saddle_id))) .materialise_async(); if (existing.length > 0) { return; } yield db.insert_async(new SaddleCapability() { capability_id = capability_id, saddle_id = saddle_id }); } /** Detaches the `saddle_capabilities` row identified by `id`. */ public async void detach_from_saddle(int64 link_id) throws Error { var link = yield db.query() .where(expr("id == $0", elem(link_id))) .first_async(); if (link != null) { yield db.delete_async((!)link); } } /** The `saddle_capabilities` attachments of `saddle_id`, oldest first. */ public async ImmutableLot list_saddle_attachments(int64 saddle_id) throws Error { return yield db.query() .where(expr("saddle_id == $0", elem(saddle_id))) .order_by(expr("id")) .materialise_async(); } /** The capabilities a saddle carries, attachment order. */ public async ImmutableLot list_for_saddle(int64 saddle_id) throws Error { var rows = new Vector(); foreach (var link in yield list_saddle_attachments(saddle_id)) { var capability = yield get_capability(link.capability_id); if (capability != null) { rows.add((!)capability); } } return rows.to_immutable_buffer(); } /** * Attaches `capability_id` to `task_id` (a required capability); * duplicates are a no-op (the pair is unique). */ public async void attach_to_task(int64 capability_id, int64 task_id) throws Error { var capability = yield get_capability(capability_id); if (capability == null) { throw new Error(Quark.from_string("clanker-saddle"), 42, @"No capability exists with id $(capability_id)"); } var existing = yield db.query() .where(expr("capability_id == $0 && task_id == $1", elem(capability_id), elem(task_id))) .materialise_async(); if (existing.length > 0) { return; } yield db.insert_async(new TaskCapability() { capability_id = capability_id, task_id = task_id }); } /** Detaches the `task_capabilities` row identified by `id`. */ public async void detach_from_task(int64 link_id) throws Error { var link = yield db.query() .where(expr("id == $0", elem(link_id))) .first_async(); if (link != null) { yield db.delete_async((!)link); } } /** The `task_capabilities` attachments of `task_id`, oldest first. */ public async ImmutableLot list_task_attachments(int64 task_id) throws Error { return yield db.query() .where(expr("task_id == $0", elem(task_id))) .order_by(expr("id")) .materialise_async(); } /** The capabilities a task requires, attachment order. */ public async ImmutableLot list_for_task(int64 task_id) throws Error { var rows = new Vector(); foreach (var link in yield list_task_attachments(task_id)) { var capability = yield get_capability(link.capability_id); if (capability != null) { rows.add((!)capability); } } return rows.to_immutable_buffer(); } /** Every `saddle_capabilities` row (for dispatch-time batching). */ public async ImmutableLot all_saddle_attachments() throws Error { return yield db.query().materialise_async(); } /** Every `task_capabilities` row (for usage counts and routing). */ public async ImmutableLot all_task_attachments() throws Error { return yield db.query().materialise_async(); } /** * True when `saddle_id`'s capability set is a superset of * `task_id`'s required set — the dispatcher's eligibility check. */ public async bool saddle_covers_task(int64 saddle_id, int64 task_id) throws Error { var required = new HashSet(); foreach (var capability in yield list_for_task(task_id)) { required.add(capability.name); } if (required.length == 0) { return true; } foreach (var capability in yield list_for_saddle(saddle_id)) { required.remove(capability.name); } return required.length == 0; } } }