commit 2d8602609c50ab2aba873a1a51c02a45ca7165d7 Author: Simon Oberzier Date: Tue Jun 9 19:29:34 2026 +0200 Initial commit diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..7208566 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,19 @@ +FROM python:3.12-slim + +# Install system dependencies and GnuCOBOL compiler +RUN apt-get update && apt-get install -y \ + gnucobol \ + gcc \ + && rm -rf /var/lib/apt/lists/* + +WORKDIR /app + +# Install Python framework dependencies +RUN pip install --no-cache-dir fastapi uvicorn pyyaml + +# Copy everything over +COPY . . + +EXPOSE 8000 + +CMD ["python", "main.py"] \ No newline at end of file diff --git a/api.yaml b/api.yaml new file mode 100644 index 0000000..cb363b8 --- /dev/null +++ b/api.yaml @@ -0,0 +1,16 @@ +version: "1.0" +project: "legacy-finance-api" +endpoints: + - path: "/accounts/{id}" + method: "GET" + cobol_source: "src/fetch_account.cbl" + procedure: "GET_ACCOUNT_DATA" + arguments: + - name: "id" + type: "PIC X(10)" + source: "path" + response: + - name: "balance" + type: "PIC 9(7)V99" + - name: "status" + type: "PIC X(2)" \ No newline at end of file diff --git a/main.py b/main.py new file mode 100644 index 0000000..de22ef8 --- /dev/null +++ b/main.py @@ -0,0 +1,102 @@ +import os +import subprocess +import ctypes +import yaml +from fastapi import FastAPI, HTTPException + +app = FastAPI(title="COBOL REST Framework") + +# 1. Load the Configuration +with open("api.yaml", "r") as f: + config = yaml.safe_load(f) + +# 2. On-the-fly Compiler Pipeline +# This makes it completely system independent! +COMPILED_LIBS = {} +for endpoint in config["endpoints"]: + cbl_source = endpoint["cobol_source"] + # Convert 'src/fetch_account.cbl' -> '/tmp/fetch_account.so' + so_name = os.path.basename(cbl_source).replace(".cbl", ".so") + so_path = os.path.join("/tmp", so_name) + + print(f"🛠️ Compiling legacy asset {cbl_source} to {so_path}...") + # Use standard Linux GnuCOBOL compilation flags + result = subprocess.run(["cobc", "-b", "-o", so_path, cbl_source], capture_output=True, text=True) + if result.returncode != 0: + print(f"❌ Compilation Failed:\n{result.stderr}") + exit(1) + + COMPILED_LIBS[cbl_source] = so_path + +# Initialize GnuCOBOL environment globally +try: + libcob = ctypes.CDLL("libcob.so", mode=ctypes.RTLD_GLOBAL) + libcob.cob_init() +except OSError: + pass + +def call_cobol(so_path, procedure, account_id): + """Dynamically loads compiled COBOL libraries and maps network payloads to buffers""" + # 1. Load the library module + cobol_lib = ctypes.CDLL(so_path) + + # 2. Use GnuCOBOL's official runtime resolver to find our PROCEDURE/PROGRAM-ID + # This acts as the bulletproof bridge between Linux and COBOL symbols + try: + libcob = ctypes.CDLL("libcob.so", mode=ctypes.RTLD_GLOBAL) + libcob.cob_resolve.restype = ctypes.c_void_p + func_ptr = libcob.cob_resolve(procedure.encode('utf-8')) + except Exception: + func_ptr = None + + # Fallback to direct symbol reading if cob_resolve isn't bound yet + if not func_ptr: + for name in [procedure, procedure.lower(), procedure.upper()]: + if hasattr(cobol_lib, name): + func_ptr = ctypes.cast(getattr(cobol_lib, name), ctypes.c_void_p).value + break + + if not func_ptr: + raise HTTPException( + status_code=500, + detail=f"COBOL Procedure symbol '{procedure}' could not be resolved." + ) + + # Cast the resolved pointer into a callable Python FFI function + cobol_func = ctypes.CFUNCTYPE(None, ctypes.c_char_p, ctypes.c_char_p, ctypes.c_char_p)(func_ptr) + + # 3. Map Input Argument: PIC X(10) + padded_id = account_id.ljust(10)[:10].encode('ascii') + id_buf = ctypes.create_string_buffer(padded_id, 10) + + # 4. Allocate Output Buffers matching the YAML spec sizes + balance_buf = ctypes.create_string_buffer(9) # 9 bytes + status_buf = ctypes.create_string_buffer(2) # 2 bytes + + # 5. Call out to the compiled COBOL logic + cobol_func(id_buf, balance_buf, status_buf) + + # 6. Decode response strings + status = status_buf.value.decode('ascii').strip() + if status == "NF": + raise HTTPException(status_code=404, detail="Account not found in COBOL storage") + + raw_balance = balance_buf.value.decode('ascii').strip() + # Turn fixed-length string '015004500' -> 1500.45 + balance = float(raw_balance[:7] + "." + raw_balance[7:9]) + + return {"balance": balance, "status": status} + +# 3. Dynamic API Route Builder +for endpoint in config["endpoints"]: + if endpoint["method"] == "GET" and endpoint["path"] == "/accounts/{id}": + + @app.get(endpoint["path"]) + def get_account(id: str): + so_file = COMPILED_LIBS[endpoint["cobol_source"]] + proc_name = endpoint["procedure"] + return call_cobol(so_file, proc_name, id) + +if __name__ == "__main__": + import uvicorn + uvicorn.run(app, host="0.0.0.0", port=8000) \ No newline at end of file diff --git a/run.sh b/run.sh new file mode 100755 index 0000000..ffd2c57 --- /dev/null +++ b/run.sh @@ -0,0 +1,2 @@ +docker build -t cobol-rest-framework . +docker run -p 8000:8000 cobol-rest-framework \ No newline at end of file diff --git a/src/fetch_account.cbl b/src/fetch_account.cbl new file mode 100644 index 0000000..066d8ae --- /dev/null +++ b/src/fetch_account.cbl @@ -0,0 +1,19 @@ + IDENTIFICATION DIVISION. + PROGRAM-ID. GET_ACCOUNT_DATA. + + DATA DIVISION. + LINKAGE SECTION. + 01 LK-ID PIC X(10). + 01 LK-BALANCE PIC X(9). + 01 LK-STATUS PIC X(2). + + PROCEDURE DIVISION USING LK-ID LK-BALANCE LK-STATUS. + MAIN-LOGIC. + IF LK-ID = "42 " + MOVE "015004500" TO LK-BALANCE + MOVE "OK" TO LK-STATUS + ELSE + MOVE "000000000" TO LK-BALANCE + MOVE "NF" TO LK-STATUS + END-IF. + GOBACK. \ No newline at end of file