# [Programming-Language-Design] Project 1 ###### tags: `programming-language-design` > Programming language design 2022 autumn Project 1 </br> ## Table of Content :::info [TOC] ::: </br> </br> ## Requirement [requirement link](https://trusted-minnow-271.notion.site/Final-Project-044948ed08f1422baeab8a01295e41a6) Requirements: 1. Able to create users 2. Let users can create their new sheet 3. Let users can print out their sheet 4. Let users can change the content of their sheet 5. Let sheet has two access rights, read only & editable, for every user. 6. Let users can share their sheet with other users 7. Let sheets can deal with rational arithmetic + , - , * , / . </br> ## Thonk :thinking_face: :brain:->:404: ### Menu and Command First we can use **command pattern** to impelement the menu console and the commands. > Note: We don't have many different reciver, so we can ignore that part and only focus on `Invoker`(Menu) and the `Command` part ```graphviz digraph { label = "[graph: implement Menu and command with command pattern]" fontcolor="blue" node [shape="record"] rankdir=TB console [label="{\<Console\>| Console(list[Command]) | handUserInput()}"] menu [label="Menu"] command [label="{\<Command\>| str getdescription() | requireArg() | execute()}"] createUser [label="{CreateUser}"] createSheet [label="CreateSheet"] menu -> console [label=implement] console -> command [label=has] createUser -> command [label=implement] createSheet -> command [label=implement] rank=same; menu; createUser; rank=same; console; command; } ``` </br> ### User and Sheet Good, now let's thoking about the relationship between `sheet` and `user` ```graphviz digraph { label = "[graph: relationship between Sheet and User] this is bad :(" fontcolor="red" node [shape="record"] sheet [label="{Sheet|data|owner|readOnlyUsers|EditableUsers}"] user [label="{User|ownedSheet|sharedSheet}"] sheet -> user [label="1..*"] user -> sheet [label="1..*"] } ``` </br> We can notice that, both `sheet` and `user` has a _one to many_ relationship to each other, which is a very **BAD** thing, because it would increase code coupling. Thus, we can create another layer between `sheet` and `user`, and call it `SheetUserTable` ```graphviz digraph { label = "[graph: SheetUserTabel]" fontcolor="blue" node [shape="record"] sheetUserTable [label=< <table> <tr> <td colspan="3" border="0">SheetUserTable</td> </tr> <tr> <td PORT="s">Sheet</td> <td PORT="u">User</td> <td>Permission</td> </tr> </table> >] } ``` Once we have the `SheetUserTable`, all the `sheet`-relative command can be turned into **qury operation** of `SheetUserTable`. For example: ```javascript declare SheetUserTable as T original 2 => create sheet named "sheet1" for user "Alice" translate to => T.addRow(alice, new Sheet("sheet1")) original 3 => check sheet "sheet1" as user "Alice" translate to => T.search("sheet1", alice) original 4 => change sheet value "sheet1" (row=1, col=2, val=3) as user "Alice" translate to => T.search("sheet1", alice).editSheet(1, 2, 3) original 5 => change sheet "sheet1" user "Alice" permission to "readOnly" translate to => T.search("sheet1", alice).changePermission("readOnly") original 6 => share sheet "sheet1" from user "Alice" to user "Bob" as permission "permission" translte to => T.search("sheet1", "Alice").sharePermission allow permission \ AND T.addRow("sheet1", "Bob", permission) ``` After that, we still didn't finish the 1-th requirement: `create user`. Thus, we need a variable `userList`. If we combine the both `SHeeUserTable` and `userList`... _\*TA-DA\*_, we now have a new class now, and we can call it a `DataBase`. _(the whole sheet operation are very simular to dataBase, so i simpply call it a database)_. ```graphviz digraph { node [shape=record] db [label="{DataBase|userList: [User]| sheetUserTable: SheetUserTable}"] } ``` :::spoiler **My _real_ implementation of `DataBase` class** :::info Because of my laziness i didn't completely follow the digraph above in my code. Instead, i wrote this: ```python # SafeDict: A type-safe dictionary that have more strictic `edit` and `add` method, # it will spit out error if you try edit non-exist filed or add existing filed class SafeDict(Generic[KT, VT]): data: dict[KT, VT] ... class Database(): # class variables _instance: Database|None = None # instance variables users: SafeDict[str, User] # name -> User sheets: SafeDict[str, Sheet] # name -> Sheet table: SafeDict[tuple[User, Sheet], Permission] # (User, Sheet) -> Permission ... ``` Because the most often used searching qury is "(user, sheet)->permission", I choose `dict[(User, Sheet), Permission]` as the data structure of the `SheetUserTable`. Using a `dict` can not only make searching easy and also speedup the searching operation. ::: </br> </br> Futhermore, because our `DataBase` would be used all over the program, and there's only `DataBase` acrros the program, we could make use of **Singleton Pattern**. :::spoiler **Somthing about Singleton** :::warning Even if there's a possibility of haveing mulity `DataBase`, we can just tweek the class a little bit to fit that situation. Yes, this is not Singleton anymore. It loose the main propurse of being Singleton which is getting global variable without worry about initilization. But, if we need multiple `DataBase` instance, we WILL init the instance manually ```python class DataBase(): static _dbList = {} static _current = None @classmethod def __init__(self, name) -> None: self.name = name @classmethod def createDB(cls, name): cls._dbList[name] = DataBase(name) @classmethod def selectDB(cls, name): if name not in cls._dbList: raise ValueError(f"{name} not found") cls._current = cls._dbList[name] @classmethod def getCurrent(cls): if cls._current: return cls._current else: raise ValueError("no DataBase has been selected") ``` ::: <!--After fiiguring out the relationship between `Sheet` and `User`, let's take a closer look on them. ~~If we look cloose enough, we can notice that both `Sheet` and `SheetUserTable` have simular property! They both have an 2d-array-like data content, and read/write method.~~ --> </br> </br> ### Permission We can just use some bit operation to implement the permission feature ```python OWNED = 0b00001 READ = 0b00010 WRITE = 0b00100 SHARE_READ = 0b01000 SHARE_WRITE = 0b10000 ALL = 0b11111 SHARE_MASK = 0b11000 SELF_MASK = 0b00110 SHARE_TO_SHIFT = 2 ``` #### Factory Use a `PermissionFactory` to instantiate a Permission Object more easily. ```python class PermissionFactory(): @classmethod def readonly(cls) -> Permission: """can only read, not able to share """ return Permission(PermissionCode(PermissionCode.READ)) @classmethod def editable(cls) -> Permission: """can rw, also share rw """ return Permission(PermissionCode(PermissionCode.READ | PermissionCode.WRITE | PermissionCode.SHARE_READ | PermissionCode.SHARE_WRITE)) @classmethod def owner(cls) -> Permission: """permission all """ return Permission(PermissionCode(PermissionCode.ALL)) ``` </br> </br> ## First Attempt :wrench: ### File structure ``` src ├── main.py | ├── command │   └── command.py | ├── controller │   └── controller.py | ├── exceptions │   └── exceptions.py | ├── menu │   ├── menu_cmds.py │   └── menu.py | ├── model │   ├── database.py │   └── data_type.py | └── permission └── permission.py ``` ### ```sequence Note over Main: entry point Main->Menu:menu_loop(arg) Note over Menu: select cmd\n from io input Menu->Menu.cmds[i]:execute() Menu.cmds[i]->controller: correspoding method controller->Database: get/find/update method Database-->controller: result/raise Error controller-->Menu.cmds[i]: None/raise Error Menu.cmds[i]-->Menu: None/raise Error Note over Menu: print error if error Menu-->Main: if(EOFError, ExitMenuException) Note over Main: end program ``` </br> ### Some small improvement I try to make the structure a more "MVC" model, but I didn't follow MVC model faithfully, because that's too much a work for this little project. I have tried to make the database structure more restrict, but there's too much work to do, and in real scenario, most of the time we will use a proper database and framework. so... I just leave it be~ ### Question ### Bad parts #### **Exception handleing problem** In my current code, the high-level functions/code needs to know the low level exp definitions in order to handle exceptions, which is bad thing. for example, in my `menu.py`, it should only handle about user inputs and routing the commands. Butt, because of those command will raising different errors like `PermissionError`, `NotfoundError`, etc. ```python # menu.python """ That's a lot of except... :( SO complecatied """ def menu_loop(self): try: """handle user inputs and rount cmds""" ... except EOFError as e: break except menu_cmds.ExitMenuException as e: break except (NotFoundErroras, Duplicated) e: print(e) except PermissionError as e: print(e) except ArgumentError as e: print('input error:', e) except Exception as e: ... # SO UGLY :( ``` ```python # database.py """ These permission checking is increasing the code coupling BAD :( """ def check_sheet(self, username: str, sheet_name: str) -> Sheet: user = self.users.get(username) sheet = self.sheets.get(sheet_name) # This permission check code is duplicated over the program #-------------------------------------------------- try: permission = self.table.get((user, sheet)) except: raise PermissionError("Permission denied") if not permission.is_readable(): raise PermissionError("Permission denied") #-------------------------------------------------- return copy.deepcopy(sheet) def update_sheet(self, username: str, sheet_name: str, new_sheet: Sheet) -> None: user = self.users.get(username) sheet = self.sheets.get(sheet_name) # Same copy-paste code again #-------------------------------------------------- try: permission = self.table.get((user, sheet)) except: raise PermissionError("Permission denied") if not permission.is_writeable(): raise PermissionError("Permission denied") #-------------------------------------------------- sheet.update_data(new_sheet) ``` #### how to solve exception handling problem From now, i just use another class called `AppError`, to seperated custom error from python internal error. Still have no idea what to solve this problem. use Monad concept maybe > Create An Exception Interface, and give those Exception basic functionality like logging, error strings, redirection, aborts... > [name=future me]