今天聊聊typestate模式。
新版oauth2-rs使用这个模式是想解决一个问题。
在Oauth2中,一般需要提前配置好auth_url, token_url, introspect_url等,才能调用后续的鉴权相关功能。比如没有token_url配置好,你就无法成功拿授权码(code)获取token。
但如果用户忘了配置,那就会在调用获取token时才得到运行时错误。
能不能在编译时就把错误暴露出来,让用户提前感知问题?
这就是typestate模式能很好解决的问题。
typestate
typestate是将状态定义到类型中,这样对于强类型约束的语言,可以很好的利用编译检查是否有状态和调用不符合的情况
比如如下两个ticket结构体表示不同状态的ticket
EmptyTicket只能new或者book
book会转化为BookedTicket, 这时只能use消费掉或cancel释放为EmptyTicket
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 
 | struct EmptyTicket {}struct BookedTicket {}
 
 impl EmptyTicket {
 fn new() -> Self {
 EmptyTicket {}
 }
 fn book(self) -> BookedTicket {
 BookedTicket {}
 }
 }
 
 impl BookedTicket {
 fn use(self) {}
 fn cancel(self) -> EmptyTicket {
 EmptyTicket {}
 }
 }
 
 | 
更进一步
那oauth2-rs如何解决文首提到的问题呢?
下边用一段代码模拟一下:
auth_url和token_url都是EndpointState trait
初始是未设置,用EndointNotSet表示状态
当其中一个设置时,返回的Client就会更新对应的状态类型为EndointSet
这样后续相应的方法基于拥有此类型的Client, 就能保证必须先设置url才能调用相应的方法
比如
Client::<EndointSet, EndpointNotSet>就声明了这个Client只能调用auth_url相关方法,不能调用token_url相关方法, 因为类型的状态上明确了token_url没有设置。
具体代码如下:
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 
 | use std::marker::PhantomData;
 struct EndointSet {}
 struct EndpointNotSet {}
 
 trait EndpointState {}
 
 impl EndpointState for EndointSet {}
 impl EndpointState for EndpointNotSet {}
 
 struct Client<HasAuthUrl = EndpointNotSet, HasTokenUrl = EndpointNotSet>
 where
 HasAuthUrl: EndpointState,
 HasTokenUrl: EndpointState,
 {
 auth_url: Option<String>,
 token_url: Option<String>,
 phantom: std::marker::PhantomData<(HasAuthUrl, HasTokenUrl)>,
 }
 
 impl<HasAuthUrl, HasTokenUrl> Client<HasAuthUrl, HasTokenUrl>
 where
 HasAuthUrl: EndpointState,
 HasTokenUrl: EndpointState,
 {
 pub fn new() -> Self {
 Client {
 auth_url: None,
 token_url: None,
 phantom: PhantomData,
 }
 }
 
 pub fn set_auth_url(self, auth_url: &str) -> Client<EndointSet, HasTokenUrl> {
 Client {
 auth_url: Some(auth_url.to_string()),
 token_url: self.token_url,
 phantom: PhantomData,
 }
 }
 
 pub fn set_token_url(self, token_url: &str) -> Client<HasAuthUrl, EndointSet> {
 Client {
 auth_url: self.auth_url,
 token_url: Some(token_url.to_string()),
 phantom: PhantomData,
 }
 }
 }
 
 impl<HasTokenUrl> Client<EndointSet, HasTokenUrl>
 where
 HasTokenUrl: EndpointState,
 {
 pub fn get_auth_url(&self) -> &str {
 self.auth_url.as_ref().unwrap()
 }
 }
 
 impl<HasAuthUrl> Client<HasAuthUrl, EndointSet>
 where
 HasAuthUrl: EndpointState,
 {
 pub fn get_token_url(&self) -> &str {
 self.token_url.as_ref().unwrap()
 }
 }
 fn main() {
 let client =
 Client::<EndointSet, EndpointNotSet>::new().set_auth_url("https://auth.example.com");
 
 
 
 println!("Auth URL: {}", client.get_auth_url());
 println!("Token URL: {}", client.get_token_url());
 }
 
 | 
彩蛋
这个pattern其实是2019年就提出来的,很感兴趣可以看看这篇博客
	
	
	如有疑问,请文末留言交流或邮件:newbvirgil@gmail.com
   本文链接 : https://newbmiao.github.io/2024/04/24/rust-typestate-pattern.html
  本文链接 : https://newbmiao.github.io/2024/04/24/rust-typestate-pattern.html