Skip to content

Commit d66bd8b

Browse files
authored
Add JSON merge patch example (#3301)
Resolves #1649. Also cleans up other examples by reducing the imports.
1 parent cd9fc1f commit d66bd8b

File tree

8 files changed

+387
-121
lines changed

8 files changed

+387
-121
lines changed

Cargo.lock

Lines changed: 23 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

eng/scripts/verify-dependencies.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ use std::{
1919

2020
static EXEMPTIONS: &[(&str, &str)] = &[
2121
("azure_core", "http"),
22+
("azure_core", "json-patch"),
2223
("azure_core", "ureq"),
2324
("azure_core_test", "dotenvy"),
2425
("azure_canary", "serde"),

sdk/core/azure_core/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ azure_security_keyvault_certificates.path = "../../keyvault/azure_security_keyva
4545
azure_security_keyvault_secrets.path = "../../keyvault/azure_security_keyvault_secrets"
4646
criterion.workspace = true
4747
http = "1.3.1"
48+
json-patch = "4.1.0"
4849
reqwest.workspace = true
4950
thiserror.workspace = true
5051
tokio.workspace = true

sdk/core/azure_core/examples/core_error_response.rs

Lines changed: 30 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -2,20 +2,18 @@
22
// Licensed under the MIT License.
33

44
use azure_core::{
5-
credentials::TokenCredential,
65
error::ErrorResponse,
7-
http::{headers::Headers, AsyncRawResponse, HttpClient, StatusCode, Transport},
6+
http::{StatusCode, Transport},
87
json,
98
};
10-
use azure_core_test::{credentials::MockCredential, http::MockHttpClient, ErrorKind};
9+
use azure_core_test::ErrorKind;
1110
use azure_security_keyvault_secrets::{
1211
models::SetSecretParameters, SecretClient, SecretClientOptions,
1312
};
14-
use futures::FutureExt;
15-
use std::sync::Arc;
13+
use example::setup;
1614

1715
/// This example demonstrates deserializing a standard Azure error response to get more details.
18-
async fn test_error_response() -> Result<(), Box<dyn std::error::Error>> {
16+
async fn example_error_response() -> Result<(), Box<dyn std::error::Error>> {
1917
let mut options = SecretClientOptions::default();
2018

2119
// Ignore: this is only set up for testing.
@@ -80,22 +78,32 @@ async fn test_error_response() -> Result<(), Box<dyn std::error::Error>> {
8078
// ----- BEGIN TEST SETUP -----
8179
#[tokio::test]
8280
async fn test_core_error_response() -> Result<(), Box<dyn std::error::Error>> {
83-
test_error_response().await
81+
example_error_response().await
8482
}
8583

8684
#[tokio::main]
8785
async fn main() -> Result<(), Box<dyn std::error::Error>> {
88-
test_error_response().await
86+
example_error_response().await
8987
}
9088

91-
#[allow(clippy::type_complexity)]
92-
fn setup() -> Result<(Arc<dyn TokenCredential>, Arc<dyn HttpClient>), Box<dyn std::error::Error>> {
93-
let client = MockHttpClient::new(|_| {
94-
async move {
95-
Ok(AsyncRawResponse::from_bytes(
96-
StatusCode::BadRequest,
97-
Headers::new(),
98-
r#"{
89+
mod example {
90+
use azure_core::{
91+
credentials::TokenCredential,
92+
http::{headers::Headers, AsyncRawResponse, HttpClient, StatusCode},
93+
};
94+
use azure_core_test::{credentials::MockCredential, http::MockHttpClient};
95+
use futures::FutureExt;
96+
use std::sync::Arc;
97+
98+
#[allow(clippy::type_complexity)]
99+
pub fn setup(
100+
) -> Result<(Arc<dyn TokenCredential>, Arc<dyn HttpClient>), Box<dyn std::error::Error>> {
101+
let client = MockHttpClient::new(|_| {
102+
async move {
103+
Ok(AsyncRawResponse::from_bytes(
104+
StatusCode::BadRequest,
105+
Headers::new(),
106+
r#"{
99107
"error": {
100108
"code": "BadParameter",
101109
"message": "The request URI contains an invalid name: secret_name",
@@ -105,11 +113,12 @@ fn setup() -> Result<(Arc<dyn TokenCredential>, Arc<dyn HttpClient>), Box<dyn st
105113
]
106114
}
107115
}"#,
108-
))
109-
}
110-
.boxed()
111-
});
116+
))
117+
}
118+
.boxed()
119+
});
112120

113-
Ok((MockCredential::new()?, Arc::new(client)))
121+
Ok((MockCredential::new()?, Arc::new(client)))
122+
}
114123
}
115124
// ----- END TEST SETUP -----
Lines changed: 212 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,212 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
use azure_core::{http::Transport, Value};
5+
use example::{setup, ExampleClient, ExampleClientOptions};
6+
7+
/// This example demonstrates deserializing a standard Azure error response to get more details.
8+
async fn example_json_merge_patch() -> Result<(), Box<dyn std::error::Error>> {
9+
let mut options = ExampleClientOptions::default();
10+
11+
// Ignore: this is only set up for testing.
12+
// You normally would create credentials from `azure_identity` and
13+
// use the default transport in production.
14+
let transport = setup()?;
15+
options.client_options.transport = Some(Transport::new(transport));
16+
17+
let client = ExampleClient::new("https://api.contoso.com", Some(options))?;
18+
19+
// Azure SDK for Rust does not implement support for JSON merge patch directly,
20+
// but does allow you to de/serialize your own models including a generic JSON `Value`.
21+
let mut resource: Value = client.get_resource("foo", None).await?.body().json()?;
22+
23+
// Change the description and update tags.
24+
resource["description"] = "an updated foo".into();
25+
if let Some(tags) = resource["tags"].as_object_mut() {
26+
tags["test"] = true.into();
27+
tags.insert("version".into(), 1.into());
28+
}
29+
30+
// Update the resource and assert expected properties.
31+
let resource = client
32+
.update_resource("foo", resource.try_into()?, None)
33+
.await?
34+
.into_model()?;
35+
36+
assert_eq!(resource.id.as_deref(), Some("foo"));
37+
assert_eq!(resource.description.as_deref(), Some("an updated foo"));
38+
39+
let tags = resource.tags.expect("expected tags");
40+
assert_eq!(tags["test"], Value::Bool(true));
41+
assert_eq!(tags["version"], Value::Number(1.into()));
42+
43+
Ok(())
44+
}
45+
46+
// ----- BEGIN TEST SETUP -----
47+
#[tokio::test]
48+
async fn test_core_json_merge_patch() -> Result<(), Box<dyn std::error::Error>> {
49+
example_json_merge_patch().await
50+
}
51+
52+
#[tokio::main]
53+
async fn main() -> Result<(), Box<dyn std::error::Error>> {
54+
example_json_merge_patch().await
55+
}
56+
57+
mod example {
58+
use azure_core::{
59+
fmt::SafeDebug,
60+
http::{
61+
headers::Headers, AsyncRawResponse, ClientMethodOptions, ClientOptions, HttpClient,
62+
Method, Pipeline, Request, RequestContent, Response, StatusCode, Url,
63+
},
64+
Bytes, Value,
65+
};
66+
use azure_core_test::http::MockHttpClient;
67+
use futures::FutureExt;
68+
use serde::{Deserialize, Serialize};
69+
use serde_json::json;
70+
use std::{collections::HashMap, sync::Arc};
71+
72+
#[allow(clippy::type_complexity)]
73+
pub fn setup() -> Result<Arc<dyn HttpClient>, Box<dyn std::error::Error>> {
74+
let client = MockHttpClient::new(|req| {
75+
let mut resource = json!({
76+
"id": "foo",
77+
"description": "just a foo",
78+
"tags": {
79+
"test": false
80+
}
81+
});
82+
async move {
83+
match req.url().path() {
84+
"/foo" if req.method() == Method::Get => Ok(AsyncRawResponse::from_bytes(
85+
StatusCode::Ok,
86+
Headers::new(),
87+
resource.to_string(),
88+
)),
89+
"/foo" if req.method() == Method::Patch => {
90+
let Ok(patch) = serde_json::from_slice::<Value>(&Bytes::from(req.body()))
91+
else {
92+
return Ok(AsyncRawResponse::from_bytes(
93+
StatusCode::BadRequest,
94+
Headers::new(),
95+
r#"{"error":{"code":"BadParameter","message":"Invalid JSON merge patch"}}"#,
96+
));
97+
};
98+
json_patch::merge(&mut resource, &patch);
99+
Ok(AsyncRawResponse::from_bytes(
100+
StatusCode::Ok,
101+
Headers::new(),
102+
resource.to_string(),
103+
))
104+
}
105+
_ => panic!("unexpected request"),
106+
}
107+
}
108+
.boxed()
109+
});
110+
111+
Ok(Arc::new(client))
112+
}
113+
114+
#[derive(Debug)]
115+
pub struct ExampleClient {
116+
endpoint: Url,
117+
pipeline: Pipeline,
118+
}
119+
120+
#[derive(Debug, Default, Clone)]
121+
pub struct ExampleClientOptions {
122+
pub client_options: ClientOptions,
123+
}
124+
125+
#[derive(Debug, Default, Clone)]
126+
pub struct ExampleClientGetResourceOptions<'a> {
127+
pub method_options: ClientMethodOptions<'a>,
128+
}
129+
130+
#[derive(Debug, Default, Clone)]
131+
pub struct ExampleClientUpdateResourceOptions<'a> {
132+
pub method_options: ClientMethodOptions<'a>,
133+
}
134+
135+
#[derive(Clone, Default, Deserialize, SafeDebug, Serialize)]
136+
#[serde(rename_all = "camelCase")]
137+
pub struct Resource {
138+
pub id: Option<String>,
139+
pub description: Option<String>,
140+
#[serde(skip_serializing_if = "Option::is_none")]
141+
pub tags: Option<HashMap<String, Value>>,
142+
}
143+
144+
impl ExampleClient {
145+
pub fn new(
146+
endpoint: &str,
147+
options: Option<ExampleClientOptions>,
148+
) -> azure_core::Result<Self> {
149+
let endpoint: Url = endpoint.parse()?;
150+
let options = options.unwrap_or_default();
151+
152+
Ok(Self {
153+
endpoint,
154+
pipeline: Pipeline::new(
155+
option_env!("CARGO_PKG_NAME"),
156+
option_env!("CARGO_PKG_VERSION"),
157+
options.client_options,
158+
Vec::new(),
159+
Vec::new(),
160+
None,
161+
),
162+
})
163+
}
164+
165+
pub async fn get_resource(
166+
&self,
167+
name: &str,
168+
options: Option<ExampleClientGetResourceOptions<'_>>,
169+
) -> azure_core::Result<Response<Resource>> {
170+
if name.is_empty() {
171+
return Err(azure_core::Error::with_message(
172+
azure_core::error::ErrorKind::Other,
173+
"parameter name cannot be empty",
174+
));
175+
}
176+
let options = options.unwrap_or_default();
177+
let ctx = options.method_options.context.to_borrowed();
178+
let mut url = self.endpoint.clone();
179+
let path = name.to_string();
180+
url = url.join(&path)?;
181+
let mut req = Request::new(url, Method::Get);
182+
req.insert_header("accept", "application/json");
183+
let resp = self.pipeline.send(&ctx, &mut req, None).await?;
184+
Ok(resp.into())
185+
}
186+
187+
pub async fn update_resource(
188+
&self,
189+
name: &str,
190+
resource: RequestContent<Resource>,
191+
options: Option<ExampleClientUpdateResourceOptions<'_>>,
192+
) -> azure_core::Result<Response<Resource>> {
193+
if name.is_empty() {
194+
return Err(azure_core::Error::with_message(
195+
azure_core::error::ErrorKind::Other,
196+
"parameter name cannot be empty",
197+
));
198+
}
199+
let options = options.unwrap_or_default();
200+
let ctx = options.method_options.context.to_borrowed();
201+
let mut url = self.endpoint.clone();
202+
let path = name.to_string();
203+
url = url.join(&path)?;
204+
let mut req = Request::new(url, Method::Patch);
205+
req.insert_header("accept", "application/json");
206+
req.set_body(resource);
207+
let resp = self.pipeline.send(&ctx, &mut req, None).await?;
208+
Ok(resp.into())
209+
}
210+
}
211+
}
212+
// ----- END TEST SETUP -----

0 commit comments

Comments
 (0)