| 1 | import json |
| 2 | from typing import Dict, List, Optional, Any |
| 3 | |
| 1 | 4 | class RestAPI: |
| 2 | 5 | def __init__(self, database: dict | None = None): |
| 3 | | pass |
| 4 | | |
| 6 | """ |
| 7 | Initialize the REST API with an optional database. |
| 8 | |
| 9 | Args: |
| 10 | database: Initial database with user information |
| 11 | """ |
| 12 | if database is None: |
| 13 | self.database = {"users": []} |
| 14 | else: |
| 15 | self.database = database |
| 16 | |
| 17 | def _find_user(self, name: str) -> Optional[Dict]: |
| 18 | """ |
| 19 | Find a user by name in the database. |
| 20 | |
| 21 | Args: |
| 22 | name: Name of the user to find |
| 23 | |
| 24 | Returns: |
| 25 | User dictionary if found, None otherwise |
| 26 | """ |
| 27 | for user in self.database["users"]: |
| 28 | if user["name"] == name: |
| 29 | return user |
| 30 | return None |
| 31 | |
| 32 | def _calculate_balance(self, user: Dict) -> float: |
| 33 | """ |
| 34 | Calculate the balance for a user based on what they owe and are owed. |
| 35 | |
| 36 | Args: |
| 37 | user: User dictionary |
| 38 | |
| 39 | Returns: |
| 40 | Calculated balance |
| 41 | """ |
| 42 | total_owed_by = sum(user["owed_by"].values()) |
| 43 | total_owes = sum(user["owes"].values()) |
| 44 | return round(total_owed_by - total_owes, 2) |
| 45 | |
| 46 | def _update_user_balances(self) -> None: |
| 47 | """Update balances for all users in the database.""" |
| 48 | for user in self.database["users"]: |
| 49 | user["balance"] = self._calculate_balance(user) |
| 50 | |
| 5 | 51 | def get(self, url: str, payload: str | None = None) -> str: |
| 6 | | pass |
| 7 | | |
| 52 | """ |
| 53 | Handle GET requests to the API. |
| 54 | |
| 55 | Args: |
| 56 | url: Request URL |
| 57 | payload: Optional JSON payload |
| 58 | |
| 59 | Returns: |
| 60 | JSON response string |
| 61 | """ |
| 62 | # Edge Case: Invalid URL |
| 63 | if url != "/users": |
| 64 | return json.dumps({"error": "Invalid URL"}) |
| 65 | |
| 66 | # Edge Case: Malformed payload |
| 67 | if payload is not None: |
| 68 | try: |
| 69 | payload_data = json.loads(payload) |
| 70 | except json.JSONDecodeError: |
| 71 | return json.dumps({"error": "Invalid JSON payload"}) |
| 72 | else: |
| 73 | payload_data = None |
| 74 | |
| 75 | # Update all user balances before returning data |
| 76 | self._update_user_balances() |
| 77 | |
| 78 | # Edge Case: No payload - return all users |
| 79 | if payload_data is None: |
| 80 | return json.dumps({"users": sorted(self.database["users"], key=lambda x: x["name"])}) |
| 81 | |
| 82 | # Edge Case: Missing 'users' key in payload |
| 83 | if "users" not in payload_data: |
| 84 | return json.dumps({"error": "Missing 'users' key in payload"}) |
| 85 | |
| 86 | # Edge Case: Empty users list in payload |
| 87 | if not payload_data["users"]: |
| 88 | return json.dumps({"users": []}) |
| 89 | |
| 90 | # Filter users based on payload |
| 91 | requested_users = payload_data["users"] |
| 92 | result_users = [] |
| 93 | |
| 94 | # Edge Case: Non-existent user requested |
| 95 | for user_name in requested_users: |
| 96 | user = self._find_user(user_name) |
| 97 | if user is not None: |
| 98 | result_users.append(user) |
| 99 | |
| 100 | return json.dumps({"users": sorted(result_users, key=lambda x: x["name"])}) |
| 101 | # Handled Edge Cases: Invalid URL, malformed payload, missing 'users' key, empty users list, non-existent user requested |
| 102 | |
| 8 | 103 | def post(self, url: str, payload: str | None = None) -> str: |
| 9 | | pass |
| 104 | """ |
| 105 | Handle POST requests to the API. |
| 106 | |
| 107 | Args: |
| 108 | url: Request URL |
| 109 | payload: JSON payload |
| 110 | |
| 111 | Returns: |
| 112 | JSON response string |
| 113 | """ |
| 114 | # Edge Case: Missing payload |
| 115 | if payload is None: |
| 116 | return json.dumps({"error": "Missing payload"}) |
| 117 | |
| 118 | # Edge Case: Malformed payload |
| 119 | try: |
| 120 | payload_data = json.loads(payload) |
| 121 | except json.JSONDecodeError: |
| 122 | return json.dumps({"error": "Invalid JSON payload"}) |
| 123 | |
| 124 | # Handle /add endpoint |
| 125 | if url == "/add": |
| 126 | # Edge Case: Missing 'user' key in payload |
| 127 | if "user" not in payload_data: |
| 128 | return json.dumps({"error": "Missing 'user' key in payload"}) |
| 129 | |
| 130 | user_name = payload_data["user"] |
| 131 | |
| 132 | # Edge Case: User already exists |
| 133 | if self._find_user(user_name) is not None: |
| 134 | return json.dumps({"error": "User already exists"}) |
| 135 | |
| 136 | # Create new user |
| 137 | new_user = { |
| 138 | "name": user_name, |
| 139 | "owes": {}, |
| 140 | "owed_by": {}, |
| 141 | "balance": 0.0 |
| 142 | } |
| 143 | |
| 144 | self.database["users"].append(new_user) |
| 145 | return json.dumps(new_user) |
| 146 | |
| 147 | # Handle /iou endpoint |
| 148 | elif url == "/iou": |
| 149 | # Edge Case: Missing required keys in payload |
| 150 | required_keys = ["lender", "borrower", "amount"] |
| 151 | for key in required_keys: |
| 152 | if key not in payload_data: |
| 153 | return json.dumps({"error": f"Missing '{key}' key in payload"}) |
| 154 | |
| 155 | lender_name = payload_data["lender"] |
| 156 | borrower_name = payload_data["borrower"] |
| 157 | amount = payload_data["amount"] |
| 158 | |
| 159 | # Edge Case: Lender and borrower are the same person |
| 160 | if lender_name == borrower_name: |
| 161 | return json.dumps({"error": "Lender and borrower cannot be the same person"}) |
| 162 | |
| 163 | # Edge Case: Invalid amount type |
| 164 | if not isinstance(amount, (int, float)): |
| 165 | return json.dumps({"error": "Amount must be a number"}) |
| 166 | |
| 167 | # Edge Case: Negative amount |
| 168 | if amount < 0: |
| 169 | return json.dumps({"error": "Amount cannot be negative"}) |
| 170 | |
| 171 | # Edge Case: Lender does not exist |
| 172 | lender = self._find_user(lender_name) |
| 173 | if lender is None: |
| 174 | return json.dumps({"error": "Lender does not exist"}) |
| 175 | |
| 176 | # Edge Case: Borrower does not exist |
| 177 | borrower = self._find_user(borrower_name) |
| 178 | if borrower is None: |
| 179 | return json.dumps({"error": "Borrower does not exist"}) |
| 180 | |
| 181 | # Update IOUs |
| 182 | # Update lender's owed_by |
| 183 | if borrower_name in lender["owed_by"]: |
| 184 | lender["owed_by"][borrower_name] += amount |
| 185 | else: |
| 186 | lender["owed_by"][borrower_name] = amount |
| 187 | |
| 188 | # Update borrower's owes |
| 189 | if lender_name in borrower["owes"]: |
| 190 | borrower["owes"][lender_name] += amount |
| 191 | else: |
| 192 | borrower["owes"][lender_name] = amount |
| 193 | |
| 194 | # Update balances |
| 195 | lender["balance"] = self._calculate_balance(lender) |
| 196 | borrower["balance"] = self._calculate_balance(borrower) |
| 197 | |
| 198 | # Return updated users sorted by name |
| 199 | result_users = sorted([lender, borrower], key=lambda x: x["name"]) |
| 200 | return json.dumps({"users": result_users}) |
| 201 | |
| 202 | # Edge Case: Invalid URL |
| 203 | else: |
| 204 | return json.dumps({"error": "Invalid URL"}) |
| 205 | # Handled Edge Cases: Missing payload, malformed payload, missing 'user' key, user already exists, missing required keys, lender and borrower are the same, invalid amount type, negative amount, lender does not exist, borrower does not exist, invalid URL |
| 206 | # Handled Edge Cases: Missing payload, malformed payload, missing 'user' key, user already exists, missing required keys, lender and borrower are the same, invalid amount type, negative amount, lender does not exist, borrower does not exist, invalid URL |