黄金容易生锈吗_生锈和休息

黄金容易生锈吗

A few months back I decided to write a command line client for Sentry because manually invoking the Sentry API for some common tasks (such as dsym or sourcemap management is just no fun). Given the choice of languages available I went with Rust. The reason for this is that I want people to be able to download a single executable that links everything in statically. The choice was between C, C++, Go and Rust. There is no denying that I really like Rust so it was already a pretty good choice for me. However what made it even easier is that Rust quite a potent ecosystem for what I wanted. So here is my lessons learned from this.

几个月前,我决定为Sentry编写一个命令行客户端,因为手动调用Sentry API来执行一些常见任务(例如dsym或Sourcemap管理很无聊)。 考虑到可用的语言选择,我选择了Rust。 原因是我希望人们能够下载一个静态链接所有内容的可执行文件。 选择是在C,C ++,Go和Rust之间。 无可否认,我真的很喜欢Rust,所以对我来说这已经是一个不错的选择。 然而,使Rust更容易的是Rust满足了我想要的强大生态系统。 所以这是我从中学到的教训。

HTTP库 (Libraries for HTTP)

To make an HTTP request you have a choice of libraries. In particular there are two in Rust you can try: hyper and rust-curl. I tried both and there are some releases with the former but I settled in rust-curl in the end. The reason for this is twofold. The first is that curl (despite some of the oddities in how it does things) is very powerful and integrates really well with the system SSL libraries. This means that when I compile the executable I get the native TLS support right away. rust-curl also (despite being not a pure rust library) compiles really well out of the box on Windows, macOS and Linux. The second reason is that Hyper is currently undergoing a major shift in how it’s structured and a bit in flux. I did not want to bet on that too much. When I started it also did not have proxy support which is not great.

要发出HTTP请求,您可以选择库。 特别是在Rust中,您可以尝试两种: hyperrust-curl 。 我尝试了两者,并且前者有一些版本,但最终我还是选择了rust-curl。 原因是双重的。 首先是curl(尽管它在做事上有一些奇怪之处)非常强大,并且与系统SSL库的集成非常好。 这意味着当我编译可执行文件时,我会立即获得本机TLS支持。 rust-curl也可以(尽管不是纯粹的rust库)可以在Windows,macOS和Linux上开箱即用。 第二个原因是,Hyper目前正在经历结构上的重大转变和变化。 我不想赌那么多。 当我开始时,它也没有代理支持,这不是很好。

For JSON parsing and serializing I went with serde. I suppose that serde will eventually be the library of choice for all things serialization but right now it’s not. It depends on compiler plugins and there are two ways to make it work right now. One is to go with nightly Rust (which is what I did) the other is to use the build script support in Rust. This is similar to what you do in Go where some code generation happens as part of the build. It definitely works but it’s not nearly as nice as using serde with nightly Rust.

对于JSON解析和序列化,我使用了serde 。 我认为serde最终将成为所有序列化的选择库,但现在不是。 它取决于编译器插件,有两种方法可以使其立即运行。 一种是每晚使用Rust(这是我所做的),另一种是使用Rust中的构建脚本支持。 这类似于您在Go中所做的工作,在Go中,部分代码生成是在构建过程中发生的。 它绝对可以工作,但是不如与每晚的Rust一起使用serde一样好。

API设计 (API Design)

The next question is what a good API design for a Rust HTTP library is. I struggeld with this quite a bit and it took multiple iterations to end up with something that I think is a good pattern. What I ended up is a collection of multiple types:

下一个问题是Rust HTTP库的良好API设计是什么。 我为此付出了很多努力,花了很多次迭代才得出我认为是好的模式。 我最终得到的是多种类型的集合:

  • Api: I have a basic client object which I call Api internally. it manages the curl handles (right now it just caches one) and also exposes convenience methods to perform certain types of HTTP requests. On top of that it provides high level methods that send the right HTTP requests and handle the responses.
  • ApiRequest: basically your request object. It’s mostly a builder for making requests and has a method to send the request and get a response object.
  • ApiResponse: contains the response from the HTTP request. This also provides various helpers to convert the response into different things.
  • ApiResult<T>: this is a result object which is returned from most methods. The error is a special API error that converts from all the APIs we call into. This means it can hold curl errors, form errors, JSON errors, IO errors and more.
  • Api :我有一个基本的客户端对象,在内部可以调用Api 。 它管理curl句柄(现在只缓存一个),还公开了执行某些类型的HTTP请求的便捷方法。 最重要的是,它提供了发送正确的HTTP请求并处理响应的高级方法。
  • ApiRequest :基本上是您的请求对象。 它主要是用于发出请求的构建器,并具有发送请求和获取响应对象的方法。
  • ApiResponse :包含来自HTTP请求的响应。 这也提供了各种帮助程序来将响应转换为不同的事物。
  • ApiResult <T> :这是从大多数方法返回的结果对象。 该错误是一种特殊的API错误,会从我们调用的所有API转换为该错误。 这意味着它可以容纳卷曲错误,表单错误,JSON错误,IO错误等。

To give you an idea how this looks like I want to show you one of the high level methods that use most of the API:

为了给您一个想法,我想向您展示使用大多数API的高级方法之一:

pubpub    fnfn    list_releaseslist_releases (( && selfself ,,    orgorg ::    && strstr ,,    projectproject ::    && strstr )) 

         ->->    ApiResultApiResult << VecVec << ReleaseInfoReleaseInfo >>>> 

{{ 

         OkOk (( selfself .. getget (( && formatformat !! (( "/projects/{}/{}/releases/""/projects/{}/{}/releases/" ,, 

                                                   PathArgPathArg (( orgorg ),),    PathArgPathArg (( projectproject )))))) ?? .. convertconvert ()() ?? )) 

}} 

(Note that I’m using the new question mark syntax ? instead of the more familiar try! macro here)

(请注意,我用的不是更熟悉的尝试问号语法?!宏这里)

So what is happening here?

那么这里发生了什么?

  1. This is a method on the Api struct. We use the get() shorthand method to make an HTTP GET request. It takes one argument which is the URL to make the request to. We use standard string formatting to create the URL path here.
  2. The PathArg is a simple wrapper that customizes the formatting so that instead of just stringifying a value it also percent encodes it.
  3. The return value of the get method is a ApiResult<ApiResponse> which provides a handy convert() method which does both error handling and deserialization.
  1. 这是Api结构上的方法。 我们使用get()速记方法来发出HTTP GET请求。 它接受一个参数,该参数是发出请求的URL。 我们在此处使用标准的字符串格式来创建URL路径。
  2. PathArg是一个简单的包装程序,可以自定义格式,以便不仅对值进行字符串化,还可以对其进行百分比编码。
  3. get方法的返回值是ApiResult <ApiResponse> ,它提供了一个方便的convert()方法,该方法可以进行错误处理和反序列化。

How does the JSON handling take place here? The answer is that convert() can do that. Because Vec<ReleaseInfo> has an automatic deserializer implemented.

JSON处理如何在这里进行? 答案是convert()可以做到这一点。 因为Vec <ReleaseInfo>具有实现的自动反序列化器。

错误洋葱 (The Error Onion)

The bulk of the complexity is hidden behind multiple layers of error handling. It took me quite a long time to finally come up with this design which is why I’m particularly happy with finally having found one I really like. The reason error handling is so tricky with HTTP requests is because you want to have both the flexibility of responding to specific error conditions as well as automatically handling all the ones you are not interested in.

复杂性的大部分隐藏在多层错误处理之后。 我花了很长时间才最终提出这个设计,这就是为什么我对终于找到我真正喜欢的设计感到特别高兴的原因。 HTTP请求的错误处理之所以如此棘手,是因为您既要灵活地响应特定的错误情况,又要自动处理所有您不感兴趣的错误情况。

The design I ended up with is that I have an ApiError type. All the internal errors that the library could encounter (curl errors etc.) are automatically converted into an ApiError. If you send a request the return value is as such Result<ApiResponse, ApiError>. However the trick here is that at this level no HTTP error (other than connection errors) is actually stored as ApiError. Instead also a failed response (because for instance of a 404) is stored as the actual response object.

我最终得到的设计是我有一个ApiError类型。 库可能遇到的所有内部错误(curl错误等)将自动转换为ApiError 。 如果发送请求,则返回值为Result <ApiResponse,ApiError> 。 但是,这里的技巧是,在此级别上,没有HTTP错误(连接错误除外)实际上存储为ApiError 。 而是还将失败的响应(例如由于404)存储为实际响应对象。

On the response object you can check the status of the response with these methods:

在响应对象上,可以使用以下方法检查响应的状态:

However what’s nice is that most of the time you don’t have to do any of this. The response method also provides a method to conver non successful responses into errors like this:

但是,最棒的是,大多数情况下您无需执行任何操作。 响应方法还提供了一种将非成功响应转换为错误的方法,如下所示:

pubpub    fnfn    to_resultto_result (( selfself ))    ->->    ApiResultApiResult << ApiResponseApiResponse >>    {{ 

         ifif    selfself .. okok ()()    {{ 

                 returnreturn    OkOk (( selfself );); 

         }} 

         ifif    letlet    OkOk (( errerr ))    ==    selfself .. deserializedeserialize ::<::< ErrorInfoErrorInfo >> ()()    {{ 

                 ifif    letlet    SomeSome (( detaildetail ))    ==    errerr .. detaildetail    {{ 

                         returnreturn    ErrErr (( ApiErrorApiError :::: HttpHttp (( selfself .. statusstatus (),(),    detaildetail ));)); 

                 }} 

         }} 

         ErrErr (( ApiErrorApiError :::: HttpHttp (( selfself .. statusstatus (),(),    "generic error""generic error" .. intointo ()))())) 

}} 

This method consumes the response and depending on the condition of the response returns different results. If everything was fine the response is returned unchanged. However if there was an error we first try to deserialize the body with our own ErrorInfo which is the JSON response our API returns or otherwise we fall back to a generic error message and the status code.

此方法消耗响应,并且根据响应的条件返回不同的结果。 如果一切正常,则返回的响应不变。 但是,如果发生错误,我们首先尝试使用我们自己的ErrorInfo反序列化主体,这是我们的API返回的JSON响应,否则我们将退回到通用错误消息和状态代码。

What’s deserialize? It just invokes serde for deserialization:

什么反序列化? 它只是调用serde进行反序列化:

One thing you can see here is that the body is buffered into memory entirely. I was torn on this in the beginning but it actually turns out to make the API significantly nicer because it allows you to reason about the response better. Without buffering up everything in memory it becomes much harder to do conditional things based on the body. For the cases where we cannot deal with this limitation I have extra methods to stream the incoming data.

您可以在这里看到的一件事是,主体已完全缓冲到内存中。 我一开始就为此感到困惑,但实际上它使API变得更好,因为它使您可以更好地推理响应。 如果不缓冲内存中的所有内容,则根据身体来执行条件操作将变得更加困难。 对于无法解决此限制的情况,我有额外的方法来传输传入的数据。

On deserialization we match on the body. The body is an Option<Vec<u8>> here which we convert into a &[u8] which satisfies the Read interface which we can then use for deserialization.

反序列化时,我们在身体上匹配。 主体是Option <Vec <u8 >>,在这里我们将其转换为满足Read接口的&[u8] ,然后可将其用于反序列化。

The nice thing about the aforementioned to_result method is that it works just so nice. The common case is to convert something into a result and to then deserialize the response if everything is fine. Which is why we have this convert method:

关于前述的to_result方法的好处是它的工作原理是如此的好。 通常的情况是将一切转换为结果,然后在一切正常的情况下反序列化响应。 这就是为什么要使用以下convert方法:

pubpub    fnfn    convertconvert << TT ::    DeserializeDeserialize >> (( selfself ))    ->->    ApiResultApiResult << TT >>    {{ 

         selfself .. to_resultto_result ().(). and_thenand_then (( || xx ||    xx .. deserializedeserialize ())()) 

}} 

复杂用途 (Complex Uses)

There are some really nice uses for this. For instance here is how we check for updates from the GitHub API:

有一些非常好的用法。 例如,这是我们检查GitHub API更新的方法:

Here we silently ignore a 404 but otherwise we parse the response as GitHubRelease structure and then look through all the assets. The call to to_result does nothing on success but it will handle all the other response errors automatically.

在这里,我们默默地忽略404,否则我们将响应解析为GitHubRelease结构,然后浏览所有资产。 调用to_result成功不会执行任何操作,但会自动处理所有其他响应错误。

To get an idea how the structures like GitHubRelease are defined, this is all that is needed:

要了解如何定义类似GitHubRelease的结构,这就是所有需要的工作:

#[derive(Debug, Deserialize)]#[derive(Debug, Deserialize)] 

structstruct    GitHubAssetGitHubAsset    {{ 

         browser_download_urlbrowser_download_url ::    StringString ,, 

         namename ::    StringString ,, 

}} 



#[derive(Debug, Deserialize)]#[derive(Debug, Deserialize)] 

structstruct    GitHubReleaseGitHubRelease    {{ 

         tag_nametag_name ::    StringString ,, 

         assetsassets ::    VecVec << GitHubAssetGitHubAsset >> ,, 

}} 

卷曲句柄管理 (Curl Handle Management)

One thing that is not visible here is how I manage the curl handles. Curl is a C library and the Rust binding to it is quite low level. While it’s well typed and does not require unsafe code to use, it still feels very much like a C library. In particular there is a curl “easy” handle object you are supposed to keep hanging around between requests to take advantage of keepalives. However the handles are stateful. Readers of this blog are aware that there are few things I hate as much as unnecessary stateful APIs. So I made it as stateless as possible.

这里不可见的一件事是我如何管理卷曲手柄。 Curl是一个C库,Rust绑定到它的级别很低。 尽管它的类型正确,并且不需要使用不安全的代码,但它仍然非常像C库。 特别是,有一个卷曲的“容易”处理对象,您应该在两次请求之间一直保持徘徊,以利用keepalive。 但是句柄是有状态的。 该博客的读者意识到,我讨厌的东西和不必要的有状态API一样多。 所以我使它尽可能无状态。

The “correct” thing to do would be to have a pool of “easy” handles. However in my case I never have more than one request outstanding at the time so instead of going with something more complex I stuff away the “easy” handle in a RefCell. A RefCell is a smart pointer that moves the borrow semantics that rust normally requires at compile time to runtime. This is rougly how this looks:

“正确”的做法是拥有“简单”的句柄池。 但是,就我而言,当时我从来没有超过一个未完成的请求,因此我不再处理更复杂的事情,而是将RefCell中的“简单”句柄进去RefCell是一个智能指针,它将生锈通常在编译时需要的借用语义移到运行时。 这看起来很糟糕:

This way if you call request twice you will get a runtime panic if the last request is still outstanding. This is fine for what I do. The ApiRequest object itself implements a builder like pattern where you can modify the object with chaining calls. This is roughly how this looks like when used for a more complex situation:

这样,如果您两次调用请求 ,则在最后一个请求仍然未解决的情况 ,您将遇到运行时恐慌。 这对我所做的事很好。 ApiRequest对象本身实现了类似于构建器的模式,您可以在其中使用链接调用来修改对象。 当用于更复杂的情况时,大致如下所示:

pubpub    fnfn    send_eventsend_event (( && selfself ,,    eventevent ::    && EventEvent ))    ->->    ApiResultApiResult << StringString >>    {{ 

         letlet    dsndsn    ==    selfself .. configconfig .. dsndsn .. as_refas_ref ().(). ok_orok_or (( ErrorError :::: NoDsnNoDsn )) ?? ;; 

         letlet    eventevent    ::    EventInfoEventInfo    ==    selfself .. requestrequest (( MethodMethod :::: PostPost ,,    && dsndsn .. get_submit_urlget_submit_url ())()) ?? 

                 .. with_headerwith_header (( "X-Sentry-Auth""X-Sentry-Auth" ,,    && dsndsn .. get_auth_headerget_auth_header (( eventevent .. timestamptimestamp )))) ?? 

                 .. with_json_bodywith_json_body (( && eventevent )) ?? 

                 .. sendsend ()() ?? .. convertconvert ()() ?? ;; 

         OkOk (( eventevent .. idid )) 

}} 

得到教训 (Lessons Learned)

My key takeaways from doing this in Rust so far have been:

到目前为止,我在Rust中所做的主要收获是:

  • Rust is definitely a great choice for building command line utilities. The ecosystem is getting stronger by the day and there are so many useful crates already for very common tasks.
  • The cross platform support is superb. Getting the windows build going was easy cake compared to the terror you generally go through with other languages (including Python).
  • serde is a pretty damn good library. It’s a shame it’s not as nice to use on stable rust. Can’t wait for this stuff to get more stable.
  • Result objects in rust are great but sometimes it makes sense to not immediately convert data into a result object. I originally converted failure responses into errors immediately and that definitely hurt the convenience of the APIs tremendously.
  • Don’t be afraid of using C libraries like curl instead of native Rust things. It turns out that Rust’s build support is pretty magnificent which makes installing the rust curl library straightforward. It even takes care of compiling curl itself on Windows.
  • Rust绝对是构建命令行实用程序的绝佳选择。 生态系统每天都在变得越来越强大,已经有很多有用的板条箱可用于非常常见的任务。
  • 跨平台支持非常出色。 与通常使用其他语言(包括Python)所经历的恐怖相比,使Windows构建顺利进行很容易。
  • serde是一个非常不错的库。 真可惜,它不能用于稳定的防锈。 等不及要让这些东西变得更稳定了。
  • 生锈的结果对象很棒,但有时不立即将数据转换为结果对象是有意义的。 我最初将故障响应立即转换为错误,这无疑极大地损害了API的便利性。
  • 不要害怕使用诸如curl的 C库而不是本机Rust的东西。 事实证明,Rust的构建支持非常出色,这使得安装rust curl库变得简单。 它甚至负责在Windows上编译curl本身。

If you want to see the code, the entire git repository of the client can be found online: getsentry/sentry-cli.

如果您想查看代码,则可以在线找到客户端的整个git存储库: getsentry / sentry-cli

翻译自: https://www.pybloggers.com/2016/07/rust-and-rest/

黄金容易生锈吗

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值