Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions libs/problematic/rust/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
//! The optional `serde` feature enables serialization and deserialization. The `schemars` feature
//! independently adds JSON Schema support to [`ProblemDetails`] and [`NoExtensions`].

#![feature(const_convert, const_destruct, const_trait_impl)]
#![cfg_attr(doc, feature(doc_cfg))]

extern crate alloc;
Expand Down
117 changes: 116 additions & 1 deletion libs/problematic/rust/src/problem_details.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use alloc::borrow::Cow;
use core::marker::Destruct;

#[cfg(feature = "serde")]
use ::serde::{Deserialize, Serialize};
Expand Down Expand Up @@ -32,7 +33,7 @@ const fn default_type_uri() -> Cow<'static, str> {
derive(schemars::JsonSchema),
schemars(title = "Problem Details")
)]
pub struct ProblemDetails<'a, E> {
pub struct ProblemDetails<'a, E = NoExtensions> {
/// A URI reference identifying the problem type. Use `about:blank` when the HTTP status code
/// fully describes the problem type. If `type` is omitted during deserialization, it defaults
/// to `about:blank`.
Expand Down Expand Up @@ -116,3 +117,117 @@ pub struct ProblemDetails<'a, E> {
)]
pub extensions: E,
}

impl<'a, E> ProblemDetails<'a, E> {
/// Sets the human-readable explanation of this occurrence.
///
/// # Examples
///
/// Pass a reference to borrow an explanation assembled at runtime:
///
/// ```
/// use std::borrow::Cow;
///
/// use problematic::{ProblemDetails, ProblemType, StatusCode};
///
/// const INVALID_PARAMETERS: ProblemType = ProblemType {
/// type_uri: Cow::Borrowed("https://example.com/problems/invalid-parameters"),
/// title: Cow::Borrowed("Invalid parameters"),
/// status: StatusCode::BAD_REQUEST,
/// };
///
/// let parameter = "limit";
/// let explanation = format!("The {parameter} parameter must be positive.");
/// let details = ProblemDetails::from(&INVALID_PARAMETERS).detail(&explanation);
///
/// assert_eq!(details.detail.as_deref(), Some(explanation.as_str()));
/// # core::assert_matches!(details.detail, Some(Cow::Borrowed(_)));
/// ```
#[must_use]
pub const fn detail(mut self, detail: impl [const] Into<Cow<'a, str>>) -> Self {
self.detail = Some(detail.into());
self
}

/// Sets the URI reference identifying this occurrence.
///
/// # Examples
///
/// Move a URI built at runtime into the occurrence:
///
/// ```
/// use std::borrow::Cow;
///
/// use problematic::{ProblemDetails, ProblemType, StatusCode};
///
/// const WRONG_ACTOR_TYPE: ProblemType = ProblemType {
/// type_uri: Cow::Borrowed("https://example.com/problems/wrong-actor-type"),
/// title: Cow::Borrowed("Wrong actor type"),
/// status: StatusCode::FORBIDDEN,
/// };
///
/// let occurrence_id = 42;
/// let details = ProblemDetails::from(&WRONG_ACTOR_TYPE)
/// .instance(format!("/problem-occurrences/{occurrence_id}"));
///
/// assert_eq!(details.instance.as_deref(), Some("/problem-occurrences/42"));
/// # core::assert_matches!(details.instance, Some(Cow::Owned(_)));
/// ```
#[must_use]
pub const fn instance(mut self, instance: impl [const] Into<Cow<'a, str>>) -> Self {
self.instance = Some(instance.into());
self
}

/// Replaces the extension members, changing their type.
///
/// # Examples
///
/// Extension members serialize alongside the standard fields:
///
/// ```
/// use std::borrow::Cow;
///
/// use problematic::{ProblemDetails, ProblemType, StatusCode};
///
/// #[derive(serde::Serialize)]
/// struct WrongActorType {
/// required_actor_type: &'static str,
/// }
///
/// const WRONG_ACTOR_TYPE: ProblemType = ProblemType {
/// type_uri: Cow::Borrowed("https://example.com/problems/wrong-actor-type"),
/// title: Cow::Borrowed("Wrong actor type"),
/// status: StatusCode::FORBIDDEN,
/// };
///
/// let details = ProblemDetails::from(&WRONG_ACTOR_TYPE)
/// .detail("This operation requires a machine actor.")
/// .extensions(WrongActorType {
/// required_actor_type: "machine",
/// });
///
/// assert_eq!(serde_json::to_value(&details)?, serde_json::json!({
/// "type": "https://example.com/problems/wrong-actor-type",
/// "title": "Wrong actor type",
/// "status": 403,
/// "detail": "This operation requires a machine actor.",
/// "required_actor_type": "machine"
/// }));
/// # Ok::<(), serde_json::Error>(())
/// ```
#[must_use]
pub const fn extensions<F>(self, extensions: F) -> ProblemDetails<'a, F>
where
E: [const] Destruct,
{
ProblemDetails {
type_uri: self.type_uri,
title: self.title,
status: self.status,
detail: self.detail,
instance: self.instance,
extensions,
}
}
}
127 changes: 126 additions & 1 deletion libs/problematic/rust/src/problem_type.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
use alloc::borrow::Cow;

use crate::StatusCode;
use crate::{NoExtensions, ProblemDetails, StatusCode};

/// Metadata shared by occurrences of a problem type.
///
/// The [`detail()`](Self::detail), [`instance()`](Self::instance), and
/// [`extensions()`](Self::extensions) methods borrow the definition's type URI and title.
///
/// ```
/// use std::borrow::Cow;
///
Expand All @@ -14,6 +17,34 @@ use crate::StatusCode;
/// title: Cow::Borrowed("Wrong actor type"),
/// status: StatusCode::FORBIDDEN,
/// };
///
/// let details = WRONG_ACTOR_TYPE
/// .detail("This operation requires a machine actor.")
/// .instance("/problem-occurrences/42");
/// ```
///
/// Use [`ProblemDetails::from`] to create an occurrence with only the shared metadata.
/// Add typed extension members with [`extensions()`](Self::extensions).
///
/// For const construction, enable the const trait features and pass string literals as
/// [`Cow::Borrowed`]:
///
/// ```
/// #![feature(const_convert, const_trait_impl)]
///
/// use std::borrow::Cow;
///
/// use problematic::{ProblemDetails, ProblemType, StatusCode};
///
/// const WRONG_ACTOR_TYPE: ProblemType = ProblemType {
/// type_uri: Cow::Borrowed("https://example.com/problems/wrong-actor-type"),
/// title: Cow::Borrowed("Wrong actor type"),
/// status: StatusCode::FORBIDDEN,
/// };
///
/// const DETAILS: ProblemDetails<'static> = WRONG_ACTOR_TYPE
/// .detail(Cow::Borrowed("This operation requires a machine actor."))
/// .instance(Cow::Borrowed("/problem-occurrences/42"));
/// ```
#[derive(Debug)]
pub struct ProblemType {
Expand All @@ -24,3 +55,97 @@ pub struct ProblemType {
/// The HTTP status code for occurrences of this problem type.
pub status: StatusCode,
}

impl ProblemType {
/// Creates an occurrence with a human-readable explanation.
///
/// See [`ProblemDetails::detail()`] for an example borrowing an explanation assembled
/// at runtime.
#[must_use]
pub const fn detail<'a>(
&'a self,
detail: impl [const] Into<Cow<'a, str>>,
) -> ProblemDetails<'a> {
ProblemDetails::from(self).detail(detail)
}

/// Creates an occurrence identified by the supplied URI reference.
///
/// See [`ProblemDetails::instance()`] for an example using a URI built at runtime.
#[must_use]
pub const fn instance<'a>(
&'a self,
instance: impl [const] Into<Cow<'a, str>>,
) -> ProblemDetails<'a> {
ProblemDetails::from(self).instance(instance)
}

/// Creates an occurrence with the supplied extension members.
///
/// See [`ProblemDetails::extensions()`] for an example serializing typed extension members.
#[must_use]
pub const fn extensions<E>(&self, extensions: E) -> ProblemDetails<'_, E> {
ProblemDetails::from(self).extensions(extensions)
}
}

const impl<'a> From<ProblemType> for ProblemDetails<'a> {
fn from(definition: ProblemType) -> Self {
Self {
type_uri: definition.type_uri,
title: definition.title,
status: definition.status.as_u16(),
detail: None,
instance: None,
extensions: NoExtensions {},
}
}
}

const impl<'a> From<&'a ProblemType> for ProblemDetails<'a> {
fn from(definition: &'a ProblemType) -> Self {
Self {
type_uri: Cow::Borrowed(match &definition.type_uri {
Cow::Borrowed(uri) => uri,
Cow::Owned(uri) => uri.as_str(),
}),
title: Cow::Borrowed(match &definition.title {
Cow::Borrowed(title) => title,
Cow::Owned(title) => title.as_str(),
}),
status: definition.status.as_u16(),
detail: None,
instance: None,
extensions: NoExtensions {},
}
}
}

#[cfg(test)]
mod tests {
use alloc::{borrow::Cow, string::String};
use core::{assert_matches, ptr};

use crate::{ProblemDetails, ProblemType, StatusCode};

#[test]
fn from_owned_metadata() {
let definition = ProblemType {
type_uri: Cow::Owned(String::from(
"https://example.com/problems/invalid-parameters",
)),
title: Cow::Owned(String::from("Invalid parameters")),
status: StatusCode::BAD_REQUEST,
};
let details = ProblemDetails::from(&definition);

assert_matches!(
details.type_uri, Cow::Borrowed(uri) if ptr::eq(uri, definition.type_uri.as_ref()),
"the type URI should borrow the definition's allocation"
);
assert_matches!(
details.title, Cow::Borrowed(title) if ptr::eq(title, definition.title.as_ref()),
"the title should borrow the definition's allocation"
);
}
}
23 changes: 9 additions & 14 deletions libs/problematic/rust/tests/extensions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,26 +3,21 @@ extern crate alloc;
use alloc::{borrow::Cow, format};
use core::{cell::Cell, fmt};

use problematic::ProblemDetails;
use problematic::{ProblemType, StatusCode};
use serde::{
Serialize, Serializer,
ser::{Error as _, SerializeMap as _},
};
use serde_json::{Number, Value, json};

const fn details<E>(extensions: E) -> ProblemDetails<'static, E> {
ProblemDetails {
type_uri: Cow::Borrowed("about:blank"),
title: Cow::Borrowed("Bad Request"),
status: 400,
detail: None,
instance: None,
extensions,
}
}
const BAD_REQUEST: ProblemType = ProblemType {
type_uri: Cow::Borrowed("about:blank"),
title: Cow::Borrowed("Bad Request"),
status: StatusCode::BAD_REQUEST,
};

fn assert_serialization_error<E: Serialize>(extensions: E, expected: &str) {
let details = details(extensions);
let details = BAD_REQUEST.extensions(extensions);
let string_error = serde_json::to_string(&details)
.expect_err("the invalid extensions should fail to serialize to a string");
let value_error = serde_json::to_value(&details)
Expand All @@ -40,7 +35,7 @@ fn assert_serialization_error<E: Serialize>(extensions: E, expected: &str) {
}

fn assert_serialization<E: Serialize>(extensions: E, expected: &Value) {
let details = details(extensions);
let details = BAD_REQUEST.extensions(extensions);
let encoded =
serde_json::to_string(&details).expect("the extensions should serialize to a string");
assert_eq!(
Expand Down Expand Up @@ -272,7 +267,7 @@ impl Serialize for CountedKey<'_> {
fn extensions_map_key_once() {
for split in [false, true] {
let calls = Cell::new(0);
let details = details(Entry {
let details = BAD_REQUEST.extensions(Entry {
key: CountedKey(&calls),
split,
});
Expand Down
Loading
Loading