今天聊聊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
1 2 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
没有设置。
具体代码如下:
1 2 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