diff --git a/main.py b/main.py index a894b24..68281c6 100644 --- a/main.py +++ b/main.py @@ -1,6 +1,6 @@ import functools from flask import Flask, jsonify, request, abort -from sqlalchemy import func, or_ +from sqlalchemy import and_, func, or_ from werkzeug.exceptions import HTTPException app = Flask(__name__) @@ -199,6 +199,79 @@ def api_hadiths_by_urns(): return jsonify({"count": len(data), "missing": missing, "data": data}) +@app.route("/v1/hadiths/refs", methods=["GET"]) +def api_hadiths_by_refs(): + # Enforce: refs must appear only once (no ?refs=a&refs=b) + if len(request.args.getlist("refs")) != 1: + abort( + 400, + "Query parameter 'refs' must be provided exactly once. Example: ?refs=bukhari:1,bukhari:2", + ) + + refs_param = request.args.get("refs", "").strip() + if not refs_param: + abort(400, "Query parameter 'refs' is required. Example: ?refs=bukhari:1,bukhari:2") + + # Parse comma-separated collection:hadithNumber refs + parts = [p.strip() for p in refs_param.split(",") if p.strip()] + if not parts: + abort(400, "Query parameter 'refs' is required. Example: ?refs=bukhari:1,bukhari:2") + + refs = [] + invalid = [] + seen = set() + + for p in parts: + if ":" not in p: + invalid.append(p) + continue + + collection, hadith_number = [x.strip() for x in p.split(":", 1)] + if not collection or not hadith_number: + invalid.append(p) + continue + + ref = (collection, hadith_number) + if ref not in seen: + seen.add(ref) + refs.append(ref) + + if invalid: + abort(400, f"Invalid ref(s): {', '.join(invalid)}") + + MAX_REFS = 100 + if len(refs) > MAX_REFS: + abort(400, f"Too many refs (max {MAX_REFS}).") + + results = ( + Hadith.query.filter( + or_( + *[ + and_( + Hadith.collection == collection, + Hadith.hadithNumber == hadith_number, + ) + for collection, hadith_number in refs + ] + ) + ) + .all() + ) + + by_ref = {(h.collection, h.hadithNumber): h for h in results} + + data = [] + missing = [] + for collection, hadith_number in refs: + h = by_ref.get((collection, hadith_number)) + if h is None: + missing.append(f"{collection}:{hadith_number}") + else: + data.append(h.serialize()) + + return jsonify({"count": len(data), "missing": missing, "data": data}) + + @app.route("/v1/hadiths/random", methods=["GET"]) @single_resource def api_hadiths_random(): diff --git a/spec.v1.yml b/spec.v1.yml index fd9e0a0..4f49a8f 100644 --- a/spec.v1.yml +++ b/spec.v1.yml @@ -336,6 +336,42 @@ paths: $ref: "#/components/schemas/Hadith" "400": description: Bad request (missing/invalid urns) + "/hadiths/refs": + get: + summary: Get multiple hadiths by collection and hadith number + description: > + Returns hadiths that match one or more collection/hadith number refs. + Provide refs as a single comma-separated query parameter. + Example: ?refs=bukhari:1,bukhari:2 + parameters: + - in: query + name: refs + required: true + description: Comma-separated list of collection:hadithNumber refs (must be provided once). + schema: + type: string + pattern: "^[^:,]+:[^,]+(,[^:,]+:[^,]+)*$" + example: "bukhari:1,bukhari:2" + responses: + "200": + description: A list of hadiths matching the requested refs. + content: + application/json: + schema: + type: object + properties: + count: + type: integer + missing: + type: array + items: + type: string + data: + type: array + items: + $ref: "#/components/schemas/Hadith" + "400": + description: Bad request (missing/invalid refs) "/hadiths/random": get: summary: Get a randomly selected hadith