From 2ac342240b117969cc41d6dcf7699afa53721c8d Mon Sep 17 00:00:00 2001 From: Vishesh 'ironeagle' Bangotra Date: Sat, 10 Jan 2026 17:59:11 +0530 Subject: [PATCH] feat(templates): add OpenAPI-first CRUD app scaffold with mock data - include CRUD OpenAPI spec - add in-memory mock data store - implement OpenAPI-bound route handlers - provide runnable FastAPI bootstrap - include end-to-end integration test --- openapi_first/templates/crud_app/data.py | 41 +++++++ openapi_first/templates/crud_app/main.py | 8 ++ openapi_first/templates/crud_app/openapi.yaml | 115 ++++++++++++++++++ openapi_first/templates/crud_app/routes.py | 50 ++++++++ .../templates/crud_app/test_crud_app.py | 97 +++++++++++++++ 5 files changed, 311 insertions(+) create mode 100644 openapi_first/templates/crud_app/data.py create mode 100644 openapi_first/templates/crud_app/main.py create mode 100644 openapi_first/templates/crud_app/openapi.yaml create mode 100644 openapi_first/templates/crud_app/routes.py create mode 100644 openapi_first/templates/crud_app/test_crud_app.py diff --git a/openapi_first/templates/crud_app/data.py b/openapi_first/templates/crud_app/data.py new file mode 100644 index 0000000..6966f36 --- /dev/null +++ b/openapi_first/templates/crud_app/data.py @@ -0,0 +1,41 @@ +""" +In-memory mock data store for CRUD example. + +This module intentionally avoids persistence and concurrency guarantees. +It is suitable for demos, tests, and scaffolding only. +""" + +from typing import Dict + +_items: Dict[int, dict] = { + 1: {"id": 1, "name": "Apple", "price": 0.5}, + 2: {"id": 2, "name": "Banana", "price": 0.3}, +} + +_next_id = 3 + + +def list_items(): + return list(_items.values()) + + +def get_item(item_id: int): + return _items[item_id] + + +def create_item(payload: dict): + global _next_id + item = {"id": _next_id, **payload} + _items[_next_id] = item + _next_id += 1 + return item + + +def update_item(item_id: int, payload: dict): + item = {"id": item_id, **payload} + _items[item_id] = item + return item + + +def delete_item(item_id: int): + del _items[item_id] diff --git a/openapi_first/templates/crud_app/main.py b/openapi_first/templates/crud_app/main.py new file mode 100644 index 0000000..cf8cdc8 --- /dev/null +++ b/openapi_first/templates/crud_app/main.py @@ -0,0 +1,8 @@ +from openapi_first.app import OpenAPIFirstApp +import routes + +app = OpenAPIFirstApp( + openapi_path="openapi.yaml", + routes_module=routes, + title="CRUD Example Service", +) diff --git a/openapi_first/templates/crud_app/openapi.yaml b/openapi_first/templates/crud_app/openapi.yaml new file mode 100644 index 0000000..ad760ed --- /dev/null +++ b/openapi_first/templates/crud_app/openapi.yaml @@ -0,0 +1,115 @@ +openapi: 3.0.3 +info: + title: CRUD Example Service + version: "1.0.0" + description: Minimal OpenAPI-first CRUD service with in-memory mock data. + +paths: + /items: + get: + operationId: list_items + summary: List all items + responses: + "200": + description: List of items + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/Item" + + post: + operationId: create_item + summary: Create a new item + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/ItemCreate" + responses: + "201": + description: Item created + content: + application/json: + schema: + $ref: "#/components/schemas/Item" + + /items/{item_id}: + get: + operationId: get_item + summary: Get item by ID + parameters: + - name: item_id + in: path + required: true + schema: + type: integer + responses: + "200": + description: Item found + content: + application/json: + schema: + $ref: "#/components/schemas/Item" + + put: + operationId: update_item + summary: Update an item + parameters: + - name: item_id + in: path + required: true + schema: + type: integer + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/ItemCreate" + responses: + "200": + description: Item updated + content: + application/json: + schema: + $ref: "#/components/schemas/Item" + + delete: + operationId: delete_item + summary: Delete an item + parameters: + - name: item_id + in: path + required: true + schema: + type: integer + responses: + "204": + description: Item deleted + +components: + schemas: + Item: + type: object + properties: + id: + type: integer + name: + type: string + price: + type: number + format: float + required: [id, name, price] + + ItemCreate: + type: object + properties: + name: + type: string + price: + type: number + format: float + required: [name, price] diff --git a/openapi_first/templates/crud_app/routes.py b/openapi_first/templates/crud_app/routes.py new file mode 100644 index 0000000..257255e --- /dev/null +++ b/openapi_first/templates/crud_app/routes.py @@ -0,0 +1,50 @@ +""" +CRUD route handlers bound via OpenAPI operationId. + +These handlers explicitly control HTTP status codes to ensure +runtime behavior matches the OpenAPI contract. +""" + +from fastapi import Response, HTTPException + +from data import ( + list_items as _list_items, + get_item as _get_item, + create_item as _create_item, + update_item as _update_item, + delete_item as _delete_item, +) + + +def list_items(): + return _list_items() + + +def get_item(item_id: int): + try: + return _get_item(item_id) + except KeyError: + raise HTTPException(status_code=404, detail="Item not found") + + +def create_item(payload: dict, response: Response): + item = _create_item(payload) + response.status_code = 201 + return item + + +def update_item(item_id: int, payload: dict): + try: + return _update_item(item_id, payload) + except KeyError: + raise HTTPException(status_code=404, detail="Item not found") + + +def delete_item(item_id: int, response: Response): + try: + _delete_item(item_id) + except KeyError: + raise HTTPException(status_code=404, detail="Item not found") + + response.status_code = 204 + return None diff --git a/openapi_first/templates/crud_app/test_crud_app.py b/openapi_first/templates/crud_app/test_crud_app.py new file mode 100644 index 0000000..32f48b2 --- /dev/null +++ b/openapi_first/templates/crud_app/test_crud_app.py @@ -0,0 +1,97 @@ +""" +End-to-end tests for the OpenAPI-first CRUD example app. + +These tests validate that all CRUD operations behave correctly +against the in-memory mock data store. + +The tests assume: +- OpenAPI-first route binding +- In-memory storage (no persistence guarantees) +- Deterministic behavior in a single process +""" + +from fastapi.testclient import TestClient + +from main import app + + +client = TestClient(app) + + +def test_list_items_initial(): + """Initial items should be present.""" + response = client.get("/items") + assert response.status_code == 200 + + data = response.json() + assert isinstance(data, list) + assert len(data) >= 2 + + ids = {item["id"] for item in data} + assert 1 in ids + assert 2 in ids + + +def test_get_item(): + """Existing item should be retrievable by ID.""" + response = client.get("/items/1") + assert response.status_code == 200 + + item = response.json() + assert item["id"] == 1 + assert "name" in item + assert "price" in item + + +def test_create_item(): + """Creating a new item should return the created entity.""" + payload = { + "name": "Orange", + "price": 0.8, + } + + response = client.post("/items", json=payload) + assert response.status_code == 201 + + item = response.json() + assert "id" in item + assert item["name"] == payload["name"] + assert item["price"] == payload["price"] + + # Verify it appears in list + list_response = client.get("/items") + ids = {i["id"] for i in list_response.json()} + assert item["id"] in ids + + +def test_update_item(): + """Updating an item should replace its values.""" + payload = { + "name": "Green Apple", + "price": 0.6, + } + + response = client.put("/items/1", json=payload) + assert response.status_code == 200 + + item = response.json() + assert item["id"] == 1 + assert item["name"] == payload["name"] + assert item["price"] == payload["price"] + + # Verify persisted update + get_response = client.get("/items/1") + updated = get_response.json() + assert updated["name"] == payload["name"] + assert updated["price"] == payload["price"] + + +def test_delete_item(): + """Deleting an item should remove it from the store.""" + response = client.delete("/items/2") + assert response.status_code == 204 + + # Verify deletion + list_response = client.get("/items") + ids = {item["id"] for item in list_response.json()} + assert 2 not in ids