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
15 changes: 15 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,21 @@ SPDX-License-Identifier: Apache-2.0

All notable changes to this project will be documented in this file.

## [1.1.1] - 2026-04-14

### Features

- Add RFC3339 Timestamp

### Bug Fixes

- Make BaseUrl schema inlined
- Fix some docs

### Documentation

- Replace doc_auto_cfg with doc_cfg

## [1.0.2] - 2025-08-01

### Features
Expand Down
15 changes: 14 additions & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 3 additions & 3 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
[package]
name = "famedly_rust_utils"
description = "Various rust utility functions and types"
version = "1.1.0"
version = "1.1.1"
authors = ["Famedly Workflows Team <workflow@famedly.com>"]
edition = "2021"
resolver = "2"
Expand All @@ -16,6 +16,7 @@ rust-version = "1.78.0"

[package.metadata.docs.rs]
all-features = true
rustdoc-args = ["--cfg", "docsrs"]

[dependencies]
figment = { version = "0.10.0", features = [
Expand All @@ -40,7 +41,7 @@ serde_json = "1.0.127"
config = ["dep:figment", "dep:serde"]
level_filter = ["dep:tracing", "dep:serde"]
reqwest = ["dep:reqwest", "dep:thiserror"]
time = ["dep:time"]
time = ["dep:time", "time/serde", "time/parsing", "time/formatting"]
schemars = ["dep:schemars", "schemars/url2"]
serde = ["dep:serde", "dep:paste"]
base_url = ["dep:url", "dep:thiserror", "dep:serde"]
Expand Down Expand Up @@ -117,7 +118,6 @@ same_functions_in_if_condition = "warn"
same_name_method = "warn"
semicolon_if_nothing_returned = "warn"
str_to_string = "warn"
string_to_string = "warn"
suboptimal_flops = "warn"
suspicious_operation_groupings = "warn"
too_many_lines = "warn"
Expand Down
16 changes: 15 additions & 1 deletion src/base_url.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
//! Workaround on [`Url::join`] [behavior](https://github.com/servo/rust-url/issues/333)
use std::ops::Deref;

#[cfg(feature = "schemars")]
use schemars::{JsonSchema, Schema, SchemaGenerator};
#[cfg(feature = "serde")]
use serde::{de::Error, Deserialize, Deserializer, Serialize};
use thiserror::Error;
Expand All @@ -24,7 +26,6 @@ use crate::GenericCombinators;
/// let foo: Foo = serde_json::from_value(serde_json::json!({"base_url": "http://example.com"})).unwrap();
/// assert_eq!(foo.base_url.as_str(), "http://example.com/");
/// ```
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[derive(Debug, PartialEq, Eq, Clone, Hash)]
#[cfg_attr(feature = "serde", derive(Deserialize, Serialize))]
#[repr(transparent)]
Expand Down Expand Up @@ -125,6 +126,19 @@ impl Deref for BaseUrl {
}
}

#[cfg(feature = "schemars")]
impl JsonSchema for BaseUrl {
fn schema_name() -> std::borrow::Cow<'static, str> {
"BaseUrl".into()
}
fn json_schema(_generator: &mut SchemaGenerator) -> Schema {
schemars::json_schema!({"type": "string", "format": "uri"})
}
fn inline_schema() -> bool {
true
}
}

/// Add trailing slash to [`Url`]
fn add_trailing_slash(url: &mut Url) {
if !url.path().ends_with('/') {
Expand Down
2 changes: 1 addition & 1 deletion src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ fn print_parse_config_errors(env_prefix: &str, error: Box<figment::Error>) {
/// configuration, which can be confusing if a config file cannot be
/// read or is misnamed.
///
/// See [`print_parse_config_errors`] for further edge cases.
/// See internal `print_parse_config_errors` function for further edge cases.
#[allow(clippy::print_stderr)]
pub fn try_parse_config<C: DeserializeOwned>(env_prefix: &str) -> Result<C, Box<figment::Error>> {
if let Some(config_path) = std::env::var_os(format!("{env_prefix}CONFIG")) {
Expand Down
26 changes: 13 additions & 13 deletions src/duration.rs
Original file line number Diff line number Diff line change
Expand Up @@ -62,20 +62,20 @@ macro_rules! define_generic_wrapper {
}
}

$(
#[cfg(feature = "schemars")]
impl JsonSchema for $name<$t> {
fn schema_name() -> std::borrow::Cow<'static, str> {
concat!("DurationIn", stringify!($name)).into()
}
fn json_schema(_generator: &mut SchemaGenerator) -> Schema {
schemars::json_schema!({"type": "integer"})
}
fn inline_schema() -> bool {
true
}
#[cfg(feature = "schemars")]
impl<D> JsonSchema for $name<D> {
fn schema_name() -> std::borrow::Cow<'static, str> {
concat!("DurationIn", stringify!($name)).into()
}
fn json_schema(_generator: &mut SchemaGenerator) -> Schema {
schemars::json_schema!({"type": "integer"})
}
fn inline_schema() -> bool {
true
}
}

$(
$( #[cfg(feature = $feat)] )?
impl<'de> Deserialize<'de> for $name<$t> {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
Expand Down Expand Up @@ -128,7 +128,7 @@ macro_rules! define_generic_wrapper {
use time::Duration as TimeDuration;

define_generic_wrapper! {
"`Duration` wrapper with [`Deserialize`] impl",
"Helper wrapper to use in configs to deserialize durations from seconds",
Seconds:

{
Expand Down
6 changes: 4 additions & 2 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
//
// SPDX-License-Identifier: Apache-2.0

#![cfg_attr(all(doc, not(doctest)), feature(doc_cfg))]
#![cfg_attr(all(docsrs, not(doctest)), feature(doc_cfg))]
//! This crate consists of incohesive generic types and functions that are
//! needed in almost every crate but are so small that making a separate crate
//! for them is too much.
Expand Down Expand Up @@ -37,6 +37,8 @@ pub mod duration;
mod level_filter;
#[cfg(feature = "reqwest")]
pub mod reqwest;
#[cfg(all(feature = "time", feature = "serde"))]
pub mod timestamp;

#[cfg(feature = "base_url")]
pub use base_url::{BaseUrl, BaseUrlParseError};
Expand Down Expand Up @@ -208,7 +210,7 @@ impl<I: Iterator> IteratorExt for I {
}
}

/// Helper wrapper to serialize types as strings using [`FromStr`] and
/// Helper wrapper to serialize types as strings using [`std::str::FromStr`] and
/// [`ToString`].
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Default)]
pub struct AsString<X>(pub X);
Expand Down
81 changes: 81 additions & 0 deletions src/timestamp.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
// SPDX-FileCopyrightText: 2026 Famedly GmbH (info@famedly.com)
//
// SPDX-License-Identifier: Apache-2.0

//! Wrapper over [`OffsetDateTime`] with RFC3339 JSON representation

#![allow(missing_docs, unused_qualifications)]

use core::{fmt, str};

#[cfg(feature = "schemars")]
use schemars::{JsonSchema, Schema, SchemaGenerator};
use serde::{Deserialize, Serialize};
use time::OffsetDateTime;

/// Timestamp with RFC3339 JSON representation
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone, Copy, Serialize, Deserialize)]
#[serde(transparent)]
#[repr(transparent)]
pub struct Timestamp {
#[serde(with = "time::serde::rfc3339")]
ts: OffsetDateTime,
}

impl AsRef<OffsetDateTime> for Timestamp {
fn as_ref(&self) -> &OffsetDateTime {
&self.ts
}
}

impl std::ops::Deref for Timestamp {
type Target = OffsetDateTime;
fn deref(&self) -> &Self::Target {
&self.ts
}
}

impl fmt::Display for Timestamp {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
use time::format_description::well_known::Rfc3339;
let ts = self.ts.format(&Rfc3339).map_err(|_| fmt::Error)?;
write!(f, "{}", ts)
}
}

impl str::FromStr for Timestamp {
type Err = time::error::Parse;
fn from_str(s: &str) -> Result<Self, Self::Err> {
use time::format_description::well_known::Rfc3339;
let ts = time::OffsetDateTime::parse(s, &Rfc3339)?;
Ok(Self { ts })
}
}

#[cfg(feature = "schemars")]
impl JsonSchema for Timestamp {
fn schema_name() -> std::borrow::Cow<'static, str> {
"Timestamp".into()
}
fn json_schema(_generator: &mut SchemaGenerator) -> Schema {
schemars::json_schema!({
"type": "string",
"format": "date-time",
})
}
fn inline_schema() -> bool {
true
}
}

impl From<OffsetDateTime> for Timestamp {
fn from(ts: OffsetDateTime) -> Self {
Self { ts }
}
}

impl From<Timestamp> for OffsetDateTime {
fn from(ts: Timestamp) -> Self {
ts.ts
}
}
Loading