# [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]