[TOC]
## 第4章 自定義異常對象
### 4-1 關于“用戶”的思考
### 4-2 構建 Client 驗證器
1. 我們使用枚舉類型來代表不同的客戶端
~~~
?from enum import Enum
??
??
?class ClientTypeEnum(Enum):
? ? ?USER_EMAIL = 100
? ? ?USER_MOBILE = 101
??
? ? ?# 微信小程序
? ? ?USER_MINA = 200
? ? ?# 微信公眾號
? ? ?USER_WX = 201
~~~
2. 然后再編寫自定義的驗證器驗證客戶端傳入過來的**客戶端類型**,先將客戶端傳入過來的數字轉換成枚舉類型
* 如果轉換成功,則表示客戶端傳入進來的數字是正確的
* 如果轉換不成功則會報錯,表示客戶端傳入的數字是錯誤的
~~~
?class ClientForm(Form):
? ? ?account = StringField(validators=[DataRequired(), length(min=8, max=32)])
? ? ?secret = StringField()
? ? ?type = IntegerField(validators=[DataRequired()])
??
? ? ?def validate_type(self, value):
? ? ? ? ?"""
? ? ? ? 這里自定義的驗證器方法名必須為 'validate_' + 字段名(類變量)
? ? ? ? 因為 flask 內部設定的,只有這樣寫才會觸發該自定義的驗證器驗證
? ? ? ? :param value: type 傳入的具體字段,是 flask 調用驗證器的時候自動傳入的
? ? ? ? :return: 如果在自定義的驗證器內拋出異常則表示驗證失敗;不拋出異常,則表示驗證成功(暫時是這么理解的)
? ? ? ? """
? ? ? ? ?from app.libs.enums import ClientTypeEnum
? ? ? ? ?try:
? ? ? ? ? ? ?client = ClientTypeEnum(value.data)
? ? ? ? ?except ValueError as e:
? ? ? ? ? ? ?raise e
? ? ? ? ?self.type.data = client
~~~
**注意**:(查看 `validate_for_api()` 源碼發現的)
* 這里自定義的驗證器方法名必須為 `'validate_' + 字段名`(類變量)
* 因為 flask 內部設定的,只有這樣寫才會觸發該自定義的驗證器驗證
* `:param value:` type 傳入的具體字段,是 flask 調用驗證器的時候自動傳入的
* `:return:` 如果在自定義的驗證器內拋出異常則表示驗證失敗;不拋出異常,則表示驗證成功(暫時是這么理解的)
* 另外在 pycharm 中會提示 `validate_type`為靜態方法,但是實際上這種自定義的驗證器方法是實例方法。絕對不能改為靜態方法,否則會報錯,因為在 `flask` 內部調用的時候是使用 `ClientForm`、`UserEmailForm`、`UserMobileForm`等的實例來調用自定義的驗證器的。

> 小技巧:
>
> 在獲取客戶端傳入的數字時,單純的使用的 value 是獲取不到信息的,需要使用 **value.data**
上述代碼的精妙之處在于兩點:
* 我們可以去判斷客戶端穿過來的數字是否是我們枚舉類型的一種
* 客戶端傳過來的是一個數字的值,但是在我們整個代碼編寫過程中我們并不希望直接使用數字,因為我們已經定義了枚舉類型,所以我們更加希望在項目中使用枚舉,因為枚舉的可讀性比數字要強。
### 4-3 處理不同客戶端注冊的方案
#### 提交數據
客戶端向服務器發送數據的兩種不同的形式:
* 表單:通常用于網頁中
* JSON 對象:移動端
#### 接收數據
服務器接收參數的方式有兩類:
* `request.json` (`request.get_json(salient=True)`)
* `request.args.to_dict()`
具體的區別稍后再說,我們先使用 request.json 來寫:
~~~
?data = request.json # 接收到 data
?form = ClientForm(data=data)
~~~
先使用 request.json 接收到 data,然后實例化一個`form`,這個 `form`就是 `ClientForm`。
下面要考慮的就是如何將 data 參數傳入 `ClientForm`中,然后 `ClientForm`才能執行校驗。我們在**Flask高級編程**中傳遞客戶端的參數是直接把數據放到 `ClientForm`的必填參數中傳遞進來的。但是如果數據是 JSON 格式的話,就需要使用 `ClientForm`的關鍵字參數`data=`傳參。
> 這種傳參方式需要深入挖掘 wtforms 的源碼才會知道。
如果數據驗證通過的話,就可以進行注冊了。但是由于客戶端的種類是不同的,不同客戶端的注冊代碼也是不同的。在其他語言中可以使用`switch case`為不用的客戶端編寫不同的注冊代碼,但是 python 中是沒有 `switch`的,所以需要一些小技巧。
可以使用字典的方式解決。解決方式:
~~~
?promise = {
? ? ? ? ? ? ?ClientTypeEnum.USER_EMAIL: __register_by_email,
? ? ? ? ? ? ?ClientTypeEnum.USER_MOBILE: __register_by_mobile,
? ? ? ? ? ? ?ClientTypeEnum.USER_MINA: __register_by_mina,
? ? ? ? ? ? ?ClientTypeEnum.USER_WX: __register_by_wx,
? ? ? ? }
~~~
構造字典:{客戶端類型:該類型的注冊函數}
調用注冊函數的方式:
~~~
?promise[form.type.data]()
~~~
### 4-4 創建 User 模型
~~~
?from sqlalchemy import Column, Integer, String, SmallInteger
?from werkzeug.security import generate_password_hash
??
?from app.models.base import Base, db
??
??
?class User(Base):
? ? ?id = Column(Integer, primary_key=True)
? ? ?email = Column(String(24), unique=True, nullable=False)
? ? ?nickname = Column(String(24), unique=True)
? ? ?auth = Column(SmallInteger, default=1)
? ? ?_password = Column('password', String(128))
??
? ? ?@property
? ? ?def password(self):
? ? ? ? ?return self._password
??
? ? ?@password.setter
? ? ?def password(self, raw):
? ? ? ? ?self._password = generate_password_hash(raw)
??
? ? ?@staticmethod
? ? ?def register_by_email(nickname, account, secret):
? ? ? ? ?with db.auto_commit():
? ? ? ? ? ? ?user = User()
? ? ? ? ? ? ?user.nickname = nickname
? ? ? ? ? ? ?user.email = account
? ? ? ? ? ? ?user.password = secret
? ? ? ? ? ? ?db.session.add(user)
~~~
上面`User`模型的`register_by_email`方法中實例化了 `user`,我們在 `User`對象內部又創建了這個對象本身,從面向對象的角度來說這是不合理的,但是如果該方法是**靜態方法**或者**類方法**的話,那么就可以說得通了。靜態方法就是跟類、示例無關的方法。類方法就是類的方法,類方法當然可以生成類的實例對象。
### 4-5 完成客戶端注冊
### 4-6 生成用戶數據
### 4-7 自定義異常對象

我們在 ginger/app/libs/error\_code.py 中自定義異常,在 client.py 中調用就可以了,拋出異常僅僅只是為了顯示出程序運行的錯誤信息而已,并沒有其他操作。此處繼承的是 APIException 不是 HTTPException。
~~~
?class ClientTypeError(APIException):
? ? ?# 400 401 403 404
? ? ?# 500
? ? ?# 200 201 204
? ? ?# 301 302
? ? ?code = 400
? ? ?msg = 'client is invalid'
? ? ?error_code = 1006
~~~
結果如下所示:

### 4-8 淺談異常返回的標準與重要性

**一個 API 寫的好不好關鍵就在于你的錯誤異常信息的表示和描述是否準確、格式是否規范、是否有一個統一的標準。**
### 4-9 自定義APIException
自定義的 APIException 需要繼承 HTTPException,同時需要覆寫 get\_body、get\_headers 方法,編寫 get\_url\_no\_param 方法獲取當前訪問的不含查詢參數的 url。
~~~
?from flask import request, json
?from werkzeug.exceptions import HTTPException
??
??
??
?class APIException(HTTPException):
? ? ?code = 500
? ? ?msg = 'sorry, we made a mistake (* ̄︶ ̄)!'
? ? ?error_code = 999
??
? ? ?def __init__(self, msg=None, code=None, error_code=None,
? ? ? ? ? ? ? ? ? headers=None):
? ? ? ? ?if code:
? ? ? ? ? ? ?self.code = code
? ? ? ? ?if error_code:
? ? ? ? ? ? ?self.error_code = error_code
? ? ? ? ?if msg:
? ? ? ? ? ? ?self.msg = msg
? ? ? ? ?super(APIException, self).__init__(msg, None)
??
? ? ?def get_body(self, environ=None):
? ? ? ? ?body = dict(
? ? ? ? ? ? ?msg=self.msg,
? ? ? ? ? ? ?error_code=self.error_code,
? ? ? ? ? ? ?request=request.method + ' ' + self.get_url_no_param()
? ? ? ? )
? ? ? ? ?text = json.dumps(body)
? ? ? ? ?return text
??
? ? ?def get_headers(self, environ=None):
? ? ? ? ?"""Get a list of headers."""
? ? ? ? ?return [('Content-Type', 'application/json')]
??
? ? ?@staticmethod
? ? ?def get_url_no_param():
? ? ? ? ?full_path = str(request.full_path)
? ? ? ? ?main_path = full_path.split('?')
? ? ? ? ?return main_path[0]
~~~
字典轉化為文本,采用 `json`序列化的方式:`json.dumps()`
> `request.full_path():`
>
> 獲取完整的請求路徑。