|
|
@@ -0,0 +1,214 @@
|
|
|
+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<OrmSession>();
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 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<Capability>()
|
|
|
+ .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>(capability);
|
|
|
+ return capability;
|
|
|
+ }
|
|
|
+
|
|
|
+ /** Loads the capability identified by `id`, or null. */
|
|
|
+ public async Capability? get_capability(int64 id) throws Error {
|
|
|
+ return yield db.query<Capability>()
|
|
|
+ .where(expr("id == $0", elem<int64?>(id)))
|
|
|
+ .first_async();
|
|
|
+ }
|
|
|
+
|
|
|
+ /** Every capability ordered by name. */
|
|
|
+ public async ImmutableLot<Capability> list_capabilities() throws Error {
|
|
|
+ return yield db.query<Capability>()
|
|
|
+ .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<TaskCapability>()
|
|
|
+ .where(expr("capability_id == $0", elem<int64?>(id)))
|
|
|
+ .materialise_async()) {
|
|
|
+ yield db.delete_async<TaskCapability>(link);
|
|
|
+ }
|
|
|
+ foreach (var link in yield db.query<SaddleCapability>()
|
|
|
+ .where(expr("capability_id == $0", elem<int64?>(id)))
|
|
|
+ .materialise_async()) {
|
|
|
+ yield db.delete_async<SaddleCapability>(link);
|
|
|
+ }
|
|
|
+ var capability = yield get_capability(id);
|
|
|
+ if (capability != null) {
|
|
|
+ yield db.delete_async<Capability>((!)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<SaddleCapability>()
|
|
|
+ .where(expr("capability_id == $0 && saddle_id == $1",
|
|
|
+ elem<int64?>(capability_id), elem<int64?>(saddle_id)))
|
|
|
+ .materialise_async();
|
|
|
+ if (existing.length > 0) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ yield db.insert_async<SaddleCapability>(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<SaddleCapability>()
|
|
|
+ .where(expr("id == $0", elem<int64?>(link_id)))
|
|
|
+ .first_async();
|
|
|
+ if (link != null) {
|
|
|
+ yield db.delete_async<SaddleCapability>((!)link);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /** The `saddle_capabilities` attachments of `saddle_id`, oldest first. */
|
|
|
+ public async ImmutableLot<SaddleCapability> list_saddle_attachments(int64 saddle_id) throws Error {
|
|
|
+ return yield db.query<SaddleCapability>()
|
|
|
+ .where(expr("saddle_id == $0", elem<int64?>(saddle_id)))
|
|
|
+ .order_by(expr("id"))
|
|
|
+ .materialise_async();
|
|
|
+ }
|
|
|
+
|
|
|
+ /** The capabilities a saddle carries, attachment order. */
|
|
|
+ public async ImmutableLot<Capability> list_for_saddle(int64 saddle_id) throws Error {
|
|
|
+ var rows = new Vector<Capability>();
|
|
|
+ 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<TaskCapability>()
|
|
|
+ .where(expr("capability_id == $0 && task_id == $1",
|
|
|
+ elem<int64?>(capability_id), elem<int64?>(task_id)))
|
|
|
+ .materialise_async();
|
|
|
+ if (existing.length > 0) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ yield db.insert_async<TaskCapability>(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<TaskCapability>()
|
|
|
+ .where(expr("id == $0", elem<int64?>(link_id)))
|
|
|
+ .first_async();
|
|
|
+ if (link != null) {
|
|
|
+ yield db.delete_async<TaskCapability>((!)link);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /** The `task_capabilities` attachments of `task_id`, oldest first. */
|
|
|
+ public async ImmutableLot<TaskCapability> list_task_attachments(int64 task_id) throws Error {
|
|
|
+ return yield db.query<TaskCapability>()
|
|
|
+ .where(expr("task_id == $0", elem<int64?>(task_id)))
|
|
|
+ .order_by(expr("id"))
|
|
|
+ .materialise_async();
|
|
|
+ }
|
|
|
+
|
|
|
+ /** The capabilities a task requires, attachment order. */
|
|
|
+ public async ImmutableLot<Capability> list_for_task(int64 task_id) throws Error {
|
|
|
+ var rows = new Vector<Capability>();
|
|
|
+ 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<SaddleCapability> all_saddle_attachments() throws Error {
|
|
|
+ return yield db.query<SaddleCapability>().materialise_async();
|
|
|
+ }
|
|
|
+
|
|
|
+ /** Every `task_capabilities` row (for usage counts and routing). */
|
|
|
+ public async ImmutableLot<TaskCapability> all_task_attachments() throws Error {
|
|
|
+ return yield db.query<TaskCapability>().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<string>();
|
|
|
+ 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;
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|