diff --git a/src/fabledassistant/routes/rulebooks.py b/src/fabledassistant/routes/rulebooks.py index d2f2f9a..2d8d99d 100644 --- a/src/fabledassistant/routes/rulebooks.py +++ b/src/fabledassistant/routes/rulebooks.py @@ -119,3 +119,122 @@ async def update_topic(topic_id: int): async def delete_topic(topic_id: int): await rulebooks_svc.delete_topic(topic_id, _uid()) return "", 204 + + +# ── Rules ─────────────────────────────────────────────────────────────── + +@rulebooks_bp.get("/rules") +@login_required +async def list_rules(): + def _opt_int(name): + raw = request.args.get(name) + return int(raw) if raw else None + + try: + rulebook_id = _opt_int("rulebook_id") + topic_id = _opt_int("topic_id") + project_id = _opt_int("project_id") + except ValueError: + return jsonify({"error": "rulebook_id, topic_id, project_id must be integers"}), 400 + + rows = await rulebooks_svc.list_rules( + user_id=_uid(), + rulebook_id=rulebook_id, + topic_id=topic_id, + project_id=project_id, + ) + return jsonify({"rules": [r.to_dict() for r in rows]}) + + +@rulebooks_bp.post("/rulebook-topics//rules") +@login_required +async def create_rule(topic_id: int): + data = await request.get_json() or {} + title = (data.get("title") or "").strip() + statement = (data.get("statement") or "").strip() + if not title or not statement: + return jsonify({"error": "title and statement are required"}), 400 + try: + rule = await rulebooks_svc.create_rule( + topic_id=topic_id, + user_id=_uid(), + title=title, + statement=statement, + why=data.get("why", ""), + how_to_apply=data.get("how_to_apply", ""), + order_index=data.get("order_index", 0), + ) + except ValueError as exc: + return jsonify({"error": str(exc)}), 404 + return jsonify(rule.to_dict()), 201 + + +@rulebooks_bp.get("/rules/") +@login_required +async def get_rule(rule_id: int): + rule = await rulebooks_svc.get_rule(rule_id, _uid()) + if rule is None: + return jsonify({"error": "rule not found"}), 404 + return jsonify(rule.to_dict()) + + +@rulebooks_bp.patch("/rules/") +@login_required +async def update_rule(rule_id: int): + data = await request.get_json() or {} + fields = { + k: v for k, v in data.items() + if k in ("title", "statement", "why", "how_to_apply", "order_index") + } + rule = await rulebooks_svc.update_rule(rule_id, _uid(), **fields) + if rule is None: + return jsonify({"error": "rule not found"}), 404 + return jsonify(rule.to_dict()) + + +@rulebooks_bp.delete("/rules/") +@login_required +async def delete_rule(rule_id: int): + await rulebooks_svc.delete_rule(rule_id, _uid()) + return "", 204 + + +# ── Subscriptions ────────────────────────────────────────────────────── + +@rulebooks_bp.post("/projects//rulebook-subscriptions") +@login_required +async def subscribe_project(project_id: int): + data = await request.get_json() or {} + rulebook_id = data.get("rulebook_id") + if not rulebook_id: + return jsonify({"error": "rulebook_id is required"}), 400 + try: + await rulebooks_svc.subscribe_project( + project_id=project_id, rulebook_id=int(rulebook_id), user_id=_uid(), + ) + except ValueError as exc: + return jsonify({"error": str(exc)}), 404 + return "", 204 + + +@rulebooks_bp.delete( + "/projects//rulebook-subscriptions/" +) +@login_required +async def unsubscribe_project(project_id: int, rulebook_id: int): + try: + await rulebooks_svc.unsubscribe_project( + project_id=project_id, rulebook_id=rulebook_id, user_id=_uid(), + ) + except ValueError as exc: + return jsonify({"error": str(exc)}), 404 + return "", 204 + + +@rulebooks_bp.get("/projects//rules") +@login_required +async def get_project_rules(project_id: int): + result = await rulebooks_svc.get_applicable_rules( + project_id=project_id, user_id=_uid(), + ) + return jsonify(result) diff --git a/tests/test_routes_rulebooks.py b/tests/test_routes_rulebooks.py index 1aa733c..6f6fa0b 100644 --- a/tests/test_routes_rulebooks.py +++ b/tests/test_routes_rulebooks.py @@ -44,6 +44,17 @@ def test_service_signatures_require_user_id(): "create_rulebook", "list_rulebooks", "get_rulebook", "update_rulebook", "delete_rulebook", "find_rulebook_by_title", "create_topic", "list_topics", "get_topic", "update_topic", "delete_topic", + "create_rule", "list_rules", "get_rule", "update_rule", "delete_rule", + "subscribe_project", "unsubscribe_project", "get_applicable_rules", ): sig = inspect.signature(getattr(svc, fn_name)) assert "user_id" in sig.parameters, f"{fn_name} missing user_id param" + + +def test_rule_and_subscription_handlers_callable(): + from fabledassistant.routes import rulebooks as rb_routes + for name in ( + "list_rules", "create_rule", "get_rule", "update_rule", "delete_rule", + "subscribe_project", "unsubscribe_project", "get_project_rules", + ): + assert callable(getattr(rb_routes, name))