| 1 | import json |
| 2 | |
| 1 | 3 | class RestAPI: |
| 2 | 4 | def __init__(self, database: dict | None = None): |
| 3 | | pass |
| 4 | | |
| 5 | """ |
| 6 | Initialize the REST API with a database. |
| 7 | |
| 8 | Args: |
| 9 | database: A dictionary containing user data or None to start with an empty database. |
| 10 | """ |
| 11 | if database is None: |
| 12 | self.database = {"users": []} |
| 13 | else: |
| 14 | self.database = database |
| 15 | |
| 5 | 16 | def get(self, url: str, payload: str | None = None) -> str: |
| 6 | | pass |
| 7 | | |
| 17 | """ |
| 18 | Handle GET requests to the API. |
| 19 | |
| 20 | Args: |
| 21 | url: The endpoint being accessed. |
| 22 | payload: Optional JSON string containing request parameters. |
| 23 | |
| 24 | Returns: |
| 25 | JSON string response. |
| 26 | |
| 27 | Raises: |
| 28 | ValueError: If the URL is not recognized. |
| 29 | """ |
| 30 | if url == "/users": |
| 31 | if payload is None: |
| 32 | # Return all users sorted by name |
| 33 | users = sorted(self.database["users"], key=lambda u: u["name"]) |
| 34 | result = {"users": [self._format_user(user) for user in users]} |
| 35 | return json.dumps(result) |
| 36 | else: |
| 37 | # Return only specified users |
| 38 | data = json.loads(payload) |
| 39 | user_names = data.get("users", []) |
| 40 | |
| 41 | # Edge Case: Empty user list in payload |
| 42 | if not user_names: |
| 43 | return json.dumps({"users": []}) |
| 44 | |
| 45 | # Filter users by name |
| 46 | users = [user for user in self.database["users"] if user["name"] in user_names] |
| 47 | # Sort by name |
| 48 | users = sorted(users, key=lambda u: u["name"]) |
| 49 | result = {"users": [self._format_user(user) for user in users]} |
| 50 | return json.dumps(result) |
| 51 | else: |
| 52 | raise ValueError(f"Unknown URL: {url}") |
| 53 | |
| 8 | 54 | def post(self, url: str, payload: str | None = None) -> str: |
| 9 | | pass |
| 55 | """ |
| 56 | Handle POST requests to the API. |
| 57 | |
| 58 | Args: |
| 59 | url: The endpoint being accessed. |
| 60 | payload: JSON string containing request data. |
| 61 | |
| 62 | Returns: |
| 63 | JSON string response. |
| 64 | |
| 65 | Raises: |
| 66 | ValueError: If the URL is not recognized or if required data is missing. |
| 67 | """ |
| 68 | if url == "/add": |
| 69 | if payload is None: |
| 70 | raise ValueError("Payload required for /add endpoint") |
| 71 | |
| 72 | data = json.loads(payload) |
| 73 | user_name = data.get("user") |
| 74 | |
| 75 | # Edge Case: Missing user name in payload |
| 76 | if user_name is None: |
| 77 | raise ValueError("User name is required") |
| 78 | |
| 79 | # Edge Case: User already exists |
| 80 | for user in self.database["users"]: |
| 81 | if user["name"] == user_name: |
| 82 | raise ValueError(f"User {user_name} already exists") |
| 83 | |
| 84 | # Create new user |
| 85 | new_user = { |
| 86 | "name": user_name, |
| 87 | "owes": {}, |
| 88 | "owed_by": {} |
| 89 | } |
| 90 | self.database["users"].append(new_user) |
| 91 | |
| 92 | return json.dumps(self._format_user(new_user)) |
| 93 | |
| 94 | elif url == "/iou": |
| 95 | if payload is None: |
| 96 | raise ValueError("Payload required for /iou endpoint") |
| 97 | |
| 98 | data = json.loads(payload) |
| 99 | lender_name = data.get("lender") |
| 100 | borrower_name = data.get("borrower") |
| 101 | amount = data.get("amount") |
| 102 | |
| 103 | # Edge Case: Missing required fields |
| 104 | if lender_name is None or borrower_name is None or amount is None: |
| 105 | raise ValueError("Lender, borrower, and amount are all required") |
| 106 | |
| 107 | # Edge Case: Lender and borrower are the same person |
| 108 | if lender_name == borrower_name: |
| 109 | raise ValueError("Lender and borrower cannot be the same person") |
| 110 | |
| 111 | # Edge Case: Negative amount |
| 112 | if amount < 0: |
| 113 | raise ValueError("Amount must be non-negative") |
| 114 | |
| 115 | # Find lender and borrower |
| 116 | lender = None |
| 117 | borrower = None |
| 118 | |
| 119 | for user in self.database["users"]: |
| 120 | if user["name"] == lender_name: |
| 121 | lender = user |
| 122 | elif user["name"] == borrower_name: |
| 123 | borrower = user |
| 124 | |
| 125 | # Edge Case: Lender or borrower does not exist |
| 126 | if lender is None: |
| 127 | raise ValueError(f"User {lender_name} does not exist") |
| 128 | if borrower is None: |
| 129 | raise ValueError(f"User {borrower_name} does not exist") |
| 130 | |
| 131 | # Update debts |
| 132 | # If borrower already owes lender, increase the amount |
| 133 | if borrower_name in lender["owed_by"]: |
| 134 | lender["owed_by"][borrower_name] += amount |
| 135 | else: |
| 136 | lender["owed_by"][borrower_name] = amount |
| 137 | |
| 138 | # If lender already owes borrower, decrease the amount (or vice versa) |
| 139 | if lender_name in borrower["owes"]: |
| 140 | borrower["owes"][lender_name] += amount |
| 141 | else: |
| 142 | borrower["owes"][lender_name] = amount |
| 143 | |
| 144 | # Simplify debts if they cancel out |
| 145 | if lender_name in borrower["owed_by"] and borrower["owed_by"][lender_name] > 0: |
| 146 | # They both owe each other, simplify |
| 147 | lender_owes_borrower = borrower["owed_by"][lender_name] |
| 148 | borrower_owes_lender = lender["owed_by"][borrower_name] |
| 149 | |
| 150 | if lender_owes_borrower > borrower_owes_lender: |
| 151 | # Borrower owes more than lender owes |
| 152 | borrower["owed_by"][lender_name] = lender_owes_borrower - borrower_owes_lender |
| 153 | del lender["owed_by"][borrower_name] |
| 154 | del borrower["owes"][lender_name] |
| 155 | del lender["owes"][borrower_name] |
| 156 | elif borrower_owes_lender > lender_owes_borrower: |
| 157 | # Lender owes more than borrower owes |
| 158 | lender["owed_by"][borrower_name] = borrower_owes_lender - lender_owes_borrower |
| 159 | del borrower["owed_by"][lender_name] |
| 160 | del lender["owes"][borrower_name] |
| 161 | del borrower["owes"][lender_name] |
| 162 | else: |
| 163 | # They owe each other exactly the same amount |
| 164 | del borrower["owed_by"][lender_name] |
| 165 | del lender["owed_by"][borrower_name] |
| 166 | del borrower["owes"][lender_name] |
| 167 | del lender["owes"][borrower_name] |
| 168 | |
| 169 | # Get updated user objects |
| 170 | updated_lender = None |
| 171 | updated_borrower = None |
| 172 | |
| 173 | for user in self.database["users"]: |
| 174 | if user["name"] == lender_name: |
| 175 | updated_lender = user |
| 176 | elif user["name"] == borrower_name: |
| 177 | updated_borrower = user |
| 178 | |
| 179 | # Return updated users sorted by name |
| 180 | users = sorted([updated_lender, updated_borrower], key=lambda u: u["name"]) |
| 181 | result = {"users": [self._format_user(user) for user in users]} |
| 182 | return json.dumps(result) |
| 183 | |
| 184 | else: |
| 185 | raise ValueError(f"Unknown URL: {url}") |
| 186 | |
| 187 | def _format_user(self, user: dict) -> dict: |
| 188 | """ |
| 189 | Format a user object to include the balance. |
| 190 | |
| 191 | Args: |
| 192 | user: A user dictionary. |
| 193 | |
| 194 | Returns: |
| 195 | Formatted user dictionary with balance calculated. |
| 196 | """ |
| 197 | total_owed_by = sum(user["owed_by"].values()) |
| 198 | total_owes = sum(user["owes"].values()) |
| 199 | balance = round(total_owed_by - total_owes, 2) |
| 200 | |
| 201 | # Edge Case: Rounding to handle floating point precision issues |
| 202 | # This ensures that values like 3.3099999999999996 become 3.31 |
| 203 | return { |
| 204 | "name": user["name"], |
| 205 | "owes": user["owes"], |
| 206 | "owed_by": user["owed_by"], |
| 207 | "balance": balance |
| 208 | } |
| 209 | # Handled Edge Cases: Empty user list in payload, Missing user name in payload, User already exists, Missing required fields, Lender and borrower are the same person, Negative amount, Lender or borrower does not exist, Rounding to handle floating point precision issues |